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!