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 で拒否します。
curl と httpx は通ります。
| スタック | 適用するのは | 持つもの |
|---|---|---|
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.tfstate、
terraform.tfstate、dns.tfstate です。アプリケーション自体はバケットを持ちません。
just infra::terraform::validate は資格情報なしで、すべてのスタックをフォーマット、バックエンドなしで
初期化、検証します。ワークフローのどの実行も最初のジョブとして同じことをします。
境界はコアスタックの 2 つのルールです。ALB のセキュリティグループは Cloudflare の公開 IPv4 レンジ
だけを受け入れ、HTTPS リスナーは Host ヘッダーが証明書のホスト名のときだけ転送するので、別の
Cloudflare ゾーンがこの ALB を自分のものとして配信することはできません。それは境界であって認証では
ありません。API はその背後で認証なしです。ホスト名の Cloudflare Access が、切り替えの前に運用者が
有効にする門です。
ブートストラップ、一度だけ
Section titled “ブートストラップ、一度だけ”アカウント 697441478678 の管理者の資格情報と aws CLI が要ります。このスタックが使う識別子
(リージョン、アカウント、プロジェクト名、バケット、ロックテーブル、ステートのプレフィックス)は
どれも 1 つの値に固定する検証つきの変数なので、書き込むものはありません。
aws login でサインインしたプロファイルは login_session を持ちキーを持ちません。Terraform の AWS
プロバイダーはそのセッションを読まず、資格情報の出どころがないと報告して EC2 メタデータ
エンドポイントへ落ちます。代わりにセッションを環境変数として渡します。
eval "$(aws configure export-credentials --profile <管理者プロファイル> --format env)"ブートストラップ自身のステートが入るバケットは、このスタックが作るものの 1 つです。そのため最初の
apply はローカルステートで走り、直後に移行します。スタックは S3 バックエンドを宣言していて、
init -backend=false ではバックエンドが未設定のままになるので、オーバーライドファイルでその一度の
apply だけローカルバックエンドに差し替え、移行の前に削除します。
cd infra/aws/terraform-prod-bootstrapprintf 'terraform {\n backend "local" {}\n}\n' > local_override.tfterraform initterraform applyrm local_override.tfterraform init -migrate-state -backend-config=backend.hcl.exampleterraform output移行はローカルステートをバケットへコピーするか一度だけ尋ねます。yes と答え、残った
terraform.tfstate と terraform.tfstate.backup を削除します。
出力の github_deploy_role_arn と github_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 つ加えました)は、管理者が同じコマンドをもう一度走らせることです。
GitHub、一度だけ
Section titled “GitHub、一度だけ”- Environment を 2 つ作ります。
production-planはレビュアーなしでデプロイブランチのルールはmainのみ。productionは必須レビュアーつきで同じブランチのルール。そのレビュアーが すべての変更操作の承認です。 - リポジトリの変数とシークレットを設定します。
| 名前 | 種類 | 値 |
|---|---|---|
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 を編集できるトークン |
- ホスト名に Cloudflare Access を有効にします。
リージョン、アカウント、プロジェクト名、ゾーン ID、バケット、ロックテーブル、ステートキーには
ワークフローに既定値があり、scripts/aws/prod-preflight.sh はそのどれかがロールを発行したときの
値からずれていると実行を拒否します。
Bedrock スイッチ
Section titled “Bedrock スイッチ”API は公開されており、Bedrock を背後に持つレーンへの呼び出しはすべてアカウントに課金されるので、
それらのレーンは PROD_BEDROCK_ENABLED が true でない限り無効です
(ADR-0040)。
無効とは、タスクがモデル変数を持たないことです。llm、ner-llm、enrich-llm は宣伝されず、
それを名指しするリクエストは 422 で拒否され、generic-vlm と hosted-text は自分を利用不可と
報告し、タスクロールは Bedrock の権限を持ちません。スイッチが無効のままモデルを指定すると、
無視されるのではなく Terraform のプランが失敗します。
有効にすると、スタックは Bedrock に対する月間の AWS Budgets コスト予算
(PROD_BEDROCK_MONTHLY_BUDGET_USD)を加えます。予測 80 パーセントでアラームトピックに通知し、
実績 100 パーセントでタスクロールに Deny bedrock:* ポリシーを付けます。AWS は予算データを
1 日に数回しか更新しないので、拒否は閾値の数時間後に届きます。暴走した請求を止めるのであって、
リクエストを止めるのではありません。拒否は運用者がアクションをリセットするまで残ります。
aws budgets execute-budget-action --account-id 697441478678 \ --budget-name indx-everything-prod-bedrock --action-id <id> --execution-type RESET_BUDGET_ACTIONどちらの向きでもスイッチの切り替えは plan と deploy です。タスク定義が変わるからです。
公開スモークは各 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_id、
rollback には rollback_run_id が要ります。どの実行も静的検証のジョブから始まり、続いて
action が名指しする 1 つのジョブが走ります。各ジョブがすることを順に示します。
production-plan で走るのでレビュアーはいませんが、main からだけ、読み取り専用ロールでだけ走ります。
- プランロールを引き受け、
prod-preflight.sh planを走らせます。呼び出し元はそのアカウントの まさにそのロールでなければならず、ステートバケットとロックテーブルが存在しなければなりません。 - コアスタックを S3 バックエンドに対して初期化し、イメージ参照
<ecr>/indx-everything-prod-indx-everything:sha-<commit>でプランします。 core-plan-summary.json(リソースのアドレスとアクション、それにプランしたコミット、ref、 リポジトリ)をaws-prod-plan-summary-<run id>としてアップロードします。
実行 ID を控えます。それがデプロイの名指しすべき plan_run_id で、そのコミットに対してだけ有効です。
deploy
Section titled “deploy”confirm=<ホスト名> と plan_run_id を渡して起動します。レビュアーが production Environment を
承認すると、次が走ります。
- ガード。ref は
main、confirmはPROD_DOMAIN_NAMEと一致、plan_run_idは数値。 その実行から承認済みのプラン要約をダウンロードします。 - デプロイロールを引き受け、
prod-preflight.sh deployを走らせます。 aws_ecr_repository.indx_everythingだけを適用し、push の前にリポジトリを存在させます。- ロールバック地点を捕捉します。サービスの現在のタスク定義を
rollback.jsonに書き、aws-prod-rollback-<run id>として 30 日保持でアップロードします。初回のデプロイにはまだ クラスターがなく、このステップは戻る先がないことを記録します。 - ランナーのディスクを空け、ECR と GHCR にログインし、重みのイメージ
ghcr.io/indxdev/indx-everything-weights:w-<key>を確保します。ないときだけ公開します。 Dockerfileをlinux/amd64でビルドし、sha-<commit>を ECR へ push します。そのタグが すでにあればスキップします。- コアスタックをもう一度プランし、承認済みの要約と比べます。出どころはこのリポジトリ、
main、 このコミットでなければならず、変更の集合は ECR リポジトリを除いて同一でなければなりません。 それ以外の差は、何も適用する前に実行を失敗させます。 - そのプランを適用します。初回の apply は ACM 証明書も発行し、その検証レコードを Cloudflare に 書きます。Cloudflare のトークンが切り替えのときだけでなくここでも要るのはそのためです。
- ECS サービスの安定と、ターゲットグループがタスクを in service と報告するのを待ちます。
prod-smoke.sh precutoverを走らせます。ホスト名がすでに解決するならpublicも走らせます。 DNS には触れません。
cutover
Section titled “cutover”confirm=<ホスト名> を渡して起動します。レビュアーが承認すると、次が走ります。
deployと同じガード。プランの実行は不要です。prod-preflight.sh cutoverを走らせ、現在の Cloudflare レコードをcloudflare-before-cutover.jsonにスナップショットし、aws-prod-cutover-snapshot-<run id>として 90 日保持でアップロードします。スナップショットの SHA-256 と既存レコードの ID が DNS スタックに渡ります。- コアと DNS の両方のステートを初期化し、稼働中のターゲットに対して切り替え前スモークを もう一度走らせます。
- DNS スタックをプランします。コアのステートから ALB 名を読み、既存レコードがあれば
インポートし、
prod-dns-plan-guard.shはこのホスト名のこの ALB を指すプロキシされた 1 レコード 以外を拒否します。リソース自体もconfirmとスナップショットのハッシュに対する事前条件とprevent_destroyを持ちます。 - apply の直前にレコードをもう一度スナップショットし、ステップ 2 から変わっていれば失敗します。
- 適用し、Cloudflare 経由で
prod-smoke.sh publicを走らせます。
rollback
Section titled “rollback”confirm=<ホスト名> と、復元するスナップショットを持つデプロイ実行の rollback_run_id を渡して
起動します。レビュアーが承認すると、次が走ります。
deployと同じガード。プランの実行の代わりにrollback_run_id。その実行のrollback.jsonをダウンロードします。prod-preflight.sh rollbackを走らせ、Terraform の外でサービスに以前のタスク定義を そのまま復元します。- 以前のイメージでコアスタックをプランして適用し、ステートを動いているものに合わせ、サービスの 安定を待ちます。
- 切り替え前スモークを走らせ、ホスト名が解決するなら公開スモークも走らせます。
ガード以降のすべてのステップは 1 つの予算を共有します。実行開始から 600 秒以内にサービスが 健全にならなければロールバックは失敗です。DNS には触れません。
ランナーは ALB に到達できません。セキュリティグループが Cloudflare だけを受け入れるからです。
そのため scripts/aws/prod-smoke.sh precutover は AWS からターゲットの健全性を読み、public は
just infra::image::verify と同じ機能のアサーションでホスト名を Cloudflare 経由で叩きます。
どちらも実行開始以降のタスクのロググループにトレースバックがないか走査します。
scripts/aws/test-prod-guards.sh はこれらのスクリプトのすべての拒否を密閉環境で試します。
- デプロイは切り替えではありません。 Bedrock スイッチの切り替えも、その他のタスク定義の変更も
planとdeployです。公開レコードが動くのはcutoverだけで、それは通常一度きりです。 - プランとデプロイは同じコミットでなければなりません。 2 つの間に
mainへマージがあると 出どころの検査が失敗するので、planをもう一度走らせます。 - 初回のデプロイは特別です。 ロールバックのスナップショットはなく、コアの apply が ACM の 検証レコードを Cloudflare に作ります。
- 予算のブレーキは遅いです。 Bedrock が有効のとき、拒否は閾値の数時間後に届き、運用者が アクションをリセットするまで残ります。
- デプロイポリシーの編集はブートストラップの再適用です。 ワークフローではなく管理者がします。