Skip to content
Refalia Defani
Go back

Provisioning Azure Infrastructure with Terraform (IaC)

Context

Manual infrastructure provisioning via the Azure Portal is error-prone, undocumented, and inconsistent. Using Terraform, I automated the entire stack declaratively—ensuring every deployment is reproducible and version-controlled.

Architecture

┌─────────────────────────────────────────────────────┐
│          Resource Group: rg-refa-terraform-cicd      │
│          Region: Southeast Asia                      │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │     Virtual Network: vnet-refacicd            │  │
│  │     Address Space: 10.0.0.0/16               │  │
│  │                                               │  │
│  │  ┌─────────────────────────────────────────┐  │  │
│  │  │   Subnet: internal (10.0.2.0/24)       │  │  │
│  │  │                                         │  │  │
│  │  │   ┌─────────────────────────────────┐   │  │  │
│  │  │   │  VM: vm-refa-terraform          │   │  │  │
│  │  │   │  Size: Standard_B1s             │   │  │  │
│  │  │   │  OS: Ubuntu 22.04 LTS           │   │  │  │
│  │  │   │  Auth: SSH Key                  │   │  │  │
│  │  │   └─────────────────────────────────┘   │  │  │
│  │  └─────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  ┌──────────────┐  ┌──────────────┐                │
│  │ Public IP    │  │ NSG          │                │
│  │ (Static)     │  │ Allow SSH:22 │                │
│  └──────────────┘  └──────────────┘                │
└─────────────────────────────────────────────────────┘

Full Terraform Configuration

Provider Setup

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

provider "azurerm" {
  features {}
  use_msi         = true
  subscription_id = "<REDACTED>"
  client_id       = "<REDACTED>"
}

Using Managed Service Identity (MSI) for authentication — more secure than storing credentials in code.

Resource Group

resource "azurerm_resource_group" "rg" {
  name     = "rg-refa-terraform-cicd"
  location = "Southeast Asia"
}

Virtual Network & Subnet

resource "azurerm_virtual_network" "vnet" {
  name                = "vnet-refacicd"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}

resource "azurerm_subnet" "subnet" {
  name                 = "internal"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.2.0/24"]
}

Public IP (Static)

resource "azurerm_public_ip" "pip" {
  name                = "pip-refa"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  allocation_method   = "Static"
  sku                 = "Standard"
}

Network Security Group (SSH Access)

resource "azurerm_network_security_group" "nsg" {
  name                = "nsg-refa"
  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 = "*"
  }
}

Network Interface + NSG Association

resource "azurerm_network_interface" "nic" {
  name                = "nic-refa"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.pip.id
  }
}

resource "azurerm_network_interface_security_group_association" "example" {
  network_interface_id      = azurerm_network_interface.nic.id
  network_security_group_id = azurerm_network_security_group.nsg.id
}

Linux Virtual Machine

resource "azurerm_linux_virtual_machine" "vm" {
  name                = "vm-refa-terraform"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  size                = "Standard_B1s"
  admin_username      = "refatf"
  network_interface_ids = [
    azurerm_network_interface.nic.id,
  ]

  admin_ssh_key {
    username   = "refatf"
    public_key = file("~/.ssh/id_rsa.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"
  }
}

Resource Dependency Graph

Terraform automatically resolves dependencies:

Resource Group
    ├── VNet
    │   └── Subnet
    │       └── NIC → Public IP
    │           └── VM
    └── NSG
        └── NSG ↔ NIC Association

Key Design Decisions

DecisionWhy
MSI authNo credentials in code, more secure than service principal secrets
Static Public IPPersistent IP for SSH — doesn’t change on VM restart
Standard_B1sCost-efficient for CI/CD tooling (burstable)
SSH key authNo passwords — more secure, standard for Linux servers
Standard SKU for PIPRequired for availability zones and load balancer integration

Deployment Commands

# Initialize Terraform (download providers)
terraform init

# Preview changes
terraform plan

# Apply infrastructure
terraform apply

# Destroy when no longer needed
terraform destroy

Results

Tech Stack


Share this post on:

Previous Post
End-to-End DevSecOps Pipeline with Blue-Green Deployment on AKS
Next Post
Automating Security Pipelines & Real-time Notifications at Lyrid.io