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.
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:
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:
> az login
Then run the command below to define your active subscription:
> 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.
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:
> 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.
For a more detailed view of the provisioned resources, go to the Azure Portal dashboard and click All Resources.
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.