Deploying Infrastructure to Azure with Terraform and GitHub Actions

Are you tired of manually deploying your infrastructure to Azure? Are you looking for a more efficient and secure way to manage your cloud resources? Look no further! In this post, we’ll walk through how to automate the deployment of Azure infrastructure using Terraform and GitHub Actions.

Prerequisites

Before we dive into the automation process, you’ll need to create an Azure service principal. Follow this step-by-step guide to create a service principal in the Azure portal. Make sure to note down the following credentials:

  • ARM_CLIENT_ID: Azure CLIENT ID.
  • ARM_CLIENT_SECRET: Azure CLIENT SECRET.
  • ARM_SUBSCRIPTION_ID: Azure SUBSCRIPTION ID.
  • ARM_TENANT_ID: Azure TENANT ID.

Next, add these credentials as secrets in your GitHub repository. These secrets will be used by GitHub Actions to authenticate with Azure during the deployment process.

Setting up the Pipeline

We’ve prepared a demo repository on GitHub to showcase the automation process. You can find the repository here.

Workflow Files

Inside the repository, you’ll find three workflow files:

terraform-plan.yml: This workflow initializes Terraform, validates configurations, and generates a Terraform plan. It uploads the plan as an artifact.

name: "TF_Plan"

on:
  workflow_call:
    inputs:
      path:
        description: "Terraform Root Path"
        required: true
        type: string
      tf_version:
        description: 'Terraform Version. e.g: 1.3.0 Default=latest.'
        required: false
        type: string
        default: latest
      tf_vars_file:
        description: 'Terraform TFVARS file name.'
        required: true
        type: string
      az_backend_resource_group:
        description: 'Azure Resource Group for the backend storage account is hosted.'
        required: true
        type: string
      az_backend_storage_acc:
        description: 'Azure Storage Account for the backend state is hosted.'
        required: true
        type: string
      az_backend_container_name:
        description: 'Azure Storage account container for backend Terraform state is hosted.'
        required: true
        type: string
      tf_key:
        description: 'Terraform state file name for this plan. Workflow artifact will use same name'
        required: true
        type: string
      # environment:
      #   description: 'manual approvals in GitHub Actions with the Environments.'
      #   required: true
      #   type: string
    secrets:
      ARM_CLIENT_ID:
        description: 'Azure CLIENT ID.'
        required: true
      ARM_CLIENT_SECRET:
        description: 'Azure CLIENT SECRET.'
        required: true
      ARM_SUBSCRIPTION_ID:
        description: 'Azure SUBSCRIPTION ID.'
        required: true
      ARM_TENANT_ID:
        description: 'Azure TENANT ID.'
        required: true

