Claude Code を Bedrock 越しに使う

レシピ事業部バックエンド基盤グループの石川です。2025 年 6 月の頭にやった仕事について走り書きのようなメモを残しておきたくなったので、この記事を書いています。

Anthropic 社が開発している Claude Code は、ターミナルの上で動作する LLM agent です: https://docs.anthropic.com/en/docs/claude-code/overview

Claude Code は Anthropic の API 経由で使う方法の他に、AWS や Google Cloud を経由して Claude のモデルを利用する道を用意してくれています: https://docs.anthropic.com/en/docs/claude-code/third-party-integrations

AWS を多用しているクックパッドにおいては、Amazon Bedrock の Claude モデル経由で Claude Code を利用できると管理上いくつかの点で利便性があると考えました。そこでこの記事では、利用に至るまでのセットアップやコスト分析の方法、そして実際に利用してみての制限について、簡単にまとめます。

前提

Claude Code のドキュメントを読むと、Bedrock 越しに利用する際は Claude Code 専用の AWS アカウントを新設して使うようにお勧めされています。コスト管理や権限管理を簡単にするためです。

We recommend creating a dedicated AWS account for Claude Code to simplify cost tracking and access control.

https://docs.anthropic.com/en/docs/claude-code/amazon-bedrock (2025-06-16 閲覧)

一方でクックパッド社内では歴史的経緯により、それなりに多くのサービスがとあるひとつの AWS アカウントに集まる形で AWS を利用しています。このためこの記事では AWS アカウントを分けることなくひとつのアカウントのまま使う前提で話を進めます。

