Claude apps gatewayをTerraformで構築する — AWS/GCP共通の勘所
AWS/GCPそれぞれの公式Terraformバンドルが何を自動化し何を手動のままにするか、2パスapplyが必要な理由、設定変更時の再ビルド運用まで解説します。
Terraformで何が省略できて何は省略できないか
Claude apps gatewayには、AWS・GCPそれぞれに公式のTerraformバンドル(GitHub上のexamples/gateway/awsとexamples/gateway/gcp)が用意されています。どちらもセキュリティグループ・IAMロール・ECRリポジトリ・RDSインスタンス・Secrets Managerのシークレット・内部ALB配下のECSサービスといった、各クラウドのウォークスルーが手作業で作る一式を宣言的に再現します。
ただしどちらのバンドルも「顧客管理インフラの動作例」であって「サポート対象の本番デプロイ成果物」ではないと、両クラウドの公式ページが明記しています。VPCとプライベートサブネット、TLS証明書(AWSならACM証明書、GCPなら内部Application Load Balancerに紐づける証明書)、IdP側のOAuthクライアント登録は前提条件のまま残り、Terraformは作りません。レビューして自分の環境に合わせて調整することが前提です。
ステップ1: 2パスapplyが必要な理由
TerraformはECR(AWSなら)・Artifact Registry(GCPなら)のリポジトリを作りますが、イメージ自体はビルドしません。一方でECSのサービス定義やCloud Runのデプロイはそのイメージを参照するため、リポジトリが存在する前にサービス定義まで一括でapplyすると、存在しないイメージタグを参照して失敗します。
# 1st pass: リポジトリだけを対象にapply
terraform apply -target=aws_ecr_repository.gateway
# ビルドしてpush
docker build -t <account>.dkr.ecr.<region>.amazonaws.com/claude-gateway:<tag> .
docker push <account>.dkr.ecr.<region>.amazonaws.com/claude-gateway:<tag>
# 2nd pass: 残り全体をapply
terraform applyAWSバンドルに同梱されるsetup.shは、この2パス構成をECS Fargateトラックに沿ってawsコマンドでそのままスクリプト化したものです。冪等に作られているため、既存リソースは検出してスキップし、再実行しても安全です。ただしOktaのOIDCクライアントシークレットとACM証明書だけは自分で作る前提で、これらが無いままだとECS/ALBのデプロイ部分をスキップし、不足している入力を名指ししてcreate-secretコマンドを表示します。GCPバンドルのsetup.shも同じ考え方で、APIの有効化から最初のデプロイまでのCloud Runパスを一通り歩きます。
setup.shとterraform/の使い分け
setup.shとterraform/はどちらも同じ最終形へ到達しますが、向くフェーズが異なります。setup.shはawsまたはgcloudコマンドを順に叩く命令的なスクリプトで、既存リソースを検出してスキップするため、最初の動作確認や検証環境の立ち上げには手早く進められます。一方terraform/は宣言的な状態管理を伴うため、変更をPRでレビューしたり、意図しないドリフト(手作業での変更)を検出したりする用途に向きます。恒久的な本番環境をチームで運用するならterraform/、まず動くものを見たいだけならsetup.sh、という使い分けが実務的です。
ステップ2: AWSとGCPでTerraformが作るリソースの違い
同じ「ゲートウェイをコードで再現する」という目的でも、両クラウドでTerraformが作るリソース群は異なります。
| 論点 | AWS | GCP |
|---|---|---|
| コンピュート | AWSECS Fargateサービス(EKSは対象外、手動デプロイのみ) | GCPCloud Run(GKEはTerraform化されていない) |
| 状態ストア | AWSRDS for PostgreSQL | GCPCloud SQL for PostgreSQL |
| シークレット | AWSAWS Secrets Manager | GCPSecret Manager |
| イメージレジストリ | AWSAmazon ECR | GCPArtifact Registry |
| upstream認証 | AWSIAMタスクロール(bedrock:InvokeModel等) | GCPサービスアカウント(roles/aiplatform.user) |
| ネットワーク境界 | AWS内部ALB + 3つのセキュリティグループ(ALB/サービス/DB) | GCP内部ロードバランサー配下のCloud Run + Private Services AccessでのCloud SQL接続 |
| invoker/ingressの既定 | AWSALB配下でinternalロードバランサーを前提 | GCPCloud Run ingressはinternal、invoker層はallUsersのrun.invoker付与が既定(手作業ページの--no-invoker-iam-checkとは逆) |
AWSのIAMロールに与える権限は、Bedrockのクロスリージョン推論プロファイルARN(us.anthropic.*)と、その裏にある基盤モデルのARNの両方を対象にする必要があります。片方だけを許可すると、モデル呼び出しが推論プロファイル経由かどうかで通ったり通らなかったりする不安定な状態になるため、Terraformのポリシー定義でも両方のARNパターンを明示的にカバーしているかを確認します。GCP側は対照的にシンプルで、サービスアカウントへroles/aiplatform.userを1つ付与するだけで、Google Cloud's Agent Platformへの呼び出しに必要な権限が揃います。
AWS側はEKSトラック、GCP側はGKEトラックがそれぞれTerraform化されていません。コンテナオーケストレーションにKubernetesを使いたい場合、Terraformバンドルが自動化するのはコンピュート以外の周辺リソース(ネットワーク・DB・シークレット・レジストリ)までで、Deployment・Service・Ingressの適用自体はデプロイと運用の手順に従って自分で書くことになります。
ステップ3: 設定ファイルとシークレットの扱い方
gateway.yaml.exampleはコメントアウトされた任意キーまで含めた設定テンプレートで、REPLACE_MEをすべて置き換えてからgateway.yamlとしてビルドコンテキストに置きます。設定ファイルにはシークレットの値が一切入りません。すべてのクレデンシャルは${VAR}形式の展開でブート時に解決されるため、設定ファイル自体はGitに残しても安全な形になります。
裏を返すと、設定を1行変えるたびにイメージの再ビルドが必要になります。AWSのsetup.shは設定ファイルのハッシュ値でイメージタグを自動的に振るため、gateway.yamlを変更すると自動的に新しいタグでビルドされ、それを参照するようTerraformの状態も更新されます。手動でタグ管理する場合は、この「設定変更 = 新タグでの再ビルド」という運用ルールを忘れると、意図した設定が反映されないまま古いイメージが動き続けることになります。
AWS版のDockerfileはさらに、sslmode=verify-fullでRDSに接続するためのAWS CA証明書バンドルをビルド時に取り込みます。setup.shはこのバンドルがビルドコンテキストに無いときだけダウンロードするため、AWSがCA証明書をローテーションした場合はバンドルファイルを手動で削除し、新しいタグで再ビルドしないと古い証明書のままになります。
Admin APIのwrite_keysをTerraformやCIから使う場合、公式の設定例でもid: terraform・id: ciのようにツールごとにキーIDを分けています。支出上限の運用でも触れた「自動化ごとに専用キーを分ける」という設計は、Terraformで構築する場合も同じ考え方で踏襲できます。
ステップ4: 両クラウドで共通して気をつける接続数とネットワークの制約
store.max_connections(既定5)はレプリカ1台あたりのPostgresコネクション数です。ECS/Cloud Runのインスタンス数上限×この値が、データベース側の接続上限を超えないようにする必要があります。GCP向けの参考実装(手動のgcloudコマンドとTerraformバンドルの両方)がdb-g1-smallティアに対してCloud Runの最大インスタンス数を8に固定しているのは、この積算がティアの接続上限を超えないようにするためです。設定できる項目の全体は設定リファレンスにまとめてあります。
Cloud Runのinvoker/ingressの既定値にも注意が必要です。バンドルはingressをinternalにする点はページのウォークスルーと同じですが、invoker層は--no-invoker-iam-checkではなくallUsersへのrun.invoker付与が既定になっており、手作業の手順とは逆の選択です。どちらでも動作しますが、どちらを選ぶかは組織のポリシー制約(Domain Restricted Sharing等)次第です。
AWS側で内部ALBをTerraformが作る場合、ALBのアイドルタイムアウトの既定値(60秒)にも注意が要ります。BedrockまたはClaude Platform on AWSをupstreamにしている場合、extended thinkingで出力が止まっている間データがまったく流れない時間が生じ、無応答が60秒続くとALBが接続を切ってしまいます(Anthropic API upstreamの場合はAPI自身のpingが中継されるため、この問題は起きません)。v2.1.229以降のゲートウェイは無応答が15秒ほど続いた時点でSSEのpingイベントを送るようになりこの問題を避けられますが、それより古いバージョンで運用する場合や念のための保険として、Terraformのidle_timeout.timeout_seconds属性を3600まで引き上げておくと安全です。
よくあるつまずき
- 1回のapplyで全リソースを作ろうとする: イメージが存在しないうちにサービス定義までapplyすると、参照エラーで失敗する。リポジトリ作成 → ビルド&push → 本applyの順を守る
gateway.yamlを変更してもイメージを再ビルドしない: 設定はイメージに焼き込まれるため、再ビルド&再デプロイを忘れると変更が反映されない- VPC・サブネット・証明書をTerraformが作ると思い込む: これらは前提条件として利用者が用意するもので、バンドルの対象外
- EKS/GKEでもTerraformが完結すると思い込む: 両クラウドともKubernetesトラックはTerraform化されておらず、周辺リソースまでしか自動化されない
- 接続数の積算を見落とす: レプリカ数×
store.max_connectionsがDBの接続上限を超えると、支出上限を使う構成では特に起動やスケールアウト時にPostgres接続エラーが起きやすくなる - AWSのIAMポリシーを片方のARN形式だけ許可する: 推論プロファイルARNと基盤モデルARNのどちらか一方だけを許可すると、呼び出し方によって成功・失敗が分かれる不安定な状態になる
まとめ
AWS・GCPどちらのTerraformバンドルも、ネットワーク・DB・シークレット・レジストリという周辺一式は宣言的に再現しますが、イメージのビルドとKubernetesトラックのデプロイは対象外です。両クラウドとも「動作例であって本番の完成品ではない」という位置付けは共通しているので、そのまま流用するのではなく、自組織の命名規則・タグ付け・ステート管理(リモートバックエンド等)に合わせて書き換える前提で読むのが安全です。イメージ参照が先に必要なため2パスでのapplyが前提になり、設定変更のたびに再ビルドが要る点、レプリカ数とstore.max_connectionsの積算がDB接続上限を超えないようにする点は両クラウド共通の注意点です。バンドルはあくまで動作例で、レビューして自分の環境に合わせて調整してください。