Skip to content

Tofu Apply

This workflow simply runs a tofu apply into a GitHub Environment following an approval of the PR and then a merge into the target branch. Also knows as "apply after merge".

Warning

The GitHub Environment has a condition that any workflow MUST be approved before it can be ran inside. This obviously then stops anyone just deploying anything without human intervention.

Workflow

.github/workflows/tofu_apply.yml
name: Tofu Apply

on:
  push:
    branches:
      - dev
      - main
    paths:
      - 'tofu/**'

defaults:
  run:
    shell: bash

permissions:
  contents: read
  id-token: write

env:
  TF_IN_AUTOMATION: "true"
  TF_INPUT: "false"

jobs:
  apply:
    runs-on: ubuntu-latest
    environment: ${{ github.ref_name == 'main' && 'prod' || 'dev' }}
    steps:
      - name: Check Out Code
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Configure OpenTofu
        uses: opentofu/setup-opentofu@12f4debbf681675350b6cd1f0ff8ecfbda62027b # v1.0.4

      - name: Print Tofu Version
        run: tofu --version

      - name: Get OIDC Token File
        run: |
          curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value > /tmp/web-identity-token

      - name: Tofu Init
        run: tofu init --backend-config="key=${{ github.event.repository.name }}/${{ github.ref_name == 'main' && 'prod' || 'dev' }}/terraform.tfstate" --var-file="${{ github.ref_name == 'main' && 'prod' || 'dev' }}.tfvars"
        working-directory: ${{ github.workspace }}/tofu

      - name: Tofu Apply
        run: tofu apply --var-file="${{ github.ref_name == 'main' && 'prod' || 'dev' }}.tfvars" -no-color --auto-approve
        working-directory: ${{ github.workspace }}/tofu

Configuration

This workflow will only run on a push to dev and main which correspond to our two environments in AWS. Additionally it will only run if the tofu/ directory has been updated as that is where the OpenTofu configuration is. It uses two environment variables that help tailor the output of messages to the system the tool is interacting with, such as a CI/CD Pipeline where it's non interactive.

Here we use the environment keyword to tell the workflow that it will run in a GitHub Environment. As we only have two environments, we use a condition using the ref_name in the github context. If the workflow is running inside the main branch, we know the environment is prod, otherwise if it is running in dev we know the environment is dev

Note

As an improvement I would make this more dynamic and not just for two environments.

As we need to authenticate with AWS, we need to grant the id-token: write permission so we can request the JWT using OIDC. The contents: read permission is to allow the PR branch to be checked out.

As mentioned in Tofu Checks, we use the TF_IN_AUTOMATION and TF_INPUT environment variables to control the output OpenTofu shows to the user. In this case a non-interactive CI/CD pipeline.

Jobs

There is only one job in this workflow:

  1. apply

apply

.github/workflows/tofu_apply.yml
apply:
  runs-on: ubuntu-latest
  environment: ${{ github.ref_name == 'main' && 'prod' || 'dev' }}
  steps:
    - name: Check Out Code
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    - name: Configure OpenTofu
      uses: opentofu/setup-opentofu@12f4debbf681675350b6cd1f0ff8ecfbda62027b # v1.0.4

    - name: Print Tofu Version
      run: tofu --version

    - name: Get OIDC Token File
      run: |
        curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value > /tmp/web-identity-token

    - name: Tofu Init
      run: tofu init --backend-config="key=${{ github.event.repository.name }}/${{ github.ref_name == 'main' && 'prod' || 'dev' }}/terraform.tfstate" --var-file="${{ github.ref_name == 'main' && 'prod' || 'dev' }}.tfvars"
      working-directory: ${{ github.workspace }}/tofu

    - name: Tofu Apply
      run: tofu apply --var-file="${{ github.ref_name == 'main' && 'prod' || 'dev' }}.tfvars" -no-color --auto-approve
      working-directory: ${{ github.workspace }}/tofu

The base branch is checked out and then OpenTofu is installed into the environment using their official Action. We confirm the version of OpenTofu being used in the workflow, and then request the OIDC Token File to authenticate with AWS.

After we initialise OpenTofu to download the providers used: aws and http. You might also notice we're using a partial backend here, as we have the workflow managing the key to the state file. We could technically pass in a .hcl file that also has the backend configuration, but wanted to make the OpenTofu workflow as simple as possible.

The apply is then automatically ran and infrastructure is deployed into the destination environment.