Intro

Terraform is one of the popular Infrastructure as Code (IaC) tools. There are simple yet powerful ideas behind the tool:
  • It uses configuration files to describe infrastructure. The configuration is written in declarative language called HCL (Hashicorp Configuration Language).
  • Thanks to rich Terraform Registry it supports dozens of providers - this obviously includes Azure along with GCP and AWS.
  • In order to create resources in proper order it creates dependency graph between them.
  • When deploying infrastructure changes it is saving state of the infrastructure. With this approach in case of change in infrastructure it is able to use the state to calculate which resources have changed and update only those.
This article starts short series which will lead you through basics of provisioning Azure resources with Terraform by building infrastructure for running serverless application with Azure Functions.

As a prerequisite you will need to execute following steps:

  • Download Terraform. You can do that using official download site: https://www.terraform.io/downloads.html or Chocolatey (in case you are using this great tool): https://chocolatey.org/packages/terraform. 
  • Make sure that you have Azure CLI installed and default subscription is set up. If you don't know how to do that refer to my older post about Azure Resource Manager which describes required steps.

Configuring provider

Code for this section is available in GitHub repository: https://github.com/sszarek/provisioning-azure-with-terraform/tree/1-create-resource-group, under 1-create-resource-group tag.

The first thing we will need is provider - provider is plugin which extends Terraform with capabilities of e.g. supporting specific cloud provider. Every provider deliver some set of resource types which can be used when writing Terraform configuration. For example, we will be using Azure provider which extend extends Terraform with such resources as storage account, resource groups or Cosmos DB (and much more - full list available in article linked at the end).

Lets start from creating main.tf file and adding following content:

terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 2.26"
}
}
}

Code above declares terraform block which is used for setting Terraform configuration. Examples of such configuration are:

  • required providers - required_providers block - defines list of providers and their versions required for running the script.
  • required Terraform version - required_version block - defines version constraints for Terraform CLI required for running the script.
  • backend - backend block - configures backend used for storing Terraform state file. I will touch this topic later in this article.
In the code above we are telling Terraform that the script depends on azurerm provider:
  • source argument defines Terraform registry URL where provider can be located. The format used in the code above consists of namespace (hashicorp) and type (azurerm) which will be used to locate provider in official Terraform registry located in: https://registry.terraform.io/. Terraform also allows to use other registries (e.g. company private registry) - in such scenario full URL to provider has to be provided.
  • version argument defines that the script requires at least version 2.26 of the module.
hashicorp/azurerm provider is official provider for Azure cloud maintained by Hashicorp, creator of Terraform. When working on Terraform scripts it is worth to keep documentation for the provider close at hand - to e.g. find list of arguments for resources or learn about some configuration edge cases. For Azure provider documentation can be found here: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs.

Now that we have defined dependency on Azure provider we need to add one more important block.

provider "azurerm" {
features { }
}

provider block contains configuration of the provider and it is required. features argument is required for Azure provider - it customizes behavior of some Azure resources. Since we do not want to customize anything here, we leave it blank. Full list of arguments available for configuring Azure provider can be found in documentation.

Adding first resource

We are ready now to add our first resource block. Resource blocks are defined in following way:
resource "<resource type>" "<local name>" {
argument1 = "foo"
argument2 = "bar"
}

As seen on snippet above, when creating creating a resource we need to provide:

  • Type (<resource type> label) of resource. This refers to resource types defined by providers e.g. Azure provider.
  • Local name (<local name> label) which allows to refer to resource within the scope of Terraform script. This should not be confused with the name of "physically" created resource in the cloud.
  • Resource configuration arguments.

It should not be surprise that the first resource we will create is Resource Group. Add code below to main.tf file.

resource "azurerm_resource_group" "rg" {
name = "terraform_example_app"
location = "centralus"
}

The code above creates Azure Resource Group in Central US region with name terraform_example_app. Inside script it will referenced as rg.

Now that we have provider configured and first resource defined it is time to deploy that stuff to Azure!

First deployment

The first thing we need to do is to call terraform init command which is responsible for:
  • Downloading providers required by the script - since the providers are not built in they have to be downloaded from registry to local directory. The directory will be created in same location where you run the command and will be called .terraform.
  • Creating (or update) lock file (.terraform.lock.hcl). The purpose of the lock file is to freeze concrete versions of dependencies - thanks to that you will always get the same version of providers no matter if you will be running from your local machine or some automated deployment.
  • Initializing backends - this topic will be covered in one of the following articles in the series.
