Video Thumbnail for Lesson
9.2: GitHub Actions Automation

Automating Terraform Deployment with GitHub Actions

In this lesson, we will learn how to automate the deployment of a Terraform configuration using GitHub Actions based on GitHub events.

We will set up a workflow that deploys our infrastructure to different environments (staging and production) and runs tests to ensure that the configuration is working as expected.

Setting up the GitHub Action Workflow

  1. Create a new file named terraform.yml in the .github/workflows directory of your GitHub repository.

  2. Add three different triggers for the workflow:

    • Push to the main branch (for deploying to the staging environment)
    • Issue a new release (for deploying to the production environment)
    • Open a pull request (for running tests)
on:
  push:
    branches:
      - main
  release:
    types:
      - created
  pull_request:

Configuring the GitHub Action workflow

These are the major steps we want the workflow to handle.

  1. Set the working directory for Terraform commands
  2. Check out the codebase
  3. Set up Terraform using a specific version
  4. Run Terraform format check
  5. Initialize Terraform
  6. Run Terraform plan if it's a pull request
  7. Display the Terraform plan results in the action interface
  8. Run Terraform tests on a pull request
  9. Determine the environment to deploy (staging or production)
  10. Apply the global Terraform configuration (e.g., DNS Zone)
  11. Apply the staging or production Terraform configuration

These can be translated into the workflow yaml as follows:

name: "Terraform"
on:
  push:
    branches:
      - main
  release:
    types: [published]
  pull_request:

jobs:
  terraform:
    name: "Terraform"
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    defaults:
      run:
        working-directory: 07-managing-multiple-environments/file-structure/staging
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 1.0.1
          terraform_wrapper: false

      - name: Terraform Format
        id: fmt
        run: terraform fmt -check

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Plan
        id: plan
        if: github.event_name == 'pull_request'
        # Route 53 zone must already exist for this to succeed!
        run: terraform plan -var db_pass=${{secrets.DB_PASS }} -no-color
        continue-on-error: true

      - uses: actions/github-script@0.9.0
        if: github.event_name == 'pull_request'
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
            <details><summary>Show Plan</summary>
            \`\`\`${process.env.PLAN}\`\`\`
            </details>
            *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
            })

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

      - uses: actions/setup-go@v2
        with:
          go-version: "^1.15.5"

      - name: Terratest Execution
        if: github.event_name == 'pull_request'
        working-directory: 08-testing/tests/terratest
        run: |
          go test . -v timeout 10m

      - name: Check tag
        id: check-tag
        run: |
          if [[ ${{ github.ref }} =~ ^refs\/tags\/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo ::set-output name=environment::production
          elif [[ ${{ github.ref }} == 'refs/heads/main' ]]; then echo ::set-output name=environment::staging
          else echo ::set-output name=environment::unknown
          fi

      - name: Terraform Apply Global
        if: github.event_name == 'push' || github.event_name == 'release'
        working-directory: 07-managing-multiple-environments/file-structure/global
        run: |
          terraform init
          terraform apply -auto-approve

      - name: Terraform Apply Staging
        if: steps.check-tag.outputs.environment == 'staging' && github.event_name == 'push'
        run: terraform apply -var db_pass=${{secrets.DB_PASS }} -auto-approve

      - name: Terraform Apply Production
        if: steps.check-tag.outputs.environment == 'production' && github.event_name == 'release'
        working-directory: 07-managing-multiple-environments/file-structure/production
        run: |
          terraform init
          terraform apply -var db_pass=${{secrets.DB_PASS }} -auto-approve

You will notice that the workflow references secrets.AWS_ACCESS_KEY_ID and secrets.AWS_SECRET_ACCESS_KEY. These must be created within GitHub and correspond to an AWS IAM user with sufficient permissions to deploy the corresponding infrastructure.

You can use the same IAM user you have been utilizing so for for local deploys.

Note: This example uses a long lived AWS_SECRET_ACCESS_KEY. It is better practice to use OIDC to generate short lived credentials instead. For more information see: Configuring OpenID Connect in Amazon Web Services

Triggering the workflows

With this configuration in place we can then test it by:

  1. Commit and push the changes to the main branch to trigger the GitHub Action workflow.

  2. Issuing a new release to deploy to the production environment

    1. Go to your GitHub repository and click on the "Releases" tab.
    2. Click on "Draft a new release."
    3. Create a new release with a version number following the format "vX.Y.Z" (e.g., v1.0.0).
    4. Choose a tag and click on "Publish."
  3. Creating a pull request to run tests

    1. Create a new branch for your changes
    git checkout -b your-feature-branch
    
    1. Make an empty commit (for demonstration purposes)
    git commit --allow-empty -m "Testing on PR"
    
    1. Push the new branch to GitHub
    git push origin your-feature-branch
    
    1. In the GitHub interface, create a pull request from your feature branch to the main branch.

Verifying the Deployed Infrastructure

After these github action workflows complete, you should have a staging and production copy of your infrastructure, deployed from the latest commit on main as well as the latest repo release.

To avoid incurring additional costs, run terraform destroy for each environment when you are finished testing.