Click here to Skip to main content
15,867,686 members
Articles / Hosted Services / Azure
Article

Introduction to Infrastructure as Code - Part 3: IaC Hands-on with Terraform and Azure

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
13 Jun 2022CPOL7 min read 4.2K   2   2
How to automatically provision resources
This is Part 3 of a 3-article series that will introduce readers to infrastructure-as-code at a high-level and then provide a hands-on look at how to implement it on Azure using Terraform. This article explains how to create declarations that provision resources based on the Terraform language, and compares that process to using ARM templates.

This article is a sponsored article. Articles such as these are intended to provide you with information on products and services that we consider useful and of value to developers

The first article of this series introduced the basic concept of infrastructure as code (IaC) and what problems it solves. It also explained the differences between vendor-specific and multi-cloud IaC tools and the templating languages they support.

The second article introduced Azure ARM templates and explained JSON and Bicep syntax differences. Then, using Bicep, it presented a tutorial on creating a template that provisions infrastructure for a web app by creating several virtual machines (VMs) and their dependencies with a PostgreSQL database.

This final article uses HCL to write a Terraform plan that provisions infrastructure for a web app by creating several virtual machines and a SQL Server database. We’ll also contrast this with the ARM template used in the previous article, and highlight the similarities and differences between the two.

Follow the steps in this tutorial to get your Azure deployment running, and check out this GitHub repository to see the final IaC template.

Requirements

To follow this tutorial, be sure to:

Terraform and HCL

HashiCorp Terraform is one of the most popular IaC tools. Terraform is open-source and supports tons of cloud providers. The creators of the Terraform language defined it with a syntax called HashiCorp Configuration Language (HCL). HCL has a declarative configuration language intended to be easily readable to humans. Additionally, it also has a JSON-based variant that machines can more efficiently parse.

Creating a Terraform Template for Provisioning VMs and an Azure SQL Database

This section uses a Terraform plan to create a template that provisions infrastructure for a web app by creating several VMs and an Azure SQL server database. The structure of this process is shown in the diagram below:

Image 1

Similar to the second article in this series, the goal here is to create two types of resources: VMs (multiple of them) and an Azure SQL Server. Those resources depend on secondary resources that we need to create.

Building and Executing a Terraform Plan

Run the following command to initiate the login session:

Azure-CLI
> az login

Then run the command below to define your active subscription:

Azure-CLI
> az account set --subscription "YOUR-SUBSCRIPTION-NAME"

Now, we’ll build a Terraform template using the HCL syntax. We’ll later use the Terraform command-line interface (CLI) to run the terraform command with subcommands to create a Terraform plan and apply it to provision our infrastructure.

Image 2

Defining Azure Resource Manager as Terraform Provider

Unlike the ARM templates we configured in the last article, Terraform doesn't work directly with the cloud. Instead, it depends on providers that act as mediators and allow it to interact with a cloud provider like Azure. In our case, Terraform needs an Azure Resource Manager provider so it can create and use Azure resources.

Create a new file named providers.tf with the following code:

terraform {

  required_version = ">=0.12"

  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "~>2.0"
    }
  }
}

provider "azurerm" {
  features {}
}

Declaring a Resource Group

Let’s create a main.tf file containing the following code:

variable "vm_instance_count" {
  description = "Specify the number of vm instances."
  type        = number
  default     = 3
}

resource "azurerm_resource_group" "rg" {
  name      = "t3rr4f0rm-rg"
  location  = "eastus"
}

In the code above, we defined a vm_instance_count variable. The vm_instance_count parameter specifies how many VM instances we provision.

Next, we added a resource declaration by using the resource keyword. We set the symbolic name rg for our resource group named t3rr4f0rm-rg with the location set to East US. Like a variable name in an imperative programming language, this symbolic name represents the resource group in other parts of the Terraform file.

Declaring a Network Security Group

