Using terraform in an Azure Devops pipeline

Victor Muchiaroni
sysadmuchi
Published in
5 min readFeb 12, 2021

--

It has been a month since I started to get my hands on Azure Devops and I have to admit it is has a nice pipeline feature that can assist to automate our cloud workload.

In order to use terraform in the pipeline, we will need to install its respective plugin from the azure marketplace. I will also use the replace tokens plugin for this example. Below are the screenshots showing where you can find those add-ons:

Search for terraform in the marketplace and choose this option
Search for replace tokens in the marketplace and choose this option

Once the plugins are installed, we can proceed with the pipeline creation.
When logged to your azure Devops project select the pipeline option in the left pane, hit “New pipeline”, select your repository from the listed options(in my case, I was using azure devops repos) and then select “Starter pipeline”. Below is the screenshot for each step:

Access the pipeline feature and select “New pipeline”
Choose you source code repo
Starter pipeline to build a pipeline from scratch

After performing these steps, you will get an editor to start to build your pipeline. The below code is an example on how to incorporate terraform to your yaml file:

#In my pipeline, I am passing the azure subscription as a parameter, This way you can execute that in different subscription using the same yaml.
#If you will execute this pipeline using only a single subscription, you can replace the subscription value in the variables block with your respective subscription and remove the parameters block.
parameters:
- name: azureSubscription
type: string
default: 'Microsoft Azure Enterprise(xxxxxx-xxxxx-xxxxx-xxxx-xxxxxxx)'
variables:
location: "YOUR LOCATION(ex. 'East US 2')"
resourcegroupname: 'YOUR RESOURCE GROUP NAME(if it does not exists it will be created)'
#storagekey: PipelineWillGetThisValueRuntime
terraformstorageaccount: 'TERRAFORM STORAGE ACCOUNT NAME'
terraformstoragerg: 'TERRAFORM RESOURCE GROUP NAME(if it does not exists it will be created)'
subscription: ${{ parameters.azureSubscription }}
trigger:
- none
pool:
vmImage: 'vs2017'
jobs:
- job: Infrastructure
steps:
#Terraform uses a storage account to keep the tfstate file
- task: AzureCLI@1
displayName: 'Create Storage account for terraform'
inputs:
azureSubscription: $(subscription)
scriptLocation: inlineScript
inlineScript: |
call az group create --location "$(location)" --name $(terraformstoragerg)

call az storage account create --name $(terraformstorageaccount) --resource-group $(terraformstoragerg) --location "$(location)" --sku Standard_LRS

call az storage container create --name terraform --account-name $(terraformstorageaccount)

call az storage account keys list -g $(terraformstoragerg) -n $(terraformstorageaccount)

- task: AzurePowerShell@3
displayName: 'Azure PowerShell script to get the storage key for the terraform storage account'
inputs:
azureSubscription: $(subscription)
ScriptType: InlineScript
Inline: |
$key=(Get-AzureRmStorageAccountKey -ResourceGroupName $(terraformstoragerg) -AccountName $(terraformstorageaccount)).Value[0]
Write-Host "##vso[task.setvariable variable=storagekey]$key"
azurePowerShellVersion: LatestVersion

#Here I am going to replace the placeholders(__string__) of the storage account from the .tf terraform files with the information of the recently created storage account(see the terraform definition file below).
- task: qetza.replacetokens.replacetokens-task.replacetokens@3
displayName: 'Replace tokens in terraform files **/*.tf'
inputs:
targetFiles: '**/*.tf'
escapeType: none
tokenPrefix: '__'
tokenSuffix: '__'

- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0
displayName: 'Install Terraform 0.12.3'

- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV1@0
displayName: 'Terraform : Install terraform CLI'
inputs:
workingDirectory: '$(System.DefaultWorkingDirectory)/Terraform'
backendServiceArm: 'Microsoft Azure Enterprise(1506eaae-943d-482b-8fa5-e821c4f5a9e3)'
backendAzureRmResourceGroupName: terraformrg
backendAzureRmStorageAccountName: $(terraformstorageaccount)
backendAzureRmContainerName: terraform
backendAzureRmKey: terraform.tfstate


- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV1@0
displayName: 'Terraform : Plan terraform configuration'
inputs:
command: plan
workingDirectory: '$(System.DefaultWorkingDirectory)/Terraform'
environmentServiceNameAzureRM: $(subscription)

- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV1@0
name: terraform_apply
displayName: 'Terraform : Apply terraform configuration'
inputs:
command: apply
workingDirectory: '$(System.DefaultWorkingDirectory)/Terraform'
commandOptions: '-auto-approve'
environmentServiceNameAzureRM: $(subscription)

Here it is an example in how my .tf file looks like. In this model, it will create an azure vm using a sftpgateway marketplace item.

terraform {
required_version = ">= 0.11"
backend "azurerm" {
storage_account_name = "__terraformstorageaccount__"
container_name = "terraform"
key = "terraform.tfstate"
access_key ="__storagekey__"
features{}
}
}
provider "azurerm" {
version = "=2.47.0"
features {}
}
resource "azurerm_resource_group" "main" {
name = "__resourcegroupname__"
location = "__location__"
}
### Virtual Machine resources
resource "azurerm_virtual_network" "main" {
name = "vm-network"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
}
resource "azurerm_subnet" "internal" {
name = "internal"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.2.0/24"]
}
resource "azurerm_public_ip" "main" {
name = "public"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
allocation_method = "Static"
}
resource "azurerm_network_interface" "main" {
name = "vm-nic"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
ip_configuration {
name = "ipconfiguration"
subnet_id = azurerm_subnet.internal.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.main.id
}
}
resource "azurerm_network_security_group" "main" {
name = "${var.prefix}-sg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "SSH"
priority = 300
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}

security_rule {
name = "HTTP"
priority = 320
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}

security_rule {
name = "HTTPS"
priority = 340
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}resource "azurerm_network_interface_security_group_association" "main" {
network_interface_id = azurerm_network_interface.main.id
network_security_group_id = azurerm_network_security_group.main.id
}
resource "azurerm_virtual_machine" "main" {
name = "my-vm"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
network_interface_ids = [azurerm_network_interface.main.id]
vm_size = "Standard_B2ms"
# Uncomment this line to delete the OS disk automatically when deleting the VM
# delete_os_disk_on_termination = true
# Uncomment this line to delete the data disks automatically when deleting the VM
# delete_data_disks_on_termination = true
storage_image_reference {
publisher = "thorntechnologiesllc"
offer = "sftpgateway"
sku = "sftpgateway"
version = "latest"
}
plan {
name = "sftpgateway"
publisher = "thorntechnologiesllc"
product = "sftpgateway"
}
storage_os_disk {
name = "my-osdisk1"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}
os_profile {
computer_name = "myhostname"
admin_username = "myuser"
admin_password = "mypassword"
}
os_profile_linux_config {
disable_password_authentication = false
}
}

I have a few more terraform examples to be applied in an azure pipeline in my github page: https://github.com/vimuchiaroni/terraform/tree/main/azure/examples

--

--