Terraform CI/CD¶
GitHub Actions runs Terraform for all three infrastructure stacks. Operators no longer terraform apply from a laptop — change lands via PR, plan is reviewed on the PR, merge applies.
This pipeline covers infrastructure only (infrastructure/gcp, infrastructure/aws, infrastructure/cloudflare). Kubernetes manifests are delivered separately by Argo CD — see Argo CD. The K8s workflow here only validates manifests (kustomize build + kubeconform); it never applies them.
Workflows¶
| Workflow | File | Trigger | Does |
|---|---|---|---|
| Terraform Plan | .github/workflows/terraform-plan.yml |
PR touching infrastructure/** |
fmt -check + plan each stack, posts the plan as a PR comment |
| Terraform Apply | .github/workflows/terraform-apply.yml |
merge to main, or manual dispatch |
applies — see the two paths below |
| K8s Validate | .github/workflows/k8s-validate.yml |
PR touching k8s/** |
kustomize build + kubeconform, no apply |
| Secret Scan | .github/workflows/secret-scan.yml |
PR or push to main |
Runs Gitleaks to detect committed secrets/keys/credentials |
| Infra Lint & Security | .github/workflows/infra-lint.yml |
PR or push to main |
Runs TFLint, Trivy IaC scanner, yamllint, and ShellCheck |
All run on GitHub-hosted runners — deliberately not the self-hosted ARC pool, which runs on the cluster we cap at 2 nodes. CI must not consume that capacity.
Auth — no long-lived keys¶
Neither cloud stores a static credential in GitHub.
- GCP — Workload Identity Federation. GitHub's OIDC token is exchanged for short-lived credentials impersonating
terraform-ci@development-485000.iam.gserviceaccount.com. The trust is scoped toassertion.repository == tomoda-labs/devops— no other repo or fork can assume it. Defined ininfrastructure/gcp/cicd_wif.tf. - AWS — an OIDC-assumable IAM role,
arn:aws:iam::373544523193:role/terraform-ci, trust scoped torepo:tomoda-labs/devops:*. Defined ininfrastructure/aws/cicd_oidc.tf.
Every stack authenticates to GCP even when it targets AWS/Cloudflare, because all three keep Terraform state in the same GCS bucket (gs://development-485000-tfstate) and the AWS/Cloudflare provider secrets are pulled from GCP Secret Manager at run time (never stored in GitHub). The wiring values live in repo variables GCP_WIF_PROVIDER, GCP_TERRAFORM_CI_SA, AWS_TERRAFORM_CI_ROLE.
See IAM → terraform-ci for the SA's roles.
Which infrastructure splits dev/prod¶
This is the crux of the pipeline's blast radius. Only AWS has a real dev/prod split.
| Stack | Splits dev/prod? | Mechanism | What actually differs |
|---|---|---|---|
| AWS | Yes | Terraform workspaces (default = dev, prod) |
S3 assets bucket, CloudFront distribution, ACM cert, IAM users (uploader + eso-reader), Secrets Manager creds, CloudFront signing key — each named …-${var.environment}, a separate copy per workspace |
| GCP | No | one project, one cluster | dev/prod split lives in K8s namespaces, not Terraform. var.environment exists but no resource consumes it. The one nuance: Artifact Registry has separate tomoda-dev-repo / tomoda-prod-repo repos, and prod-only capacity (enable_stateful_pool, node ceilings) is a variable toggle inside the same state — not a separate environment |
| Cloudflare | No | single copy | Access, tunnel, worker — one of each |
Consequence: "releasing prod" only ever means moving the AWS prod workspace. There is no separate GCP prod environment to deploy.
AWS prod workspace needs TF_VAR_environment=prod
var.environment defaults to dev and nothing maps the workspace name to it. Selecting the prod workspace without setting TF_VAR_environment=prod makes Terraform want to rename every prod resource to its dev name — destroying the prod bucket, IAM users, and secrets. The apply workflow's prod path sets this explicitly. Never run a prod apply without it.
The two apply paths¶
Dev — automatic on merge¶
Merging a PR to main that touched infrastructure/** runs the apply-dev job: it applies all three stacks — GCP (the single project), AWS default (dev) workspace, and Cloudflare.
The gate is PR review. Nothing reaches main without a reviewed, merged PR, and the Plan workflow has already posted the real diff on that PR. Review the posted plan before approving.
Prod — manual, AWS-only¶
Prod is a manual workflow_dispatch on the Apply workflow (env = prod). It runs the apply-prod job, which applies only the AWS prod workspace — the assets bucket + CloudFront, the sole resources with a dev/prod split. GCP and Cloudflare are not touched on this path; their prod-flavoured changes ride the normal merge-to-main dev apply.
Actions → Terraform Apply → Run workflow → env: prod → Run
Gate limitations on the free plan¶
Two GitHub features this pipeline would otherwise use require a paid plan and are unavailable:
- Branch protection / required status checks — can't force the Plan job to pass before merge (403 "Upgrade to GitHub Pro or make this repository public"). Mitigation: PR review is the enforced gate; the reviewer must eyeball the posted plan.
- Environment approval gates — can't insert a human-approval pause before apply. Mitigation: prod is strictly manual dispatch; dev's gate is PR review.
If the repo upgrades to Pro, add branch protection requiring plan (gcp) / plan (aws) / plan (cloudflare) and wrap apply in an Environment with required reviewers.
Serialization¶
Applies share one GCS state lock per stack. The Apply workflow uses a tf-apply concurrency group and max-parallel: 1 on the stack matrix so runs queue rather than collide on the lock. cancel-in-progress: false — a half-finished apply is never cancelled.
Image vulnerability scanning¶
Not part of this Terraform pipeline — it belongs in the app-side Cloud Build (tomoda monorepo). AR's built-in scan is off by cost decision; a trivy build step is the plan. See Cloud Build → Vulnerability scanning.
Adding infra that needs a new secret or variable¶
- Non-secret var (like
project_id) — set it inline in the workflow's stack-vars step. - Secret (provider token, password) — put it in GCP Secret Manager and add a
gcloud secrets versions accessline to the "Load provider secrets" step. Never add it as a GitHub secret. See Secrets Management.