このページでは、AWS上にJinba Trailのコントロールプレーンを立ち上げる実際のInfrastructure-as-Codeである deploy/terraform/ を説明します。対象読者は、これを適用するSREまたはプラットフォームエンジニアであり、Flow作成者ではありません。flow がどのようにデプロイされ、ロールバックされるか(Pack digestをfrontに公開すること)を探している場合、それはここでいう「deploy」とは別の、無関係な意味です。代わりに デプロイとロールバック を参照してください。
ここでは、deploy/terraform/*.tfdeploy/docker/*docs/11-production-deployment-aws.md に照らして検証された、現時点のモジュールを説明します。Terraformモジュールは terraform validate に合う形で、レビュー可能に作られています。実際に適用するには、本物のAWS認証情報、Route53ゾーンを持つ登録済みドメイン、そしてECRにすでにpush済みのコンテナイメージ(service_imagebrowser_imageweb_image)が必要です。以下のインフラが存在した後に、それらのイメージが実際にどのようにビルドされ、ロールアウトされるかは CI/CDパイプライン を参照してください。

アーキテクチャ概観

このデプロイメントは、4つのECS Fargateサービス、4つのサブネット階層、1つのRDS Postgresインスタンス、1つのRedis replication groupで構成されます。

4つのECS Fargateサービス

apiwebworkerbrowser(3つではありません)

4つのサブネット階層

publicappdatabrowser

1つのRDS Postgres

ラッパーストアとエンジンのruns/journal/plane storeの両方を支える

1つのRedis replication group

後述するマルチレプリカのギャップを埋める
ここにあるものは、flowがどう実行されるか を変えません。このモジュールは、エンジン既存の WrapperStore / Kms / BrowserCapability / Store インターフェイスの背後にあるバックエンドを選び、配線するだけです。

Network — AZごとの4つのサブネット階層

network.tf は、VPC(var.vpc_cidr、デフォルト 10.42.0.0/16)を、cidrsubnet(vpc_cidr, 4, i) によってAZごとに4つの連続した /20 階層へ切り分けます。AZ数は var.az_count(2または3)です。
階層ホストEgressIngress
publicALB、NAT gatewaysInternet GatewayInternet (443/80)
appjinbatrail-apijinbatrail-webworker ECS tasksNAT(VPC内のRDS/Redis/EFS、Secrets Manager、KMS、SQS、Anthropic API)ALBのみ (8080, 3000)
dataRDS、ElastiCache、EFS mount targetsなしapp security groupのみ (5432, 6379, 2049)
browserchromium ECS taskNAT、public 443/80/53へのallow-listapp security groupのみ、CDP :9222
要になるのは browser tier です。これは、CDP経由で信頼できない記録済みflowを実行するため、オープンインターネットへ意図的に到達する唯一のコンポーネントです。security groupができるのは allow だけなので、実際のSSRF封じ込めは専用のNetwork ACL(aws_network_acl.browser)で行います。このNACLは、広いインターネット許可より小さいルール番号で、すべてのRFC1918範囲とlink-local IMDSブロック(169.254.0.0/16)を deny します。そのため、記録済みflowが 169.254.169.254(instance metadata)や内部サービスCIDRへ移動しようとしても、SGのegressルールが何を許していても物理的に到達できません。狭く高い番号のNACL allowは、app サブネットからのCDP戻り経路のephemeral ports(stateless NACLでは明示的に必要)と、VPC内ICMP(Path-MTU Discovery。CDP WebSocketの大きなフレームがcross-AZ hopで黙ってblack-holeしないようにするため)にだけ存在します。
single_nat_gateway は、共有NAT 1つ(安価で、staging.tfvars はこれを true に設定)とAZごとに1つ(false、本番形状のデフォルト)のトレードオフです。NAT gatewayのAZに問題が起きたときの、コスト対blast radiusの選択です。

Data tier

RDS Postgres、Multi-AZ、RDS管理のmaster password

1つの aws_db_instance(Postgres 16、gp3)が、1つの接続文字列の背後で 両方 のストアを支えます。ラッパーストア(JT_DATABASE_URL ?? DATABASE_URL — orgs、API keys、Automations)と、エンジンのruns/journal/plane store(BROWSERFLOW_DATABASE_URL ?? DATABASE_URL)です。どちらも同じ DATABASE_URL に解決されるため、1つのRDSエンドポイントで3つすべてを満たせます。
  • manage_master_user_password = true — master credentialはRDS自身が所有し、AWSが約7日ごとに自動ローテーションするSecrets Manager secretに入ります。Secrets Managerに DATABASE_URL secretはありません。理由は後述の entrypointが起動時にDATABASE_URLを組み立てる理由 を参照してください。
  • multi_az = var.environment == "production" — Multi-AZは本番のみです。staging/devはフットプリントとコストを最小にするためsingle-AZで動きます。
  • Storageとmaster-user secretはどちらも、このモジュール自身のKMS CMK(aws_kms_key.envelope — 後述の KMS CMK を参照)で暗号化されます。
  • backup_retention_period = 14deletion_protectionperformance_insights_enabledenvironment == "production" によって有効化されます(Performance Insightsはburstableな db.t4g.micro/t3 クラスではサポートされておらず、staging.tfvars が使うのはまさにそれです)。
  • parameter groupレベルの rds.force_ssl = 1 により、転送中のTLSが必須になります。組み立てられる DATABASE_URLsslmode=no-verify を持ちます(TLSは有効、ただしRDSのCA chainは検証しません。require/verify-full ではなく no-verify にする理由はentrypointのセクションを参照してください)。

ElastiCache Redis

1つの aws_elasticache_replication_group(Redis 7.1、TLS + 同じCMKによるat-rest encryption、automatic failover付きMulti-AZ)が、同じ JT_REDIS_URL から読み取られる3つの用途を同時に支えます。
  • 分散rate limiter(createRedisRateLimiter)。これにより /v1/run のquotaはプロセス単位ではなくfleet全体で強制されます。
  • live-view/authoringのpub-sub relay。あるレプリカで生成されたCDP frameを、別のレプリカに接続しているviewerへfan outできます。
  • 後述のEventBridge tickと並ぶscheduler-firing coordination。

EFS — content-addressed registry

aws_efs_file_system.registry は、すべての api/worker taskから /data/registryBROWSERFLOW_DATA_DIR)にmountされます。access pointはscopedな /registry rootとPOSIX identity uid=gid=1000 に固定されており、taskはscoped pathをmountし、filesystem rootはmountしません。Packとflow digestはcontent-addressedで、到着時に検証されるため、全taskにまたがる同時readerは構造上安全です。publishはまれで、冪等です。

Compute — 4つのECS Fargateサービス

compute.tf は1つのcluster上で 4つ のサービスを実行します。execution role(image pull、logs、secret injection)は共有し、task roleは分割します(最小権限、階層ごと)。
サービスイメージポート役割ALB配下か
jinbatrail-apivar.service_image8080Honoコントロールプレーン: keys、Automations、/v1/run、live-view/authoring、MCP。JT_RUN_DISPATCH=sync のときはP1/P2 in-process executorも兼ねます。はい — api.*
jinbatrail-webvar.web_image3000Next.js SSR run inspector + canvas⇄YAML editor。APIへのsame-origin proxy。secrets、EFS、DBはありません。はい — app.*(本番のみ、その前段にCloudFront)
workervar.service_image、commandを server-worker.ts にoverrideSQS run-dispatch queueをpollし、APIのin-process pathと 同じ plane.rider.run を呼び出します。ALBはありません。queue depthでautoscaleします。いいえ
chromium (browser tier)var.browser_image9222 (CDP)self-hosted Chromium。api/worker からCDP経由で到達します。分離された browser subnet/SGに置かれます。Cloud Map経由で browser.<name>.internal:9222 として発見できます。いいえ — internalのみ
apiworker は同じimageと同じ common_environment/common_secrets 配線を共有します。worker の違いは、container command["bun","run","packages/service/src/server-worker.ts"])とload balancerを持たないことだけです。chromium taskのIAM role(browser_task)は意図的に ほぼ空 です。信頼できない記録済みflowを実行するため、container escapeが完全に成功してもAWS APIは呼び出せません。api/worker の共有task roleは、1つのCMKに対する kms:Encrypt/Decrypt/GenerateDataKey/DescribeKey、dispatch queue + DLQに対する sqs:*、そしてこのstack自身のsecretに対する secretsmanager:GetSecretValue だけに絞られています。
browser tierのスケーリング軸はCPUではなくメモリです(cpu = 1024memory = 4096 — hard-codedで、変数ではありません)。imageの CMDchromium.launchServer によってChromiumを直接起動し、--no-sandbox --disable-dev-shm-usage --disable-extensions を付けます。Playwright base imageの $PATH には素の chromium binaryは存在しないため、それをexecしようとするECS command overrideは起動に失敗します。
api deploymentはtaskを1つずつrollし(deployment_minimum_healthy_percent = 100maximum_percent = 200)、deployまたはscale-inで古いtaskがkillされる前に、in-flightな同期Runをdrainするため120秒(deregistration_delay)を与えます。

Edge — ALB (api.*) + CloudFront (app.*)

1つのALBが、単一のHTTPS (443) listener上のhost-based listener ruleで両方のhostnameをfrontします。HTTP (80) はそこへredirectされます。
  • app.<domain>web target group(priority 10)— Next.js SSR app。
  • api.<domain>api target group(priority 20)— ただし意図されたpublic surfaceだけです: /v1/*/api/*/health/ready/internal/tick。priority 30の2つ目のruleが、hosted MCP endpoint用に /mcp/mcp/* を追加します。これは 別の ruleです。ALBの path_pattern conditionは最大5つの値までで、最初のruleがすでにその上限に達しているためです。
  • api.<domain> 上のその他すべて → 固定の 403 {"error":"forbidden"} response。 これはlistenerの default_action です。composed engineのコントロールプレーン(/runs/flows/deploys/secrets/schedules/registry)は、その層では設計上unauthenticatedだからです。wrapper自身のapp-level gateはdefense in depthであって、唯一の防衛線ではありません。ALBでdeny-by-defaultにすることで、app-layer gateが将来regressしてもギャップを閉じられます。
このモジュールは、2つの不変条件を明示的にvalidateまたはdocumentしています。どちらも、live-view/authoring sessionが本当にstatefulで、単一taskが所有するresourceだからです(CDP pageは移動できません)。
  • alb_idle_timeout >= 300variables.tf のhard validationです。serviceはSSE/WS connectionを255秒(server.ts Bun idleTimeout)開いたままにするため、ALB timeoutがそれより短いとlive-view streamがsession途中で切れます。
  • Stickiness onlb_cookie、3600秒)を api target groupで有効にします。これにより、特定のauthoring/live-view sessionは、そのbrowser pageを実際に所有しているレプリカへ着地し続けます。
regional WAF(AWSManagedRulesCommonRuleSet + 2000-req/5-min-per-IPのrate-based rule)がALBの前段に置かれます。これは粗い外側のguardであり、本当のquotaではありません。本当のquotaは上記のper-org Redis limiterです。 app.<domain> は本番でのみCloudFrontを通りますlocal.enable_cdn = var.environment == "production")。staging/devでは、app.* はALBへ直接aliasされ、ALBが同じ jinbatrail-web ECS serviceへhost-routeします。これにより、CDNを必要としない環境で、CloudFrontの約40分のprovisioning timeと空bucketのstatic pathを避けられます。CloudFrontが有効な場合は、Origin Access Controlによりprivate S3 bucketからbuild済みNext.js static assetsをserveします(CloudFrontだけがbucket readerです)。その前段にはCloudFront専用scopeのWAFがあります。AWSの要件により、これはdeploymentのhome regionに関係なく us-east-1 に置く必要があるため、aws.us_east_1 provider aliasを使います。

KMS CMK — secret envelope + at-rest key

1つのcustomer-managed key(aws_kms_key.envelope、年次auto-rotation)が二重の役割を持ちます。
  1. Secret envelope master key。 var.use_kms_envelope = true(デフォルト)のとき、serviceは JINBA_KMS_KEY_ARN を設定して動きます。これは JINBA_MASTER_KEY bridgeより優先されます。master keyはKMSから出ず、すべてのsecret unwrapは独立したCloudTrail監査対象の kms:Decrypt callになります。
  2. At-rest encryption。RDS、Redis、EFS、SQS queues、S3 web bucket、CloudWatch log groupを暗号化します。監査されるkeyは1つなので、rotationはserviceごとのre-encrypt sweepではなく、単一のCMK operationになります。
key policyはaccount rootにfull adminを付与し、AWS service principals(RDS/ElastiCache/EFS/SQS/Secrets Manager)には kms:ViaService でscopeされたencrypt/decryptを付与します。task-role accessは別途、compute.tf 自身のIAM policyで付与されます。これは、CMKとそれを使うroleの間の循環依存を避けるためです。
use_kms_envelope = falsestaging.tfvars が実行する設定)は、JINBA_MASTER_KEY bridgeへfallbackします。これはSecrets Manager内にある、本物の生成済み64-hex keyで、すべてのtaskで同一です。restartをまたいで残り、レプリカ間で共有されますが、task environmentを通過します。bridgeとしては許容できますが、本番で推奨されるpathではありません。このモジュールはKMS CMKと JINBA_MASTER_KEY secretの両方を無条件で生成するため、この変数を切り替えるのはconfig changeであり、新しいresourceの再applyではありません。

SQS run-dispatch — FIFOではなくstandard queue

queue.tf はFIFO queueではなく、standard SQS queue(aws_sqs_queue.dispatch)とDLQをprovisionします。POST /v1/run(async mode、JT_RUN_DISPATCH=queue)は {org, flow, digest, inputs} をenqueueして run_id を返します。worker serviceはそれをlong-pollし(receive_wait_time_seconds = 20)、APIのsynchronous pathが使うものと同一の plane.rider.run を呼び出します。Standard SQSはat-least-onceであり、exactly-onceでも厳密な順序付きでもありません。ここではそれで問題ありません。エンジンのJournalが手順ごとの冪等性キーを持つため、再配信されたmessageは二重実行ではなくRunを 再開 します。FIFOのより厳格なordering/dedup保証は、この粒度では不要です。visibility_timeout_seconds = 360 は、AWSがworkerが死んだとみなして再配信するまで、workerがmessageを保持できる時間の上限です。messageがDLQに入るまでの maxReceiveCount = 5 で、DLQにはpaging SNS topicへ配線された独自のdepth alarmがあります。worker serviceの desired_count は、step-scaling CloudWatch alarmsを通じて ApproximateNumberOfMessagesVisible に基づいてautoscaleします(backlogが5を超えたら素早くscale outし、queueが3回連続の5分間空になったらゆっくりscale in)。API request rateとは完全に切り離されています。

EventBridge → signed /internal/tick

serviceはscheduleをPostgresに 保存 しますが、自分ではfireしません。単純に全レプリカでcronerを動かすと、それぞれのcronが N 回fireしてしまいます。scheduler.tf はleader electionを作る代わりに、timerをAWSに所有させることでこれを解決します。
1

従来型のEventBridge ruleが毎分fireする

schedule_expression = "rate(1 minute)"aws_cloudwatch_event_rule です。これはresolutionであり、scheduleがfireするgranularityではありません。これは通常のEventBridge Rule + API Destinationであり、別サービスの “Amazon EventBridge Scheduler”(aws_scheduler_schedule)ではありません。
2

署名付きでAPI DestinationへPOSTする

API_KEY auth付きの aws_cloudwatch_event_connection が、aws_cloudwatch_event_api_destination の各invocationに、JT_INTERNAL_TICK_SECRET(secretとして api taskへinjectされる同じ生成値)を含む X-Jinba-Tick-Secret headerを付与します。invocation_endpointhttps://api.<domain>/internal/tick です。
3

serviceが共有secretを検証し、自身のdue scheduleを解決する

/internal/tick routeは JT_INTERNAL_TICK_SECRET が設定されているときだけmountされ、matching signatureを持つrequestだけを受け付けます。tickはこのEventBridge ruleからしか来られません。fireされた各scheduleは、通常のRunとJournalになります。他のRunとまったく同じようにinspectでき、replayできます。
失敗したtick(/internal/tick からの5xx)は120秒以内に最大3回retryされ、その後、失敗したRunと 同じrun-dispatch DLQ に入ります(EventBridge targetの dead_letter_config)。1つのalarmが両方のfailure classをカバーします。/internal/tick がALB層でinternet-reachableである(api_allow listener ruleのpath listに入っている)のは、network isolationではなくHMAC verificationがそのtrust boundaryだからです。

entrypointが起動時にDATABASE_URLを組み立てる理由

deploy/docker/entrypoint.service.sh(containerの ENTRYPOINT で、imageの CMD をwrapするもの)は、ある1つの相互作用のためだけに存在します。manage_master_user_password = true は、AWSがRDS master passwordを約7日ごとに、その内容をTerraformが管理しない専用のSecrets Manager secretへ自動ローテーションすることを意味します。 以前の設計では、terraform apply 時に DATABASE_URL を1回組み立て、static secretに入れていました。そのsecretは次のrotationでstaleになります。新しいECS taskはPostgres auth(28P01)に失敗し、rotation windowをまたいだdeployでは毎回 aws ecs wait services-stable がtimeoutしていました。修正では、組み立てを apply time から task start time へ移しました。
  • Terraformは、rotationをまたいでも変わらないpieceだけをplainな非secret environmentとしてinjectします。JT_DB_HOSTaws_db_instance.main.address、hostのみで .endpoint ではない)、JT_DB_PORTJT_DB_USERJT_DB_NAMEJT_DB_SSLMODE=no-verify です。
  • 現在の rotating passwordは、RDS-managed secretから直接 secrets referenceとしてinjectします。:password:: JSON-key selector("${local.rds_master_secret_arn}:password::")を使います。ECSはtask startごとにこれを解決するため、新しくrollされたtaskは常に、その瞬間にliveなpasswordを受け取ります。
  • containerの最初のprocessとして実行されるentrypointが、これらのpieceからconnection stringを組み立て、実際の CMD(または後述のmigration taskのようなECS command override)を exec する前に DATABASE_URLexport します。
if [ -z "${DATABASE_URL:-}" ] && [ -n "${JT_DB_PASSWORD:-}" ] && [ -n "${JT_DB_HOST:-}" ]; then
  _enc_pw="$(bun -e 'process.stdout.write(encodeURIComponent(process.env.JT_DB_PASSWORD||""))')"
  export DATABASE_URL="postgres://${JT_DB_USER}:${_enc_pw}@${JT_DB_HOST}:${JT_DB_PORT}/${JT_DB_NAME}?sslmode=${JT_DB_SSLMODE}"
fi
exec "$@"
connection failureをdebugしているなら、知っておく価値があるdetailが2つあります。まずpasswordは最初に URL-encoded されます(使い捨ての bun -e 経由の encodeURIComponent)。RDS-managed passwordには + & / % などの文字が含まれる場合があり、そのままだとconnection stringが壊れるためです。次に、sslmode=no-verifyrequireverify-full ではない)は意図的です。rds.force_ssl=1 は転送中TLSを引き続き強制しますが、RDS CAがcontainerのtrust storeに入っておらず、node-postgres >=8.13 はそうでなければ SELF_SIGNED_CERT_IN_CHAIN でconnectionを拒否するため、no-verify はCA-chain verificationをskipします。 DATABASE_URL がすでに設定されている場合は変更しないため、まったく同じimageがdev(DATABASE_URL を直接設定する環境)とこのECS topologyの両方で無修正で動きます。one-off migration task(migrate.tf)は同じimage、role、env/secrets配線を再利用します。違いは commandpackages/service/src/migrate.ts にoverrideされることだけなので、connection stringは同じ方法で解決されます。
DB migrationの実行は、現時点では 手動の運用手順 であり、deploy.yml が自動的にtriggerするものではありません。schema-changing imageをrollする前に、migrate.tf 自身のheader commentに従い、aws ecs run-task --task-definition <migrate_task_definition_arn output> --network-configuration <app subnets/SG> を実行します。runnerは冪等です(現在のschemaに対して再実行しても安全)が、schema-changing deployがそれを実行済みであることを現在は何もgateしていません。自動だと仮定する前に、これがその後CIへ配線されたかどうかをownerに確認してください。

モジュールの適用

1

remote-state backendをaccountごとに1回bootstrapする

main moduleのstateは、暗号化されversioningされたS3 bucketとDynamoDB lock tableに置かれます。ただし、そのbucket/table自体を、それが保持するstateの中に置くことはできません。別の bootstrap/ module(local state)がそれらを作成します。
cd deploy/terraform/bootstrap
terraform init
terraform apply -var 'state_bucket=jinbatrail-tfstate-<account_id>' -var 'lock_table=jinbatrail-tflock'
2

環境ごとに、そのbackendに対してmain moduleをinitする

cd deploy/terraform
terraform init \
  -backend-config="bucket=jinbatrail-tfstate-<account_id>" \
  -backend-config="key=staging/terraform.tfstate" \
  -backend-config="region=us-east-1" \
  -backend-config="dynamodb_table=jinbatrail-tflock"
各環境(dev/staging/production)は独自のstate keyを持つため、staging applyが production のresourceに触れることはありません。
3

必須inputを指定してplan/applyする

domain_name にはdefaultがありません。domainが欠けていれば、placeholderをshipするのではなく plan が明確に失敗します。anthropic_api_key はauthoring agentのkeyを1回seedします(その後はconsole内でrotateしてください。Terraformの lifecycle.ignore_changes により、次回applyでconsole rotationが上書きされることはありません)。
terraform plan  -var domain_name=jinbatrail.example -var 'anthropic_api_key=…'
terraform apply -var domain_name=jinbatrail.example
container image(service_imagebrowser_imageweb_image)はすでにECRに存在している必要があります。このモジュールはECR repoもimage buildもprovisionしません。それは CI/CDパイプライン の仕事です。
4

alarmを人へ配線する

すべてのCloudWatch alarm(ALB 5xx、RDS CPU/storage/connections、DLQ depth、unhealthy API targets)は、configurationに関係なく1つのSNS topicへpublishします。ただし、-var 'alarm_email=oncall@example.com' を設定して(AWSから送られるsubscription emailをconfirmして)いない限り、またはSlack/PagerDuty/OpsGenie integrationを alarm_sns_topic_arn outputへsubscribeしていない限り、誰にもpageしません。
staging.tfvars は、最小フットプリントのための実際に適用されたreferenceです。db.t4g.micro / cache.t4g.micro、単一の共有NAT gateway、KMS envelopeではなく JINBA_MASTER_KEY bridge、そして api_desired_count = 1 を使います。

api_desired_count を1より大きくしないこと

var.api_desired_count のデフォルトは 1 であり、このモジュールは現在、それを安全に上げることをサポートしていません。 variables.tf のcommentはその理由を明示しています。“a defaults-only apply must NOT silently run 2 replicas before the [multi-replica] gaps land.” この値の変更は、日常的なscaling knobではなく、ownerのsign-offが必要な判断として扱ってください。
serviceにはこのための実際のcode-level readiness gate(packages/service/src/readiness.ts)があり、本番でのhard boot-failureとしても、ALBがhealth-checkする GET /ready routeとしても評価されます。このgateは、replicaCount > 1 かつ 4つのsingle-process gapのいずれかが閉じていないときにtripします。4つのgapとは、共有rate limiter、重複しないscheduler firer、sticky/relayed live-view、共有engine storeです。このTerraformモジュールは、それらのgapに必要なpiece(Redis replication group、per-replica cronerの代わりのEventBridge tick、sticky ALB sessions、そして両store用のRDS Postgres)をprovisionします。またcomposition rootは、Redis、JT_INTERNAL_TICK_SECRET、engine DATABASE_URL がすべて構成されているときにgapが閉じていると扱います。このdeploymentでは、それらはデフォルトで構成されています。 ただし、それはこのモジュールが現時点で水平スケールして安全であることと 同じではありません。コード上で確認できる具体的な理由が2つあります。
  1. gateが確認するreplica countは自己申告であり、ここでは何もそれを報告していません。 replicaCountJT_REPLICA_COUNT から来ます。これはデフォルトで 1 であり、environment variables reference の自身のdocumentationにある通り、“informational … not used to configure anything.” です。compute.tfcommon_environment は、どこでも var.api_desired_count から JT_REPLICA_COUNT を設定していません。具体的には、このモジュールで今 api_desired_count2 に上げても、/ready gateは2レプリカで動いていることを認識しません。1 を読み続け、greenのままです。意図されたhard-fail protectionは、それが観測すべきTerraform knobに配線されていません。
  2. Live-view session ownershipは、gap closureでは消えない物理的制約です。 active authoring sessionのCDP pageは、あるtaskのprocessによって開かれ、移動できません。Redis pub-sub relayはすでにcaptureされた frames を他のレプリカへfan outしますが、2つ目のレプリカは、自分が開いていないpageを引き継いで control することはできません。sticky ALB sessionsがこれを成立させており、gap closureだけで成立しているのではありません。そしてstickiness pinningはbest-effort cookieであって、すべてのfailure modeでのhard guaranteeではありません(task recycleがsession途中で起きれば、そのsessionのbrowserはやはりdropします)。
api replicaを2つ以上必要とする場合、それは terraform apply -var api_desired_count=2 ではなくengineering taskとして扱ってください。gateが実際に見られるように JT_REPLICA_COUNT を本当のdesired countへ配線し、Redis/scheduler/engine-DBがtarget environmentで本当に到達可能であることを確認し、sticky-session failoverをload下で検証してから本番で信頼してください。workerbrowser にはこの制約はありません。worker のqueue-depth autoscaler(queue.tf)とbrowser poolは、最初から複数taskで動くように設計されています。

Observability

observability.tf は、プロダクトがすでに持つrun inspectorの上に、運用レイヤーを追加します。
  • 1つのSNS topic。下記のすべてのalarmがそこへpublishします(alarm_sns_topic_arn)。任意のemail subscriptionは var.alarm_email でgateされます。subscriberがいなくてもalarmは常に存在しdashboardを動かすため、このモジュールのapplyが黙って誰かにpageし始めることはありません。
  • ALB: ELB-generatedおよびtarget-generatedの5xx counts、api target groupのunhealthy-host count(これはboot/readiness gateがinfra signalとして現れたものです。/health に失敗するtaskはhealthy targetになりません)、そしてlive-view WS/SSE relayのconnection dropのproxyとしての高い TargetConnectionErrorCount
  • RDS: CPU、free storage、connection-count thresholds。それぞれ単純な var.*_alarm_threshold で、instance sizeごとにtuneできます。
  • SQS: queue-backlog high/low(worker autoscaling policiesを駆動)とDLQ-not-empty(Run、またはscheduler tickがすべてのredeliveryを使い切った状態)。
  • CloudWatch dashboard${name}-ops)。4つのwidgetを持ちます: ALB requests/5xx、target health/latency、RDS CPU/connections/storage、queue/DLQ depth。
  • Tracing は変数でgateされます(enable_tracing、デフォルト false — opt inするまでコストゼロ)。X-Ray write IAM policyを付与し、OTEL_* envをadvertiseするだけです。実際にspanをexportするには、task上のADOT/OTel collector sidecarがまだ必要ですが、このモジュールはそれを追加しません。

CI/CDパイプライン

このモジュールが実行するcontainer imageが実際にどのようにビルドされ、本物のPostgresに対してgateされ、4つのECS serviceへrollされるか。

環境変数

このモジュールがinjectするすべての JT_* / JINBA_* / BROWSERFLOW_* variable。defaultと本番要件も含みます。

コントロールプレーン

このインフラが実際に実行するもの。2つ目のエンジンになることなく、flowをscheduleしobserveするcoordinatorです。

シークレット

このモジュールのKMS CMKが支えるsecrets brokerが、run timeにworker上で secret.* referenceを解決する方法。