Azure CLI #
Install Azure CLI (Linux) #
# Install Azure CLI
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
Login to Azure #
# Login to Azure: Desktop version
az login
# Login to Azure: Server version (Device Code Authentication)
az login --use-device-code
# Shell output: (Open URL in Browser and pose the code)
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code C2763DNGS to authenticate.
Create Service Principal #
Find your Azure subscription ID: https://portal.azure.com/#view/Microsoft_Azure_Billing/SubscriptionsBladeV2
# Create a Service Principal: Syntax
az ad sp create-for-rbac --name "terraform-playground" --role contributor --scopes /subscriptions/SUBSCRIPTION_ID
# Create a Service Principal: Example
az ad sp create-for-rbac --name "terraform-playground" --role contributor --scopes /subscriptions/12345678-1234-some-id...
# Shell output:
Creating 'contributor' role assignment under scope '/subscriptions/12345678-1234-some-id...
'
The output includes credentials that you must protect. Be sure that you do not include these credentials in your code or check the credentials into your source control. For more information, see https://aka.ms/azadsp-cli
{
"appId": "5a56c6d8-some-app-id...",
"displayName": "terraform-playground",
"password": "bMF8Q~some-password...",
"tenant": "30d87815-some-tenant"
}
-
--name "terraform-playground"
service principal name -
--role contributor”
The Contributor role has permissions to create and manage all types of Azure resources but does not have permission to grant access to others. -
--scopes /subscriptions/SUBSCRIPTION_ID”
Defines the scope at which the role assignment is applied.
Set Environment Variables #
Set the service principal authentication details as environment variable so that Terraform can access Azure:
# Set environment variables: Bash
export ARM_CLIENT_ID="5a56c6d8-some-app-id..."
export ARM_CLIENT_SECRET="bMF8Q~some-password..."
export ARM_SUBSCRIPTION_ID="12345678-1234-some-id..."
export ARM_TENANT_ID="30d87815-some-tenant"
# Set environment variables: PowerShell
$Env:ARM_CLIENT_ID = "5a56c6d8-some-app-id..."
$Env:ARM_CLIENT_SECRET = "bMF8Q~some-password..."
$Env:ARM_SUBSCRIPTION_ID = "12345678-1234-some-id..."
$Env:ARM_TENANT_ID = "30d87815-some-tenant"
Terraform Prerequisites #
Install Terraform #
Use the following script to install Terraform ob Debian based Linux distributions:
# Install the HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
# Verify the GPG key fingerprint
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
# Add the official HashiCorp repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Install Terraform
sudo apt update && sudo apt-get install terraform
Verify the installation:
# Verify the installation / check version
terraform version
Example: Deploy VM with SG #
Overview #
This Terraform example configuration creates the following Azure resources:
- Resource Group: “terraform-playground”
- Virtual Network: “vnet” “10.0.0.0/16”
- Subnet: “vnet-subnet-1” “10.0.10.0/24"”
- Security Group: “security-group-1” allow SSH, HTTP, HTTPS
- Network Interface: “vm-1-nic”
- Public IP “vm-1-publicip”
- Virtual Machine: “vm-1” based on Ubuntu 22.04
GitHub repository: https://github.com/jueklu/terraform-azure-server-example
The file structure looks as follows:
├── main.tf
├── network.tf
├── outputs.tf
├── provider.tf
├── resource-group.tf
├── security-group.tf
Terraform Project #
Project Folder & Azure Provider #
Use the following script to create a Terraform project folder and the necessary provider configuration for Azure:
TF_PROJECT_NAME=terraform-azure-playground
mkdir $TF_PROJECT_NAME
cat << EOF >> "$TF_PROJECT_NAME/provider.tf"
# Terraform Provider
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=3.0.2"
}
}
}
# Configure the Microsoft Azure Provider
provider "azurerm" {
features {}
}
EOF
Azure Resource Group #
- resource-group.tf
# Resource Group
resource "azurerm_resource_group" "rg" {
name = "terraform-playground" # Define the resource group name
location = "West Europe" # Define Azure region
}
Network #
- network.tf
# Virtual Network
resource "azurerm_virtual_network" "vnet" {
name = "vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
# Subnet
resource "azurerm_subnet" "subnet-1" {
name = "vnet-subnet-1"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.10.0/24"]
}
# Public IP
resource "azurerm_public_ip" "vm_public_ip" {
name = "vm-1-publicip"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
allocation_method = "Dynamic" # Define static or dynamic
sku = "Basic"
}
# Network Interface
resource "azurerm_network_interface" "nic-1" {
name = "vm-1-nic"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.subnet-1.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.vm_public_ip.id
}
}
Optional: Define a static private IP address for the network interface
# Network Interface
resource "azurerm_network_interface" "nic-1" {
name = "vm-1-nic"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.subnet-1.id
private_ip_address_allocation = "Static"
private_ip_address = "10.0.10.5" # The first addresses in the subnet range are blocked
public_ip_address_id = azurerm_public_ip.vm_public_ip.id
}
}
Security Group #
- security-group.tf
# Network Security Group and Rules
resource "azurerm_network_security_group" "sg-1" {
name = "security-group-1"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
# Allow SSH
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 = "*"
}
# Allow HTTP
security_rule {
name = "HTTP"
priority = 1002
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# Allow HTTPS
security_rule {
name = "HTTPS"
priority = 1003
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
# Connect Security Group to Network Interface
resource "azurerm_network_interface_security_group_association" "nic-1-sg-1" {
network_interface_id = azurerm_network_interface.nic-1.id
network_security_group_id = azurerm_network_security_group.sg-1.id
}
SSH Key #
# Create a SSH key pair to access the Azure VM
ssh-keygen -t rsa -b 4096 -f ~/.ssh/tf-azure
Virtual Machine #
Note: Azure disallows command user names like “admin”.
- main.tf
# Virtual Machine
resource "azurerm_linux_virtual_machine" "vm-1" {
name = "vm-1"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
size = "Standard_F2"
admin_username = "tfadmin"
network_interface_ids = [
azurerm_network_interface.nic-1.id,
]
admin_ssh_key {
username = "tfadmin"
public_key = file("/home/ubuntu/.ssh/tf-azure.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
}
Outputs #
- outputs.tf
# Resource Group ID
output "resource_group_id" {
value = azurerm_resource_group.rg.id
}
# VM-1 Public IP
output "vm_public_ip" {
value = azurerm_public_ip.vm_public_ip.ip_address
description = "The public IP address of the virtual machine vm-1"
}
# VM-1 Private IP
output "vm_private_ip" {
value = azurerm_network_interface.nic-1.ip_configuration[0].private_ip_address
description = "The private IP address of the virtual machine vm-1"
}
Initialize Project & Deploy Resources #
# Initialize the Terraform project
terraform init
# Validate the configuration files
terraform validate
# Apply the configuration
terraform apply
# Apply the configuration: Skip approval
terraform apply -auto-approve
# Shell output:
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
resource_group_id = "/subscriptions/12345678-1234-some-id.../resourceGroups/terraform-playground"
vm_private_ip = "10.0.10.4"
vm_public_ip = ""
Output VM IP #
# Refresh the Terraform state to get the VM public IP
terraform refresh
# Shell output:
resource_group_id = "/subscriptions/12345678-1234-some-id.../resourceGroups/terraform-playground"
vm_private_ip = "10.0.10.4"
vm_public_ip = "13.80.158.150"
# Manually list the VM IP addresses (after the refresh)
terraform output vm_public_ip
terraform output vm_private_ip
# Shell output:
"13.80.158.150"
"10.0.10.4"
Verify Deployment Details #
The resource group should now be available in the Azure webinterface: https://portal.azure.com/#browse/resourcegroups
# List the resource group details: Azure CLI
az group show --name "terraform-playground"
# List resources in resource group
az resource list --resource-group "terraform-playground"
# List resources in resource group: Only name and type
az resource list --resource-group "terraform-playground" --query "[].{Name:name, Type:type}"
# Shell output:
[
{
"Name": "vnet",
"Type": "Microsoft.Network/virtualNetworks"
},
{
"Name": "vm-1-publicip",
"Type": "Microsoft.Network/publicIPAddresses"
},
{
"Name": "security-group-1",
"Type": "Microsoft.Network/networkSecurityGroups"
},
{
"Name": "vm-1-nic",
"Type": "Microsoft.Network/networkInterfaces"
},
{
"Name": "vm-1",
"Type": "Microsoft.Compute/virtualMachines"
},
{
"Name": "vm-1_OsDisk_1_6890be87dbaf4e969005f5ba19622653",
"Type": "Microsoft.Compute/disks"
}
]
# List deployment details
terraform show
Access the VM #
# SSH into vm-1
ssh -i ~/.ssh/tf-azure tfadmin@13.80.158.150
Delete Resources #
# Delete resources: Ask for confirmation
terraform destroy
# Delete resources: Auto approve
terraform destroy -auto-approve
Example: Dynamically Scale VM Deployment #
Overview #
This Terraform example uses the “provider.tf” and “resource-group.tf” configurations from the previous example. It dynamically deployes VMs and their dependencies based on a defined number.
GitHub repository: https://github.com/jueklu/terraform-azure-server-scaling-example
Terraform Project #
Network #
- network.tf
# Virtual Network Resource
resource "azurerm_virtual_network" "vnet" {
name = "vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
# Subnet
resource "azurerm_subnet" "subnet-1" {
name = "vnet-subnet-1"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.10.0/24"]
}
# Public IPs
resource "azurerm_public_ip" "vm_public_ip" {
count = 3 # Define number of VMs
name = "vm-${count.index + 1}-publicip"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
allocation_method = "Dynamic"
sku = "Basic"
}
# Network Interface
resource "azurerm_network_interface" "nic-1" {
count = 3 # Define number of VMs
name = "vm-${count.index + 1}-nic"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.subnet-1.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.vm_public_ip[count.index].id
}
}
Security Group #
- security-group.tf
# Network Security Group and Rules
resource "azurerm_network_security_group" "sg-1" {
name = "security-group-1"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
# Allow SSH
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 = "*"
}
# Allow HTTP
security_rule {
name = "HTTP"
priority = 1002
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# Allow HTTPS
security_rule {
name = "HTTPS"
priority = 1003
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
# Connect Security Group to Network Interface
resource "azurerm_network_interface_security_group_association" "nic-1-sg-1" {
count = 3 # Define number of VMs
network_interface_id = azurerm_network_interface.nic-1[count.index].id
network_security_group_id = azurerm_network_security_group.sg-1.id
}
Virtual Machine #
Note: Azure disallows command user names like “admin”.
- main.tf
# Virtual Machine
resource "azurerm_linux_virtual_machine" "vm-1" {
count = 3 # Define the VM count
name = "vm-${count.index + 1}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
size = "Standard_F2"
admin_username = "tfadmin"
network_interface_ids = [azurerm_network_interface.nic-1[count.index].id]
admin_ssh_key {
username = "tfadmin"
public_key = file("/home/ubuntu/.ssh/tf-azure.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
}
Outputs #
- outputs.tf
# Resource Group ID
output "resource_group_id" {
value = azurerm_resource_group.rg.id
}
# Output Public IPs of all VMs
output "vm_public_ips" {
value = { for i in range(length(azurerm_public_ip.vm_public_ip)) : "VM-${i + 1}" => azurerm_public_ip.vm_public_ip[i].ip_address }
description = "The public IP addresses of all virtual machines, mapped by VM identifier."
}
# Output Private IPs of all VMs
output "vm_private_ips" {
value = { for i in range(length(azurerm_network_interface.nic-1)) : "VM-${i + 1}" => azurerm_network_interface.nic-1[i].ip_configuration[0].private_ip_address }
description = "The private IP addresses of all virtual machines, mapped by VM identifier."
}
Initialize Project & Deploy Resources #
# Initialize the Terraform project
terraform init
# Validate the configuration files
terraform validate
# Apply the configuration
terraform apply
# Apply the configuration: Skip approval
terraform apply -auto-approve
# Shell output:
resource_group_id = "/subscriptions/12345678-1234-some-id.../resourceGroups/terraform-playground"
vm_private_ips = {
"VM-1" = "10.0.10.4"
"VM-2" = "10.0.10.6"
"VM-3" = "10.0.10.5"
}
vm_public_ips = {
"VM-1" = "104.40.228.166"
"VM-2" = "52.166.61.159"
"VM-3" = "23.97.242.234"
}
Output VM IP #
# Refresh the Terraform state to get the VM public IP
terraform refresh
# Manually list the VM IP addresses (after the refresh)
terraform output vm_public_ips
terraform output vm_private_ips
# Shell output:
terraform output vm_private_ips
{
"VM-1" = "104.40.228.166"
"VM-2" = "52.166.61.159"
"VM-3" = "23.97.242.234"
}
{
"VM-1" = "10.0.10.4"
"VM-2" = "10.0.10.6"
"VM-3" = "10.0.10.5"
}
Delete Resources #
# Delete resources: Ask for confirmation
terraform destroy
# Delete resources: Auto approve
terraform destroy -auto-approve
Links #
# Access Azure Portal
https://portal.azure.com