他のアカウントと分けずに Bedrock を利用する場合、コスト分析の観点では Claude Code のために利用した Bedrock の料金とそれ以外の料金とを分けて集計できるようにしておきたいです。このために使えるものとして、Bedrock の application inference profiles というものがあります: https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-use.html。これを使うと Bedrock の InvokeModel API などを呼び出した場合にかかった料金について、cost allocation tags を使った分類ができるようになります。より詳しくは、何らかの基盤モデル (foundation model) を直接呼び出すのではなく、基盤モデルを束ねる概念である inference profile を呼び出すことにし、この inference profile にタグ付けしておくことによって、Bedrock の利用料金をタグごとに分類できるようにするという訳です。このような取り組みは、インフラコストの妥当性を説明する手助けになります (https://techlife.cookpad.com/entry/how-to-describe-infra-cost)。

Application Inference Profiles の作成

Claude Code では Claude 系列のいくつかの種類のモデルを使い分けることになります。ひとつのモデルに対してひとつの application inference profile を定義する必要があり、たとえば Terraform で書くと以下のようになります。なお Bedrock はリージョンごとに使える基盤モデルが異なり、ここでは us-east-1 を想定しています。

locals {
  # System-defined inference profiles for Anthropic Claude models.
  model_sources = toset([
    "us.anthropic.claude-3-5-haiku-20241022-v1:0",
    "us.anthropic.claude-3-5-sonnet-20240620-v1:0",
    "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
    "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
    "us.anthropic.claude-3-haiku-20240307-v1:0",
    "us.anthropic.claude-3-opus-20240229-v1:0",
    "us.anthropic.claude-3-sonnet-20240229-v1:0",
    "us.anthropic.claude-opus-4-20250514-v1:0",
    "us.anthropic.claude-sonnet-4-20250514-v1:0",
  ])
}

data "aws_bedrock_inference_profile" "model_source" {
  for_each = local.model_sources

  inference_profile_id = each.value
}

resource "aws_bedrock_inference_profile" "claude_code" {
  for_each = local.model_sources

  name        = "claude-code_${replace(replace(each.value, ".", "_"), ":", "_")}"
  description = "Inference profile used by Claude Code based on ${each.value}"

  model_source {
    copy_from = data.aws_bedrock_inference_profile.model_source[each.value].inference_profile_arn
  }

  tags = {
    Project = "claude-code"
  }
}

このようにして作った application inference profiles は、Claude Code 用の環境変数である ANTHROPIC_MODEL や ANTHROPIC_SMALL_FAST_MODEL に ARN をセットしておくことによって Claude Code から利用できます。詳しくはドキュメントをご覧ください: https://docs.anthropic.com/en/docs/claude-code/amazon-bedrock。自分が忘れがちだったこととして AWS_REGION もセットする必要があります。

権限管理

Claude Code は開発者それぞれの手元の環境からモデルの呼び出しを行うことになるため、各開発者に対して AWS IAM で bedrock:InvokeModel などについて権限を許可することになります。

上記のように inference profiles 経由でモデルを呼び出したい場合、 inference profiles をリソースとする bedrock:InvokeModel を許可するだけでは権限が足りず、inference profiles の背後にいる基盤モデルについても bedrock:InvokeModel を許可する必要があります。

ただしその両方について素朴に許可すると基盤モデルに対する直接の API 呼び出しも許可されてしまうため、基盤モデルに対する呼び出しについては inference profiles 越しの利用に限って許可するように書くことができます。具体的には condition key として bedrock:InferenceProfileArn を利用します。ドキュメントはこちらです: https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html

たとえば InvokeModel 周りについては以下のような policy が考えられます。基盤モデルの方にだけ Condition があるのがポイントです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "bedrock:InvokeModel",
                "bedrock:InvokeModelWithResponseStream"
            ],
            "Resource": [
                "arn:aws:bedrock:*::foundation-model/anthropic.claude-3-5-haiku-20241022-v1:0",
                "arn:aws:bedrock:*::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0",
                "arn:aws:bedrock:*::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0",
                "arn:aws:bedrock:*::foundation-model/anthropic.claude-3-7-sonnet-20250219-v1:0",
                "arn:aws:bedrock:*::foundation-model/anthropic.claude-3-haiku-20240307-v1:0",
                "arn:aws:bedrock:*::foundation-model/anthropic.claude-3-opus-20240229-v1:0",
                "arn:aws:bedrock:*::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0",
                "arn:aws:bedrock:*::foundation-model/anthropic.claude-opus-4-20250514-v1:0",
                "arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-20250514-v1:0"
            ]
            "Condition": {
                "ArnLike": {
                    "bedrock:InferenceProfileArn": "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/*"
                }
            },
        },
        {
            "Effect": "Allow",
            "Action": [
                "bedrock:InvokeModel",
                "bedrock:InvokeModelWithResponseStream"
            ],
            "Resource": [
                "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/aaaaaaaaaaaa",
                "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/bbbbbbbbbbbb",
                "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/cccccccccccc",
                "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/dddddddddddd",
                "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/eeeeeeeeeeee",
                "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/ffffffffffff",
                "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/gggggggggggg",
                "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/hhhhhhhhhhhh",
                "arn:aws:bedrock:us-east-1:012345678901:application-inference-profile/iiiiiiiiiiii"
            ]
        }
    ]
}

開発者ごとのコスト分析

Application inference profiles を使うと Claude Code を使うにあたってかかっているコストがざっくり見えるようになります。一方で上記のやり方だと開発者一人ひとりについてどのくらい使っているのかを分析するところまではできません。Anthropic の API を使うのと比べてオトクなのか……みたいなことを考え出すと開発者あたりのお値段も出したくなってくるので、こちらも分析できるようにしてみます。

このためには Bedrock の model invocation logging が使えます: https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html。それぞれの InvokeModel について、どのモデルが呼び出され input と output でどのくらい token が消費されたかの情報がログとして蓄積されます。出力先として Amazon S3 を選べるので、適切に AWS Glue のテーブル定義を作っておけば Amazon Athena で集計できるという寸法です。Athena は便利。

詳細は省きますがたとえば Glue を使い以下のような定義で partition projection 付きのテーブルを作っておけば、Athena で開発者ごとの情報を集計できるようになります。ただしそれぞれの開発者ごとに別々の identity が割り振られている想定です。

