Terraform Azure DevOps to Azure example pipeline

I finally had the opportunity to work with Terraform on one of my recent projects. I have been building Infrastructure as Code with ARM templates or Bicep for years. Together with two friends, I even wrote a book on that! Terraform was always on my list of tools to work with. I had played around with it a little in my spare time but never got the opportunity to put it to use in an actual project. This blog will help you get started!

One of the things that I noticed while starting with Terraform is that the learning curve is a bit steeper than Bicep. That is because Terraform works with this state that it stores somewhere in a file. Where Bicep sends your templates to Azure and the Resource Manager in Azure figures out what to do, it will ensure that your environment in Azure reaches your desired state. In Terraform, the changes are being calculated against your Azure environment and this state file. Terraform needs its state file because Terraform can target a lot more endpoints than Bicep can. That, at the moment, only supports Azure. So, that file needs to be stored somewhere, and that needs to happen securely. There is some additional setup besides the state’s storage before you can create your first resources on Azure using Terraform. Again, that is a bit more work compared to Bicep.

Another powerful feature of Terraform is that before you run the apply, you can run the plan command. That will give you a complete overview of what will be changed when you run the apply and allows you to validate that. This again makes the pipeline a bit more complicated. Above are some reasons I decided to create a Terraform started pipeline that covers all the basic setup stuff. The complete source code and pipeline can be found here.

Azure DevOps Pipeline

In this example, the Azure DevOps pipeline targets two environments, development and production, and consists of four stages. Below you find the two stages targeting dev. The two stages for production are very similar. The first stage is the plan stage. It will first run the creation of prerequisites. That script will create a resource group and storage account in Azure to store the state. I will then run the Terraform plan command and store the result as an artifact in Azure DevOps. Storing that artifact is important because it can then be used in the Terraform apply in the next stage. That ensures that we apply what was in the plan.

Terraform Plan in Azure DevOps

trigger: none

pool:
  vmImage: ubuntu-latest

variables:
  stateContainerName: 'terraform-state'
  stateFileName: 'terraform.tfstate'

stages:
##### DEV Environment #####
  - stage: PlanDEV
    displayName: "Terraform plan - DEV"
    variables:
      - template: ../development-vars.yml
    jobs:
      - template: job/prerequisites.yml
        parameters:
          serviceConnection: ${{ variables.serviceConnectionName }}
          subscriptionId: ${{ variables.stateSubscriptionId }}
          resourceGroup: ${{ variables.stateResourceGroupname }}
          storageAccount: ${{ variables.stateStorageAccountName }}
          containerName: ${{ variables.stateContainerName }}
          publicNetworkAction: "Allow"

      - template: job/plan.yml
        parameters:
          dependsOn: TerraformPrerequisites
          deployment: dev
          serviceConnection: ${{ variables.serviceConnectionName }}
          resourceGroup: ${{ variables.stateResourceGroupname }}
          storageAccount: ${{ variables.stateStorageAccountName }}
          containerName: ${{ variables.stateContainerName }}
          terraformStateKey: ${{ variables.stateFileName }}
          terraformDirectory: ""

  - stage: ApplyDEV
    displayName: "Terraform apply - DEV"
    variables:
      - template: ../development-vars.yml
    jobs:
      - template: job/apply.yml
        parameters:
          environment: terraform-azure-example-dev
          deployment: dev
          serviceConnection: ${{ variables.serviceConnectionName }}
          resourceGroup: ${{ variables.stateResourceGroupname }}
          storageAccount: ${{ variables.stateStorageAccountName }}
          containerName: ${{ variables.stateContainerName }}
          terraformStateKey: ${{ variables.stateFileName }}
          terraformDirectory: ""

You can view that plan on the pipeline run in Azure DevOps when the plan phase is completed. You click ‘Terraform Plan’ and see the plan(s) for each environment.

Terraform Plan in Azure DevOps

When you look into the ‘job/apply.yml’ file you will see that this job uses an environment to do its deployment.

jobs:
  - deployment: TerraformApply
    displayName: Terraform Apply
    dependsOn: ${{ parameters.dependsOn }}
    environment: ${{ parameters.environment }}
    strategy:
      runOnce:
        deploy:
          steps:

That is a nice feature in Azure DevOps that allows you to get an overview of deployments to a specific environment. It has, in this case, another nice benefit. You can add approvals on an environment, allowing you to pause the pipeline to look at the plan before running the Apply.

Local development

I created two helper scripts to make things a little easier during local deployment: plan-tf.sh and apply-tf.sh. These two files help you run the plan and apply commands without having to type them out.