Azure, VSTS and terraform
Since the internet has been so lovely helping me with all my problems getting this to work, I thought it only fair that I also write about this adventure in the hopes that it can help the next person. Ideally after this blog post you know how to deploy infrastructure for microservices to a development, and production environment with Terraform and Azure.
VSTS
I am of the opinion that the code deploy and the infrastructure deploy should be separate build pipelines. Ideally you deploy code more than you make infrastructure changes so if you can save time in a build pipeline, you should. So this is how I do it in Visual Studio Team Services (VSTS).

This way has a couple advantages. Testing is made easier since we can see the output of the terraform plan build and visually inspect it. If we’re happy with it, we manually run the terraform apply build. I would imagine the workflow could be something like
- Make changes and make a PR to master
- Someone approves PR and merges
- QA kicks of the plan build and tests
- If pass, someone kicks off the apply build and if fail, fix master :)
Let’s take a deeper dive into these builds.
Our build pipeline is essentially 2 steps. First create a file locally (I use bash) that populates it with all the secret variables that you want. I use a variable group that links with an Azure Key Vault that just populates these variables for you. The script itself is quite easy
echo ‘secret_key = “$(secret_key)”‘ > $(terraform-path)/secrets.tfvarsThis way you keep your secrets out of your build/code repository.
The next build step just uses a Terraform package on the marketplace where you just fill out your variables.

Boom! VSTS done.
Terraform
I wrote an internal post of why I prefer Terraform over Azure Resource Manager (ARM) but the general gist was, writing Terraform is nicer and the documentation is better. Yes we’d be running behind on the API but that’s a small price to pay for my sanity.
Here’s the Terraform that we use for a service. I added comments in the Terraform that explain some of the extra hoops we have to jump through.
terraform {
backend "azurerm" {}
}resource "azurerm_resource_group" "default" {
name = "${var.service_name}-${var.env}-default"
location = "${var.region}"
}resource "azurerm_app_service" "default" {
name = "web-api"
location = "${azurerm_resource_group.default.location}"
resource_group_name = "${azurerm_resource_group.default.name}"
app_service_plan_id = "${azurerm_app_service_plan.default.id}"site_config {
dotnet_framework_version = "v4.0"
remote_debugging_enabled = true
remote_debugging_version = "VS2015"
http2_enabled = true
always_on = true
}app_settings {
"secret_key" = "${var.secret_key}"
}
}resource "azurerm_app_service_slot" "default" {
name = "staging"
location = "${azurerm_resource_group.default.location}"
resource_group_name = "${azurerm_resource_group.default.name}"
app_service_plan_id = "${azurerm_app_service_plan.default.id}"
app_service_name = "${azurerm_app_service.default.name}"
}/*
Here's the weirdness. SSL and app services in terraform aren't fully supported yet. So we have to manually put our cert into azure keyvault and then reference that cert here. Keep an eye on https://github.com/terraform-providers/terraform-provider-azurerm/issues/1136 to see if they solve this in a nicer way. Also you have to give permissions to app services to talk to keyvault which you do with the following command: Set-AzureRmKeyVaultAccessPolicy -VaultName VAULTNAME -ServicePrincipalName abfa0a7c-a6b6-4736-8310-5855508787cd -PermissionsToSecrets get
Disgusting I know.
*/resource "azurerm_template_deployment" "ssl_certificate_default" {
name = "${format("%s-arm-certs", "default")}"
resource_group_name = "${azurerm_resource_group.default.name}"
deployment_mode = "Incremental"template_body = <<DEPLOY
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"sslCertificateName": {
"type": "string"
},
"keyVaultId" :{
"type": "string"
},
"servicePlanId": {
"type": "string"
},
"appServiceName":{
"type": "string"
},
"appServiceFQDN":{
"type": "string"
},
"thumbprint":{
"type": "string"
}
},
"resources": [
{
"type":"Microsoft.Web/certificates",
"name":"[parameters('sslCertificateName')]",
"apiVersion":"2016-03-01",
"location":"[resourceGroup().location]",
"properties":{
"keyVaultId":"[parameters('keyVaultId')]",
"keyVaultSecretName":"[parameters('sslCertificateName')]",
"serverFarmId": "[parameters('servicePlanId')]"
}
},
{
"type":"Microsoft.Web/sites/hostnameBindings",
"name":"[concat(parameters('appServiceName'), '/', parameters('appServiceFQDN'))]",
"apiVersion":"2016-03-01",
"location":"[resourceGroup().location]",
"properties":{
"sslState":"SniEnabled",
"thumbprint":"[parameters('thumbprint')]"
},
"dependsOn": [
"[concat('Microsoft.Web/certificates/',parameters('sslCertificateName'))]"
]
}
]
}
DEPLOYparameters {
"appServiceName" = "${azurerm_app_service.default.name}"
"appServiceFQDN" = "*.mycoolurl.com"
"servicePlanId" = "${element(azurerm_app_service_plan.default.*.id, count.index)}"
"sslCertificateName" = "mycoolurl-pfx-01"
"keyVaultId" = "/subscriptions/{ID}/resourceGroups/{name}/providers/Microsoft.KeyVault/vaults/{name}"
"thumbprint" = "${var.thumbprint}"
}depends_on = [
"azurerm_app_service.default",
"azurerm_app_service_plan.default"
]
}
There’s also a variables.tf where you put all your variables and this is the dev.tfvars that I use.
service_name = "my-cool-api"
env = "dev"
thumbprint = "Some SLL thumbprint"
...In the dev.tfvars (or prod or stage) you put all your non secret variables. And the secret variables get pulled in during the build pipeline.
Gotchas and Tips
- Sometimes creation fails with no error code or message. In that case go into the resource in the azure portal and look for failed deployments in the Activity log. If you’re lucky it has an error message there and if you’re not, it will have something like “Please try again later”
- Creation of an application gateway can take up to 30 mins. Also mine got into a stuck state where there was some sort of internal deadlock so I had to manually delete it and fiddle with Terraform
- Once in a while completely destroy all your infrastructure and rebuild it. That’s just good practice
- Some things you’ll only need to create once, like a bucket where you store your state files. I do this manually instead of writing Terraform. Another way is to add a RunOnce folder in github and add some terraform there so you’ll at least have the history
- Write your Terraform in Visual Studio Code, the support is quite good
- While you are working on this, you’ll make a lot of changes and as soon as it is in a “working state” you’ll stop and maybe deploy infrastructure once a week. Keep this in mind when you are deciding on process and workflow .