Claude Media
Claude apps gatewayをTerraformで構築する — AWS/GCP共通の勘所

Claude apps gatewayをTerraformで構築する — AWS/GCP共通の勘所

AWS/GCPの公式Terraformバンドルが作るもの・作らないもの、2パスapplyの手順、設定変更の反映方法の違い、destroyで詰まる点までを解説します。

Terraformで何が省略できて何は省略できないか

Claude apps gatewayには、AWS・GCPそれぞれに公式のTerraformバンドル(GitHub上のexamples/gateway/awsとexamples/gateway/gcp)があります。どちらも各クラウドのウォークスルー(手作業の構築手順)を宣言的に再現するものです。

AWSバンドルのterraform/が作るのは、セキュリティグループ・IAMロール・ECRリポジトリ・RDSインスタンス・Secrets Managerのシークレット・内部ALB配下のECS Fargateサービスです。GCPバンドルのterraform/は、APIの有効化からサービスアカウント、Artifact Registry、VPCとPrivate Services Access、プライベートIPのCloud SQL(PostgreSQL 16)、シークレット、Direct VPC egress付きのCloud Runまでを作ります。

どちらも「顧客管理インフラの動作例」であって、サポート対象の本番デプロイ成果物ではありません。レビューして自分の環境に合わせる前提です。プロバイダーの引数を調べながら直すなら、Terraform MCPサーバーでRegistry検索をClaude Codeに任せる手もあります。

くらべる

Terraformが作るものと、利用者が用意するもの

ECS Fargateのみ

AWSバンドル

VPCとプライベートサブネットは変数(vpc_id・private_subnet_ids)で渡す前提で、ネットワークは作りません。ACM証明書(acm_certificate_arn)とBedrockのモデルアクセス、使用目的フォームの提出も事前に済ませます。

Cloud Runのみ

GCPバンドル

専用のVPCとサブネットまで作ります。作らないのは内部Application Load Balancerと、run.appを社内から引くためのPrivate Service Connect・Cloud DNS・オンプレ側の経路です。

どちらもEKS・GKEのトラックはTerraform化されていません。Kubernetesで動かすなら、Deployment・Service・Ingressはデプロイと運用の手順に従って自分で書きます。

初回applyを2パスに分ける手順

TerraformはECR(AWS)・Artifact Registry(GCP)のリポジトリを作りますが、イメージはビルドしません。サービス定義はそのイメージを参照するため、1回のapplyで全部作ろうとすると、存在しないタグを参照して失敗します。

手順

2パスapplyの流れ

  1. 1

    リポジトリだけを作る

    -targetでレジストリのリポジトリだけをapplyします。-targetの警告が出ますが、想定どおりの挙動です。

  2. 2

    イメージをビルドしてpushする

    Claude Codeのlinux-x64リリースバイナリ(gatewayサブコマンド入り)をビルドコンテキストに置き、バンドルのDockerfileでビルドしてpushします。

  3. 3

    残り全体をapplyする

    terraform.tfvarsのimage_tagにpushしたタグを入れ、terraform applyを実行します。

バンドルのREADMEにある主なコマンドは次のとおりです。DockerfileはCAバンドル(rds-global-bundle.pem)をCOPYするので、ビルド前に取得が要ります。ターゲット名はAWSがaws_ecr_repository.repo、GCPがgoogle_artifact_registry_repository.repoです。

cp terraform.tfvars.example terraform.tfvars   # 編集する
terraform init
 
# 1. ECRリポジトリだけを作る
terraform apply -target=aws_ecr_repository.repo
 
# 2. CAバンドルを取得し、ECRにログインしてからビルド・push(ビルドコンテキストは1つ上のaws/)
curl -fL --proto '=https' -o ../rds-global-bundle.pem \
  https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
docker build --platform=linux/amd64 --provenance=false \
  -f ../Dockerfile --build-arg CLAUDE_BINARY=claude \
  --build-arg GATEWAY_CONFIG=gateway.yaml \
  -t <account-id>.dkr.ecr.us-east-1.amazonaws.com/claude-gateway:<version> ..
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/claude-gateway:<version>
 
# 3. image_tagを入れて全体をapply
terraform apply

--platform=linux/amd64 --provenance=falseは省略できません。GCPのトラブルシューティング表にも、arm64ホストやOCIイメージインデックスでビルドしたイメージがCloud Runで弾かれる例(must support amd64/linux)が載っています。

setup.shとterraform/の使い分け

