Introduction
As development teams implement more and more Infrastructure as Code (IaC) leveraging Azure DevOps(ADO) there is a need for to ADO tasks that integrate easily and help improve the software development process. This post will specifically focus on the Azure DevOps Terraform Task
Update: To see this task in action check out my TheYAMLPipelineOne GitHub Repo
The Problem
With growth in popularity on Azure DevOps Microsoft Hosted Agents we need the ability to consistently deploy software on machines that organizations don’t maintain and managed. Because of this there needs to be task(s) to install/validate that the require software for build/deployment is configured correctly on the hosted agent.
An additional ask would be feature/functionality that easily integrates into ADO and provides for a better overall developer experience.
The Azure DevOps Implementation
The Azure DevOps Terraform Task does both of these. First installing Terraform can be configured to a specific version passed in at build:
parameters:
- name: terraformVersion
type: string
steps:
- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0
displayName: install terraform
inputs:
terraformVersion: ${{ parameters.terraformVersion }}
This ensure that the correct version of Terraform is installed on the Microsoft hosted agent. After this we can use the additional commands via TerraformCLI@0
.
For instance, to initialize Terraform against an azurerm
backend.
parameters:
- name: serviceName
type: string
- name: TerraformDirectory
type: string
- name: AzureSubscriptionServiceConnectionName
type: string
- name: TerraformStateStorageAccountResourceGroupName
type: string
- name: TerraformStateStorageAccountName
type: string
- name: TerraformStateStorageAccountContainerName
type: string
steps:
- task: TerraformCLI@0
displayName: 'Terraform : init'
inputs:
command: init
backendType: azurerm
workingDirectory: ${{ parameters.TerraformDirectory }}
backendServiceArm: ${{ parameters.AzureSubscriptionServiceConnectionName }}
backendAzureRmResourceGroupName: ${{ parameters.TerraformStateStorageAccountResourceGroupName }}
backendAzureRmStorageAccountName: ${{ parameters.TerraformStateStorageAccountName }}
backendAzureRmContainerName: ${{ parameters.TerraformStateStorageAccountContainerName }}
backendAzureRmKey: ${{ parameters.serviceName }}.tfstate
Then a plan:
parameters:
- name: TerraformDirectory
type: string
- name: AzureSubscriptionServiceConnectionName
type: string
- name: commandOptions
default: '-out=$(System.DefaultWorkingDirectory)/terraform.tfplan -detailed-exitcode'
- name: additionalParameters
type: object
default: []
steps:
- task: TerraformCLI@0
displayName: 'Terraform : plan'
inputs:
command: plan
workingDirectory: ${{ parameters.TerraformDirectory }}
publishPlanResults: ${{ parameters.AzureSubscriptionServiceConnectionName }}
environmentServiceName: ${{ parameters.AzureSubscriptionServiceConnectionName }}
commandOptions: ${{ parameters.commandOptions }}
Take note on this one of the publishPlanResults and the commandOptions. We will show what these translate to later.
Finally, the apply:
parameters:
- name: TerraformDirectory
type: string
- name: AzureSubscriptionServiceConnectionName
type: string
- name: additionalParameters
type: object
default: []
steps:
- task: TerraformCLI@0
displayName: 'Terraform : apply'
condition: and(succeeded(), eq(variables['TERRAFORM_PLAN_HAS_CHANGES'],'true'))
inputs:
command: apply
workingDirectory: ${{ parameters.TerraformDirectory }}
commandOptions: '$(System.DefaultWorkingDirectory)/terraform.tfplan'
environmentServiceName: ${{ parameters.AzureSubscriptionServiceConnectionName }}
Notice a condition on this apply checking against TERRAFORM_PLAN_HAS_CHANGES
this is a variable created by the plan command and writes back if the Terraform will actually make any infrastructure changes. In the scenario where IaC is stored and deployed with application code (which I strongly recommend) this will skip the Terraform apply step if there aren’t any changes to apply.
Here is a screenshot confirming the apply was skipped in the case of no changes being detected:
The Azure DevOps Experience
In addition to providing the ability to skip over the apply step if no changes have occurred another key thing this task provides is the publishing of the plan directly to ADO.
Notice we have a new tab on the ADO pipeline called ‘Terraform Plan’
This tab lets me choose which plan to evaluate, in my case the plan was done per environment. Here is Dev where no changes were detected:
And here is an example of a UAT environment where changes were detected:
This information was previously available in the output of the tasks; however, by moving this to a new tab can quickly navigate and see any changes. Plus, with the push to have business users release code this is less daunting for someone without a technical background to look at.
Conclusion
Using the Azure Terraform CLI task can greatly improve the experience when working with Terraform within ADO.