Terraform Lab 101

This workshop will guide you through the basics of Terraform and how to use it to deploy resources in Azure. This workshop is suppose to be easy and guide you in any step. If the instructions is unclear or wrong please let me know so I can fix it.

Naming of the terraform files isn’t important as long as they have the extension .tf. You can think of it like all the tf files in the same folder are seen as one file. So splitting code between files are more of a way to organize your code.

Don’t forget to remove you resources when you are done, that is easy to to with terraform, jump to the last chapter called Clean up to see how.

Table of content

Prerequisites
Login to Azure
Providers and commands
Working with resources
Variables
Organize your code / Modules
Clean up

Prerequisites

Azure subscription

You will need an Azure subscription to complete this lab. If you don’t have one, you can create a free account by following the instructions at Create your free Azure account.

Azure CLI

You will need the Azure CLI installed on your machine. If you don’t have it installed, you can follow the instructions at Install the Azure CLI.

To verify that you have the Azure CLI installed, run the following command:

> az version

If you already have Azure CLI installed make sure to update it to the latest version by running the following command:

> az upgrade

Terraform

You will need Terraform installed on your machine. If you are using a Windows machine, the easiest way to install Terraform is to use the Chocolatey package manager. You may need to restart your computer after the installation of choco and/or terraform. To install Terraform using Chocolatey (after you installed it), run the following command:

> choco install terraform

If you are using an Mac, the easiest way to install Terraform is to use the Homebrew package manager. To install Terraform using Homebrew (after you installed it), run the following command in an elevated prompt:

> brew install terraform

You can also install it manually follow the instructions at Install Terraform.

Verify your installation by running the following command:

> terraform version

If you already have it installed upgrade your installation of Terraform to the latest version by running the following command in an elevated prompt:

> choco upgrade terraform

or

> brew upgrade terraform

Visual Studio Code

You will need Visual Studio Code installed on your machine. If you don’t have it installed, you can follow the instructions at Download Visual Studio Code.

Optional Extensions to Visual Studio Code

Setup project and login to Azure

Start Visual Studio Code and open the folder where you want to store the Terraform configuration files for this labs. Then open a terminal window in Visual Studio Code by by selecting Terminal -> New Terminal from the menu.

Then you need to login to Azure and select the correct subscription, you should do this ever time you start Visual Studio Code for this workshop. In the terminal window run the following commands:

> az login

Visual Studio Code will open a browser window where you can login to your Azure account. After you have logged in, Visual Studio Code will show you a list of your subscription for that account and pre select the default one. To verify what subscription is selected run the following command:

> az account show

If you need to change subscription to use for this labs, run the following command:

> az account set -s <subscription-id>

Replace <subscription-id> with the subscription id of the subscription you want to use for this lab, the id can be found in the list of subscriptions that was shown in the previous step (name “id”).

Now you are ready to start coding.

Providers and commands

Now let us familiarize ourselves with Terraform files and commands by creating a simple Terraform configuration file and running it.

Find the documentation for Azure Provider

The first thing we need to do is to find the documentation for the Azure Provider. We will use the Azure Provider to create resources in Azure. The Azure Provider is a plugin for Terraform that adds support for the Azure Resource Manager API. The Azure Provider is maintained by Microsoft.

Create the provider terraform code

Now we need to specify that we are using the Azure Provider in our Terraform configuration.

  • On the webpage click on the USE PROVIDER button to display a snippet code of the provider configuration
  • Copy that code and paste it into a new file called ‘providers.tf’ in the root of your project folder

Provider versions

The snippet is specifying the provider version as a constraint. This is a good practice to ensure that you are using a version of the provider that is compatible with your configuration. If you don’t specify a version constraint, Terraform will automatically download the latest version of the provider when you run the init command.

Create the resource terraform code

Next we will specify a resource for terraform to create. In this case we will create a resource group in Azure.

  • From the Azure Provider page (that we browsed to in previous steps) Follow the like called Documentation
  • In the search field on the left side, type ‘resource group’
  • Select the ‘azurerm_resource_group’ resource in the ‘Resources’ section
  • Copy the example code and paste it into a new file called ‘main.tf’ in the root of your project folder.
  • Update the resource name to something more descriptive. Change the last string in the first row that states “example” to “resource_group_lab” in the block name.
  • Update the name attribute to what you want your resource group should be name to in Azure. Change the string “example” in the second line to “rg-terraformlab-cs”.
  • Don’t forget to save the file.
    resource "azurerm_resource_group" "resource_group_lab" {
        name     = "rg-terraformlab-cs"
        location = "West Europe"
    }