どちらも同じ最終形に着きますが、性格が違います。setup.shはawsやgcloudを順に叩く冪等なスクリプトで、既存リソースを検出してスキップするため再実行しても安全です。動作確認や検証環境の立ち上げ向きです。

terraform/は状態管理を伴います。変更をPRでレビューしたり、手作業の変更によるドリフトを検出したりするなら、こちらです。

setup.shにはTerraformにない手当てもあります。たとえばGCP版は、リリースバイナリのダウンロードとsha256の照合まで自動で行います。AWS版でOktaのOIDCクライアントシークレットとACM証明書が無いまま走らせると、ECS/ALBのデプロイを飛ばし、足りない入力を名指ししてcreate-secretコマンドを表示します。

設定を変えたとき、どう反映されるか

gateway.yaml.exampleは、任意キーをコメントアウトで含んだテンプレートです。gateway.yamlにコピーしてREPLACE_MEをすべて置き換えます。REPLACE_MEが残っていると、Terraformはplan時のpreconditionで止まります。

ここから先は、AWSとGCPで設定の渡し方が違います。

観点AWSGCP
設定の持ち方AWSイメージに焼き込みGCPSecret Manager(gateway-config)から/etc/claude/gateway.yamlへマウント
設定変更の反映AWS新しいタグで再ビルドしimage_tagを更新GCPgateway.yamlを変えてterraform apply
タグの扱いAWSECRのタグは不変(immutable)GCPimage_tagはClaude Codeのリリースバージョン

AWSのECRは不変タグなので、同じタグへの上書きpushはできません。setup.shは<version>-cfg<gateway.yamlのsha8>の形でタグを付け、設定変更のたびにタグが変わる仕組みです。手動で運用するなら、この「設定変更なら新タグ」を自分で守ります。

AWS版のDockerfileは、sslmode=verify-fullでRDSに接続するためのAWS CA証明書バンドルも焼き込みます。setup.shはバンドルがビルドコンテキストに無いときだけダウンロードします。AWSがCA証明書をローテーションしたら、ファイルを削除して新しいタグで再ビルドします。

設定ファイルにシークレットの値は入りません。認証情報はすべて${VAR}展開で起動時に解決されます。ただしバンドルの.gitignoreはgateway.yamlを除外しているため、Gitで管理したいなら自分で調整します。

AWSでoidc_client_secretを空のままにすると、シークレットのバージョンが無いためECSタスクがResourceInitializationErrorで起動しません。GCPでも同様にデプロイが失敗します。

Terraformのstateにはシークレットが入ります。共有で使うなら、リモートバックエンドを用意し(GCPバンドルのREADMEにGCSの例があります)、Gitには置きません。

Admin APIのwrite_keysをTerraformやCIから使う場合は、ツールごとにキーIDを分けます。支出上限の運用で触れた「自動化ごとに専用キー」という考え方が、そのまま使えます。

AWSとGCPで違うリソースと既定値

同じ「ゲートウェイをコードで再現する」でも、作るものと既定値は違います。

論点AWSGCP
コンピュートAWSECS FargateGCPCloud Run(最小インスタンス数の既定は1)
状態ストアAWSRDS for PostgreSQL(既定はdb.t4g.micro)GCPCloud SQL(既定はdb-g1-small)
upstream認証AWSECSタスクロールでBedrockを呼ぶGCPサービスアカウントにroles/aiplatform.user
入口AWS内部ALBのHTTPS(443)のみGCP既定は内部専用ingress
削除保護AWSRDSとALBでdeletion_protectionが既定trueGCPCloud SQLとCloud Runで既定true

AWSの内部ALBはip_address_type = "ipv4"に固定されています。デュアルスタックの内部ALBは公開レンジのAAAAレコードを出し、Claude Codeの/loginが行うプライベートネットワーク判定に弾かれるためです。applyのあとには、自前の内部ホスト名をRoute 53のプライベートホストゾーンでALBへaliasする作業が残ります。ALB自身の*.elb.amazonaws.com名には、ACM証明書を載せられません。

IAMロールの権限は、Bedrockのクロスリージョン推論プロファイルARN(us.anthropic.*)と、その裏の基盤モデルARNの両方が対象です。公式のトラブルシューティングでは、片方が欠けるとBedrockが403 AccessDeniedExceptionを返す原因の1つに挙がっています。バンドルのIAMポリシーは両方を含みます。

