Build Infrastructure in Azure using GitHub Actions

This page will walk through deploying infrastructure in Azure using GitHub Actions and Terraform IaC

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.

What will we build

  • Azure resource group
  • Azure storage account
  • Enable the storage account to host a static website
  • Publish a website to the static website

Requirements for this example project

Azure subscription somewhere to build the resources

Azure storage account to host the Terraform state file

Authentication to Azure that allows GitHub access to build resources in Azure

GitHub repository to host the code and provide a platform for the GitHub actions to run from

GitHub action is code that creates a workflow

Terraform code that builds some Azure resources

Azure subscription

Sign up for a free Azure subscription or use an existing one. It does not matter so long as you can create and configure the remainder of this guide.

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 and Azure Storage provides an excellent platform for this purpose.

You require an Azure resource group, storage account, and blob container to exist and know the name of each resource so you can code then in your Terraform deployment. The key value is what the name of the state file being created and it should be unique to each deployment.

You can check my other page for details about how this works and how to configure it using the AZ CLI.

Azure Remote state file

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. I will use OIDC for this example as it enhances security.

Authenticate to Azure from GitHub Actions

GitHub repository

This example will use a GitHub repository that is dedicated to a Azure subscription based on what permissions I 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 process and understand is the more permissions you give your OIDC profile the wider the blast radius meaning the more damage it could do it something goes bad.

For this example repository, there will be 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 Terrafrom 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

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 define in the same repository and each one can target 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, in other words, what causes the action to execute. You can use on push, pull request, or as here set it to manual so the operator must choose to run the workflow in the GitHub action window

defaults can be used to set the working-directory from which the Terraform code will be pulled. So 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. But Ubuntu is a good choice as it’s generally faster to spin up than Windows and as Terraform runs on Linux it makes it a good choice.

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 can be used that will create 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 you need to respect that Terraform only executes code in the same working directory, you can still use modules etc as normal of course.

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.

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"  
}
Last modified July 21, 2024: update (e2ae86c)