Solar Atlas: a multi-environment AWS platform managed entirely through Terraform and pull requests
A flat, prod-only Terraform repository refactored into reusable modules and separate dev and prod stacks, with a GitOps path from pull request to plan, dev apply, and gated promotion to prod.
- 1A pull request triggers terraform plan, and the plan is posted back as a comment for review.
- 2Merging to main applies dev. The workflow assumes an AWS role through GitHub OIDC, so no long-lived keys exist.
- 3Promotion to prod is a separate, deliberate step.
- 4Prod applies the same seven modules with its own variables. Each stack keeps its own state key.
flowsupporting
The problem
One flat repository meant one environment: every change touched production, nothing could be rehearsed, and the structure could not be reused for a second stack.
Design decisions
- Modules plus one stack per environment
- Seven reusable modules (network, data, iam, lambdas, ecs-ec2-backend, frontend-cdn, observability) are called by live/dev and live/prod with their own variable files, so dev is a true rehearsal of prod.
- One state key per stack, with native S3 locking
- global, dev and prod each get their own key in an S3 backend. A bad apply in one stack cannot corrupt another, and native locking removes the need for a separate lock table.
- GitHub OIDC instead of stored cloud credentials
- An account-level OIDC provider lets workflows assume deploy roles for the duration of a run. No long-lived AWS keys live in the repository or its secrets.
- Plan on the PR, apply on merge, promote on purpose
- Separate workflows for plan, apply-dev, apply-prod, drift detection and hotfix. Reviewers see the plan before merge, and prod only changes through promotion or a deliberate hotfix path.
- Private origin behind a CDN
- The frontend is a private S3 bucket served through CloudFront with origin access control and a Route 53 alias, so the bucket is never public.
- A written go-live order for the billable steps
- Delegate DNS first, then per environment: repository, image, then the rest, then secrets out-of-band and a forced redeploy. Ordering mistakes are the expensive ones.