Simplifying Azure Infrastructure Management with Terraform and GitHub Actions

26 Sep, 2023 | 6 minutes read

Infrastructure as code allows us to manage Microsoft Azure cloud environments with the same ease as we manage application code. By defining Azure infrastructure through configuration scripts, we gain the superpower of managing resources like virtual machines, compute, and cloud services rapidly, reliably, and repeatably. 

In this article, we’ll explore how Terraform’s robust configuration language integrates with GitHub Actions to automate infrastructure deployments across hybrid cloud and Azure environments.

Terraform, a tool by the HashiCorp, empowers us to define and automate our cloud architecture as code. Concurrently, GitHub Actions, a platform for continuous integration and continuous delivery (CI/CD) seamlessly integrated with GitHub, facilitates the automation of our build, test, and deployment pipelines.

Follow along as we discover how these tools can help you implement infrastructure as code on Microsoft Azure and step closer to DevOps heaven. The journey begins now…

Prerequisites

  • Azure CLI configured on your local machine
  • A valid Azure subscription
  • Azure Remote Backend for Terraform
  • GitHub Account and GitHub Repository

Our initial step will be to create an Azure Service Principal. To do so, we must first authenticate with Azure. Enter the following command to authenticate using Azure CLI:

az login

After authentication, the process will launch the browser, and we will be ready to proceed. To obtain a list of Azure subscriptions, execute the following command:

az account list --output table

Next, use the following command to select the subscription — you should find the subscription ID from the previous prompt where you logged in.

az account set --subscription <Azure-SubscriptionId>

Then, to create the service principal account, execute the following command:

az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/SUBSCRIPTION_ID" --name="GitHub-Actions"

If needed you can change the name “GitHub-Actions” to something more descriptive.

The output will provide us with the following values for our Terraform variables:

  • Azure `appId` corresponds to Terraform `client_id`.
  • Azure `password` corresponds to Terraform `client_secret`.
  • Azure `tenant` corresponds to Terraform `tenant_id`.

Our Terraform state file must be stored in a remote backend. To achieve this, you can use either Azure CLI or Terraform to perform the following steps in order to create a remote backend for Terraform state.

Using Azure CLI

First, create a Resource Group

az group create -n tfstates -l eastus2

Next, create a Storage Account

az storage account create -n iwtfstateaccount -g tfstates -l eastus2 --sku Standard_LRS

Finally, create a Storage Account Container

az storage container create -n tfstate --account-name iwtfstateaccount

These commands create the Azure resources required to manage your Terraform state.

Using Terraform

Alternatively, you can configure Terraform’s remote backend using Terraform itself. Here’s a Terraform configuration example for configuring Azure Storage as a remote backend:

Create `main.tf` to Configure the Azure Resources

# Azure Resource Management Configuration
provider "azurerm" {
  features {}
}

# Create an Azure Resource Group
resource "azurerm_resource_group" "tfstates" {
  name     = "tfstates"
  location = "East US 2"
}

