After writing a few notes on “Azure DevOps and Terraform,” I thought of exploring the idea of integrating Azure DevOps and Terraform a little further.
Generally speaking, in Azure Pipelines (classic editor), a build definition (pipeline in Azure DevOps services) is used to compile and package an artifact. Then, under Releases, we have a release definition associated with the build definition used to deploy the artifacts across different environments like -Dev, Test, Stage, and Prod. These environments consist of different stages and can be mapped to separate deployment groups. The Azure DevOps pipeline enables us to build once and deploy multiple -across environments.
Note: I have a separate note where I write in detail about Pipelines and their components.
Terraform, as we know, is an infrastructure automation tool that follows the sequence of init -> validate -> plan -> apply
(and if necessary, destroy
too). So the idea was to use both Azure DevOps and Terraform to automate infrastructure provisioning across environments.
That meant that I had to map the Terraform commands into Azure pipelines build definition and release definition. In a traditional CI/CD approach, we compile and create the package in CI (continuous integration). However, in the case of Terraform, there is no code to compile. And then, in CD (continuous deploy), we apply the package to an environment. I realize that terraform plan and terraform apply belong to CD because they are environment specific and will vary from environment to environment. And hence, by the rule of elimination, terraform init
and terraform validate
belong to CI. There is no hard and fast rule as such. I attempt to retrofit terraform workflow with Azure DevOps.
In the case of an Azure DevOps build definition, the workflow would look something like below:
-checkout files from code repo,
-run terraform init
, which downloads all dependencies and initializes the backend to store the remote state. It is at this stage that we also provide credentials (AWS or Azure) to be able to communicate with the remote state
-run terraform validate
, which checks for correctness of syntax,
-create a package with the .terraform folder and the .tf files in it.
The package is then ready to have terraform plan
and terraform apply
run on them, which belongs to the continuous deploy step defined in a release definition.
So the release definition workflow would look like this:
-download the package from the associated build definition,
-run terraform plan
-run terraform apply
Conceptually, this would work but has certain limitations.
The idea of a CI/CD pipeline is to build once-deploy multiple. However, a terraform configuration has a remote state where terraform stores the state file of changes applied to an environment. Terraform stores the remote state configuration value in a backend.tf file, and that file cannot have any variables.
Here is an example of a backend.tf file in case of AWS:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
terraform { | |
backend "s3" { | |
bucket = "$(BackendBucketName)" # the name of the S3 bucket that was created | |
encrypt = true | |
key = "$(PathToTFStateFile)" # the path to the terraform.tfstate file stored inside the bucket | |
region = "$(BucketRegion)" # the location of the bucket | |
dynamodb_table = "$(BackendLockTableName)" # the name of the table to store the lock | |
} | |
} |
Effectively, there is an S3 bucket and a key, and these cannot be variables.
And here is an example of a backend.tf file in case of Azure:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
terraform { | |
backend "azurerm" { | |
resource_group_name = "$(ResourceGroupToStoreTerraformBackendResources)" | |
storage_account_name = "$(UniqueStorageAccountName)" | |
container_name = "$(StorageContainerName)" | |
key = "terraform.tfstate" | |
access_key = "$(StorageAccountAccessKey)" | |
} | |
} |
Let me explain through an example. Say I have a terraform configuration to provision a virtual machine in AWS. The requirement is to provision the VM in the following four environments: Dev, Test, Stage, and Prod. Generally, it is a good security practice to map environments to separate AWS accounts. And when it comes to a CI/CD pipeline, the changes need to flow from environment to environment. Hence, a typical Azure DevOps CI/CD pipeline can have a workflow like: deploy to Dev environment -> validate the change in Dev environment->deploy to Test environment-> validate the change in Test environment ->deploy to Stage environment -> validate the change in Stage environment->deploy to Prod environment-> validate the change in Prod environment. By having separate stages, we can protect a higher environment from having un-validated changes introduced.
In such a scenario, how do we manage to apply the Terraform configurations across different environments?
And as I had mentioned, we cannot have variables in the backend.tf, which implies that we cannot extend the remote state such that for the Dev environment, the remote state is such, and for Test, it is such. There can be only one value there.
Hashicorp had thought of this, and they introduced the concept of workspaces. A terraform workspace allows the backend to extend such that we can use it to store the remote state of different environments.
I hope this note helped you understand the limitation that I presented. Please read my following note where I do a deep dive on terraform workspace. I then implement the idea discussed using YAML-based Azure pipelines and Powershell in the next note titled –CI/CD of Terraform workspace with YAML based Azure Pipelines.
2 thoughts on “CI/CD using Terraform and Azure Pipelines -ideation”