DevOps on Azure, part 6: One batch — the whole stack in Terraform
Part 6from the DevOps on Azure series · 6 parts in all
The finale. MapleCart's complete Azure stack — resource group, VNet with private endpoints,
Key Vault, Azure SQL, Redis, App Service with slots and managed identity — in one Terraform
configuration. terraform apply, and the store exists.
Structure
maplecart-azure/
├── providers.tf # azurerm provider + remote state (Storage backend)
├── network.tf # resource group, VNet, subnets, NSGs
├── data.tf # Key Vault, Azure SQL, Redis
├── compute.tf # App Service plan, web app, slots, identity
├── edge.tf # private endpoints, DNS
└── outputs.tf
providers.tf and network.tf
terraform {
backend "azurerm" { # state in Azure Storage, locked
resource_group_name = "maplecart-tfstate"
storage_account_name = "maplecarttfstate"
container_name = "tfstate"
key = "prod.tfstate"
}
}
provider "azurerm" { features {} }
resource "azurerm_resource_group" "rg" { name = "maplecart-rg"; location = "West US 2" }
resource "azurerm_virtual_network" "vnet" {
name = "maplecart-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_subnet" "app" {
name = "app-subnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.2.0/24"]
# delegation to App Service goes here; private endpoints live in a data subnet
}
data.tf — Key Vault, SQL, Redis
resource "azurerm_key_vault" "kv" {
name = "maplecart-kv"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
enable_rbac_authorization = true # RBAC, not access policies
}
resource "random_password" "sql" { length = 32; special = true }
resource "azurerm_mssql_server" "sql" {
name = "maplecart-sql"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
version = "12.0"
administrator_login = "mapleadmin"
administrator_login_password = random_password.sql.result
public_network_access_enabled = false # private endpoints only
azuread_administrator { # Entra admin for identity logins
login_username = "maplecart-admins"; object_id = data.azurerm_client_config.current.object_id
}
}
resource "azurerm_mssql_database" "orders" {
name = "orders"
server_id = azurerm_mssql_server.sql.id
sku_name = "GP_S_Gen5_2" # serverless: autoscale + autopause
min_capacity = 0.5
auto_pause_delay_in_minutes = 60
short_term_retention_policy { retention_days = 14 }
}
resource "azurerm_redis_cache" "cache" {
name = "maplecart-cache"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
capacity = 1
sku_name = "Standard"
minimum_tls_version = "1.2"
}
compute.tf — App Service with slots and identity
resource "azurerm_service_plan" "plan" {
name = "maplecart-plan"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
os_type = "Linux"
sku_name = "P1v3" # slots + autoscale
}
resource "azurerm_linux_web_app" "web" {
name = "maplecart-web"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_service_plan.plan.location
service_plan_id = azurerm_service_plan.plan.id
identity { type = "SystemAssigned" } # managed identity: no passwords
site_config {
always_on = true
application_stack { docker_image = "maplecart.azurecr.io/web"; docker_image_tag = "v42" }
}
app_settings = {
"DB_HOST" = azurerm_mssql_server.sql.fully_qualified_domain_name
"KV_URI" = azurerm_key_vault.kv.vault_uri
# secrets pulled by identity at runtime, not stored here
}
slot "staging" { # the warm-swap slot from part 5
site_config { always_on = true }
}
}
# The identity can now read Key Vault (RBAC role) and log into SQL (Entra admin)
resource "azurerm_role_assignment" "kv_reader" {
scope = azurerm_key_vault.kv.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_linux_web_app.web.identity[0].principal_id
}
Apply and verify
terraform init # state backend + provider
terraform plan -out plan.tfplan
terraform apply plan.tfplan # ~10 minutes: RG, VNet, SQL, Redis, App Service, KV
az webapp browse -g maplecart-rg -n maplecart-web
az sql db list -g maplecart-rg -s maplecart-sql -o table
terraform destroy # the practice loop: everything gone, zero cost
Compare this finale with the AWS one and the lesson of both series lands: the resource names differ, the state backends differ, but the shape — network, identity-first data, PaaS compute with slots/staging, infrastructure as reviewed diffs — is the shape. The third series applies it to Google Cloud, where it holds one more time.