コンテンツにスキップ

AWS

indx が AWS でどう動くか。3 つの Terraform スタック、手動起動のワークフローとその 4 つの操作の手順、そして認証の代わりに立つ境界。

英語版が原文です。

1 つの Fargate タスクがイメージを、Cloudflare のエッジだけを受け入れる ALB の背後で動かし、プロキシされた 1 つの Cloudflare レコードが公開ホスト名です。ここにあるものは すべて infra/aws/.github/workflows/deploy-aws-prod.yml で、別のデプロイと共有するものは ありません。

タスクにはデータベースもバケットもシークレットもありません。重みはイメージの中にあり、オフラインで 読まれます。Bedrock にはタスクロールの資格情報で、モデル変数が設定されたときだけ到達します。 そうでなければ LLM レーンは自分を利用不可と報告し、ロールは Bedrock の文を持ちません。

タスクはバイト列をインラインで受け取り、それ以外は受け取りません。INDX_URI_SCHEMES=none がすべての URI ソースを無効にするので、呼び出し元はタスクのディスク上のファイルも VPC 内の URL も名指しできません。第二の層として INDX_LOADER_FILE_ROOTS はサンプルのディレクトリに 閉じています(ADR-0042)。同じ理由で、このホストではプレイグラウンドの URI タブは無効です。 公開スモークは resolvable.schemes が空であること、file: URI が拒否されること、インラインの アップロードは依然として計画できることを検証し、just test::bdd::hardened は同じ構成で起動した サーバーに対して同じシナリオをローカルで実行します。ホストそのものに対しては、 just test::bdd::deployed https://<hostname>@deployed の受け入れシナリオをすべて実行し (デプロイのワークフローは公開スモークの後にこれを実行します)、 just frontend::e2e-deployed https://<hostname> がブラウザ経由で Web アプリの仕様を 実行します。スクリプトから呼ぶ場合の注意を一つ: Cloudflare は Python 既定の Python-urllib ユーザーエージェントをエラー 1010 で拒否します。 curlhttpx は通ります。

スタック 適用するのは 持つもの
infra/aws/terraform-prod-bootstrap 管理者が一度 ステートバケット、ロックテーブル、GitHub Actions が引き受ける 2 つの IAM ロール
infra/aws/terraform ワークフローの deploy VPC、ALB、ECR、ECS、IAM、ログ、アラーム
infra/aws/terraform-prod-dns ワークフローの cutover 唯一の Cloudflare レコード

3 つのステートはすべて、ブートストラップのスタックがこのプロジェクトのためだけに作るバケットと ロックテーブルの中、indx-everything-prod/aws/ の下にあります。bootstrap.tfstateterraform.tfstatedns.tfstate です。アプリケーション自体はバケットを持ちません。 just infra::terraform::validate は資格情報なしで、すべてのスタックをフォーマット、バックエンドなしで 初期化、検証します。ワークフローのどの実行も最初のジョブとして同じことをします。

境界はコアスタックの 2 つのルールです。ALB のセキュリティグループは Cloudflare の公開 IPv4 レンジ だけを受け入れ、HTTPS リスナーは Host ヘッダーが証明書のホスト名のときだけ転送するので、別の Cloudflare ゾーンがこの ALB を自分のものとして配信することはできません。それは境界であって認証では ありません。API はその背後で認証なしです。ホスト名の Cloudflare Access が、切り替えの前に運用者が 有効にする門です。

アカウント 697441478678 の管理者の資格情報と aws CLI が要ります。このスタックが使う識別子 (リージョン、アカウント、プロジェクト名、バケット、ロックテーブル、ステートのプレフィックス)は どれも 1 つの値に固定する検証つきの変数なので、書き込むものはありません。

aws login でサインインしたプロファイルは login_session を持ちキーを持ちません。Terraform の AWS プロバイダーはそのセッションを読まず、資格情報の出どころがないと報告して EC2 メタデータ エンドポイントへ落ちます。代わりにセッションを環境変数として渡します。

