Blog

Setting Up a Terraform Pipeline in Azure DevOps

(3 minutes to read)
Categories:

Are you ready to automate your Terraform deployments using Azure DevOps? In this short post, I’ll walk you through creating a Terraform pipeline using Azure YAML Pipelines.

Directory Structure

Before we dive into the pipeline setup, make sure your project follows this directory structure:

.
└── dev

Each folder is a separate environment the state of which we would like to track.

Now, let’s explore the steps of our Azure YAML Terraform pipeline:

Step 1: Terraform Version Configuration

To begin, it is important to ensure that the correct Terraform version is in use. This step is essential for maintaining consistency.

Step 2: Initializing Terraform and Creating a Plan

In this phase, we initialize Terraform and create an execution plan. The plan outlines the changes Terraform intends to make to your infrastructure.

Step 3: Manual Validation

You want to manually review Terraform plan and approve changes before applying them. This step allows for such validation.

Step 4: Applying the Terraform Plan

When you’re confident in your changes, it’s time to apply the Terraform plan. This action executes the planned infrastructure updates.

Customization Tip

Feel free to adapt this pipeline to your specific workflow. For instance, you can add conditions to ensure it runs exclusively from the main branch of your version control system.

Pipeline Best Practices

While you might be tempted to optimize the pipeline for efficiency (reducing repetition), it’s often better to prioritize simplicity and readability. Yes, there may be some repetition in the pipeline, but this simplicity is invaluable when troubleshooting issues. It’s much easier to skim through one file than to search through multiple templates to find the source of a problem.

For a concrete example of this pipeline see below:

trigger: none

parameters:
- name: environment
  type: string
  values:
  - dev

variables:
  - name: terraform_version
    value: "1.5.5"
  - name: terraform_sha
    value: "ad0c696c870c8525357b5127680cd79c0bdf58179af9acd091d43b1d6482da4a"
  - ${{ if eq(parameters.environment, 'dev') }}:
    - name: storage_account_name
      value: ""
    - name: storage_account_resource_group_name
      value: ""
    - name: service_connection_name
      value: ""

name: ${{ parameters.environment }}_$(Date:yyyyMMdd).$(Rev:r)

pool: ubuntu-latest

jobs:
- job: terraform_plan
  displayName: "Terraform Plan"
  steps:
  - checkout: self
    fetchDepth: 1

  - bash: |
      curl -SL "https://releases.hashicorp.com/terraform/$(terraform_version)/terraform_$(terraform_version)_linux_amd64.zip" --output terraform.zip
      echo "$(terraform_sha) terraform.zip" | sha256sum -c -
      unzip terraform.zip
      chmod +x terraform
      ./terraform --version
      rm terraform.zip      
    displayName: download terraform of specific version
    workingDirectory: ${{ parameters.environment }}

  - task: AzureCLI@2
    displayName: terraform init and plan
    inputs:
      azureSubscription: $(service_connection_name)
      addSpnToEnvironment: true
      scriptLocation: inlineScript
      inlineScript: |
        export ARM_CLIENT_ID=$servicePrincipalId
        export ARM_CLIENT_SECRET=$servicePrincipalKey
        export ARM_TENANT_ID=$tenantId
        export ARM_SUBSCRIPTION_ID=$(az account show --query 'id' --output tsv)
        export ARM_SKIP_PROVIDER_REGISTRATION=true

        ./terraform init \
          -backend-config="storage_account_name=$(storage_account_name)" \
          -backend-config="container_name=terraform-state" \
          -backend-config="key=${{ parameters.environment }}.tfstate" \
          -backend-config="resource_group_name=$(storage_account_resource_group_name)"

        ./terraform plan -input=false -out plan.tf        
      workingDirectory: ${{ parameters.environment }}

  - task: PublishPipelineArtifact@1
    inputs:
      targetPath: terraform
      artifactType: pipeline
      artifactName: plan

- job: validation
  displayName: Validate Plan
  pool: server
  dependsOn: terraform_plan
  steps:
  - task: ManualValidation@0
    timeoutInMinutes: 30
    inputs:
      notifyUsers: |
        $(Build.RequestForEmail)        
      instructions: |
        Review Terraform plan        
      onTimeout: reject

- job: terraform_apply
  displayName: Terraform Apply
  dependsOn: validation
  steps:
  - checkout: none

  - task: DownloadPipelineArtifact@1
    inputs:
      buildType: current
      artifactName: plan
      targetPath: $(System.DefaultWorkingDirectory)
    displayName: download plan artifact

  - task: AzureCLI@2
    displayName: terraform apply
    inputs:
      azureSubscription: $(service_connection_name)
      addSpnToEnvironment: true
      scriptLocation: inlineScript
      inlineScript: |
        export ARM_CLIENT_ID=$servicePrincipalId
        export ARM_CLIENT_SECRET=$servicePrincipalKey
        export ARM_TENANT_ID=$tenantId
        export ARM_SUBSCRIPTION_ID=$(az account show --query 'id' --output tsv)
        export ARM_SKIP_PROVIDER_REGISTRATION=true

        chmod -R +x .terraform/providers
        chmod +x terraform

        ./terraform apply -input=false plan.tf        
    workingDirectory: ${{ parameters.environment }}
✨ If you found this post helpful and want to say "Thank you! 💖", you can treat me with a cup of tea.