Skip to content

iac: add deployment workflow infra - #1310

Draft
jackrhoa wants to merge 29 commits into
devfrom
iac
Draft

jackrhoa wants to merge 29 commits into
devfrom
iac

Conversation

@jackrhoa

@jackrhoa jackrhoa commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Moves production onto Terraform-managed AWS infrastructure (iac/), deployed from GitHub Actions through OIDC roles instead of static AWS keys, and adds scheduled database dumps to S3.

Infrastructure (iac/app)

  • Naming: stack renamed to tcf-prod (project_name=tcf, environment=prod)
  • State: S3 backend (tcf-terraform-state-099933383052) with KMS encryption and lockfile
  • Networking
    • ALB is now internal (private subnets) and reachable only via a CloudFront VPC origin (saves ~$7/month)
    • Port 80 redirect listener removed
    • ALB ingress limited to the CloudFront VPC Origins security group
    • New origin.<domain> record; ACM cert gains it as a SAN for CloudFront → ALB TLS
  • ECS
    • Target-tracking autoscaling on CPU (60%) and memory (75%), 1–10 tasks
    • Terraform ignores task_definition/desired_count so code deploys and autoscaling own them
    • Adds AWS_ELB_URL and AWS_CLOUDFRONT_URL env vars
  • ECR
    • Tags are immutable (except buildcache)
    • Untagged images expire after 1 day; only the last 10 tagged images are kept
  • Cognito: pre-sign-up Lambda only allows UVA computing-ID @virginia.edu emails
  • IAM: all app roles carry the tcf-prod-role-boundary permissions boundary
  • DB dumps: nightly EventBridge Scheduler → Fargate pg_dump task → encrypted S3 bucket (cron(0 4 * * ? *) America/New_York)
  • Fixes
    • Updated ElastiCache arguments to the current provider schema
    • Cognito branding JSON normalized, so terraform apply no longer shows a diff every run

Bootstrap (iac/bootstrap, new)

Separate Terraform project that creates:

  • GitHub Actions OIDC provider
  • tcf-terraform-deployer, the Terraform apply role
  • tcf-github-deployer, the code deploy role, scoped to ECR/ECS
  • tcf-prod-role-boundary permissions boundary
  • KMS-encrypted, versioned state bucket, which also stores saved plans

See iac/bootstrap/README.md for setup.

CI/CD

  • terraform-deploy.yml (new): plan → upload saved plan to S3 → apply that exact plan
    • Gated by the terraform-plan / terraform-prod environments
    • Manual dispatch requires repo admin
  • aws.yml
    • Triggered on push (master, iac) and runs CI as a reusable workflow
    • Gated by the prod environment with a concurrency lock
    • Assumes tcf-github-deployer via OIDC (no more access-key secrets)
    • Skips the image build when an image for the SHA already exists
    • Targets the new tcf-prod-* cluster/service/repo/task family
    • Release task no longer runs invalidate_cachalot (stops flushing the whole query cache on every deploy)
  • ci.yml: adds workflow_call

App / local dev

  • Container port changed from 80 to 8000 (Dockerfile, gunicorn bind, compose port mapping)
  • Local S3 replaced: MinIO → RustFS, with bucket init via aws-cli instead of mc
  • scripts/prod_dump.sh rewritten
    • Runs the scheduled dump task on demand, or downloads the newest dump with --latest
    • Requires AWS SSO admin credentials
    • EC2/SSH variables removed from .env.example
  • .gitignore ignores Terraform state, .terraform/, and *.tfvars
  • Removed the Donate link from the footer
  • About page: restored Brian Tran to alums and updated Aleena Patel's headshot

Notes

  • Aurora Serverless v2 was tried and reverted; the database is still RDS PostgreSQL.
  • tcf-terraform-dns in the DNS account is managed by hand and is not in Terraform.

jackrhoa and others added 29 commits October 5, 2026 12:33
Add a GitHub Actions OIDC provider and trust the deployer role from the
terraform-test environment, so CI assumes it without AWS access keys.

Stop managing the DNS-account role in bootstrap. It is maintained by hand
in account 011713309463 and referenced by ARN, so bootstrap no longer
needs credentials outside the application account.

Add a workflow that plans iac/app on pushes to dev and applies only on a
manual admin-triggered run.
Plan test infrastructure from the iac branch as well as dev, so the
workflow can be exercised before it reaches the default branch.
Drop the workflow_dispatch condition on the apply step so pushes to iac
deploy directly. Restore the admin-gated condition before merging to dev.
Split the workflow into a plan job and an apply job. The plan job writes
the plan to the run summary and uploads it, and the apply job applies that
saved artifact behind the terraform-test environment, so reviewers approve
the plan they have read. Plans run in a separate terraform-plan environment
without reviewers, and the deployer trust policy accepts both.

