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
| Decision | Why |
|---|---|
| MSI auth | No credentials in code, more secure than service principal secrets |
| Static Public IP | Persistent IP for SSH — doesn’t change on VM restart |
| Standard_B1s | Cost-efficient for CI/CD tooling (burstable) |
| SSH key auth | No passwords — more secure, standard for Linux servers |
| Standard SKU for PIP | Required 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
- Provisioning time: Manual (30+ min) →
terraform apply(~3 min) - Configuration drift: Eliminated — infrastructure matches code
- Reproducibility: Same config, same result, every time
- Cost control:
Standard_B1skeeps monthly cost minimal
Tech Stack
- IaC: Terraform (HCL) + AzureRM Provider v3
- Cloud: Microsoft Azure (Southeast Asia)
- Auth: Managed Service Identity (MSI)
- Resources: Resource Group, VNet, Subnet, NSG, Public IP, NIC, Linux VM
- OS: Ubuntu 22.04 LTS (Canonical)