Infrastructure Conventions
AWS is the primary cloud; GCP is secondary (Maps, ML, BigQuery only). Vercel is the primary
Next.js target. All infrastructure is Terraform. Local development is Docker Compose.
Non-negotiables (apply to every infrastructure change)
- No secrets in the repo, ever. No credentials in
.tf, .tfvars, .env, task definitions,
CI YAML, or Dockerfiles. Secrets live in AWS Secrets Manager (single source of truth) and
are referenced, not copied.
- Prefer no credential at all. CI authenticates by federation, not keys — OIDC to AWS,
Workload Identity Federation to GCP. A credential that does not exist cannot leak, expire, or
be rotated late. A static key is the fallback, not the default; when one is genuinely
unavoidable it goes in AWS Secrets Manager like any other.
- Pin every version. Image tags, provider versions,
Gemfile.lock, package-lock.json.
latest is never allowed in a committed file; deploys use immutable git-SHA tags.
A GitHub Action tag is not a pin — uses: foo/bar@v4 is a mutable tag the author can
move, so a third-party action runs code you never reviewed with your token. Pin to the full
commit SHA and let Dependabot bump it (references/github-actions.md).
- Tag every resource:
project, environment, team, managed-by = "terraform" (plus
cost-center where spend is tracked). Enforce via provider default_tags, not copy-paste.
terraform plan before terraform apply. Read the plan. Remote state with locking always
(S3 + DynamoDB on AWS, GCS on GCP), one state file per environment.
- Everything is an environment variable. Maintain
.env.example with every required key and
empty values. Canonical names: RAILS_ENV, DATABASE_URL, REDIS_URL, CENTRIFUGO_API_KEY.
dotenv-rails is development-only.
- Production containers run as non-root from a multi-stage build on a slim base.
- No direct pushes to
main. Every PR passes lint + test + security scan.
Layout
Terraform is organized by environment directory, not workspace:
terraform/
├── modules/ # networking, database (RDS+PostGIS), redis (ElastiCache), ecs, centrifugo
├── environments/ # dev, staging, production — each with its own state file
└── shared/ # ECR, IAM
The AWS stack (defaults — deviate only with a reason)
| Concern |
Service |
| Compute |
ECS Fargate (Rails, Sidekiq, Centrifugo) |
| Database |
RDS PostgreSQL, PostGIS extension enabled |
| Cache / Queues |
ElastiCache Redis (Sidekiq needs maxmemory-policy = noeviction) |
| Storage / CDN |
S3 (ActiveStorage) + CloudFront |
| DNS |
Route 53 |
| Secrets |
AWS Secrets Manager |
| Monitoring |
CloudWatch logs + metrics |
| Real-time |
Centrifugo on ECS behind an ALB with a raised idle timeout (WebSockets) |
| CI/CD |
GitHub Actions → ECS, authenticating via OIDC role assumption (no static keys) |
Local development
docker-compose.yml (committed) runs Rails, PostgreSQL + PostGIS, Redis, Centrifugo.
docker-compose.override.yml is gitignored for per-developer settings. Named volumes
(postgres_data, redis_data) for persistence. Health checks on every service, and dependents
wait with condition: service_healthy. .dockerignore must exclude .git, node_modules,
tmp, log, .env*.
Deployment flow
- PR → lint (rubocop, eslint), test (rspec, vitest), security scan, build verification.
Cache gems and
node_modules. Web PRs also run tsc --noEmit and the bundle budget check
(300 KB initial JS for Vite, 200 KB client bundle for Next.js).
- Merge to
develop → auto-deploy to staging.
- Merge to
main → production behind a manual approval gate.
- Migrations run as a separate step before the deploy — never in a container entrypoint.
- Smoke tests run after every deployment.
- Frontend targets: Vite SPA → S3 + CloudFront; Next.js → Vercel (ECS as the alternative).
Deep guides (read on demand, do not preload)
- Compose stacks, multi-stage Dockerfiles,
.dockerignore, env config → references/docker-and-compose.md
- Terraform layout, remote state, variable declaration, tagging, getting secrets into resources →
references/terraform-mechanics.md
- RDS PostgreSQL + PostGIS parameter groups, ElastiCache Redis for cache/Sidekiq →
references/aws-data-services.md
- ECS Fargate task definitions and services, autoscaling, ALB with WebSockets →
references/aws-compute-and-networking.md
- GCP Workload Identity Federation (keyless CI), the
attribute_condition that scopes it to your repo, when a key is unavoidable and where it lives, GCS backend, service selection → references/gcp-secondary-cloud.md
- GitHub Actions PR checks (rubocop/rspec/vitest/security), OIDC to AWS →
references/ci-pipeline.md
- Workflow supply chain (pin actions to a SHA, Dependabot),
permissions least privilege, concurrency groups for deploys, reusable workflows vs composite actions, environments, the pull_request_target hole → references/github-actions.md
- Rails → ECS deploy: immutable SHA tags, migrations as a task, wait, smoke test →
references/backend-deploys.md
- Vite SPA → S3/CloudFront, Next.js → Vercel or ECS standalone, bundle budgets →
references/frontend-deploys.md
- Savings Plans, right-sizing, budgets and anomaly detection, storage lifecycle, FinOps cadence →
references/cost-optimization.md
1---2name: std-infrastructure3description: Infrastructure standards — Terraform, Docker Compose, AWS, GCP, Vercel, CI/CD. Use when changing IaC, containers, pipelines, or deployment config.4---56# Infrastructure Conventions78AWS is the primary cloud; GCP is secondary (Maps, ML, BigQuery only). Vercel is the primary9Next.js target. All infrastructure is Terraform. Local development is Docker Compose.1011## Non-negotiables (apply to every infrastructure change)1213- **No secrets in the repo, ever.** No credentials in `.tf`, `.tfvars`, `.env`, task definitions,14 CI YAML, or Dockerfiles. Secrets live in **AWS Secrets Manager** (single source of truth) and15 are referenced, not copied.16- **Prefer no credential at all.** CI authenticates by **federation, not keys** — OIDC to AWS,17 Workload Identity Federation to GCP. A credential that does not exist cannot leak, expire, or18 be rotated late. A static key is the fallback, not the default; when one is genuinely19 unavoidable it goes in AWS Secrets Manager like any other.20- **Pin every version.** Image tags, provider versions, `Gemfile.lock`, `package-lock.json`.21 `latest` is never allowed in a committed file; deploys use immutable git-SHA tags.22 **A GitHub Action tag is not a pin** — `uses: foo/bar@v4` is a *mutable* tag the author can23 move, so a third-party action runs code you never reviewed with your token. Pin to the full24 commit SHA and let Dependabot bump it (`references/github-actions.md`).25- **Tag every resource**: `project`, `environment`, `team`, `managed-by = "terraform"` (plus26 `cost-center` where spend is tracked). Enforce via provider `default_tags`, not copy-paste.27- **`terraform plan` before `terraform apply`.** Read the plan. Remote state with locking always28 (S3 + DynamoDB on AWS, GCS on GCP), one state file per environment.29- **Everything is an environment variable.** Maintain `.env.example` with every required key and30 empty values. Canonical names: `RAILS_ENV`, `DATABASE_URL`, `REDIS_URL`, `CENTRIFUGO_API_KEY`.31 `dotenv-rails` is development-only.32- **Production containers run as non-root** from a multi-stage build on a slim base.33- **No direct pushes to `main`.** Every PR passes lint + test + security scan.3435## Layout3637Terraform is organized by environment directory, not workspace:3839```40terraform/41├── modules/ # networking, database (RDS+PostGIS), redis (ElastiCache), ecs, centrifugo42├── environments/ # dev, staging, production — each with its own state file43└── shared/ # ECR, IAM44```4546## The AWS stack (defaults — deviate only with a reason)4748| Concern | Service |49|---|---|50| Compute | ECS Fargate (Rails, Sidekiq, Centrifugo) |51| Database | RDS PostgreSQL, PostGIS extension enabled |52| Cache / Queues | ElastiCache Redis (Sidekiq needs `maxmemory-policy = noeviction`) |53| Storage / CDN | S3 (ActiveStorage) + CloudFront |54| DNS | Route 53 |55| Secrets | AWS Secrets Manager |56| Monitoring | CloudWatch logs + metrics |57| Real-time | Centrifugo on ECS behind an ALB with a raised idle timeout (WebSockets) |58| CI/CD | GitHub Actions → ECS, authenticating via **OIDC role assumption** (no static keys) |5960## Local development6162`docker-compose.yml` (committed) runs Rails, PostgreSQL + PostGIS, Redis, Centrifugo.63`docker-compose.override.yml` is gitignored for per-developer settings. Named volumes64(`postgres_data`, `redis_data`) for persistence. Health checks on every service, and dependents65wait with `condition: service_healthy`. `.dockerignore` must exclude `.git`, `node_modules`,66`tmp`, `log`, `.env*`.6768## Deployment flow6970- **PR** → lint (rubocop, eslint), test (rspec, vitest), security scan, build verification.71 Cache gems and `node_modules`. Web PRs also run `tsc --noEmit` and the bundle budget check72 (300 KB initial JS for Vite, 200 KB client bundle for Next.js).73- **Merge to `develop`** → auto-deploy to staging.74- **Merge to `main`** → production behind a **manual approval gate**.75- **Migrations run as a separate step before the deploy** — never in a container entrypoint.76- **Smoke tests run after every deployment.**77- Frontend targets: Vite SPA → S3 + CloudFront; Next.js → Vercel (ECS as the alternative).7879## Deep guides (read on demand, do not preload)8081- Compose stacks, multi-stage Dockerfiles, `.dockerignore`, env config → `references/docker-and-compose.md`82- Terraform layout, remote state, variable declaration, tagging, getting secrets into resources → `references/terraform-mechanics.md`83- RDS PostgreSQL + PostGIS parameter groups, ElastiCache Redis for cache/Sidekiq → `references/aws-data-services.md`84- ECS Fargate task definitions and services, autoscaling, ALB with WebSockets → `references/aws-compute-and-networking.md`85- GCP Workload Identity Federation (keyless CI), the `attribute_condition` that scopes it to your repo, when a key is unavoidable and where it lives, GCS backend, service selection → `references/gcp-secondary-cloud.md`86- GitHub Actions PR checks (rubocop/rspec/vitest/security), OIDC to AWS → `references/ci-pipeline.md`87- Workflow supply chain (pin actions to a SHA, Dependabot), `permissions` least privilege, concurrency groups for deploys, reusable workflows vs composite actions, environments, the `pull_request_target` hole → `references/github-actions.md`88- Rails → ECS deploy: immutable SHA tags, migrations as a task, wait, smoke test → `references/backend-deploys.md`89- Vite SPA → S3/CloudFront, Next.js → Vercel or ECS standalone, bundle budgets → `references/frontend-deploys.md`90- Savings Plans, right-sizing, budgets and anomaly detection, storage lifecycle, FinOps cadence → `references/cost-optimization.md`