note that you need to keep the {-token on the same line as the resource name otherwise you will get an error.

Blocks explained

A resource block (like the one above) is the most common kind of block in Terraform configuration. It has two labels, the first one is the resource block type and the second one is the resource block name. The block type is just the name of the resource in the context of terraform, not the name of the resource in Azure. The resource name is used to refer to the resource from elsewhere in the configuration, so it is important to pick a name that will be meaningful in context. Inside the block we have the configuration for the resource. These attributes can be a string (like we did) or a more complex type like a list or a map or even a nested block.

Terraform commands

Now lets try out some Terraform commands.

Terraform init

Now that we have created our Terraform configuration files, we need to initialize the project. This will download the Azure Provider and any other providers that are needed for the project. Use the terminal in Visual Studio Code to run the following command:

    terraform init

In the output you will now see that the hashicorp/azurerm provider has been installed.

Terraform validate

To make sure you have correct syntax in your Terraform configuration files, run the following command:

    terraform validate

Terraform fmt

To make sure your Terraform configuration files are formatted nicely, run the following command:

    terraform fmt

And if you have your code organized in modules (folders) you need to add the flag –recursive to the command:

    terraform fmt --recursive

Terraform plan

To see what Terraform will do when you run the Terraform apply command, run the following command:

    terraform plan

Terraform will now tell you that the features block is missing, the code we copied from the documentation is generic and does not contain all the required configurations. Let us fix that. Go to the providers.tf file and modify provider block named “azurerm” to look like this:

    provider "azurerm" {
        features {}
    }

Save the changes and run the plan command again. You will now get an output of what will happen if it is applied. There should be 1 resource group to add with the name rg-terraformlab-cs.

Terraform apply

Next up is to let Terraform create the resource group. To do that, run the following command:

    terraform apply

Terraform will plan the changes again and prompt you to type yes to confirm that you want to apply the changes. Type yes and press enter. Terraform will now create the resource group in Azure. When it is done, you will see a message that the resource group has been created. Use the Azure portal to verify that the resource group has been created. https://portal.azure.com

Terraform destroy

The Destroy command removes everything that have been created by Terraform from this configuration. To destroy the resource group, run the following command:

    terraform destroy

Terraform will now ask you to verify that you want to destroy your resources by typing yes. Type yes and press enter. Terraform will now destroy the resource group in Azure. When it is done, you will see a message that the resource group has been destroyed.

There are more commands but these are the once you will need all the time so let us move on to the next step.

Working with resources

Let us create some more resources in Azure. We will create a resource group, a storage account, and a storage container and an app service. As well as how to reference already present resources.

Resource group

Make sure you have the following code in your main.tf file:

resource "azurerm_resource_group" "resource_group_lab" {
    name     = "rg-terraformlab-cs"
    location = "West Europe"
}

Storage account

Use the documentation to create a storage account in the resource group. The documentation can be found here: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account. You can use the same resource group block that you created in the previous step.

Start by copying the azurerm_storage_account block and change the name of the resource block to azurerm_storage_account_lab.

And then for the resource group name and location use the values from the resource group block. There is three benefits with doing this:

  • first is that you don’t have to type in the same values again (and maybe misspell them).
  • Second is that if you change the values in the resource group block you don’t have to change them in the storage account block.
  • And third is that terraform keeps track of the dependencies, making sure that the resource group is created before the storage account.

And as a bonus if you move the storage account block to another folder (module) you don’t have to change the values in the storage account block. But more about that later on.

To reference a attribute of an other resource you specify the block type name followed by the block resource name and then the attribute name. So in this case of the resource group name would be azurerm_resource_group.resource_group_lab.name. There are more attributes that can be referenced from the resource group block. The once that you can configure for that resource and some that are added by Azure. You can find all them in the documentation for the resource group block.

You also need to update the name attribute to something that is Azure globally unique. It needs to be between 3 and 24 characters and can only contain lowercase letters and numbers.

The block should look something like this:

resource "azurerm_storage_account" "azurerm_storage_account_lab" {
    name                     = "unique629517"
    resource_group_name      = azurerm_resource_group.resource_group_lab.name
    location                 = azurerm_resource_group.resource_group_lab.location
    account_tier             = "Standard"
    account_replication_type = "LRS"
}

Use the terraform commands to format and validate your code. (terraform fmt and terraform validate)

Modify the provider

When working with storage you need to set a attribute called storage_use_azuread on your provider configuration block. This is because the storage account is created with a attribute called ‘allowBlobPublicAccess’ set to false. This means that you can’t access the storage account without authenticating. That also applies to running terraform to access it. To authenticate you need to use Azure Active Directory.

change your provider block to look like this:

provider "azurerm" {
    features {}
    storage_use_azuread = true
}

Storage container

Now that we have a storage account we can add a storage container to it. Use the documentation to create a storage container in the storage account. Search the documentation for ‘storage container’ and then select the ‘azurerm_storage_container’ resource. You will use the same storage account block and resource block from the previous step so you only need the ‘azurerm_storage_container’ block. Make sure the block is referencing the storage account block name attribute for the attribute ‘storage_account_name’. Set the attribute “name” to the value of “images”.

The new block should look something like:

resource "azurerm_storage_container" "azurerm_storage_container_lab" {
    name                  = "images"
    storage_account_name  = azurerm_storage_account.azurerm_storage_account_lab.name
    container_access_type = "private"
}

Use the terraform commands to format and validate your code and then apply the changes. (terraform fmt, terraform validate and terraform apply)

App Service

Use the documentation to create an app service in the resource group you created before. Search the documentation for ‘app service’ and then select the ‘azurerm_app_service’ resource. As stated in the documentation that terraform block is now deprecated and you should use azurerm_linux_web_app or azurerm_windows_web_app instead. For this lab use the azurerm_linux_web_app. As you can see in the documentation you need to add a service plan block as well, but we will pretend to use one plan that we already have. To do that let’s add one quickly with the help of some Azure CLI. In the Visual Studio Code terminal type in the following command:

az group create --name shared-lab --location westeurope
az appservice plan create --name asp-terraform-lab --resource-group shared-lab --sku F1 --is-linux --location westeurope

Now let’s add a reference to the plan in the main.tf file. Search for ‘service plan’ in the documentation now select ‘azurerm_service_plan’ but this time use the link under ‘Data Sources’ in the tree menu. modify the data block to have a name of ‘asp-terraform-lab’ and a resource_group_name of ‘shared’. Also give the block a meaningful name, let’s use ‘service_plan_lab’. The code should look like:

data "azurerm_service_plan" "service_plan_lab" {
    name                = "asp-terraform-lab"
    resource_group_name = "shared-lab"
}

Now we can proceed with adding the app service block. Once again give it a meaningful block-name, let’s use ‘app_service_lab’. Use the same resource group and location (resource_group_lab) as before and for service plan id add a reference to the data block for the service plan. A difference here is that you start with “data” and then add the block-type-label and then the block-name-label like this: data.azurerm_service_plan.service_plan_lab.id .
The name of the app service needs to be unique since we are using a shared service plan. Type random in the terminal to create a random number to use as the name. You also need to add a site config value with the attribute always_on set to false. Because always_on must be explicitly set to false when using Free, F1, D1, or Shared Service Plans (like we do). The code should look like:

resource "azurerm_linux_web_app" "app_service_lab" {
    name                = "<your random number>"
    resource_group_name = azurerm_resource_group.resource_group_lab.name
    location            = azurerm_resource_group.resource_group_lab.location
    service_plan_id     = data.azurerm_service_plan.service_plan_lab.id

    site_config {
        always_on = false
    }
}

Use the terraform commands to format and validate your code and then apply the changes. (terraform fmt, terraform validate and terraform apply). Then verify that the app service was created in the Azure Portal. https://portal.azure.com

Variables

Time to start using variables, we will use local variables, input variables as well as output variables.

Local variables

This is variables that are accessible within the folder (also called module). Create a file called locals.tf (they can be added to main.tf as well but let’s add some structure to your code). In the file create a locals block and then add add two variables called “region” and “workload”. The variable region should have the value of “West Europe” and workload should have the value of “terraformlab”. Add the following code to the locals.tf file:

locals {
    region = "West Europe"
    workload = "terraformlab"
}

Use local variables

Now you can use the local variables in your configuration files. Go to the main.tf file and change the location of the resource group to use the local variable:

    location = local.region

since you have referenced the resource groups location for all your other resources you don’t need to change any other location value.

Now use the workload name as part of the name of the resource group and do some concatenation. Change the name of the resource group to:

    resource "azurerm_resource_group" "resource-group-lab" {
        name     = "rg-${local.workload}-cs"

Now run terraform fmt, terraform validate and terraform plan. Note that there is no changes to be made since the resource have the same name as before, just that we are using a variable to create it.

Add input variables and use them in the configuration

Input variables are variables that are passed to the Terraform configuration at runtime. They can have default value, validation rule and a description.

Create a file called variables.tf and add blocks of type variable and configure them with the following:

  • always_on: type = bool, default = false
  • costcenter: type: string, default = “TerraformLab”
  • creator: type: string, default = value of your name

The code for it looks like this:

    variable "always_on" {
        description = "Specify if the app service should always be on"
        type = bool
        default = false
    }

    variable "costcenter" {
        description = "Specify the costcenter where the resources belong to"
        type = string
        default = "TerraformLab"
    }

    variable "creator" {
        description = "Specify the creator of the resources"
        type = string
        default = "<your name>"
    }

replace the <your name> with your name. Now add a local variable (in locals.tf) called tags with a map containing the costcenter and creator variables. The local variable should now look something like this:

locals {
  region   = "West Europe"
  workload = "terraformlab"

  tags = {
    "costcenter" = var.costcenter
    "creator"    = var.creator
  }

}

and modify the azurerm_linux_web_app to use the tags local variable as well as the always_on variable also change the name to the random number you generated. The block should look like this:

resource "azurerm_linux_web_app" "app_service_lab" {
    name                = "<your random number>"
    resource_group_name = azurerm_resource_group.resource_group_lab.name
    location            = azurerm_resource_group.resource_group_lab.location
    service_plan_id     = data.azurerm_service_plan.service_plan_lab.id

    site_config {
        always_on = var.always_on
    }

    tags = local.tags
}

Run format, validate and plan. Plan should now show a change on the app service for the new tags. Run terraform apply and then verify in the Azure Portal that the tags you specified is added.

Use input instead of default values

Remove the default values for creator in the variables.tf file and run terraform plan. The Description of that variable will now be used to prompt for the value. Enter the same value that you used for the default value. The plan should now contains no changes.

An other option to provide the values is to add them with the -var option.

    terraform plan -var creator="<your name>"

to use several variables you can add them separated by a space:

    terraform plan -var creator="<your name>" -var costcenter="TerraformLab" -var always_on=false

Use input variables in a variable file

Create a file called terraform.dev.tfvars and add the following content:

environment = "dev"
costcenter = "TerraformLab"
creator = "<your name>"

To use the file you need to add to the terraform command the -var-file option:

terraform plan -var-file terraform.dev.tfvars

With this you can easily set up different environments with different values for the variables by creating a new file for each environment.

Organize your code / Modules

The term module has been mentioned before. A Module is just a folder with Terraform configuration files. You can use modules to organize your code and/or to reuse code. You can also use modules to create reusable components that you can share with others. Let’s try to create a module for the app service.

Create a module

Create a folder called appservice and create the following files (the name of the file is not important for the module but it is easier to follow allong the instructions if you name them the same as bellow):

  • main.tf
  • variables.tf
  • outputs.tf

Copy the following blocks from main.tf in the root folder to the main.tf file in the appservice folder:

  • data.azurerm_service_plan.service_plan_lab
  • azurerm_linux_web_app.app_service

Add some input variables to the module. Add the following variables to the variables.tf file in the appservice folder:

  • app_service_plan_name, type = string
  • app_service_plan_resource_group_name, type = string
  • name, type = string
  • resource_group_name, type = string
  • location, type = string
  • tags, type = map
  • always_on, type = bool, default = false

Also add description to the variables if you like.

The code should look like this:

variable "app_service_plan_name" {
    description = "Specify the name of the app service plan"
    type = string
}

variable "app_service_plan_resource_group_name" {
    description = "Specify the name of the resource group where the app service plan is located"
    type = string
}

variable "name" {
    description = "Specify the name of the app service"
    type = string
}

variable "resource_group_name" {
    description = "Specify the name of the resource group where the app service is located"
    type = string
}

variable "location" {
    description = "Specify the location of the app service"
    type = string
}

variable "always_on" {
    description = "Specify if the app service should always be on"
    type = bool
    default = false
}

variable "tags" {
    description = "Specify the tags for the app service"
    type = map
}

Now we need to modify the main.tf file in the appservice folder to use the input variables. The block should look like this:

data "azurerm_service_plan" "service_plan_lab" {
    name                = **var.app_service_plan_name**
    resource_group_name = **var.app_service_plan_resource_group_name**
}

resource "azurerm_linux_web_app" "app_service_lab" {
    name                = **var.name**
    resource_group_name = **var.resource_group_name**
    location            = **var.location**
    service_plan_id     = data.azurerm_service_plan.service_plan_lab.id

    site_config {
        always_on = **var.always_on**
    }

    tags = **var.tags**
}

Now it’s time use the module in. Into the main.ft file in the root folder add a block of type module and name the block app_service. Set the attribute source to the folder path of the module. The code should look like this:

module "app_service" {
  source = "./app_service"
}

Then we need to add in all the variables that doesn’t have values in the module and optional the ones that have default values. Take the values from the resources that you copied to the module. After you got all the values you need to remove those resources from the main.tf file in the root folder. The code should look like this:

module "app_service" {
  source = "./appservice"

  app_service_plan_name = "asp-terraform-lab"
  app_service_plan_resource_group_name = "shared-lab"
  name = "1492022956"
  resource_group_name = azurerm_resource_group.resource_group_lab.name
  location = azurerm_resource_group.resource_group_lab.location
  always_on = var.always_on
  tags = local.tags
}

You need to run terraform init to make terraform install the module, then run a terraform fmt --recursive the recursive flag is added so the code in the module(folder) will be formatted as well. Then run terraform validate and terraform plan (don’t forget the vars file). The plan should now show that the app service will be destroyed and recreated. This is because we moved the code to a module and terraform don’t know that it is the same resource. To fix this we need to add a block called moved To the main.tf file in the root folder and set the attributes from with the value of the old app service block and to with the value of the new app service inside the module. The code should look like this:

moved {
  from = azurerm_linux_web_app.app_service_lab
  to   = module.app_service.azurerm_linux_web_app.app_service
}

Running terraform plan again will now show that no resources are going to be destroyed or added (if you used the same name for the app service in your input). Once you have run apply these moved directives can safely be removed again.

With this new module you can now create multiple app services. Add the following code to the main.tf file in the root module:

module "app_service_2" {
    source = "./appservice"

    resource_group_name = azurerm_resource_group.resource_group_lab.name
    location = azurerm_resource_group.resource_group_lab.location

    app_service_plan_name = "asp-terraform-lab"
    app_service_plan_resource_group_name = "shared-lab"
    name = "appservice-test-8057" #something unique
    always_on = var.always_on
    tags = local.tags
}

You now need to run terraform init again since every module reference needs to be added to terraform. After this you can run plan and apply to create the second app service. You will need to provide the var-files on both plan and apply.

Module Outputs

You can also add outputs to your modules. This way you can reference the output of a module in another module (or in the root module/catalog). Add the following code to the output.tf file in the app_service folder:

output "app_service_url" {
    value = azurerm_linux_web_app.app_service_lab.default_hostname
}
output "app_service_name" {
    value = azurerm_linux_web_app.app_service_lab.name
}

To use this output you reference it with the keyword module, followed by your module instance name and then the module output name. The reference code looks like this:

module.app_service.app_service_url

You can also output information from the root module. Create an output.tf file in the root module/folder and add the following code:

output "app_service_url" {
    value = module.app_service.app_service_url
}
output "app_service_2_url" {
    value = module.app_service_2.app_service_url
}
output "app_service_name" {
    value = module.app_service.app_service_name
}
output "app_service_2_name" {
    value = module.app_service_2.app_service_name
}
output "resource_group_name" {
    value = azurerm_resource_group.resource_group_lab.name
}

These output can be picked up in a pipeline or similar to use in other steps. You can also use them as input in other modules. Run a terraform plan and apply to see the outputs. Don’t forget to provide the var-file.

Clean up

To remove everything you created in this workshop you can run the following commands:

    terraform destroy
    az group delete --name shared-lab --yes