When initialization is finished it is time for reviewing changes which Terraform is going to apply. For this purpose we will run terraform plan. The command will verify what changes it will need to make in current infrastructure and will output them to console. After running the command you should see output similar to one on screenshot below.

As you can see, the output is very descriptive: all the resources (well.. the single resource which is azurerm_resource_group) which are going to be created are marked with green plus sign. 

We are now finally ready to apply changes - this is done with terraform apply command. In its first phase it will display output similar to one from plan command, letting you review changes one more time. After confirming, Terraform will start creating resources and after it completes you will be able to see newly created Resource Group available in Azure Portal.

State file

If you look closer you will notice that after running terraform apply there was new file created - it is terraform.tfstate. The file contains JSON representation of the current state of infrastructure managed by Terraform. It maps resources defined in scripts (identified by resource type and local name) with "physical" resources in the cloud (identified by resource id assigned by cloud provider). Below you will find state file similar to one on your machine - red rectangles mark script local and physical resource identifiers.

Further in this article we will modify already created scripts and add other resources. When we will be deploying that changes Terraform will use information from terraform.tfstate in order to build execution plan and determine which resources will be modified, deleted or added.

You should avoid manual editing of the file as it might yield undesired results.

Variables

Code for this section is available in GitHub repository under 2-parametrize-with-variables tag: https://github.com/sszarek/provisioning-azure-with-terraform/tree/2-parametrize-with-variables.

Imagine that you would like to re-use script we have just created to provision Resource Group in different region. This seems to be as easy as changing value of location argument for rg resource however there is better way of doing that.

Terraform comes with the support of variables which allow you to parametrize your scripts. Lets create new variables.tf file.

variable "location" {
default = "centralus"
description = "Azure region where resources will be provisioned"
type = string
}

Block above defines location variable. There are three arguments set here:

Now lets get back to main.tf and replace hardcoded value for location argument with variable. We will replace: "centralus" with var.location, updated resource block looks on the snippet below:
resource "azurerm_resource_group" "rg" {
name = "terraform_example_app"
location = var.location
}

If you run terraform plan after this refactoring, you should see nice green message: "No changes. Infrastructure is up-to-date". This should be no surprise, after all we have not changed any resources but only did some refactoring.

When I was starting this section I was talking about case when we would like to deploy resources to different region than Central US which is currently used and set as default value for variable. To do so we need to explicitly set its value. We can do that in two ways.

Setting value as argument for CLI

First approach is to set variable value when calling terraform plan/apply commands. To provide value for variable when running one of the commands you will need to use -var argument, which has following format: -var="<variable name>=<variable value>". Lets check it in action by setting location variable to East US for terraform plan.
terraform plan -var="location=eastus"

Screenshot below presents output similar to one you should see on your machine.


Since we have provided new region for our Resource Group it needs to be recreated: resource in Central US will be destroyed and new one will be created in East US. It is also worth to take a look at the location argument on the output above: there is information in red next to it saying: "forces replacement" - when working on more complicated scripts you might run into situation where Terraform will be trying to replace some resource and this information is priceless for determining the reason of this.

Using tfvars file

Another way of providing values for variables is through *.tfvars file. This approach is convenient when you would like to group value for different environments. Lets create development.tfvars file and put following code there: 
location = "eastus"

Now that the file is created we will run terraform plan with -var-file argument which accepts path to tfvar file.

terraform plan -var-file="development.tfvars"

The result of calling the command should be the same as in previous case when we were providing variable value with -var command line argument.

Applying with variables

In previous section I was using plan command to present capabilities of variables, however this command is not making any changes to infrastructure. To make things happen we must use apply. The command accepts variable values in the same way as its done for plan:
  • Using -var command line argument where you provide values directly in command line.
  • Using -var-file command line argument where you provide name of file which contains values for variables used in scripts.

Cleaning up

Since we are getting to the end of this article it is good idea to clean up, previously created Azure resources. It should be no surprise that Terraform has separate command to handle this as well. The command is terraform destroy - it deletes all the resources which are tracked in terraform.tfstate.

Links

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs - Documentation for Azure provider