Terraform and OpenTofu with GitHub Actions and AWS OIDC (No Access Keys)

· 2 min read · Terraform & OpenTofu Tutorials

A CI/CD pipeline for infrastructure without stored AWS keys #

The Terraform CI/CD tutorial explains the concepts. This guide shows a concrete pipeline for GitHub Actions. The key point is authentication: instead of saving an AWS access key and secret in GitHub, the workflow proves its identity to AWS with an OpenID Connect (OIDC) token and receives temporary credentials.

1. Create the OIDC provider and the role in AWS #

Create these resources once, with Terraform, in the AWS account where the pipeline will deploy:

github-oidc.tf
resource "aws_iam_openid_connect_provider" "github" {
  url            = "https://token.actions.githubusercontent.com"
  client_id_list = ["sts.amazonaws.com"]
}

data "aws_iam_policy_document" "assume" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:my-org/my-infra:*"]
    }
  }
}

resource "aws_iam_role" "terraform" {
  name               = "github-terraform"
  assume_role_policy = data.aws_iam_policy_document.assume.json
}

Attach to the role the IAM permissions that your code needs. Use two roles: a read-only role for plan (used in pull requests) and a write role for apply (only on main). See IAM roles and policies.

2. The workflow #

.github/workflows/infra.yml
name: infra

on:
  pull_request:
    paths: ["infra/**"]
  push:
    branches: [main]
    paths: ["infra/**"]

permissions:
  id-token: write   # needed to request the OIDC token
  contents: read
  pull-requests: write

env:
  AWS_REGION: eu-west-1

jobs:
  plan:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: infra
    steps:
      - uses: actions/checkout@v4

      - uses: opentofu/setup-opentofu@v1
        with:
          tofu_version: 1.10.0

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111111111111:role/github-terraform-plan
          aws-region: ${{ env.AWS_REGION }}

      - run: tofu fmt -check -recursive
      - run: tofu init -input=false
      - run: tofu validate
      - run: tofu plan -input=false -out=tfplan

      - uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: infra/tfplan

  apply:
    needs: plan
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production        # add required reviewers in GitHub
    defaults:
      run:
        working-directory: infra
    steps:
      - uses: actions/checkout@v4

      - uses: opentofu/setup-opentofu@v1
        with:
          tofu_version: 1.10.0

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111111111111:role/github-terraform-apply
          aws-region: ${{ env.AWS_REGION }}

      - uses: actions/download-artifact@v4
        with:
          name: tfplan
          path: infra

      - run: tofu init -input=false
      - run: tofu apply -input=false tfplan

Replace the account ID, role names, region and the versions of the tools with yours. To use Terraform, replace the setup action with hashicorp/setup-terraform and the tofu commands with terraform.

How it works #

  • permissions: id-token: write lets the job request the OIDC token from GitHub.
  • configure-aws-credentials exchanges the token for temporary credentials of the role. They expire in about an hour and are never stored.
  • On pull requests only the plan job runs, with the read-only role.
  • On merge to main, apply runs the saved plan (tfplan), so what is applied is exactly what was reviewed. The environment setting lets you require a manual approval.

Good practices #

  • Use a remote backend with locking, since two runs must never apply at the same time. Add a concurrency group to the workflow too.
  • Do not print secrets: see secrets management.
  • Add security scanning and tests before the plan.
  • Pin third-party actions to a full commit SHA in sensitive repositories.
  • Show the plan in the pull request comments with a tool such as Atlantis, Spacelift or a plan-comment action, so reviewers see the changes.

#Terraform #OpenTofu #AWS #AWS IAM #Security