Terminal window
eval "$(aws configure export-credentials --profile <管理者プロファイル> --format env)"

ブートストラップ自身のステートが入るバケットは、このスタックが作るものの 1 つです。そのため最初の apply はローカルステートで走り、直後に移行します。スタックは S3 バックエンドを宣言していて、 init -backend=false ではバックエンドが未設定のままになるので、オーバーライドファイルでその一度の apply だけローカルバックエンドに差し替え、移行の前に削除します。

Terminal window
cd infra/aws/terraform-prod-bootstrap
printf 'terraform {\n backend "local" {}\n}\n' > local_override.tf
terraform init
terraform apply
rm local_override.tf
terraform init -migrate-state -backend-config=backend.hcl.example
terraform output

移行はローカルステートをバケットへコピーするか一度だけ尋ねます。yes と答え、残った terraform.tfstateterraform.tfstate.backup を削除します。

出力の github_deploy_role_arngithub_plan_role_arn の 2 つを控えます。

ロールはリポジトリの immutable GitHub OIDC subject repo:INDXDev@209891251/indx-everything@1301159823:environment:<name> だけを信頼するので、 それらの Environment の外のワークフローはロールを引き受けられず、リポジトリ名を変更しても信頼は 移りません。プランロールは読み取り専用とステートのロックです。デプロイロールのポリシーは infra/aws/terraform/ci-prod-deploy-policy.json で、このプロジェクトのスタックが名指しするリソースに 絞られ、別のプロジェクトのタグを持つものの変更は拒否します。

デプロイポリシーはこのスタックの terraform apply のたびに読み直されるので、そのファイルの変更 (Bedrock の予算が 1 つ加えました)は、管理者が同じコマンドをもう一度走らせることです。

  1. Environment を 2 つ作ります。production-plan はレビュアーなしでデプロイブランチのルールは main のみ。production は必須レビュアーつきで同じブランチのルール。そのレビュアーが すべての変更操作の承認です。
  2. リポジトリの変数とシークレットを設定します。
名前 種類
PROD_AWS_ROLE_ARN 変数 デプロイロールの ARN
PROD_AWS_PLAN_ROLE_ARN 変数 プランロールの ARN
PROD_DOMAIN_NAME 変数 indx.jp 配下の公開ホスト名
PROD_BEDROCK_ENABLED 変数、任意 true で Bedrock のレーンを有効化。未設定は無効
PROD_BEDROCK_LLM_MODEL 変数、任意 global.anthropic.claude-sonnet-4-6
PROD_BEDROCK_EMBED_MODEL 変数、任意 cohere.embed-multilingual-v3
PROD_BEDROCK_EMBED_DIMENSION 変数、モデル指定時は必須 モデルのベクトル次元数。cohere.embed-multilingual-v3 なら 1024
PROD_BEDROCK_MONTHLY_BUDGET_USD 変数、任意 呼び出しが拒否される月間 Bedrock 支出。既定は 50
PROD_CLOUDFLARE_API_TOKEN シークレット indx.jp ゾーンの DNS を編集できるトークン
  1. ホスト名に Cloudflare Access を有効にします。

リージョン、アカウント、プロジェクト名、ゾーン ID、バケット、ロックテーブル、ステートキーには ワークフローに既定値があり、scripts/aws/prod-preflight.sh はそのどれかがロールを発行したときの 値からずれていると実行を拒否します。

API は公開されており、Bedrock を背後に持つレーンへの呼び出しはすべてアカウントに課金されるので、 それらのレーンは PROD_BEDROCK_ENABLEDtrue でない限り無効です (ADR-0040)。 無効とは、タスクがモデル変数を持たないことです。llmner-llmenrich-llm は宣伝されず、 それを名指しするリクエストは 422 で拒否され、generic-vlmhosted-text は自分を利用不可と 報告し、タスクロールは Bedrock の権限を持ちません。スイッチが無効のままモデルを指定すると、 無視されるのではなく Terraform のプランが失敗します。

