Skip to content

ci: run terraform plan/apply in CI via OIDC#86

Merged
johncarmack1984 merged 2 commits into
mainfrom
ci/terraform-in-ci
Jun 30, 2026
Merged

ci: run terraform plan/apply in CI via OIDC#86
johncarmack1984 merged 2 commits into
mainfrom
ci/terraform-in-ci

Conversation

@johncarmack1984

Copy link
Copy Markdown
Owner

Moves Terraform off the laptop: plan on PRs, apply on release — both in CI via OIDC, no local AWS creds.

How it works

  • PRsterraform.yml runs validate + a real plan (read-only lux-terraform-plan role, -lock=false) and writes the diff to the run summary, so changes are reviewable before merge.
  • Apply → a terraform-apply job in release.yml, gated on release_created, runs terraform apply when the release-please version-bump PR is merged (i.e. when a release is cut). It's the only place apply runs.
  • An infra PR therefore merges first and applies with the next release; its diff was the PR plan. (Flagged here so it isn't surprising.)

Two roles (infra/terraform-ci.tf)

role perms trust
lux-terraform-plan ReadOnlyAccess any ref of this repo (PRs can plan)
lux-terraform-apply curated least-privilege refs/heads/main only — only a release apply can assume it

The apply policy is scoped per-service to lux's resources (IAM on role/lux* + the cargo-lambda-role-*, Lambda/Logs on lux*, DynamoDB lux-sync, Cognito, IoT in-account, Secrets Manager read on lux/*, and S3 state on the lux/ prefix only).

Policy validated (read-only, nothing mutated)

Verified with iam:SimulateCustomPolicy against the real resource ARNs:

  • 15/15 allowed — every action a real plan+apply needs.
  • 3/3 implicitDeny — a non-lux role, a non-lux table, and another project's state prefix are all denied, proving the scoping actually constrains.

⚠️ One-time bootstrap required (chicken-and-egg)

CI can't create the role it then assumes, so the two roles must be created once by an admin:

cd infra && terraform apply   # creates lux-terraform-plan + lux-terraform-apply (4 to add, 0 change, 0 destroy)

Until that runs, this PR's own plan job fails (the plan role doesn't exist yet) — expected. After the bootstrap, re-run the job (it'll pass) and merge. From then on every infra change flows through CI.

Add two OIDC roles in infra/terraform-ci.tf: lux-terraform-plan
(ReadOnlyAccess, assumable from any ref) and lux-terraform-apply
(curated least-privilege, main branch only). terraform.yml gains a
plan job on PRs (read-only, -lock=false, written to the run summary);
release.yml applies the infra when a release-please version-bump PR is
merged (release_created), via the main-only apply role. Infra changes
no longer apply from a laptop.
Replace the gitignored secrets.auto.tfvars variable with a
data.aws_secretsmanager_secret_version read of lux/discord-public-key,
so terraform plan/apply get the value in CI via OIDC and nothing is
committed to this public repo. Grant the read-only plan role
GetSecretValue on just that one secret; the apply role's secret:lux/*
grant already covers it.
@johncarmack1984 johncarmack1984 merged commit 695822d into main Jun 30, 2026
6 checks passed
@johncarmack1984 johncarmack1984 deleted the ci/terraform-in-ci branch June 30, 2026 06:36
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.

1 participant