Trust is earned, not given

A different perspective

2024-08-21 · Projects

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.