Skip to content

Set up OIDC for tofu-plan / tofu-apply

tofu-plan and tofu-apply authenticate to AWS via OIDC — no access keys, ever. Read Explanation: OpenTofu state and caching for the why; this page is the setup checklist.

1. Provision the OIDC trust on the AWS side

You need an IAM OIDC identity provider trusting gitlab.com, and an IAM role whose trust policy accepts your project's OIDC subject. This is infrastructure-side setup outside these components — do it once per AWS account (or reuse terraform-aws-bootstrap's automation-role module if you already have it).

The audience your GitLab pipeline presents (aud) must match the trust policy's aud condition and the OIDC provider's client_id_list. The components default aud to "sts.amazonaws.com" — leave it unless your account pinned something else.

2. Fix the consumer-side gotcha: no static profile

This is the single most common way OIDC auth silently fails. If your aws provider block hardcodes a profile, it short-circuits the web-identity credential chain before OIDC gets a chance — the provider looks for ~/.aws/credentials on a runner that has none, and the job fails with an opaque "no valid credential sources" error. Use a variable that's null in CI:

variable "aws_profile" {
  description = "Local AWS CLI profile. Leave null in CI — OIDC is used instead."
  type        = string
  default     = null
}

provider "aws" {
  region  = var.region
  profile = var.aws_profile   # null in CI
}

3. Wire tofu-plan

include:
  - component: gitlab.com/phpboyscout/cicd/[email protected]
    inputs:
      role_arn: "arn:aws:iam::<account-id>:role/phpboyscout-automation"
      working_directory: "src/my-stack"

tofu-plan runs on MR pipelines (to review the plan) and the default branch (to bank a plan artifact) — no further gating needed for the common case.

4. Choose how tofu-apply retrieves the plan

Two options, and they're not independent choices — see Explanation: OpenTofu state and caching for why.

Same-pipeline (job mode, the default) — plan and apply run in the same pipeline, normally the default branch:

include:
  - component: gitlab.com/phpboyscout/cicd/[email protected]
    inputs:
      role_arn: "arn:aws:iam::<account-id>:role/phpboyscout-automation"
      working_directory: "src/my-stack"
      apply_when: manual   # click "apply" in the UI after reviewing the plan

Cross-pipeline (ref mode) — for a tag-gated release flow, where a main push banked the plan and a release tag applies it:

include:
  - component: gitlab.com/phpboyscout/cicd/[email protected]
    inputs:
      role_arn: "arn:aws:iam::<account-id>:role/phpboyscout-automation"
      working_directory: "src/my-stack"
      plan_source: ref
      plan_ref: main
      apply_when: on_success

ref mode has an extra requirement: your IAM role's trust policy must accept a tag OIDC subject (ref_type:tag:ref:*), not just an MR/branch one — tofu-apply in this mode runs from a tag pipeline.

5. On GitLab Free: override plan_token

If you're using ref mode on GitLab Free, the default plan_token ($CI_JOB_TOKEN) will fail — job-token auth for the jobs-artifacts API is Premium/Ultimate only. Create a personal or group access token with job-artifact read (fine-grained: the Job Artifact resource, Read scope), store it as a Masked CI/CD variable, and pass it:

inputs:
  plan_token: $TF_ARTIFACT_PAT

job mode never uses plan_token — this only matters for ref mode.

6. Verify

Open an MR that touches the stack. tofu-plan should run and post a plan-summary widget on the MR. Merge to the default branch (or cut a release tag, for ref mode); tofu-apply should either wait for manual approval or auto-apply, depending on apply_when.

See also