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

> Run Terraform or OpenTofu plan on pull requests and apply on merge with GitHub Actions, authenticating to AWS with OIDC and short-lived credentials.

- Source: https://www.itwonderlab.com/terraform-github-actions-aws-oidc/
- Published: 2026-08-16
- Updated: 2026-08-16
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

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

The [Terraform CI/CD tutorial](https://www.itwonderlab.com/aws-terraform-tutorial-terraform-cicd/) 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:

```hcl title="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
}
```

> [!IMPORTANT]
> The `sub` condition is what restricts which repository (and branch or environment) can use the role. Never leave it open. For the apply role, restrict it to the main branch or to a protected GitHub environment, for example `repo:my-org/my-infra:ref:refs/heads/main`.

Attach to the role the [IAM](https://www.itwonderlab.com/terraform-aws-iam-users/) 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](https://www.itwonderlab.com/aws-terraform-tutorial-aws-iam-roles-policies/).

### 2. The workflow

```yaml title=".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](https://www.itwonderlab.com/aws-terraform-tutorial-terraform-backends/) 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](https://www.itwonderlab.com/terraform-secrets-management/).
- Add [security scanning](https://www.itwonderlab.com/terraform-security-scanning-tflint-checkov-trivy/) and [tests](https://www.itwonderlab.com/terraform-testing-opentofu-test/) 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.