有効にすると、スタックは Bedrock に対する月間の AWS Budgets コスト予算 (PROD_BEDROCK_MONTHLY_BUDGET_USD)を加えます。予測 80 パーセントでアラームトピックに通知し、 実績 100 パーセントでタスクロールに Deny bedrock:* ポリシーを付けます。AWS は予算データを 1 日に数回しか更新しないので、拒否は閾値の数時間後に届きます。暴走した請求を止めるのであって、 リクエストを止めるのではありません。拒否は運用者がアクションをリセットするまで残ります。

Terminal window
aws budgets execute-budget-action --account-id 697441478678 \
--budget-name indx-everything-prod-bedrock --action-id <id> --execution-type RESET_BUDGET_ACTION

どちらの向きでもスイッチの切り替えは plandeploy です。タスク定義が変わるからです。 公開スモークは各 Bedrock レーンが、そのモデルが設定されているときに限って存在することを確かめます。 ホスト名への Cloudflare のレート制限ルールが 2 つ目のブレーキで、運用者の設定です。

すべての操作は main 上の Deploy to AWS (production) を手動で起動します。push はデプロイしません。

action Environment 起きること
plan production-plan コアスタックの読み取り専用プラン。その要約が、デプロイが名指しすべきアーティファクトです。
deploy production タグがなければイメージを ECR へビルドし、ロールバック地点を捕捉し、再プランして承認済みの実行と ECR ブートストラップ以上に差があれば拒否し、適用し、ターゲットの健全化を待ち、切り替え前スモークを走らせ、ホスト名がすでに解決するなら公開スモーク、@deployed のシナリオ、Web アプリの仕様も走らせます。DNS は変えません。
cutover production 現在のレコードをスナップショットし、ターゲットの健全性を再確認し、DNS スタックをプランし、ホスト名のプロキシされた 1 レコード以外を拒否し、再スナップショットし、適用し、公開スモークを走らせます。
rollback production デプロイ実行が捕捉したタスク定義を復元し、Terraform を同じイメージに合わせ、600 秒以内に健全でなければなりません。

confirm はすべての変更操作でホスト名と一致しなければなりません。deploy には plan_run_idrollback には rollback_run_id が要ります。どの実行も静的検証のジョブから始まり、続いて action が名指しする 1 つのジョブが走ります。各ジョブがすることを順に示します。

production-plan で走るのでレビュアーはいませんが、main からだけ、読み取り専用ロールでだけ走ります。

  1. プランロールを引き受け、prod-preflight.sh plan を走らせます。呼び出し元はそのアカウントの まさにそのロールでなければならず、ステートバケットとロックテーブルが存在しなければなりません。
  2. コアスタックを S3 バックエンドに対して初期化し、イメージ参照 <ecr>/indx-everything-prod-indx-everything:sha-<commit> でプランします。
  3. core-plan-summary.json(リソースのアドレスとアクション、それにプランしたコミット、ref、 リポジトリ)を aws-prod-plan-summary-<run id> としてアップロードします。

実行 ID を控えます。それがデプロイの名指しすべき plan_run_id で、そのコミットに対してだけ有効です。

confirm=<ホスト名>plan_run_id を渡して起動します。レビュアーが production Environment を 承認すると、次が走ります。

  1. ガード。ref は mainconfirmPROD_DOMAIN_NAME と一致、plan_run_id は数値。 その実行から承認済みのプラン要約をダウンロードします。
  2. デプロイロールを引き受け、prod-preflight.sh deploy を走らせます。
  3. aws_ecr_repository.indx_everything だけを適用し、push の前にリポジトリを存在させます。
  4. ロールバック地点を捕捉します。サービスの現在のタスク定義を rollback.json に書き、 aws-prod-rollback-<run id> として 30 日保持でアップロードします。初回のデプロイにはまだ クラスターがなく、このステップは戻る先がないことを記録します。
  5. ランナーのディスクを空け、ECR と GHCR にログインし、重みのイメージ ghcr.io/indxdev/indx-everything-weights:w-<key> を確保します。ないときだけ公開します。
  6. Dockerfilelinux/amd64 でビルドし、sha-<commit> を ECR へ push します。そのタグが すでにあればスキップします。
  7. コアスタックをもう一度プランし、承認済みの要約と比べます。出どころはこのリポジトリ、main、 このコミットでなければならず、変更の集合は ECR リポジトリを除いて同一でなければなりません。 それ以外の差は、何も適用する前に実行を失敗させます。
  8. そのプランを適用します。初回の apply は ACM 証明書も発行し、その検証レコードを Cloudflare に 書きます。Cloudflare のトークンが切り替えのときだけでなくここでも要るのはそのためです。
  9. ECS サービスの安定と、ターゲットグループがタスクを in service と報告するのを待ちます。
  10. prod-smoke.sh precutover を走らせます。ホスト名がすでに解決するなら public も走らせます。 DNS には触れません。

