Skip to content

Build Infrastructure in Azure using GitHub Actions

This page walks through building infrastructure in Azure using GitHub Actions and Terraform IaC.


flowchart TD
  subgraph Detect_Changes_and_Build_Matrix
    B1[Checkout Repository]
    B2[Detect Changed Terraform Files]
    B3{Any TF changes?}
    B4[Early Exit: No TF Changes]
    B5[Determine CI Mode per Dir]
    B6[Build Matrix dir→mode]
    B1 --> B2 --> B3
    B3 -- "No" --> B4
    B3 -- "Yes" --> B5 --> B6
  end

  subgraph Run_Terraform_Per_Directory
    C1[Checkout Repository]
    C2[Configure Git for Private Modules]
    C3[Cache Terraform Providers]
    C4[Set up Terraform]
    C5[Ensure TF Lockfile Exists]
    C6[Terraform Format Check]
    C7[Run TFLint]
    C8[Terraform Validate]
    C9{Mode: plan or deploy?}
    C10[Azure Login with OIDC]
    C11[Terraform Plan]
    C12{Mode: deploy?}
    C13[Terraform Apply]
    C14[Sanitize Artifact Name]
    C15[Upload Artifact]

    C1 --> C2 --> C3 --> C4 --> C5 --> C6 --> C7 --> C8 --> C9
    C9 -- "Yes" --> C10 --> C11 --> C12
    C9 -- "No" --> C14
    C12 -- "Yes" --> C13 --> C14
    C12 -- "No" --> C14
    C14 --> C15
  end

  B6 --> C1

You can build resources in Azure using Terraform IaC and have GitHub Actions work as a pipeline to deploy your code automatically for you. The combination of GitHub Actions and Terraform IaC introduces a level of automation and consistency that is hard to achieve with manual CLI processes. GitHub Actions automate the deployment process, significantly reducing the need for manual interventions and thereby lowering the risk of human error. With automated workflows, operations such as initiating Terraform scripts can be set to trigger automatically in response to specific events, like code commits or pull requests.

Azure subscription

Sign up for a free Azure subscription or use an existing one.

Azure storage account

Terraform should be set up to store the state file remotely. As you are offloading the execution of the Terraform code to a GitHub server and by default Terraform stores the state file in the working directory, you should store the state file remotely — Azure Storage provides an excellent platform for this purpose. You require an Azure resource group, storage account, and blob container to exist, and you need to know the name of each resource so you can reference them in your Terraform deployment.

Setup Azure Storage for Terraform State File

GitHub repository

This example uses a GitHub repository that is dedicated to an Azure subscription, based on what permissions you gave to the OIDC profile configured in the previous step. You can limit this further by restricting the OIDC profile to specific resource group(s), or make it wider and open up multiple Azure subscriptions. The key point to understand is that the more permissions you give your OIDC profile, the wider the blast radius — meaning the more damage it could do if something goes bad.

For this example repository, there are two main working areas. The first is where the GitHub Actions YAML files are stored, in .github/workflows, and the second is the working directory where the Terraform code is stored, in ./codefolder1 and ./codefolder2. These code folders are referenced specifically in the Actions file as explained below.

This is the directory layout as described above.

<GitHub-account>
|_<GitHub-repository>
|__/.github
|___/workflows
|   |<ACTION-FILE1>.yaml
|   |<ACTION-FILE2>.yaml
|__/terraform
|  |main.tf
|__/website
|  |main.tf

Authenticate to Azure

Your GitHub repository must be configured with access to Azure. There are two main methods to achieve this: either use a service principal name and hardcode the username and password as variables, or use OIDC and use tokens instead. This example uses OIDC, as it enhances security.

GitHub action

Actions are YAML files that must exist in a specific directory in your chosen GitHub repository. This is an example action, but there are many variations on how this can be configured which won't be explained here. A key point here is that this action is defined to execute based on the targeted working-directory, meaning you can have multiple actions defined in the same repository, each targeting a different working directory.

name is the name of the action and is what is displayed in the workflow page in the GitHub portal

