Deploy to Azure with Terraform & Github Actions

The Plan

In this post we will be going through the process of setting up automated Azure infrastructure deployments using Terraform and GitGub Actions. We will also be implementing some integration tests that will be run as an Action prior to the deployment. This means the model of deployment will be:

  • Create a branch from master
  • Make changes in new branch
  • Create a pull request to master - Integration tests Action will run
  • Once tests have passed, merge the pull request - Deploy Action will run

We will end up with something like this:

Setting up GitHub Project

There are a few things we need to do to get our GitHub project setup for GitHub actions to be able to authenticate with Azure.

Create Azure AD App Registration

If you look at the Terraform documentation for the Azure provider you will notice there are numerous methods that can be used for Authentication. In this case we will be using a Service Principal with a Client Secret and generating the credentials via an Azure AD App Registration. This way we can have a set of credentials specifically for Terraform. To do this:

  • Go to the Subscriptions service and take note of your target Subscription ID
  • Browse to App registration in the Azure Active Directory service and select New registration
  • Set the name to something like Terraform
  • Leave the Supported account types as the default Single tenant option
  • We can ignore the Redirect URI
  • Hit Register to create the App Registration

Once you have registered the application there will be a few values to take note of:

  • Application (client) ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  • Directory (tenant) ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

You will then need to go to Certificates & secrets section of your app registration and create a new client secret. Take note of this value also.

GitHub Secrets

Once you have the required ID and Secrets the next step is to add them the secret store in your GitHub project. You can name these whatever you like, I have named mine like this:

Creating a GitHub Action for Integration Testing

All GitHub actions live in your projects .github/workflows/ folder. For this integration testing we will create a tests.yml file in this folder. This action will use Hashicorp’s terraform actions to perform terraform fmt, validate and plan. The action below assumes your terraform is in a directory called terraform.

This is configured to run on a pull request. Below we will configure a deploy job to run on a merge to master.

name: 'Validate and plan deploy'
on:
  - pull_request
jobs:
  validate-terraform:
    name: 'Validate and plan deploy'
    env:
      ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
      ARM_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
      ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout'
        uses: actions/checkout@master

      - name: 'Terraform Format'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'fmt'
          tf_actions_working_dir: "./terraform"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          
      - name: 'Terraform Init'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'init'
          tf_actions_working_dir: "./terraform"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: 'Terraform Validate'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'validate'
          tf_actions_working_dir: "./terraform"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          
      - name: 'Terraform Plan'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'plan'
          tf_actions_working_dir: "./terraform"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Note in the env block, if you have named your secrets in GitHub differently to mine you will need to update the references here.

Creating a GitHub Action for the Deployment

For this one we will create a deploy.yml in the .github/workflows/ directory. This is configured to run on a merge to master. Again, you may need to update the GitHub secret references.

name: 'Deploy'
on:
  push:
    branches:
      - master
jobs:
  deploy-to-azure:
    name: 'Deploy to Azure'
    env:
      ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
      ARM_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
      ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout'
        uses: actions/checkout@master

      - name: 'Terraform Init'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'init'
          tf_actions_working_dir: "./terraform"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: 'Terraform Apply'
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: 0.12.24
          tf_actions_subcommand: 'apply'
          tf_actions_working_dir: "./terraform"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Setting up Terraform State in Azure

Since we are deploying into Azure via automation, we need to configure Terraform to store its state in Azure. This way no matter who triggers a deployment the state is consistent. This is configured in the backend block of the terraform deployment, in this case we are specifying an azure storage container for the state to be in. The key needs to be unique for each deployment:

terraform {
  backend "azurerm" {
    resource_group_name  = "AZURE-TERRAFORM-STATE"
    storage_account_name = "azureterraformstate"
    container_name       = "tfstate"
    key                  = "prod.terraform.blog.tfstate"
  }
}

Putting it all Together

Now that we have some GitHub action created and some terraform to deploy. The whole thing can be put together by:

  • Create a branch from master
  • Make changes in new branch
  • Create a pull request to master - Integration tests Action will run
  • Once tests have passed, merge the pull request - Deploy Action will run

The results of the tests.yml action will be posted to the pull request so it can be easily reviewed before merging and triggering a deploy.