# Create an Azure Storage Account
resource "azurerm_storage_account" "iwtfstateaccount" {
  name                     = "iwtfstateaccount"
  resource_group_name      = azurerm_resource_group.tfstates.name
  location                 = azurerm_resource_group.tfstates.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

# Create an Azure Storage Container
resource "azurerm_storage_container" "tfstate" {
  name                  = "tfstate"
  storage_account_name  = azurerm_storage_account.iwtfstateaccount.name
  container_access_type = "private"
}

# Output the login server for the Azure Storage Account
output "storage_account_name" {
  value = azurerm_storage_account.iwtfstateaccount.name
}

# Output the name of the Storage Container
output "storage_container_name" {
  value = azurerm_storage_container.tfstate.name
}

In the code snippet above, we have already created a main.tf file to define the Azure resources we intend to manage with Terraform.

Configure the Terraform Backend in `backend-tf`

Now, in a separate ‘backend-tf’ file, we’ll configure the Terraform backend. This ensures that the Terraform state is stored in Azure Storage.

# Azure Terraform Backend Configuration
terraform {
  backend "azurerm" {
    resource_group_name  = "tfstates"
    storage_account_name = "iwtfstateaccount"
    container_name       = "tfstate"
    key                  = "terraform.tfstate"
  }
}

Following this, let’s create a private GitHub repository. Afterward, clone the repository and add your Terraform code to it. The next crucial step is to create secrets for Azure, which will be securely stored in our repository. To do this, go to the Settings menu, expand the Secrets section, and then select Actions.

This will redirect you to the page where you may properly manage the secrets of your repository. You will then be prompted to enter a Name and a Secret. It’s important to avoid including any spaces or pressing the enter key after inputting the Secret, as doing so could result in pipeline issues.

manage the secrets of your repository

Your setup is complete and ready to begin once you’ve successfully entered all of the essential secrets. This secures sensitive information while maintaining the smooth execution of the project’s workflow.

secures sensitive information while maintaining the smooth execution of the project's workflow

In the following Terraform code snippet, we demonstrate how to build an Azure provider, create the necessary Azure resources, and configure a Terraform backend to securely store state files. This setup ensures that Terraform can efficiently manage your infrastructure.

# Azure Resource Management Configuration
provider "azurerm" {
  features {}
}

# Create an Azure Resource Group
resource "azurerm_resource_group" "tfstates" {
  name     = "tfstates"
  location = "East US 2"
}

# Create an Azure Storage Account
resource "azurerm_storage_account" "iwtfstateaccount" {
  name                     = "iwtfstateaccount"
  resource_group_name      = azurerm_resource_group.tfstates.name
  location                 = azurerm_resource_group.tfstates.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

# Create an Azure Storage Container
resource "azurerm_storage_container" "tfstate" {
  name                  = "tfstate"
  storage_account_name  = azurerm_storage_account.iwtfstateaccount.name
  container_access_type = "private"
}

# Output the login server for the Azure Storage Account
output "storage_account_name" {
  value = azurerm_storage_account.iwtfstateaccount.name
}

# Output the name of the Storage Container
output "storage_container_name" {
  value = azurerm_storage_container.tfstate.name
}

# Azure Terraform Backend Configuration
terraform {
  backend "azurerm" {
    resource_group_name  = azurerm_resource_group.tfstates.name
    storage_account_name = azurerm_storage_account.iwtfstateaccount.name
    container_name       = azurerm_storage_container.tfstate.name
    key                  = "terraform.tfstate"
  }
}

In this configuration, we:

  • Configure the Azure provider to manage Azure cloud resources;
  • Create a “tfstates” Azure Resource Group in the “East US 2” region;
  • Create an Azure Storage Account named “iwtfstateaccount” within the same resource group, ensuring it uses Standard tier storage with LRS replication;
  • Create an Azure Storage Container called “tfstate” in the storage account, with private access;
  • Utilize Terraform outputs to specify the names of the Azure Storage Account and Storage Container;
  • Set the Terraform backend to save state files in the specified Azure Storage Container, using the key “terraform.tfstate” for state file storage;

You can securely maintain your infrastructure’s Terraform state files within an Azure Storage Account using this configuration, enabling collaborative and consistent infrastructure management. Now, let’s move on to GitHub Actions.

We have the option of using pre-made Terraform templates through the Terraform by HashiCorp template or creating a custom workflow using a YAML file to configure our GitHub Action Workflow for Terraform.

For the purpose of this guide, we’ll be creating our custom script by choosing the “Set up a workflow yourself” option. Use the provided code snippet, name the workflow “terraform-plan-apply.yml,” and then proceed to Start Commit.

name: Terraform Plan/Apply Workflow

on:
  push:
    branches:
      - main
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  terraform:
    name: Terraform
    env:
      ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
      ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
      ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
      ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
    runs-on: ubuntu-latest
    environment: production

    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout
        uses: actions/checkout@v3

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

      - name: Terraform Init
        run: terraform init

      - name: Terraform Plan
        run: terraform plan -input=false

      - name: Terraform Apply
        run: terraform apply -auto-approve -input=false

This script automates the use of Terraform to manage Azure infrastructure by initializing, planning, and applying changes whenever there’s a code update to the main branch or when manually triggered.

After creating this file and committing it, you will find it in the `.github/workflows` directory of your repository. In contrast, we’re going to create a “terraform-destroy.yml” file that serves as a GitHub workflow script to destroy our infrastructure whenever our tasks are completed.

name: Terraform Destroy

on:
  workflow_dispatch: # This workflow is manually triggered

permissions:
  contents: read

jobs:
  terraform_destroy:
    name: Terraform Destroy
    env:
      ARM_CLIENT_ID: ${{ secrets. ARM_CLIENT_ID }}
      ARM_CLIENT_SECRET: ${{ secrets. ARM_CLIENT_SECRET }}
      ARM_SUBSCRIPTION_ID: ${{ secrets. ARM_SUBSCRIPTION_ID }}
      ARM_TENANT_ID: ${{ secrets. ARM_TENANT_ID }}
    runs-on: ubuntu-latest
    environment: production

    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout
        uses: actions/checkout@v2

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

      - name: Terraform Init
        run: terraform init

      - name: Terraform Destroy
        run: terraform destroy -auto-approve -input=false

In this manually triggered script, we use Azure credentials saved in secrets to destroy our Terraform infrastructure when necessary.

After committing the GitHub Actions Workflow file, it will be executed, and you can inspect its execution by clicking on the Workflow’s output, as shown below:

Conclusion

In summary, with the seamless integration of Azure, Terraform, and GitHub Actions, we empower our teams to streamline development processes effortlessly. This enables us to confidently verify and deploy changes onto Azure with remarkable ease. This dynamic technological synergy propels us towards a more efficient and agile DevOps ecosystem.