Scope the deployer's IAM role actions to the application role prefix and
service-linked roles instead of every role in the account.

Move bootstrap state into the existing S3 bucket now that it exists.

Remove the footer donate link, which opened a modal that does not exist.
Give GitHub Actions its own OIDC deployer role limited to the application
stack, and require a permissions boundary on every role the stack creates.
The existing deployer role is now for local applies only.
Rename the workflow to terraform-deploy, run it only on pushes to iac,
assume tcf-github-terraform-deployer, and ship the Lambda zip built during
plan with the plan artifact so apply can use it.
Run the Terraform plan and apply pipeline as tcf-terraform-deployer through
GitHub OIDC, so CI uses the role the DNS account already trusts.

Rename the GitHub role to tcf-github-deployer and scope it to code deploys:
push iac-test images, register task definitions, run release tasks, and update
iac-test services. aws.yml now assumes it through OIDC from the aws-deploy
environment and targets the iac-test stack instead of using access keys.

Require the iac-test-role-boundary permissions boundary on every role
tcf-terraform-deployer creates or changes, limit PassRole to ECS tasks and
Lambda, and deny removing role boundaries.
Run aws.yml on pushes to master and iac instead of after the CI workflow
completes. workflow_run jobs always run as the default branch with its copy
of the workflow, which the prod environment rejects. The ci job now calls
ci.yml against the pushed commit, and the deploy job waits for approval in
the prod environment before deploying, mirroring the Terraform pipeline.

Assume tcf-github-deployer in the account from the prod environment's
AWS_ACCOUNT_ID variable, and trust the prod environment from bootstrap.
Ignore task_definition changes on the ECS service so a Terraform apply no
longer rolls the service back to the test image after a code deploy.
Terraform still manages the task definition family, which code deploys copy
when they register a new revision.
Make iac-test-app tags immutable except buildcache, so a commit SHA always
points at the image built for it while the build cache tag can still move.
Expire untagged images after a day and keep the newest 10 tagged images.

Skip the image build in aws.yml when the commit SHA is already in ECR, so
re-running a deploy reuses the image instead of failing on the immutable tag,
and let tcf-github-deployer describe images for that check.
Saved plans embed a full copy of state, so the tfplan artifacts on this public
repository exposed every secret in state to anyone signed in to GitHub. The
plan job now stores the plan and Lambda zip under plans/<run_id>/ in the state
bucket, and the apply job reads them from there and deletes them afterwards.

Encrypt the state bucket by default with a dedicated KMS key using S3 Bucket
Keys, deny requests that do not use TLS, expire old app state versions after
30 days, and expire saved plans after a day. Drop encrypt = true from the
backends, since without a KMS key it makes Terraform write SSE-S3 and override
the bucket default.
EC2 rejects apostrophes in security group rule descriptions, which failed the
apply that creates the CloudFront VPC origin ingress rule.
Legacy prod regularly bursts to 16 ACU and averages 2-4 ACU through
enrollment, which a db.t3.micro cannot serve. Replace the RDS instance with an
Aurora PostgreSQL 18.6 Serverless v2 cluster that scales from 0 to 16 ACU and
pauses after five idle minutes, and point the database credentials secret at
the cluster writer endpoint.
The web service ran a single task in one subnet, so enrollment traffic had
nowhere to go. Track average CPU and memory to scale between ecs_min_count and
ecs_max_count tasks, spread tasks across both public subnets, and leave
desired_count to the autoscaler.
The release task ran invalidate_cachalot tcf_website, which empties the query
cache for every table and sends all traffic to the database at once. During
enrollment that load spike took the site down for 30-60 seconds per deploy.
Cachalot already invalidates each table a migration changes, since
CACHALOT_INVALIDATE_RAW catches the ALTER, CREATE, DROP and data statements
that migrate runs, and the release task shares the web tasks' Redis cache.
This reverts commit e6c42b4, "feat(iac): switch the database to Aurora
Serverless v2". The November 2025 outage came from exhausted database
connections behind RDS Proxy, not from fixed instance capacity, and Aurora
never paused under steady bot traffic, so it costs more off-peak.
@jackrhoa
jackrhoa requested a review from gyoge0 October 5, 2026 18:04
@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch was successfully deployed

3 active deployments
prod — ebf7d625 Deployed Oct 5, 2026 by jackrhoa via deploy #149
terraform-prod — ebf7d625 Deployed Oct 5, 2026 by jackrhoa via Terraform apply #16
terraform-plan — ebf7d625 Deployed Oct 5, 2026 by jackrhoa via Terraform plan #16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants