# Terraform Patterns

Alternative-path patterns for AppOnboard scaffold. Used when `.tf` files detected or user overrides `iacFormat`. Per-resource config comes from `mcp_azure_mcp_azureterraformbestpractices` at runtime — this file covers layout, provider, naming, state, tagging, and wiring.

## File Structure

### Default (greenfield or user override)

```
infra/
├── main.tf              # Root module — provider, resource group, module calls
├── variables.tf         # Input variables (all configurable values)
├── outputs.tf           # Exported values (endpoints, resource IDs)
├── backend.tf           # State backend config (local default, Azure Storage for prod)
├── terraform.tfvars     # Default variable values (from prepare-plan.json)
└── modules/
    ├── app-service/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── outputs.tf
    ├── container-app/
    ├── sql-database/
    ├── key-vault/
    ├── log-analytics/
    └── ...
```

### Non-Azure IaC coexistence (GCP/AWS TF already in repo)

When `detectedInfraProvider.terraform` is `"gcp"`, `"aws"`, or `"multi"` (without `azurerm`), write Azure TF to a **separate directory** from existing non-Azure TF. Never overwrite or modify existing IaC files.

**Output directory rule:** If existing TF is NOT at `infra/`, write to `infra/`. If existing TF IS at `infra/` (or any path containing `infra`), write to `infra-azure/`. Same module structure as default layout.

Each Azure service gets its own module. `main.tf` orchestrates resource group + module calls.

## Provider Configuration

```hcl
terraform {
  required_version = ">= 1.5"

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

provider "azurerm" {
  features {}
  subscription_id                 = var.subscription_id
  resource_provider_registrations = "none"
}
```

> ⛔ Never pin to exact patch versions (e.g., `= 4.1.0`). Use `~> 4.0` to allow minor/patch updates. `azurerm` manages API versions internally — if a resource isn't available in `azurerm`, use `azapi_resource` with the latest stable ARM API version.

> **Conditional access (AADSTS530084):** `azurerm` provider re-requests tokens that violate device-binding policies. Fix: (1) switch to Bicep, or (2) use service principal auth (`ARM_CLIENT_ID` + `ARM_CLIENT_SECRET` + `ARM_TENANT_ID`).

## variables.tf

Required variables: `environment_name` (string, default "dev"), `location` (string, default "eastus"), `subscription_id` (string), `session_id` (string), `deployed_by` (string). All configurable values MUST be variables — no hardcoded regions, names, or SKUs.

## terraform.tfvars

Populate from `prepare-plan.json`: `environment_name`, `location`, `subscription_id`, `session_id`. See naming-patterns.md for naming convention.

## Backend

Local backend by default: `backend "local" { path = "terraform.tfstate" }`. Recommend Azure Storage backend in `postDeployRecommendations[]` for production.

## Resource Group

Use `rg-${var.project_name}-${var.environment_name}-${random_string.suffix.result}` with `tags = local.tags`. Suffix prevents collisions across AppOnboard sessions.

## Naming Convention

```hcl
resource "random_string" "suffix" {
  length  = 4
  special = false
  upper   = false
}

locals {
  # Pattern: {type}-{appname}-{env}-{suffix}
  app_name    = "app-${var.environment_name}-${random_string.suffix.result}"
  kv_name     = "kv-${var.environment_name}-${random_string.suffix.result}"
  sql_name    = "sql-${var.environment_name}-${random_string.suffix.result}"
  # Storage/ACR: alphanumeric only, no hyphens
  storage_name = "st${replace(var.environment_name, "-", "")}${random_string.suffix.result}"
  acr_name     = "cr${replace(var.environment_name, "-", "")}${random_string.suffix.result}"
}
```

Cross-reference naming with [prepare/references/naming-patterns.md](../../prepare/references/naming-patterns.md) — Terraform names must match `prepare-plan.json.naming.resources[]`.

## Resource Tags — Mandatory

Apply all 5 AppOnboard tags via `local.tags` — see [iac-generation-rules.md § Session Tags](iac-generation-rules.md) for tag names and values.

```hcl
locals {
  tags = {
    "app-onboard-skill"       = "true"
    "app-onboard-session-id"  = var.session_id
    "created-at"      = timestamp()
    "environment"     = var.environment_name
    "deployed-by"     = var.deployed_by
  }
}
```

> ⚠️ `timestamp()` changes on every plan. Add `lifecycle { ignore_changes = [tags["created-at"]] }` on every resource.

## Secrets — random_password, Not random_string

```hcl
resource "random_password" "db_password" {
  length  = 32
  special = true
  lifecycle { ignore_changes = [result] }
}

resource "azurerm_key_vault_secret" "db_password" {
  name         = "db-password"
  value        = random_password.db_password.result
  key_vault_id = azurerm_key_vault.kv.id
}
```

> ⛔ NEVER use `random_string` for secrets — it is not marked `sensitive` in state. Always use `random_password`.

## Container Apps — Two-Phase Wiring

Same circular dependency as Bicep — see [bicep-container-apps.md](bicep-container-apps.md). Phase 1: placeholder image, no ACR/KV refs. Phase 2: build + push, assign AcrPull, update via `az containerapp update --image` outside Terraform.

```hcl
# Phase 1: Placeholder image
resource "azurerm_container_app" "app" {
  template {
    container {
      name   = "app"
      image  = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
      cpu    = 0.25
      memory = "0.5Gi"
    }
  }
  identity {
    type = "SystemAssigned"
  }
}
```

## outputs.tf

Export: `resource_group_name`, `app_url` (https://${hostname}), `resource_ids` (list of all deployed resource IDs for deploy-result.json).

## Security Defaults

Apply same security rules as Bicep — see [bicep-patterns-security.md](bicep-patterns-security.md). Terraform-specific syntax:

| Rule | Terraform HCL |
|------|---------------|
| Managed identity | `identity { type = "SystemAssigned" }` |
| ⛔ No SQL admin password | `azuread_authentication_only = true`. Never generate `administrator_login_password` |
| Key Vault RBAC | `enable_rbac_authorization = true` on `azurerm_key_vault` |
| KV secret reference | `app_settings = { KEY = "@Microsoft.KeyVault(VaultName=..;SecretName=..)" }` |
| HTTPS only | `https_only = true`, `minimum_tls_version = "1.2"` |
| Storage | `https_traffic_only_enabled = true`, `allow_nested_items_to_be_public = false`, `min_tls_version = "TLS1_2"` |
| ⛔ Cosmos DB RBAC | `azurerm_cosmosdb_sql_role_assignment`, NOT `azurerm_role_assignment` — see [rbac-roles.md](rbac-roles.md) |
| RBAC assignments | `principal_type = "ServicePrincipal"` REQUIRED — see [rbac-roles.md](rbac-roles.md) |
| SCM/FTP auth | `scm.allow: true` (scaffold), `ftp.allow: false` (always) — use `azapi_resource` |