CREATE EXTERNAL TABLE `example.bedrock_model_invocation_logs_use1`(
  `schematype` string COMMENT 'from deserializer', 
  `schemaversion` string COMMENT 'from deserializer', 
  `timestamp` string COMMENT 'from deserializer', 
  `accountid` string COMMENT 'from deserializer', 
  `identity` struct<arn:string> COMMENT 'from deserializer', 
  `region` string COMMENT 'from deserializer', 
  `requestid` string COMMENT 'from deserializer', 
  `operation` string COMMENT 'from deserializer', 
  `modelid` string COMMENT 'from deserializer', 
  `input` struct<inputcontenttype:string,inputtokencount:int,cachereadinputtokencount:int,cachewriteinputtokencount:int> COMMENT 'from deserializer', 
  `output` struct<outputcontenttype:string,outputtokencount:int> COMMENT 'from deserializer', 
  `errorcode` string COMMENT 'from deserializer', 
  `inferenceregion` string COMMENT 'from deserializer')
PARTITIONED BY ( 
  `day` string COMMENT '')
ROW FORMAT SERDE 
  'org.apache.hive.hcatalog.data.JsonSerDe' 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  ''
LOCATION
  's3://example-bucket-use1/'
TBLPROPERTIES (
  'projection.day.format'='yyyy/MM/dd', 
  'projection.day.interval'='1', 
  'projection.day.interval.unit'='DAYS', 
  'projection.day.range'='2025/01/01,NOW', 
  'projection.day.type'='date', 
  'projection.enabled'='true', 
  'storage.location.template'='s3://example-bucket-use1/AWSLogs/012345678901/BedrockModelInvocationLogs/us-east-1/${day}')

なお model invocation logging で入力や出力の内容(テキストや画像など)も含めてログに出力している場合は、これだとクエリ時にエラーが出る場合があります。上記で対象としているログオブジェクト以外にもオブジェクトが生成されるためです。この場合は少し雑ですが serde として org.openx.data.jsonserde.JsonSerDe を使って ignore.malformed.json を使うことにすれば、多少の null を許しつつエラー回避が可能です(追記:クエリ時に $PATH でフィルターすると null も防げます)。あるいはきちんとは試していませんが Glue Crawler で良い感じにできるかもしれません。我々の現在の運用では入出力の具体的な内容についてのログは不要なため、すべて無効にしています。

現実的な制限

以上のようなセットアップを経て、管理上欲しくなってくる点は抑えつつ、開発者に Bedrock 経由で Claude Code を利用してもらうことができました。一方で実際に 2〜3 週間使ってみると微妙な点も出てきています。

まず小さな不満として、各開発者に application inference profile を使ってもらう方式だと、開発者としては今どのモデルを選んで使っているのか分かりづらくなる点があります。us.anthropic.claude-opus-4-20250514-v1:0 のような文字列が abcdefghijkl のような ID の文字列になってしまうためです。いちおう、選んでいるモデルごとに shell alias なり何なりを設定して使い分けるなどの工夫ができはします。

次にそれよりも大きな不満として、Bedrock 経由の利用だとどうしても InvokeModel がエラーになってしまいがちという点があります。返ってくるエラーは 429 Too many tokens や 503 Model is getting throttled で、特に現状は Claude Opus 4 で顕著です。これは Bedrock の基盤モデルごとに存在する quota に引っかかっている訳ではなく、on-demand な呼び出しをする際はクラウド側の都合でスロットリングを受ける可能性があるということです。Claude Code が InvokeModel をリトライしている内に呼び出しが成功したりはするのですが、どうしても待ちの時間が生まれてしまうので今後の改善に期待したいところです。現状はエラーが頻発するようであれば別のモデルへ切り替えることにしています。

以上、Claude Code を Bedrock 越しに使うにあたって自分が設定したことの振り返りでした。これらの情報は 2025 年 6 月上旬時点のものであり、LLM 関係の話はいまだ変化が激しいため、近い将来に状況が変わるかもしれないことはご承知おきください。Anthropic API 経由での利用も含め色々な形を試行錯誤しているところです。

またクックパッドでは現在絶賛採用活動中です。毎日の料理を楽しみにしたい皆様からのご応募を熱烈歓迎しております。まずはいま開いている枠を眺めてみてくださいませ。

cookpad.careers