on is the trigger — what causes the action to execute. You can trigger on push, pull request, or, as here, set it to manual so the operator must choose to run the workflow in the GitHub Actions window

defaults can be used to set the working-directory from which the Terraform code will be pulled. Using the example directory tree above, the value here would be ./codefolder1

runs-on defines the OS of the runner, which here is Ubuntu, but you can choose others as required. Ubuntu is a good choice as it's generally faster to spin up than Windows, and since Terraform runs on Linux, it makes it a good fit

Az CLI login: Uses the azure/login@v1 action to authenticate with Azure using the provided credentials (client ID, tenant ID, and subscription ID)

Checkout the working directory: Uses the actions/checkout@v2 action to fetch the contents of the repository to the runner, specifically the code folder

Setup Terraform: Uses the hashicorp/setup-terraform@v1 action to install and configure the Terraform CLI on the runner

Terraform Init: Runs the terraform init command to initialize the Terraform working directory. Sets environment variables (ARM_CLIENT_ID, ARM_SUBSCRIPTION_ID, ARM_TENANT_ID, and ARM_USE_OIDC) for Azure authentication

Terraform Validate: Runs the terraform validate command to check the configuration files for any syntax errors and required input variables

Terraform Plan: Runs the terraform plan command to generate an execution plan for creating or modifying infrastructure resources. Sets the same Azure authentication environment variables as in the Terraform Init step

Terraform Apply: Runs the terraform apply --auto-approve command to apply the changes defined in the Terraform configuration and create or modify the infrastructure resources. Sets the same Azure authentication environment variables as in the previous steps

Note: The Azure authentication environment variables (ARM_CLIENT_ID, ARM_SUBSCRIPTION_ID, ARM_TENANT_ID, and ARM_USE_OIDC) are populated using the secrets stored in the GitHub repository's settings.

name: GitHub Actions Example

on: [workflow_dispatch]

permissions:
  id-token: write
  contents: read

defaults:
  run:
    working-directory: ./codefolder1

jobs:
  login:
    runs-on: ubuntu-latest
    steps:
      - name: 'Az CLI login'
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: 'Checkout the working directory'
        uses: actions/checkout@v2

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1

      - name: Terraform Init
        run: terraform init
        env:
          ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          ARM_USE_OIDC: true

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        run: terraform plan
        env:
          ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          ARM_USE_OIDC: true

      - name: Terraform Apply
        run: terraform apply --auto-approve
        env:
          ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          ARM_USE_OIDC: true

Terraform code

In your GitHub repository you need some Terraform code to execute with your GitHub action. The same GitHub repository needs to host the code and the action. The example below creates an Azure resource group — you can use the same values or edit as needed.

This example puts all the Terraform code into the same main.tf file, but you can of course structure the Terraform code as you need. The key point is that Terraform only executes code in the same working directory — you can still use modules etc. as normal.

Terraform block: Specifies the required providers for this configuration. In this case, it requires the azurerm provider

Backend block: Configures the backend for storing Terraform state. Uses the azurerm backend, which stores the state in an Azure resource. Specifies the resource group, storage account, container, and key for storing the state file

Provider block: Configures the azurerm provider for interacting with Azure. Sets the use_oidc parameter to true, enabling OpenID Connect (OIDC) authentication for the provider. Includes an empty features block, indicating that no specific features are enabled

Resource block: Defines an Azure resource group using the azurerm_resource_group resource type. Sets the name of the resource group to rg-github-actions-example. Specifies the location of the resource group as eastus

Info

Support for OpenID Connect was added in version 3.7.0 of the Terraform AzureRM provider. Note the use_oidc = true line in the provider block.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=3.7.0"
    }
  }

  backend "azurerm" {
    resource_group_name  = "rg-ghactions"
    storage_account_name = "saghactions"
    container_name       = "tfstate"
    key                  = "github-actions-example.tfstate"
  }
}

provider "azurerm" {
  use_oidc = true
  features {}
}

resource "azurerm_resource_group" "github-actions-example" {
  name     = "rg-github-actions-example"
  location = "eastus"
}