Deploying Terraform Infrastructure to Azure using Azure DevOps Pipelines
Martin Humlund Clausen
Posted on August 28, 2023
In my experience knowing how to deploy a service intro production is as important as knowing the application itself. As Software Engineers we’ll like to manage the risk of deployments, and deploy as often and early as possible in the release cycle without downtime. However, every now and then you have to deploy infrastructure as well, so why not subject it, to the same level of automation as your applications?
This is the Second part of how to use Terraform with Azure, so if you haven’t already go read my first blog entry on how to setup a terraform repository and create the necessary service principles to get your started. Alternatively you can go to the repository on Github see what is there.
What kind of Infrastructure are we deploying?
When working with bigger Cloud Platforms you quickly discover that not all infrastructure are deployed at the same time, and more often than not, we gave contain multiple layers of infrastructure. To drive the point through consider these two layers
- Global Infrastructure, this typically includes changes to an Azure Kubernetes Cluster, a Azure Container Registry, or an Azure Service Bus. Global Infrastructure is the shared components that your applications can leverage. Also global infrastructure can be deployed independently of the rest of your application stack.
- Service Infrastructure is the the infrastructure that is needed to run your application, this includes Databases, Redis cache etc. And is deployed together with the service deployment.
In our organisation we have a couple of more layers, but lets just keep it simple for demonstration purpose, so for this article, I would like to focus on the global infrastructure, since we its easy to zoom in on the pipeline itself.
What does our deployment pipeline look like?
For this sample pipeline we are going to build two stages
- Validate Terraform - First stage we validate that our terraform configuration is correctly configured. This includes Terraform syntax and connectivity to our Azure Backend Store.
- Release to Dev - We have some infrastructure to deploy, so lets release it to our development environment
I have chosen to organizing the pipeline in the following way.
├── azure-pipelines.yaml
├── main.yaml
└── stages
├── tf-release.yaml
└── tf-validate.yaml
-
azure-pipelines.yaml
is the entry point for the pipelines. It contains all the triggers, variables and other pipeline controls -
main.yaml
can be though of an orchestrator of stages. It contains a list stages that we would like to execute -
tf-release.yaml
is our terraform release stage, that is responsible for doing the actual release of terraform. Note here that we do not denote the environment as to which we would like to deploy. We can simply reuse the stage and give it other parameters on runtime. -
tf-validate.yaml
is or Terraform Validation Stage
Deploying your infrastructure
All the magic happens in the tf-release.yaml
. Here we basically do the exact same things, as if we had run the command locally.
terraform init
terraform plan
terraform apply
Starting at the file, we define three variables
-
tf_version
- We need to parse in the terraform version into the release stage to ensure that our whole pipeline is using the same version of terraform. Since we use terraform in multiple pipelines this variable is defined in the azure-pipelines.yaml and parsed through all stages -
working_directory
- this will be our base directory where we are executing commands -
environment
- is the name of the environment. We can use this to do some magic display names, or reference azure pipelines variables using naming conventions.
Additionally I will be pulling in, the variable group that we created in the to get the service principle along with the access key to our storage account, plus three additional variables to help with location of our terraform artifacts.
parameters:
tf_version:
working_directory:
environment:
stages:
- stage: release_${{ parameters.environment }}
displayName: Release to ${{ parameters.environment }}
variables:
- group: root-terraform-backend-credentials
- name: backend_tfvars
value: "${{ parameters.working_directory }}/tf/backend/backend.ci.tfvars"
- name: variable_tfvars
value: "${{ parameters.working_directory }}/tf/environments/${{ parameters.environment }}/variables.tfvars"
- name: infrastructure_workingdirectory
value: "${{ parameters.working_directory }}/tf"
Install Terraform on the build agent
Until we have explicitly installed terraform on our build agent, we’ll not be able to execute any Terraform. This is however pretty straight forward. Though you might have to install Terraform Extension into your Azure DevOps Organization
- task: TerraformInstaller@0
displayName: "Use Terraform ${{ parameters.tf_version }}"
inputs:
terraformVersion: ${{ parameters.tf_version }}
Terraform Init
When initialising terraform we must provide it the initial backend file that we would like to use. We first have to replace the tokens in our backend.ci.tfvar
file. You might also have to install Replace Tokens Extension into your Azure DevOps Organization.
After we have replace the token, we can proceed to initializing terraform
- task: qetza.replacetokens.replacetokens-task.replacetokens@3
displayName: "Replace tokens in backend.tfvars with variables from the CI/CD environment vars"
inputs:
targetFiles: $(backend_tfvars)
encoding: "auto"
writeBOM: true
actionOnMissing: "fail"
keepToken: false
tokenPrefix: "#{"
tokenSuffix: "}#"
- bash: |
terraform init -backend-config=$(backend_tfvars) -input=false
env:
ARM_CLIENT_ID: $(sp_clientId)
ARM_CLIENT_SECRET: $(sp_clientSecret)
ARM_SUBSCRIPTION_ID: $(sp_subscriptionId)
ARM_TENANT_ID: $(sp_tenantId)
displayName: Initialize configuration
workingDirectory: $(infrastructure_workingdirectory)
failOnStderr: true
See the -input=false
flag at the end? This tells Terraform that it should not prompt for input, and is important not to miss when running in a CI environment.
Terraform Plan
Terraform plan, is the action of making the change set for our infrastructure. For this command we are parsing providing a -out
parameter which is a mechanism to store that change set to a file on disk, and since this is running in a single build agent job, we can use that file to apply the infrastructure in a later task.
💡 You can export the terraform plan file and use it in another stage. This is especially useful if you want to add an additional approval step, or inspect the file manually.
- bash: |
terraform plan -var-file=$(variable_tfvars) -input=false -out=tfplan
env:
ARM_CLIENT_ID: $(sp_clientId)
ARM_CLIENT_SECRET: $(sp_clientSecret)
ARM_SUBSCRIPTION_ID: $(sp_subscriptionId)
ARM_TENANT_ID: $(sp_tenantId)
displayName: Create execution plan
workingDirectory: $(infrastructure_workingdirectory)
failOnStderr: true
Terraform Apply
Last thing we need to do is to apply the changes. For this example, we parse the -auto-approve
flag, which will just apply the changes that we have, without prompting us. Additionally we are using the tfplan
that we created in the previous task.
- bash: |
terraform apply -input=false -auto-approve tfplan
terraform output -json > output.${{ parameters.environment }}.json
env:
ARM_CLIENT_ID: $(sp_clientId)
ARM_CLIENT_SECRET: $(sp_clientSecret)
ARM_SUBSCRIPTION_ID: $(sp_subscriptionId)
ARM_TENANT_ID: $(sp_tenantId)
displayName: Apply execution plan
workingDirectory: $(infrastructure_workingdirectory)
failOnStderr: true
Conclusion
In my previous article Getting Started with Terraform and Azure, we started by setting up the the initial terraform, which would serve as the foundation for constructing a Cloud Platform using Terraform.
For this article we have gone through how to apply the infrastructure using Azure Pipelines.
This is a very basic example, and I am sure that you need to do adjustments to fit your development flow. However here are some ideas, that you can consider for your next project
- Have the pipeline add an additional approval step before applying the infrastructure structure
- Have the multistage Azure Pipeline, Plan your live environment, after you have deployed to Development and see how the change will affect your live environment, before approving it.
- Configure a web hook that tells your team what changes are being deployed.
You can find the full code sample on Github, with more examples to come.
References
- Github Repository: Cloud.Platform.Foundation
- Azure DevOps Extension: ReplaceTokens
- Azure DevOps Extension: Terraform
Posted on August 28, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.