Chapter 12 of 24

GitLab CI for Terraform and OpenTofu

Use OIDC, a saved plan, and a protected manual gate so CI changes only what was reviewed.

Learning objectives

Pipeline contract

StageSafety property
Format, validate, native testNo cloud mutation.
PlanOIDC identity preflight and one binary plan artifact.
ApplyManual protected production action that consumes exactly that artifact.
Verify and driftRead outputs, then use -refresh-only; record cancellation or partial-failure evidence.

The example is opt-in. The root GitLab Pages pipeline does not include it and therefore cannot execute Terraform, OpenTofu, or AWS commands.

OIDC is a bounded identity

GitLab issues a short-lived ID token with audience sts.amazonaws.com. AWS verifies the OIDC issuer and the CI role trust restricts both audience and the subject claim for one project path and protected ref. The role starts without generic administrator permissions. Add only the exact state, KMS, and service actions supported by the reviewed plan.

Lab: review the sample locally

Read the opt-in README and inspect the YAML. Do not add it to the root pipeline or run cloud commands for this course task.

# Terraform command pair for the native mock test.
terraform -chdir=learn-terraform/capstone/infra/modules/network test -test-directory=tests

# OpenTofu command pair for the same native mock test.
tofu -chdir=learn-terraform/capstone/infra/modules/network test -test-directory=tests

# Static review only: prove the Pages pipeline has no include.
python3 -m unittest tests/test_course.py -v
python3 tools/check_course.py learn-terraform index.html

Review before enabling

  1. Confirm the account-level OIDC provider already exists and its issuer is publicly reachable.
  2. Check trust restricts issuer, aud, project path, branch ref, and protected production environment.
  3. Confirm the plan job writes a binary plan plus commit, tool, region, account, and backend digest metadata with a short expiry.
  4. Confirm resource_group serializes plan, apply, verify, and drift for the production state.
  5. Confirm the apply job is manual, has no automatic rule, and invokes only apply tfplan.

Failure drill

Do not replace a failed saved-plan apply with a fresh automatic plan. A plan becomes stale when state changes. The evidence artifact records the job, status, and commit even on cancellation or partial failure. Review the failed state and create a new plan only after deciding why it changed.

Verify the controls

  1. Run format checks for both CLIs and the native mocked module tests.
  2. Confirm each pinned Linux AMD64 ZIP passes its official SHA256 check before unzip.
  3. Parse the YAML with the GitLab CI linter before any include is proposed.
  4. Check no static AWS access-key variables, no cleanup job, and no automatic apply exist.
  5. Confirm the drift job is -refresh-only and its result is evidence, not a mutation.

Cleanup

Disable IAC_PIPELINE or remove the local include used for an experiment. Let the one-hour plan and evidence artifacts expire. Do not add a CI cleanup job: resource removal is a separate reviewed change with dependency, retention, and recovery checks.

Quick check

Why does the apply job not run plan again?

It must apply the reviewed plan artifact, not recalculate a potentially different change.

Does a manual button make a role safe by itself?

No. The trust claims, least-privilege permissions, protected environment, artifact checks, and state serialization all matter.

Recap and next

CI is a narrow execution path: ephemeral identity, one reviewed artifact, one protected operator action, and evidence when the path stops.

The AWS production foundation is complete. Next, the capstone can add EKS access with the same state and identity boundaries.