Deploying Terraform Infrastructure to Azure using Azure DevOps Pipelines

martinhc

Martin Humlund Clausen

Posted on August 28, 2023

Deploying Terraform Infrastructure to Azure using Azure DevOps Pipelines

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

  1. Validate Terraform - First stage we validate that our terraform configuration is correctly configured. This includes Terraform syntax and connectivity to our Azure Backend Store.
  2. Release to Dev - We have some infrastructure to deploy, so lets release it to our development environment

Release Pipeline

I have chosen to organizing the pipeline in the following way.



├── azure-pipelines.yaml
├── main.yaml
└── stages
    ├── tf-release.yaml
    └── tf-validate.yaml


Enter fullscreen mode Exit fullscreen mode
  • 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


Enter fullscreen mode Exit fullscreen mode

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"


Enter fullscreen mode Exit fullscreen mode

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 }}


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode




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
Enter fullscreen mode Exit fullscreen mode




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

💖 💪 🙅 🚩
martinhc
Martin Humlund Clausen

Posted on August 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related