Deployment and operations
Accounts per environment, Terraform, GitOps on EKS, promotion and rollback.
Secure File Transfer is delivered with Terraform for AWS, GitHub Actions with OIDC for builds, and Argo CD for what runs in Kubernetes. The same signed artifacts move from dev to staging to prod, and a rollback is a Git revert.
Accounts and environments
Each environment has its own AWS account under AWS Organizations (ADR-0006). The management account holds no workloads. Four member accounts sit in three organizational units:
| Account | Holds |
|---|---|
| dev | Tokyo only, smallest sizes, torn down when idle |
| staging | Prod's shape at smaller sizes, including the Osaka pilot light; where drills run first |
| prod | Tokyo across three AZs, plus the Osaka pilot light |
| shared | The container registry, web bundle artifacts, the public DNS zone and the organization's log archive |
Why separate accounts. A leaked dev or staging credential can't reach prod. Service control policies bind every principal in a member account, administrators included: they keep workloads in Tokyo and Osaka, protect KMS keys and logs, and stop deploy roles from widening their own trust. Each environment tears down inside its own account, and bills and quotas separate cleanly. The cost is cross-account wiring.
People reach AWS only through IAM Identity Center with MFA by security key or passkey. Nobody holds admin access standing; it is assigned for at most an hour.
Terraform
Terraform owns every AWS resource (ADR-0005), split into small root modules per account, environment, region and layer: bootstrap, network, data, cluster, cluster bootstrap, edge, and a retained stack in prod. Tokyo and Osaka are built from the same modules, so the pilot light can't fall behind.
- State lives in one S3 bucket per account and region, encrypted with that account's state key and locked with S3 native lock files. Osaka's state lives in Osaka, so a failover needs nothing from Tokyo.
- GitHub Actions with OIDC. Each account has a plan role that trusts only pull requests and a deploy role that trusts only its GitHub environment, which accepts deployments from
mainalone. No long-lived AWS keys exist anywhere. Prod applies wait for approval. - Static checks on every pull request: naming and backend conventions,
terraform fmtandvalidate, tflint with the AWS ruleset, and Trivy's misconfiguration scan. None of them reaches AWS.
The bootstrap stacks, the organization with its guardrails, and the network module are written. The other environment layers are skeletons that later issues fill.
Clusters and registry
Every workload runs on Amazon EKS, one cluster per environment and region (ADR-0004).
- Karpenter provisions Graviton nodes in two pools: main, and a tainted scan pool that keeps hostile files off the API's nodes. A small managed
systemnode group runs Karpenter, Argo CD, Kyverno, CoreDNS and the egress proxy. - KEDA autoscales the API on CPU, request rate and active download streams, and the workers on queue age.
- EKS Pod Identity gives each workload its own IAM role. The cluster API endpoint is private.
ECS on Fargate was the main alternative, with fewer parts to run. It lost because it has no GitOps controller: rollback and fencing a returning Tokyo cluster would become pipeline steps rather than Git changes.
Registry. Images live in Amazon ECR in the shared account, encrypted with the registry key and replicated to Osaka once staging exists (ADR-0007). Tags are immutable. Each build is tagged with its Git SHA, signed with cosign, carries an SBOM and provenance, and is deployed by digest. Kyverno rejects anything else. ECR keeps image pulls inside AWS through VPC endpoints, so a failover to Osaka doesn't depend on the internet or GitHub.
Build, promote and roll back
- Build once. Each merge to
mainthat changes code builds the arm64 image on an Arm runner, pushes it to ECR by digest, scans it with Trivy and signs it. The design builds the web app bundle once too, stores it by version and promotes it the same way; that pipeline is still to come. - Deploy to dev. The build opens a pull request that pins dev's deploy config to the new digest. Merging it is the deploy: Argo CD syncs from
main. - Promote. A promotion to staging, then prod, is a pull request that copies the previous environment's digests. Its required
promotion-gatecheck passes only if that environment is synced, healthy and passing the synthetic probe on those digests. Merging the prod pull request is prod's approval. - Roll back. Revert the promotion commit. The gate passes a revert at once when it restores a digest the environment ran before, and Argo CD syncs the previous version. The target is under 10 minutes (NFR-18).
Zero-downtime deploys. On shutdown a pod fails its readiness probe, a preStop hook holds it until the load balancer stops sending traffic, and in-flight requests get 20 seconds to finish. Download streams get up to 120 seconds, then the browser resumes them under the same grant. Migrations run in an Argo CD PreSync job and are expand-only, so the previous release keeps working against the new schema. A rollback never runs a down-migration. Argo CD runs with auto-sync, prune and self-heal everywhere, and nobody syncs an application by hand.
Observability
The API and workers emit OpenTelemetry traces, Prometheus metrics and JSON logs (ADR-0009). The service already does all three. In each cluster, OpenTelemetry collectors feed self-hosted Prometheus with Alertmanager, Loki for logs, Tempo for traces and Grafana for dashboards. Logs and traces are stored in S3 in the cluster's region.
Why self-hosted. Telemetry stays in Japan and goes away with each environment at teardown. Dashboards and alert rules are files in Git, so a test can fail any alert without a runbook. There is no per-series charge. The trade-off is that the stack shares the cluster's fate, so CloudWatch keeps the AWS-service alarms and an out-of-cluster watchdog.
Rebuild, teardown and cost
| Target | Value |
|---|---|
| Rebuild prod from Terraform and Argo CD (NFR-19) | ≤ 2 hours |
| Destroy prod (NFR-19) | ≤ 1 hour, leaving only the retained audit archive stack, at ≤ $7 a month |
| Budget alarms (NFR-20) | AWS Budgets at 50 %, 80 % and 100 % of the monthly budget |
dev and staging are torn down when idle. Prod runs for an operating period, then is destroyed. The prod audit archive, its key, and the state, trail and alert topic that guard them stay until the archive's 365-day retention ends. Both targets will be proven by a timed rebuild and teardown, with the bill checked afterwards.
Local development
Milestones M0 to M3 run entirely on a laptop. make up starts the API with a local stack that stands in for AWS, with no AWS account needed.
| Local service | Stands in for |
|---|---|
| PostgreSQL 17 | Aurora PostgreSQL |
| S3Mock | S3, with versioned buckets and an Object Lock audit archive |
| ElasticMQ | SQS, with dead-letter queues |
| Valkey | ElastiCache for Valkey |
| Mailpit | SES |
| ClamAV | The clamd pods |
| Keycloak | The Cognito user pool |
make check runs the same lint, type checks and tests as CI. The API never connects as a superuser locally, so row-level security is tested on a laptop too. Every local credential is fake.
Source: Infrastructure, Architecture, ADR-0004, ADR-0005, ADR-0006, ADR-0007, ADR-0009, Local development