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.
nameis the name of the action and is what is displayed in the workflow page in the GitHub portal
onis 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
defaultscan be used to set theworking-directoryfrom which the Terraform code will be pulled. Using the example directory tree above, the value here would be./codefolder1
runs-ondefines 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 theazure/login@v1action to authenticate with Azure using the provided credentials (client ID, tenant ID, and subscription ID)
Checkout the working directory: Uses theactions/checkout@v2action to fetch the contents of the repository to the runner, specifically the code folder
Setup Terraform: Uses thehashicorp/setup-terraform@v1action to install and configure the Terraform CLI on the runner
Terraform Init: Runs theterraform initcommand to initialize the Terraform working directory. Sets environment variables (ARM_CLIENT_ID,ARM_SUBSCRIPTION_ID,ARM_TENANT_ID, andARM_USE_OIDC) for Azure authentication
Terraform Validate: Runs theterraform validatecommand to check the configuration files for any syntax errors and required input variables
Terraform Plan: Runs theterraform plancommand 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 theterraform apply --auto-approvecommand 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 theazurermprovider
Backend block: Configures the backend for storing Terraform state. Uses theazurermbackend, 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 theazurermprovider for interacting with Azure. Sets theuse_oidcparameter to true, enabling OpenID Connect (OIDC) authentication for the provider. Includes an emptyfeaturesblock, indicating that no specific features are enabled
Resource block: Defines an Azure resource group using theazurerm_resource_groupresource type. Sets the name of the resource group torg-github-actions-example. Specifies the location of the resource group aseastus
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"
}