The network security group (NSG) filters network traffic to and from Azure resources in a virtual network. Network security group rules allow or deny inbound or outbound traffic from our created VMs and database resources. The rules allow us to specify the source, destination, port, and protocol.

resource "azurerm_network_security_group" "myterraformnsg" {
  name                = "t3rr4f0rm-nsg"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  security_rule {
    name                       = "SSH"
    priority                   = 1001
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

Declaring a Virtual Network

The virtual network (VNet) enables our VMs and database server to securely communicate with each other, the Internet, and on-premises networks. To create a VNet, add this code to the main.tf file:

resource "azurerm_virtual_network" "myterraformnetwork" {
  name                = "t3rr4f0rm-vn"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}

resource "azurerm_subnet" "myterraformsubnet" {
  name                 = "t3rr4f0rm-sn"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.myterraformnetwork.name
  address_prefixes     = ["10.0.1.0/24"]
}

Declaring Public IPs

VMs depend on a network interface, which depends on public IP (PIP) addresses. To create a public IP address, declare the following resource in the main.tf file:

resource "azurerm_public_ip" "publicIPs" {
  count               = var.vm_instance_count
  name                = "t3rr4f0rm-ip-${count.index}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  allocation_method   = "Dynamic"
}

You can use the count property to iterate over items in a collection and create multiple resources. The code above uses an integer index to make many instances of the public IP address. The number of IP addresses depends on the vm_instance_count variable.

Declaring a Network Interface

In Azure, a network interface connects a VM and the underlying software network. Here, we declare a collection of network interfaces with dynamic public IP addresses.

resource "azurerm_network_interface" "myterraformnic" {
  count               = var.vm_instance_count
  name                = "t3rr4f0rm-nic-${count.index}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  ip_configuration {
    name                          = "ipconfig1"
    public_ip_address_id          = azurerm_public_ip.publicIPs[count.index].id
    private_ip_address_allocation = "Dynamic"
    subnet_id                     = azurerm_subnet.myterraformsubnet.id
  }
}

resource "azurerm_network_interface_security_group_association" "example" {
  count                     = var.vm_instance_count
  network_interface_id      = azurerm_network_interface.myterraformnic[count.index].id
  network_security_group_id = azurerm_network_security_group.myterraformnsg.id
}

Declaring Virtual Machines

After declaring the secondary resources, we can finally declare a collection of VMs. But you must discover which VM size is available for your subscription. Fortunately, there’s an Azure CLI command for that. For example, since I’m hosting my VMs in the East US region, I can run the following command and pick one of the VM sizes where the Restrictions column has the None value:

Azure-CLI
> az vm list-skus --location eastus  --all --output table   
Name                       Zones    Restrictions
-------------------------  -------  -----------------------------------
Standard_B2s               1,2,3    None
Standard_B4ms              1,2,3    None
Standard_D1                3        ['NotAvailableForSubscription, ...
Standard_D11               3        ['NotAvailableForSubscription, ...

Now you can define your VM resource with the appropriate VM size. In my case, I’ve chosen Standard_B2s because it was available for my subscription in the East US region. But be aware that you might have to select another VM size. Other VM properties include storage profile, OS, and network profiles:

resource "azurerm_linux_virtual_machine" "myterraformvm" {
  count                 = var.vm_instance_count
  name                  = "t3rr4f0rm-vm-${count.index}"
  location              = azurerm_resource_group.rg.location
  resource_group_name   = azurerm_resource_group.rg.name
  network_interface_ids = [azurerm_network_interface.myterraformnic[count.index].id]
  size                  = "Standard_B2s"

  os_disk {
    name                 = "t3rr4f0rm-os-disk-${count.index}"
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  computer_name                   = "t3rr4f0rm-vm-${count.index}"
  admin_username                  = "azureuser"
  admin_password                  = "Adm1nP@55w0rd"
  disable_password_authentication = false
}

Declaring an Azure SQL Server Database Instance

Finally, we declare a resource for an Azure SQL Server database, a relational database-as-a-service. We define the SQL Server version and the administrator credentials:

resource "azurerm_sql_server" "example" {
  name                         = "t3rr4f0rmsqlserver"
  resource_group_name          = azurerm_resource_group.rg.name
  location                     = azurerm_resource_group.rg.location
  version                      = "12.0"
  administrator_login          = "MyAdministrator"
  administrator_login_password = "Adm1nP@55w0rd"
}

Deploying with the Terraform File

Let’s initialize a working directory containing the Terraform configuration files. This is the first command that you should run after writing a new Terraform configuration:

> terraform init

Run the terraform plan command to create an execution plan from the main.tf file:

terraform plan -out main.tfplan

In the above code, the terraform plan command generates an execution plan that calculates the actions required to create the configuration declared by the HCL syntax. This planning step allows you to review and approve the execution plan before creating new or changing existing resources.

Finally, run the terraform apply command to apply the execution plan to your cloud infrastructure:

terraform apply main.tfplan

The terraform apply command above executes the main.tfplan terraform plan file created previously and creates or modifies the resources in your cloud infrastructure.

The Final Provisioned Infrastructure

Now let’s see what our fully-provisioned infrastructure looks like on Azure.

On the Azure Portal, open the t3rr4f0rm-rg resource group and click the Resource Visualizer menu. The Resource Visualizer allows you to view and understand the components deployed with your Terraform template.

Image 3

For a more detailed view of the provisioned resources, go to the Azure Portal dashboard and click All Resources.

Image 4

Bicep and HCL Compared

Azure Bicep and HashiCorp’s HCL are declarative languages, and both offer CLI tools we can use to build and deploy applications on the cloud. We used Bicep to create an ARM template in the second article in this series. In this article, we used HCL to build a Terraform plan. Unlike the ARM templates, we configured in the last article, Terraform doesn't work directly with the cloud. Instead, it depends on providers that act as mediators and allow it to interact with a cloud provider like Azure. In our case, Terraform needs an Azure Resource Manager provider so it can create and use Azure resources.

The different way in which ARM templates and Terraform plans work with the cloud makes it essential to consider your target cloud environment when comparing ARM templates to Terraform plans for managing cloud infrastructure. Bicep is specific to Azure and not intended to work with other cloud services. Terraform is a multi-purpose language that you can use in different cloud environments.

If your goal is to automate deployments to the following environments, Terraform is likely a better option because it supports:

  • virtualization environments.
  • multiple cloud situations, such as Azure with other clouds.
  • on-premises workloads.

However, if your company is using ARM templates and heavily or solely using Azure Cloud to house infrastructure, Bicep is likely the better option.

Conclusion

In this final article of the IaC series, we introduced Terraform and the HashiCorp Configuration Language (HCL). We used HCL to write a Terraform plan that provisions infrastructure for a web app by creating several virtual machines and a SQL Server database.

Using the HCL syntax, we created a template to provision infrastructure for a web app by creating several virtual machines and an Azure SQL database. By the end of the article, we demonstrated the fully-provisioned infrastructure on Azure.

Finally, the article compared Terraform with Bicep, which we used in the previous article, explaining some of the similarities and differences between both languages.

You can learn even more about how to build consistent Azure infrastructure and IaC by visiting the Azure website.

To learn more about how you can use Bicep to define your Azure networking resources, check out the resource Create virtual network resources by using Bicep.

This article is part of the series 'Introduction to Infrastructure as Code View All

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Instructor / Trainer Alura Cursos Online
Brazil Brazil

Comments and Discussions

 
QuestionTerraform support for on-premises deployments Pin
rvaquette15-Jun-22 5:28
rvaquette15-Jun-22 5:28 
PraiseExcellent article! Pin
Member 802784713-Jun-22 5:30
Member 802784713-Jun-22 5:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.