Terraform, GitHub Actions & OIDC on AWS

Terraform, GitHub Actions & OIDC on AWS
Photo by Mohammad Rahmani / Unsplash

I've posted here how to configure the OIDC AWS Provider & GitHub Enterprise integration; however, nothing is better than an example of it working, and this post covers just that!

The Use Case

We use the Terraform GitHub Provider to manage organisations within our GitHub Enterprise instance. More information on that is posted here.

Before the OIDC Provider was available, we had to define an IAM User with the necessary permissions for Terraform to manage the state in S3 & DynamoDB. Official doco here.

That was horrible as we have long-lived keys in our environment defined as secrets for the target repository.

Terraform needs the following permissions, as shown in this Cloudformation resource block.

With OIDC, we can easily assign the above permissions to the role we will assume from the GitHub Action workflow.

So now Terraform will have short-lived credentials within the workflow to do its stuff.

But how do we put it all together?

The Actions Workflow

Firstly we are going to be triggering this workflow off our main branch, and we also want to be able to start a Terraform plan from the UI.

---
on:
  pull_request:
    branches: [master, main]
  workflow_dispatch:

Next, we define our GITHUB_TOKEN permissions for the workflow.

jobs:
  Validation:
    needs: docs
    name: Terraform Plan Workflow
    runs-on: [self-hosted]
    permissions:
      id-token: write         # Required for OIDC
      contents: read          # Required for accessing the repo contents
      issues: write           # Required to create issues
      pull-requests: write.   # Required to update the pull-request

Next up, we grab the latest version of the terraform binary from DockerHub. I'm happy to use the latest here, but others may like to pin it on a specific version.

    container:
      image: hashicorp/terraform:latest
      env:
        GITHUB_TOKEN: ${{ secrets.ORG_RW_PAT }}
        GITHUB_OWNER: ${{ secrets.ORG }}

The vital point to note here is that we inject environment variables into the container, which map to secrets defined in the repository.

These environment variables are required to authenticate to GitHub via the GitHub Terraform Provider.

  • GITHUB_TOKEN - This token has permission for all the actions Terraform needs to make within the GitHub Organisation.
  • GITHUB_OWNER - This is the target GitHub organisation or individual user account to manage.
    container:
      image: hashicorp/terraform:latest
      env:
        GITHUB_TOKEN: ${{ secrets.ORG_RW_PAT }}
        GITHUB_OWNER: ${{ secrets.ORG }}

The workflow now defines the steps.

Checkout the repository

steps:
  - name: Checkout Code
    uses: actions/checkout@v2

Authenticate to AWS

Uses the public action from AWS to authenticate and assume the role we defined for Terraform.

  - name: Configure AWS Credentials
    uses: aws-actions/configure-aws-credentials@v1
    with:
      role-to-assume: arn:aws:iam::123456789012:role/assumerole
      aws-region: ap-southeast-2
      audience: https://github.com/organisation

Terraform Initialization

Initialize the Terraform state. Important to note that the terraform_state.tfvars file is configured to point to the S3 & DynamoDB resources that we granted access to in our assume role.

  - name: Terraform Initialization
    run: |
      terraform init  -backend-config="terraform_state.tfvars" -upgrade
    id: terraform_init

Terraform Validation

Validate things before we consider kicking off a plan.

  - name: Terraform Validation
    run: terraform validate
    id: terraform_validate

Terraform Formatting

This is a great way to make sure your terraform code aligns to standards. I'd recommend always having this in place.

  - name: Terraform Format
    run: terraform fmt -check -diff -recursive
    id: terraform_fmt

Terraform Plan

Next we kick off the plan and continue the flow even if it errors out.

  - name: Terraform Plan
    id: terraform_plan
    if: github.event_name == 'pull_request'
    run: terraform plan -no-color
    continue-on-error: true

GitHub Script to update Pull Request

This step will post our plan summary back into the pull request.

  - name: Update Pull Request
    uses: actions/github-script@v4.0.2
    if: github.event_name == 'pull_request'
    with:
      github-token: ${{ secrets.GITHUB_TOKEN }}
      script: |
        const output = `${ "${{ steps.terraform_fmt.outcome }}" == "success" ? "\u2714" : "\u274c" } Terraform Format and Style 🖌
        ${ "${{ steps.terraform_init.outcome }}" == "success" ? "\u2714" : "\u274c" } Terraform Initialization ⚙️
        ${ "${{ steps.terraform_plan.outcome }}" == "success" ? "\u2714" : "\u274c" } Terraform Plan 📖
        ${ "${{ steps.terraform_validate.outcome }}" == "success" ? "\u2714" : "\u274c" } Terraform Validation 🤖

        *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

        github.issues.createComment({
          issue_number: context.issue.number,
          owner: context.repo.owner,
          repo: context.repo.repo,
          body: output
        })

Fail workflow if Terraform Plan errored

Finally, if the plan returned an error code, it fails the overall workflow so the user can investigate the output.

  - name: Terraform Plan Status
    if: steps.terraform_plan.outcome == 'failure'
    run: exit 1

The Terraform HCL

As per the documentation, the GitHub Terraform Provider has native support for environment variables to configure the token and owner values.

provider "github" {
  token = var.token # or `GITHUB_TOKEN`
  owner = var.owner # or `GITHUB_OWNER`
}

The plan output will display any resources you have defined in your terraform code for the GitHub Organisation.

Terraform Plan Workflow Output

Summary

This example showed one way to wrap up the Terraform workflow in GitHub Actions using OIDC.

I hope this helps someone else!