In december we've built a chatbot on Azure. We're currently deploying it directly from Azure DevOps with great ease. The only problem, the infrastructure isn't managed at all. Hard problem? Not really if you use something like Terraform.
In this post I will show you how we converted our manually created Azure resources into a set of resources managed by Terraform in a matter of hours.
What is terraform?
Before we dive into the gory details of the bot infrastructure, let's talk about terraform and what it is.
Terraform is an application developed by Hashicorp. Its goal is to make it easy to build Azure and other cloud infrastructure as code.
In terraform you define what resources you want with their settings. Then you take this definition file and let terraform deploy it for you. Terraform will check if the resources are in the desired state and if not it will deploy/change or remove resources so they match the desired state described in your file.
Using a desired state configuration model instead of manual deployment makes it a lot easier to manage your cloud infrastructure.
Need to redeploy because of a disaster? No problem, just run the configuration file and copy the binaries of your application to the right instances.
Need to move the configuration from one subscription to another? You could move them in the Azure Portal but if you don't have access you're in a pickle. Instead, take the scripts and redeploy them on the other environment.
In my experience it takes only a few minutes to deploy about 30-40 resources to Azure using terraform. After you've tested the scripts on one environment you can be sure they work on another Azure environment too.
How does terraform work?
Working with terraform configurations is done in three steps:
- Create a configuration
- Initialize the terraform state
- Apply the configuration
Step 1: Create a configuration
Creating a configuration in terraform is done by writing a new file with the extension .tf
. It looks a little like this:
provider "azurerm" {
}
resource "azurerm_resource_group" "my_resource_group" {
name = "test-group"
location = "West europe"
}
The first four lines describe the configuration settings for the Azure Resource Manager provider which goes by the name of azurerm
in terraform.
Then we create a new resource of type azurerm_resource_group
with the name my_resource_group
. It has a name and a location. In this case we're deploying to test-group in west europe.
Step 2: Initialize the terraform state
Before you can start to apply this configuration to your azure environment you need to invoke the following command in the folder where you stored your configuration file:
terraform init
This command downloads the required modules for your repository and setup any local files that terraform needs.
Note that you can run this command multiple times. It may raise errors but your original setup will remain untouched. Also, if you just cloned your sources on a fresh machine you should also run this command.
Step 3: Apply the configuration
Once initialized it is time to apply your configuration to your azure environment. It's important to know that terraform uses credentials stored by the Azure CLI to access the Azure resource manager.
So before you can actually deploy, run the following commands:
az login
az account set --subscription {subscriptionId}
The first step will open up the devicelogin page so you can connect to your Azure environment. The second step configures the currently active subscription to deploy to.
You can find your subscription Id on the Azure Portal. Search for Subscriptions
in the search bar at the top of the page. Now select the subscriptions item from the dropdown. Then search for the subscription you want to use and copy its subscription ID and use it in the second command to set the correct subscription.
Once authenticated, run the following command to apply your configuration:
terraform apply
This will first compile a new desired state for your environment. Then the changes are applied to the environment. Any new resources are created and existing resources are updated with new settings. Resources that you've removed from the configuration file are automatically destroyed by terraform.
Unleashing the power of terraform
Deploying random resources isn't exactly exciting. However, when you are working in a complex environment you will find that terraform proves really powerful.
Referencing other resources
For example, when I have a bunch of resources I want in the same resource group and in the same location I can use variables to connect resources together:
resource "azurerm_resource_group" "my_resource_group" {
name = "test-group"
location = "West europe"
}
resource "azurerm_app_service_plan" "my_serviceplan" {
name = "my-service-plan"
resource_group_name = "${azurerm_resource_group.my_resource_group.name}"
location = "${azurerm_resource_group.my_resource_group.location}"
kind = "Windows"
sku {
size = "S1"
tier = "Standard"
}
}
resource "azurerm_app_service "my_website" {
name = "my_website"
location = "${azurerm_resource_group.my_resource_group.location}"
app_service_plan_id = "${azurerm_app_service_plan.my_serviceplan.id}"
resource_group_name = "${azurerm_resource_group.my_resource_group.name}"
site_config {
default_documents = ["index.html"]
}
}
I now have a resource group, service plan and app service configured to be in the same location. Change the location in one spot and everything moves accordingly.
Using variables
Referencing other resources is not the only use for variables in your configuration. You can also define variables to parameterize the deployment. For example:
variable "deployment_name" {
type = "string"
}
resource "azurerm_app_service "my_website" {
name = "${var.deployment_name}-my_website"
location = "${azurerm_resource_group.my_resource_group.location}"
app_service_plan_id = "${azurerm_app_service_plan.my_serviceplan.id}"
resource_group_name = "${azurerm_resource_group.my_resource_group.name}"
site_config {
default_documents = ["index.html"]
}
}
We've defined a variable deployment_name
and used this variable to prefix the name of our website.
Using randomly generated values
Sometimes, however, you don't want to use fixed names. For example, when you want the same configuration to be deployed under a different name.
To generate a random name in your configuration you can use the random_id
resource:
variable "deployment_name" {
type = "string"
}
resource "random_id" "website_name" {
keepers = {
deployment_name = "${var.deployment_name}"
}
byte_length = 8
}
resource "azurerm_app_service "my_website" {
name = "${var.deployment_name}-${random_id.website_name.hex}"
location = "${azurerm_resource_group.my_resource_group.location}"
app_service_plan_id = "${azurerm_app_service_plan.my_serviceplan.id}"
resource_group_name = "${azurerm_resource_group.my_resource_group.name}"
site_config {
default_documents = ["index.html"]
}
}
We've extended the configuration to include a random_id
by the name website_name
. The keepers
section controls when the random data changes. We want our random name to change when we change the name of the deployment.
You can use the random_id
in other resources to get a random string. For example, in the app service resource we generate the website name using the deployment name and a hex representation of random_id
resource.
Using configuration from terraform in other tools
Once you have a bunch of resources in Azure you want to know which URL to use in that one configuration file that your application has. But with all the random strings and variables it's quite hard to keep track of the name of that one server you deployed in Azure.
Fear not, you can get hold of configuration settings in terraform and extract them so you can use them in tools like MSDeploy to copy your ASP.NET application to the azure website you just created.
Change the configuration and include outputs to store information you want to extract from the configuration:
output "siteUrl" {
value = "${azurerm_app_service.my_website.default_site_hostname}"
depends_on = ["my_website"]
}
We've defined an output with the name siteUrl
and stored the default site hostname from our azure website in it. We only want this output to be defined when the my_website
resource exists.
Now after we've applied our configuration with terraform we can extract the outputs using the following command:
terraform output -json
This gives us the outputs as JSON object data. You can store this in a file or if you're using Powershell you can use that to turn it into data that you can use to execute more tools:
$DeploymentProperties = terraform output -json | ConvertFrom-Json
$siteUrl = $DeploymentProperties.siteUrl.value
First we invoke the terraform output command and convert it from a JSON String into a Powershell hashtable. Then we create a new variable $siteUrl
and store the value of the siteUrl configuration property.
Once you have the siteUrl property you can use it for all sorts of things in Powershell.
Give it a shot!
There are loads of desired state configuration tools out there, this is just one of them. Although I've found this is one of the easiest to manage. Give it a shot and let me know on Twitter what you think!