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.
Type: AWS::IAM::ManagedPolicy
Properties:
Description: 'IAM Roles for Terraform State Resources'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:DeleteItem
- dynamodb:Describe*
- dynamodb:ListTagsOfResource
Resource:
- !Sub 'arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/TerraformDynamoDBTable'
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::TerraformBucket/*'
- Effect: Allow
Action:
- s3:List*
- s3:Get*
- s3:Describe*
Resource:
- !Sub 'arn:${AWS::Partition}:s3:::TerraformBucket'
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.
Summary
This example showed one way to wrap up the Terraform workflow in GitHub Actions using OIDC.
I hope this helps someone else!
Cheers