jobs:
  build-apply:
    runs-on: ubuntu-latest
    # environment: ${{ inputs.environment }}
    
    defaults:
      run:
        shell: bash
        working-directory: ${{ inputs.path }}

    env:
      STORAGE_ACCOUNT: ${{ inputs.az_backend_storage_acc }}
      CONTAINER_NAME: ${{ inputs.az_backend_container_name }}
      RESOURCE_GROUP: ${{ inputs.az_backend_resource_group }}
      TF_KEY: ${{ inputs.tf_key }}.tfstate
      TF_VARS: ${{ inputs.tf_vars_file }}
      ###AZURE Client details###
      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 }}
    
    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: tfsec
      uses: aquasecurity/[email protected]
      with:
        version: latest
        soft_fail: true 

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3
      with:
        terraform_version: ${{ inputs.tf_version }}
     
    - name: Terraform Init
      run: terraform init --backend-config="storage_account_name=$STORAGE_ACCOUNT" --backend-config="container_name=$CONTAINER_NAME" --backend-config="resource_group_name=$RESOURCE_GROUP" --backend-config="key=$TF_KEY"
  
    - name: Terraform Validate
      run: terraform validate 
      
    - name: Terraform Plan
      id: plan
      run: terraform plan --var-file=$TF_VARS --out=plan.tfplan
      continue-on-error: true
     
    - name: Terraform Plan Status
      if: steps.plan.outcome == 'failure'
      run: exit 1

    - name: Compress TF Plan artifact
      run: zip -r ${{ inputs.tf_key }}.zip ./*

    - name: Upload Artifact
      uses: actions/[email protected]
      with:
        name: "${{ inputs.tf_key }}"
        path: "${{ inputs.path }}/${{ inputs.tf_key }}.zip"
        retention-days: 5

terraform-apply.yml: This workflow applies the Terraform plan generated in the previous step, deploying the infrastructure to Azure.

name: "TF_Apply"

on:
  workflow_call:
    inputs:
      path:
        description: "Terraform Root Path"
        required: true
        type: string
      tf_version:
        description: 'Terraform Version. e.g: 1.3.0 Default=latest.'
        required: false
        type: string
        default: latest
      tf_vars_file:
        description: 'Terraform TFVARS file name.'
        required: true
        type: string
      az_backend_resource_group:
        description: 'Azure Resource Group for the backend storage account is hosted.'
        required: true
        type: string
      az_backend_storage_acc:
        description: 'Azure Storage Account for the backend state is hosted.'
        required: true
        type: string
      az_backend_container_name:
        description: 'Azure Storage account container for backend Terraform state is hosted.'
        required: true
        type: string
      tf_key:
        description: 'Terraform state file name for this plan. Workflow artifact will use same name'
        required: true
        type: string
      # environment:
      #   description: 'manual approvals in GitHub Actions with the Environments.'
      #   required: true
      #   type: string
    secrets:
      ARM_CLIENT_ID:
        description: 'Azure CLIENT ID.'
        required: true
      ARM_CLIENT_SECRET:
        description: 'Azure CLIENT SECRET.'
        required: true
      ARM_SUBSCRIPTION_ID:
        description: 'Azure SUBSCRIPTION ID.'
        required: true
      ARM_TENANT_ID:
        description: 'Azure TENANT ID.'
        required: true

jobs:
  build-apply:
    runs-on: ubuntu-latest
    # environment: ${{ inputs.environment }}
    
    defaults:
      run:
        shell: bash
        working-directory: ${{ inputs.path }}

    env:
      STORAGE_ACCOUNT: ${{ inputs.az_backend_storage_acc }}
      CONTAINER_NAME: ${{ inputs.az_backend_container_name }}
      RESOURCE_GROUP: ${{ inputs.az_backend_resource_group }}
      TF_KEY: ${{ inputs.tf_key }}.tfstate
      ###AZURE Client details###
      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 }}
    
    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Download Artifact
      uses: actions/[email protected]
      with:
        name: ${{ inputs.tf_key }}
        path: ${{ inputs.path }}

    - name: Decompress TF Plan artifact
      # run: unzip ${{ inputs.tf_key }}.zip
      run: echo "y" | unzip -o ${{ inputs.tf_key }}.zip
      
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3
      with:
        terraform_version: ${{ inputs.tf_version }}
    
    - name: Terraform Init
      run: terraform init --backend-config="storage_account_name=$STORAGE_ACCOUNT" --backend-config="container_name=$CONTAINER_NAME" --backend-config="resource_group_name=$RESOURCE_GROUP" --backend-config="key=$TF_KEY"

    - name: Terraform Apply
      run: terraform apply plan.tfplan 

pipeline.yml: This file orchestrates the overall pipeline by calling the terraform-plan.yml and terraform-apply.yml workflows.

name: 'Infra_build'

on:
  push:
    branches:
      - main
  pull_request:

permissions:
  contents: read

jobs:
    Dev_Plan:
        uses: littleworks-inc/azure_terraform_demo/.github/workflows/terraform-pan.yml@main
        with:
          path: .
          tf_version: latest
          az_backend_resource_group: terraform_test      
          az_backend_storage_acc: amarazureteststorage   
          az_backend_container_name: terraform-test 
          tf_key: gitlab-terraform            
          tf_vars_file: dev.tfvars 
          # environment: dev
        secrets:
          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 }}

    Dev_Deploy:
        needs: Dev_Plan
        uses: littleworks-inc/azure_terraform_demo/.github/workflows/terraform-apply.yml@main
        with:
          path: .
          tf_version: latest
          az_backend_resource_group: terraform_test      
          az_backend_storage_acc: amarazureteststorage   
          az_backend_container_name: terraform-test 
          tf_key: gitlab-terraform            
          tf_vars_file: dev.tfvars 
          # environment: dev
        secrets:
          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 }}

Running the Pipeline

Once you’ve set up the secrets and added the workflow files to your repository, every push to the main branch will trigger the pipeline. The pipeline consists of two jobs: planning and deployment. The planning job creates a Terraform plan, while the deployment job applies the plan, provisioning the infrastructure in Azure.

Conclusion

By leveraging Terraform and GitHub Actions, you can automate the deployment of your Azure infrastructure, saving time and reducing the risk of manual errors. With just a few simple steps, you can set up a robust pipeline for managing your cloud resources efficiently.

Check out the demo repository and give it a try yourself! If you have any questions or need further assistance, feel free to reach out.

Happy automating!