リージョンが米国商用4リージョン(us-east-1/2、us-west-1/2)以外だと、plan時のpreconditionで失敗します。他リージョンで使うなら、gateway.yamlのmodels:ブロックで推論プロファイルを固定し、ARNのプレフィックスを広げたうえでallow_non_us_region = trueにします。

AWSのsecretsmanager:GetSecretValueは、バンドルが作る3つのシークレット(gateway-postgres-url・gateway-jwt-secret・gateway-oidc-client-secret)のARNだけに絞られています。名前を変えれば次のapplyで追従します。

Cloud Runのinvoker設定は、ページとREADMEで言い方が違う

GCPでは、Cloud Runのinvoker IAMチェックを開くか無効にする必要があります。ゲートウェイ自身がOIDCを持ち、クライアントはGCPのトークンを持たないためです。

公式ページは、成果物の既定を「allUsersへのrun.invoker付与」と書き、手作業の--no-invoker-iam-checkとは逆だとしています。変数の既定値もそのとおりで、allow_unauthenticatedがtrue、invoker_iam_disabledがfalseです。

一方でバンドルのREADMEは、invoker_iam_disabled = trueを「Preferred」としています。allUsersの束縛が要らず、Domain Restricted Sharing(組織外へのIAM付与の制限)の下でも動くためです。allUsersのほうは、DRSの組織では拒否されます。

組織の制約取れる選択
制約なし取れる選択どちらでも動く。READMEはinvoker_iam_disabled = trueを推す
DRSあり取れる選択invoker_iam_disabled = trueにし、allow_unauthenticatedをfalseにする
requireInvokerIamで無効化が拒否される取れる選択allow_unauthenticated = trueに切り替える
両方が組織ポリシーで止まる取れる選択GKEトラックに回る(Terraformは対象外)

LBを前に置いてもinvokerチェックは外れません。READMEにも、LBはIAMチェックを迂回しないと書かれています。

接続数の積算とALBのタイムアウト

store.max_connections(既定5)は、レプリカ1台がPostgresに持つ接続数です。インスタンス数の上限にこの値を掛けた数が、DBの接続上限を超えないようにします。GCPバンドルのmax_instancesが8なのは、db-g1-smallの接続上限(約50)に収めるためです。ティアを上げる前にmax_instancesだけ上げると、スケールアウト時に接続エラーが起きます。設定項目の全体は設定リファレンスにあります。

AWS側では、ALBのアイドルタイムアウト(既定60秒)も効いてきます。BedrockまたはClaude Platform on AWSをupstreamにすると、extended thinkingで出力が止まっている間は何も流れません。無応答が60秒続けば、ALBが接続を切ります。Anthropic APIをupstreamにした場合は、API自身のpingが中継されるので起きません。

v2.1.229以降のゲートウェイは、無応答が約15秒続くとSSEのpingを送ります。それより古いバージョンでは、ALBのidle_timeout.timeout_secondsを3600にします。バンドルのmain.tfはidle_timeout = 3600を設定済みなので、バンドルを使う限り手を入れる必要はありません。自前で作るALBでは、この値を自分で入れます。

検証環境を片付けるときに詰まる点

terraform destroyは、そのままでは通りません。AWSもGCPも、削除保護が既定でオンだからです。

手順

destroyの前にやること

  1. 1

    削除保護を外す

    deletion_protection = falseにしてterraform applyを実行します。プロバイダーが見るのは設定ではなくstateの値なので、この中間のapplyが要ります。

  2. 2

    destroyを実行する

    AWSはこのままterraform destroyで消えます。GCPはVPCの手前で止まります。

  3. 3

    GCPはPSAピアリングを手で消す

    Private Services Accessのピアリングは、同じVPC上の他サービスと共有されうるため、deletion_policy = ABANDONで意図的に残されます。gcloud services vpc-peerings deleteで消してから、もう一度destroyします。

AWSには、もう1つ罠があります。シークレット3つは名前が固定で、既定の30日の復旧期間付きで消すと、その間は名前が予約されたままです。再applyが名前の衝突で失敗します。

deletion_protection = falseでdestroyすれば即時削除されます。保護を付けたまま消したときは、aws secretsmanager delete-secret --force-delete-without-recoveryで予約を解いてから再applyします。

まとめ

初回は、どちらのクラウドでも2パスのapplyと、片付け時の削除保護の解除を前提に段取りを組むことになります。AWSとGCPの両方を触るチームでは、設定変更の反映経路が逆になる点を、手順書の段階で分けておく選択肢があります。

この記事を共有:XはてブLinkedIn