confirm=<ホスト名> を渡して起動します。レビュアーが承認すると、次が走ります。

  1. deploy と同じガード。プランの実行は不要です。
  2. prod-preflight.sh cutover を走らせ、現在の Cloudflare レコードを cloudflare-before-cutover.json にスナップショットし、aws-prod-cutover-snapshot-<run id> として 90 日保持でアップロードします。スナップショットの SHA-256 と既存レコードの ID が DNS スタックに渡ります。
  3. コアと DNS の両方のステートを初期化し、稼働中のターゲットに対して切り替え前スモークを もう一度走らせます。
  4. DNS スタックをプランします。コアのステートから ALB 名を読み、既存レコードがあれば インポートし、prod-dns-plan-guard.sh はこのホスト名のこの ALB を指すプロキシされた 1 レコード 以外を拒否します。リソース自体も confirm とスナップショットのハッシュに対する事前条件と prevent_destroy を持ちます。
  5. apply の直前にレコードをもう一度スナップショットし、ステップ 2 から変わっていれば失敗します。
  6. 適用し、Cloudflare 経由で prod-smoke.sh public を走らせます。

confirm=<ホスト名> と、復元するスナップショットを持つデプロイ実行の rollback_run_id を渡して 起動します。レビュアーが承認すると、次が走ります。

  1. deploy と同じガード。プランの実行の代わりに rollback_run_id。その実行の rollback.json をダウンロードします。
  2. prod-preflight.sh rollback を走らせ、Terraform の外でサービスに以前のタスク定義を そのまま復元します。
  3. 以前のイメージでコアスタックをプランして適用し、ステートを動いているものに合わせ、サービスの 安定を待ちます。
  4. 切り替え前スモークを走らせ、ホスト名が解決するなら公開スモークも走らせます。

ガード以降のすべてのステップは 1 つの予算を共有します。実行開始から 600 秒以内にサービスが 健全にならなければロールバックは失敗です。DNS には触れません。

ランナーは ALB に到達できません。セキュリティグループが Cloudflare だけを受け入れるからです。 そのため scripts/aws/prod-smoke.sh precutover は AWS からターゲットの健全性を読み、publicjust infra::image::verify と同じ機能のアサーションでホスト名を Cloudflare 経由で叩きます。 どちらも実行開始以降のタスクのロググループにトレースバックがないか走査します。 scripts/aws/test-prod-guards.sh はこれらのスクリプトのすべての拒否を密閉環境で試します。

  • デプロイは切り替えではありません。 Bedrock スイッチの切り替えも、その他のタスク定義の変更も plandeploy です。公開レコードが動くのは cutover だけで、それは通常一度きりです。
  • プランとデプロイは同じコミットでなければなりません。 2 つの間に main へマージがあると 出どころの検査が失敗するので、plan をもう一度走らせます。
  • 初回のデプロイは特別です。 ロールバックのスナップショットはなく、コアの apply が ACM の 検証レコードを Cloudflare に作ります。
  • 予算のブレーキは遅いです。 Bedrock が有効のとき、拒否は閾値の数時間後に届き、運用者が アクションをリセットするまで残ります。
  • デプロイポリシーの編集はブートストラップの再適用です。 ワークフローではなく管理者がします。