Skip to content

microsoftexpert/tf-mod-azure-function-app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ Azure Function App Terraform Module

One composite boundary for the entire Azure Functions family β€” Linux, Windows, Flex Consumption, deployment slots, in-app functions, hybrid connections, Service Connector links, and the legacy primary β€” all deeply typed, secure-by-default, and driven by for_each map collections. Built for AzureRM v4.x.

Terraform AzureRM module type resources


🧩 Overview

  • 🟦 Creates Linux, Windows, and Flex Consumption Function Apps from typed map(object) collections β€” any mix, keyed by your own stable strings.
  • πŸŸͺ Manages the legacy azurerm_function_app as the single primary this resource (optional β€” set to null and use the modern collections instead).
  • πŸ” Deployment slots for every app family, plus active-slot swaps for blue/green promotion.
  • 🧬 In-app functions, hybrid connections, and Service Connector links to downstream Azure resources.
  • πŸ” Managed identity (system- and user-assigned) on every app, with principal_id exposed for role assignments.
  • πŸ”Ž Read-only data lookups for existing apps and their host keys.
  • πŸ›‘οΈ Secure-by-default: https_only = true, public_network_access_enabled = false, secret-bearing collections marked sensitive.

πŸ’‘ Why it matters: one module, one state contract, one review surface for every shape an Azure Functions workload can take β€” instead of a different module per OS and a fan-out of loose resources.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


πŸ—ΊοΈ Where this fits in the family

flowchart LR
 RG[tf-mod-azure-resource-group] --> SP[tf-mod-azure-app-service-plan]
 RG --> ST[tf-mod-azure-storage-account]
 SP -->|service_plan_id| FA
 ST -->|storage_account_name / access_key| FA
 FA[tf-mod-azure-function-app]
 FA -->|principal_id| RA[tf-mod-azure-role-assignment]
 FA -->|id| PE[tf-mod-azure-private-endpoint]
 FA -->|id| DS[tf-mod-azure-monitor-diagnostic-setting]
 FA -->|default_hostname| DNS[tf-mod-azure-dns-zone records]

 style FA fill:#8957E5,color:#fff
 style SP fill:#0078D4,color:#fff
Loading

This module consumes a Service/App Service Plan ID, a storage account, and a resource group; it emits app IDs, default hostnames, and identity principal IDs that downstream modules compose on. See the Cross-Module Contract.


🧬 What this module builds

flowchart TD
 subgraph tf-mod-azure-function-app
 THIS["azurerm_function_app.this<br/>(legacy primary Β· optional)"]
 LIN["azurerm_linux_function_app.linux<br/>(for_each)"]
 WIN["azurerm_windows_function_app.windows<br/>(for_each)"]
 FLEX["azurerm_function_app_flex_consumption.flex<br/>(for_each)"]

 LSLOT["azurerm_linux_function_app_slot.linux_slot"]
 WSLOT["azurerm_windows_function_app_slot.windows_slot"]
 SLOT["azurerm_function_app_slot.slot<br/>(legacy)"]
 ACTIVE["azurerm_function_app_active_slot.active_slot"]

 FUNC["azurerm_function_app_function.function"]
 HYB["azurerm_function_app_hybrid_connection.hybrid_connection"]
 CONN["azurerm_function_app_connection.connection"]
 end

 LIN -->|function_app_id| LSLOT
 WIN -->|function_app_id| WSLOT
 THIS -->|function_app_name| SLOT
 LSLOT -->|slot_id| ACTIVE
 WSLOT -->|slot_id| ACTIVE
 SLOT -->|slot_id| ACTIVE

 LIN -->|function_app_id| FUNC
 WIN -->|function_app_id| FUNC
 LIN -->|function_app_id| HYB
 WIN -->|function_app_id| HYB
 LIN -->|function_app_id| CONN
 WIN -->|function_app_id| CONN

 style THIS fill:#8957E5,color:#fff
Loading

Resource inventory (11 resources + 4 data sources):

  • πŸŸͺ azurerm_function_app.this β€” primary, the legacy (deprecated) Function App; optional.
  • azurerm_linux_function_app.linux β€” Linux Function Apps (for_each).
  • azurerm_windows_function_app.windows β€” Windows Function Apps (for_each).
  • azurerm_function_app_flex_consumption.flex β€” Flex Consumption apps (for_each).
  • azurerm_linux_function_app_slot.linux_slot β€” Linux deployment slots.
  • azurerm_windows_function_app_slot.windows_slot β€” Windows deployment slots.
  • azurerm_function_app_slot.slot β€” legacy deployment slots.
  • azurerm_function_app_active_slot.active_slot β€” active-slot swap (blue/green promotion).
  • azurerm_function_app_function.function β€” functions deployed into an app.
  • azurerm_function_app_hybrid_connection.hybrid_connection β€” Relay hybrid connections.
  • azurerm_function_app_connection.connection β€” Service Connector links to target resources.
  • data.azurerm_function_app / data.azurerm_function_app_host_keys / data.azurerm_linux_function_app / data.azurerm_windows_function_app β€” read-only lookups.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
AzureRM >= 4.0, < 5.0
Provider validated against azurerm v4.77.0 binary schema

⚠️ azurerm_function_app (the legacy primary) is deprecated and is scheduled for removal in AzureRM v5.0. terraform validate emits a Deprecated Resource warning whenever var.function_app is set. Prefer linux_function_apps / windows_function_apps / flex_consumption_apps for all new work.

ℹ️ Schema notes that bite in 4.x: legacy uses app_service_plan_id / client_cert_mode / enable_builtin_logging; the modern resources use service_plan_id / client_certificate_mode / builtin_logging_enabled / functions_extension_version. Flex Consumption introduces runtime_name + runtime_version and the storage_container_* / storage_authentication_type triple.


πŸ“ Module Structure

tf-mod-azure-function-app/
β”œβ”€β”€ providers.tf # Terraform + azurerm version pins (no provider block)
β”œβ”€β”€ variables.tf # deeply-typed schemas for every app family + child collection
β”œβ”€β”€ main.tf # primary this + for_each child resources + data lookups
β”œβ”€β”€ outputs.tf # id / name / default_hostname / principal_id maps
β”œβ”€β”€ SCOPE.md # cross-module contract (consumes / emits / gotchas)
└── README.md # this file

βš™οΈ Quick Start

module "function_app" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-function-app?ref=v1.0.0"

  resource_group_name = module.resource_group.name
  location            = "eastus2"

  linux_function_apps = {
    api = {
      name            = "fapi-fie2-dev-001"
      service_plan_id = module.app_service_plan.id

      storage_account_name       = module.storage_account.name
      storage_account_access_key = module.storage_account.primary_access_key

      site_config = {
        application_stack = {
          python_version = "3.11"
        }
      }
      identity = { type = "SystemAssigned" }
    }
  }

  tags = { environment = "dev", workload = "payments-api" }
}

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
resource_group_name string tf-mod-azure-resource-group.name
location string caller / tf-mod-azure-resource-group.location
service_plan_id (per app) string tf-mod-azure-app-service-plan.id
storage_account_name / storage_account_access_key (per app) string / string (sensitive) tf-mod-azure-storage-account.name / .primary_access_key
virtual_network_subnet_id (per app) string tf-mod-azure-subnet.id (VNet integration)
target_resource_id (per connection) string any downstream tf-mod-azure-*.id
relay_id (per hybrid connection) string tf-mod-azure-relay-hybrid-connection.id

Emits

Output Description Consumed by
resource_group_name RG passthrough sibling modules in the same RG
function_app_id Legacy primary ID (null if not created) tf-mod-azure-private-endpoint, diagnostic settings
function_app_name Legacy primary name (sensitive) downstream name references
function_app_default_hostname Legacy primary *.azurewebsites.net DNS records, gateway backends
function_app_principal_id Legacy primary system identity object ID tf-mod-azure-role-assignment
linux_ids / windows_ids / flex_ids Maps of app IDs tf-mod-azure-private-endpoint, diagnostic settings
linux_names / windows_names / flex_names Maps of app names (sensitive) downstream name references
linux_default_hostnames / windows_default_hostnames / flex_default_hostnames Maps of hostnames DNS, API gateway backends
linux_principal_ids / windows_principal_ids / flex_principal_ids Maps of identity principal IDs tf-mod-azure-role-assignment
slot_ids / linux_slot_ids / windows_slot_ids (+ names/hostnames/principal_ids) Slot maps swap automation, DNS
connection_ids / function_ids / hybrid_connection_ids / active_slot_ids Child collection ID maps dependency wiring
connection_names Map of connection names (sensitive) downstream name references
function_names Map of in-app function names dependency wiring
host_keys Map of { default_function_key, master_key } (sensitive) callers invoking functions

ℹ️ *_names outputs for the secret-bearing collections (function_app_name, linux_names, windows_names, flex_names, slot *_names, connection_names) are marked sensitive = true because the name is supplied through a sensitive input. IDs, hostnames, and principal IDs are provider-computed and non-sensitive.


πŸ“š Example Library

1️⃣ Minimal β€” one Linux Python app
module "function_app" {
  source              = "git::https://github.com/microsoftexpert/tf-mod-azure-function-app?ref=v1.0.0"
  resource_group_name = module.rg.name
  location            = "eastus2"

  linux_function_apps = {
    api = {
      name                       = "fapi-fie2-dev-001"
      service_plan_id            = module.plan.id
      storage_account_name       = module.sa.name
      storage_account_access_key = module.sa.primary_access_key
      site_config = {
        application_stack = { python_version = "3.11" }
      }
    }
  }
}
2️⃣ Windows.NET app
windows_function_apps = {
  reporting = {
    name                        = "frpt-fie2-dev-001"
    service_plan_id             = module.plan.id
    storage_account_name        = module.sa.name
    storage_account_access_key  = module.sa.primary_access_key
    functions_extension_version = "~4"
    site_config = {
      application_stack = {
        dotnet_version              = "v8.0"
        use_dotnet_isolated_runtime = true
      }
    }
  }
}
3️⃣ Flex Consumption app (modern serverless)
flex_consumption_apps = {
  events = {
    name            = "fevt-fie2-dev-001"
    service_plan_id = module.flex_plan.id # SKU-GATED: must be an FC1 Flex plan

    runtime_name    = "python"
    runtime_version = "3.11"

    storage_container_type      = "blobContainer"
    storage_container_endpoint  = "${module.sa.primary_blob_endpoint}deployments"
    storage_authentication_type = "SystemAssignedIdentity"

    instance_memory_in_mb  = 2048
    maximum_instance_count = 100

    identity = { type = "SystemAssigned" }
  }
}

πŸ’‘ Flex Consumption uses a deployment blob container, not a file share, and scales to 1,000 instances. Slots are not supported on Flex.

4️⃣ Identity-based storage (no access key)
linux_function_apps = {
  api = {
    name                          = "fapi-fie2-prd-001"
    service_plan_id               = module.plan.id
    storage_account_name          = module.sa.name
    storage_uses_managed_identity = true # AzureWebJobsStorage via managed identity
    identity                      = { type = "SystemAssigned" }
    site_config                   = { application_stack = { node_version = "20" } }
  }
}

πŸ”’ Requires storage_account_access_key = null (omit it) and shared_access_key_enabled = false on the storage account, plus a Storage Blob Data role assignment for the app identity.

5️⃣ Private networking (VNet integration, no public access)
linux_function_apps = {
  api = {
    name                          = "fapi-fie2-prd-002"
    service_plan_id               = module.plan.id # Elastic Premium or Dedicated
    storage_account_name          = module.sa.name
    storage_account_access_key    = module.sa.primary_access_key
    public_network_access_enabled = false # secure-by-default (explicit here)
    virtual_network_subnet_id     = module.snet.id
    https_only                    = true # secure-by-default
    site_config = {
      vnet_route_all_enabled = true
      ftps_state             = "Disabled"
      minimum_tls_version    = "1.2"
      application_stack      = { python_version = "3.11" }
    }
  }
}

⚠️ A network-secured storage account is not supported on the Consumption plan β€” use Elastic Premium / Dedicated / Flex for private storage.

6️⃣ Easy Auth v2 (auth_settings_v2 with Entra ID)
linux_function_apps = {
  secured = {
    name                       = "fsec-fie2-dev-001"
    service_plan_id            = module.plan.id
    storage_account_name       = module.sa.name
    storage_account_access_key = module.sa.primary_access_key
    auth_settings_v2 = {
      auth_enabled           = true
      require_authentication = true
      unauthenticated_action = "RedirectToLoginPage"
      active_directory_v2 = {
        client_id                  = var.aad_client_id
        tenant_auth_endpoint       = "https://login.microsoftonline.com/${var.tenant_id}/v2.0"
        client_secret_setting_name = "AAD_CLIENT_SECRET"
      }
      login = { token_store_enabled = true }
    }
  }
}
7️⃣ Deployment slot + active-slot swap (blue/green)
linux_function_app_slots = {
  api_staging = {
    name                       = "staging"
    function_app_id            = module.function_app.linux_ids["api"]
    storage_account_name       = module.sa.name
    storage_account_access_key = module.sa.primary_access_key
    site_config                = { application_stack = { python_version = "3.11" } }
  }
}

active_slots = {
  api_promote = {
    slot_id = module.function_app.linux_slot_ids["api_staging"]
  }
}

⚠️ Slots are unavailable on Consumption and Flex Consumption; Premium allows 3, Dedicated up to 20.

8️⃣ In-app functions + hybrid connection
functions = {
  http_trigger = {
    name            = "HttpTrigger1"
    function_app_id = module.function_app.linux_ids["api"]
    config_json = jsonencode({
      bindings = [{ name = "req", type = "httpTrigger", direction = "in", authLevel = "function", methods = ["get"] }]
    })
  }
}

hybrid_connections = {
  onprem_sql = {
    function_app_id = module.function_app.linux_ids["api"]
    relay_id        = module.relay.hybrid_connection_id
    hostname        = "sql.corp.internal"
    port            = 1433
  }
}
9️⃣ Service Connector to a downstream resource
connections = {
  to_cosmos = {
    name               = "cosmos_link"
    function_app_id    = module.function_app.linux_ids["api"]
    target_resource_id = module.cosmos.id
    client_type        = "python"
    authentication     = { type = "systemAssignedIdentity" }
  }
}
πŸ”Ÿ Backup, mounted storage, and connection strings
windows_function_apps = {
  legacysvc = {
    name                       = "flgc-fie2-prd-001"
    service_plan_id            = module.plan.id
    storage_account_name       = module.sa.name
    storage_account_access_key = module.sa.primary_access_key

    connection_string = [{
      name  = "SqlDb"
      type  = "SQLAzure"
      value = var.sql_connection_string # provider-marked sensitive
    }]

    storage_account = [{
      name         = "reports"
      type         = "AzureFiles"
      account_name = module.sa.name
      share_name   = "reports"
      access_key   = module.sa.primary_access_key
      mount_path   = "C:\\reports"
    }]

    backup = {
      name                = "daily"
      storage_account_url = var.backup_sas_url
      schedule            = { frequency_interval = 1, frequency_unit = "Day" }
    }
  }
}
1️⃣1️⃣ for_each at scale β€” a fleet of apps from a map
locals {
  apis = {
    payments = { stack = { python_version = "3.11" } }
    ledger   = { stack = { python_version = "3.11" } }
    notify   = { stack = { node_version = "20" } }
  }
}

linux_function_apps = {
  for k, cfg in local.apis : k => {
    name                       = "f${k}-fie2-prd-001"
    service_plan_id            = module.plan.id
    storage_account_name       = module.sa.name
    storage_account_access_key = module.sa.primary_access_key
    identity                   = { type = "SystemAssigned" }
    site_config                = { application_stack = cfg.stack }
  }
}

πŸ’‘ Keys are the lifecycle anchor. payments, ledger, notify are stable map keys β€” renaming one is a destroy + recreate of that app only. Pick keys that outlive the app name.

1️⃣2️⃣ Managed identity β†’ RBAC (principal_ids into tf-mod-azure-role-assignment)
module "function_app" {
  source              = "git::https://github.com/microsoftexpert/tf-mod-azure-function-app?ref=v1.0.0"
  resource_group_name = module.rg.name
  location            = "eastus2"
  linux_function_apps = {
    api = {
      name                       = "fapi-fie2-prd-003"
      service_plan_id            = module.plan.id
      storage_account_name       = module.sa.name
      storage_account_access_key = module.sa.primary_access_key
      identity                   = { type = "SystemAssigned" }
    }
  }
}

module "role" {
  source               = "git::https://github.com/microsoftexpert/tf-mod-azure-role-assignment?ref=v1.0.0"
  scope                = module.key_vault.id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = module.function_app.linux_principal_ids["api"]
}
1️⃣3️⃣ Read-only lookups (existing app + host keys)
function_app_lookups = {
  shared = { name = "fshared-fie2-prd-001", resource_group_name = "rg-shared" }
}
host_keys_lookups = {
  shared = { name = "fshared-fie2-prd-001", resource_group_name = "rg-shared" }
}
# module.function_app.host_keys["shared"].master_key (sensitive)
1️⃣4️⃣ Legacy primary (DEPRECATED β€” migration only)
function_app = {
  name                       = "flegacy-fie2-dev-001"
  app_service_plan_id        = module.plan.id
  storage_account_name       = module.sa.name
  storage_account_access_key = module.sa.primary_access_key
  os_type                    = "linux" # IMMUTABLE
  site_config                = { linux_fx_version = "PYTHON|3.11" }
}

⚠️ Emits a deprecation warning and will break under AzureRM v5.0. Use only to import/manage pre-existing legacy apps during migration.

πŸ—οΈ 1️⃣5️⃣ End-to-end composition (mandatory)
module "rg" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-resource-group?ref=v1.0.0"
  name   = "rg-fie2-functions-prd"
}

module "plan" {
  source              = "git::https://github.com/microsoftexpert/tf-mod-azure-app-service-plan?ref=v1.0.0"
  name                = "asp-fie2-prd-001"
  resource_group_name = module.rg.name
  location            = module.rg.location
  os_type             = "Linux"
  sku_name            = "EP1"
}

module "sa" {
  source              = "git::https://github.com/microsoftexpert/tf-mod-azure-storage-account?ref=v1.0.0"
  name                = "stfie2funcprd001"
  resource_group_name = module.rg.name
  location            = module.rg.location
}

module "function_app" {
  source              = "git::https://github.com/microsoftexpert/tf-mod-azure-function-app?ref=v1.0.0"
  resource_group_name = module.rg.name
  location            = module.rg.location

  linux_function_apps = {
    api = {
      name                       = "fapi-fie2-prd-010"
      service_plan_id            = module.plan.id
      storage_account_name       = module.sa.name
      storage_account_access_key = module.sa.primary_access_key
      identity                   = { type = "SystemAssigned" }
      site_config                = { application_stack = { python_version = "3.11" } }
    }
  }

  linux_function_app_slots = {
    api_staging = {
      name                       = "staging"
      function_app_id            = module.function_app.linux_ids["api"]
      storage_account_name       = module.sa.name
      storage_account_access_key = module.sa.primary_access_key
    }
  }

  tags = { environment = "prd", workload = "payments" }
}

module "role" {
  source               = "git::https://github.com/microsoftexpert/tf-mod-azure-role-assignment?ref=v1.0.0"
  scope                = module.sa.id
  role_definition_name = "Storage Blob Data Owner"
  principal_id         = module.function_app.linux_principal_ids["api"]
}

πŸ“₯ Inputs

Module-level

  • resource_group_name (string, required) β€” default RG for every app; per-app override available.
  • location (string, default null) β€” default region for every app; per-app override available.
  • tags (map(string), default {}) β€” merged into every resource; per-app tags merged on top.

App families (each a map(object), default {}, all sensitive)

  • function_app (single object, default null, sensitive) β€” the legacy primary this.
  • linux_function_apps, windows_function_apps, flex_consumption_apps
  • function_app_slots, linux_function_app_slots, windows_function_app_slots
  • connections

Child collections (map(object), default {})

  • functions, hybrid_connections, active_slots (non-sensitive)

Data lookups (map(object({ name, resource_group_name }), default {})

  • function_app_lookups, host_keys_lookups, linux_function_app_lookups, windows_function_app_lookups
Full nested object schema (linux/windows apps)
map(object({
 name = string # globally unique hostname label
 service_plan_id = string
 storage_account_name = optional(string)
 storage_account_access_key = optional(string) # SENSITIVE
 storage_uses_managed_identity = optional(bool)
 https_only = optional(bool, true) # secure-by-default
 public_network_access_enabled = optional(bool, false) # secure-by-default
 functions_extension_version = optional(string)
 client_certificate_mode = optional(string) # Required | Optional | OptionalInteractiveUser
 virtual_network_subnet_id = optional(string)
 resource_group_name = optional(string) # inherits module-level var
 location = optional(string) # inherits module-level var

 identity = optional(object({ type = string, identity_ids = optional(set(string)) }))

 site_config = optional(object({
 always_on = optional(bool)
 ftps_state = optional(string) # AllAllowed | FtpsOnly | Disabled
 minimum_tls_version = optional(string)
 vnet_route_all_enabled = optional(bool)
 application_stack = optional(object({
 dotnet_version = optional(string)
 node_version = optional(string)
 python_version = optional(string) # Linux only
 java_version = optional(string)
 powershell_core_version = optional(string)
 use_dotnet_isolated_runtime = optional(bool)
 docker = optional(list(object({ # Linux only
 registry_url = string, image_name = string, image_tag = string
 })), [])
 }))
 cors = optional(object({ allowed_origins = optional(set(string)), support_credentials = optional(bool) }))
 ip_restriction = optional(list(object({ /* action, ip_address, service_tag, priority, headers, … */ })), [])
 }))

 auth_settings_v2 = optional(object({ /* active_directory_v2, login, custom_oidc_v2, … */ }))
 backup = optional(object({ name = string, storage_account_url = string, schedule = optional(object({...})) }))
 connection_string = optional(list(object({ name = string, type = string, value = string })), []) # value SENSITIVE
 storage_account = optional(list(object({ name, type, account_name, share_name, access_key, mount_path })), [])
 sticky_settings = optional(object({ app_setting_names = optional(list(string)), connection_string_names = optional(list(string)) }))
}))

ℹ️ The four .tf files are the authoritative schema.


🧾 Outputs

Output Description Notes
resource_group_name RG passthrough β€”
function_app_id Legacy primary ID null when var.function_app is null
function_app_name Legacy primary name πŸ”’ sensitive
function_app_default_hostname Legacy primary hostname null when not created
function_app_principal_id Legacy primary system identity object ID non-sensitive
linux_ids / windows_ids / flex_ids Map of app IDs keyed by your map key
linux_names / windows_names / flex_names Map of app names πŸ”’ sensitive
linux_default_hostnames / windows_default_hostnames / flex_default_hostnames Map of hostnames β€”
linux_principal_ids / windows_principal_ids / flex_principal_ids Map of identity principal IDs null per key when no identity block
slot_ids / linux_slot_ids / windows_slot_ids (+ *_names πŸ”’ / *_default_hostnames / *_principal_ids) Slot maps β€”
connection_ids Map of Service Connector IDs β€”
connection_names Map of connection names πŸ”’ sensitive
function_ids / function_names Map of in-app function IDs / names β€”
hybrid_connection_ids Map of hybrid connection IDs β€”
active_slot_ids Map of active-slot resource IDs β€”
host_keys Map of { default_function_key, master_key } πŸ”’ sensitive

🧠 Architecture Notes

Managed identity β†’ principal_id β†’ role assignment

Every app family exposes identity = { type, identity_ids }. With type = "SystemAssigned" (or "SystemAssigned, UserAssigned"), Azure mints an object ID surfaced as *_principal_ids[<key>]. That value is intentionally non-sensitive β€” it is not a secret, and downstream tf-mod-azure-role-assignment calls need it to grant the app access to Key Vault, Storage, Cosmos, etc. User-assigned identities are passed in via identity.identity_ids (a set(string) of identity resource IDs) and do not produce a system principal_id; reference the user-assigned identity's own principal_id from tf-mod-azure-managed-identity instead.

The for_each map(object) model β€” keys are the contract

Every collection is a map(object) keyed by a caller-supplied stable string, never count. The key β€” not the app name β€” is the Terraform resource address (azurerm_linux_function_app.linux["api"]). Consequences:

  • Renaming a key is a destroy + recreate of only that one app; the rest of the map is untouched. Choose keys that outlive cosmetic name changes.
  • Adding/removing a key is a pure create/destroy of that instance β€” no churn across the collection.
  • Because the secret-bearing collections are sensitive = true, the module iterates them with for_each = nonsensitive(toset(keys(var.linux_function_apps))). Only the keys are de-sensitized for iteration (keys aren't secret); the values stay sensitive in state and plan. This is the supported pattern for for_each over a sensitive map.

Partial / targeted applies on a large composite

With 11 resource types behind one boundary, you can safely act on a slice using resource-address targeting, e.g.:

terraform apply -target='module.function_app.azurerm_linux_function_app.linux["api"]'

Because each instance is keyed independently and child resources reference parents by ID (function_app_id, slot_id) rather than implicit ordering, targeting one key does not disturb siblings. Slots depend on their parent app, and active_slot depends on its slot β€” Terraform's dependency graph enforces the correct order automatically.

The optional primary

azurerm_function_app.this is rendered with for_each = nonsensitive(var.function_app == null ? {}: { "this" = true }), so the legacy primary is 0-or-1. Outputs use one(values(azurerm_function_app.this)), returning the single instance or null. A caller managing only modern apps simply leaves function_app = null.


🧱 Design Principles

  • πŸ”’ Secure by default β€” https_only = true, public_network_access_enabled = false; the empty-est real call is the locked-down one.
  • 🧬 The type is the contract β€” deeply-typed object schemas mirror the provider; a fat-fingered nested key is a plan-time type error, not a runtime surprise.
  • πŸ›‘οΈ Secrets stay sensitive β€” secret-bearing collections are sensitive = true; iteration uses nonsensitive(keys(...)) so values never leak through for_each.
  • πŸ” for_each, never count β€” stable keys, surgical lifecycle, no positional drift.
  • 🧩 One boundary, one contract β€” every Functions shape behind a single SCOPE.md.

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan
terraform apply
terraform output

⚠️ Pin by tag, never by branch. Always reference ?ref=v1.0.0 (immutable) β€” never ?ref=main.


πŸ§ͺ Testing

The offline proof gate (no cloud needed):

terraform fmt -check # zero formatting diffs
terraform validate # zero errors (one expected Deprecated Resource warning if function_app is set)
tflint # lint pass

πŸ’¬ Example Output

linux_ids = {
  "api" = "/subscriptions/…/resourceGroups/rg-fie2-functions-prd/providers/Microsoft.Web/sites/fapi-fie2-prd-010"
}
linux_default_hostnames = {
  "api" = "fapi-fie2-prd-010.azurewebsites.net"
}
linux_principal_ids = {
  "api" = "8f3c…-…-…-…-7b21"
}

πŸ” Troubleshooting

  • Deprecated Resource warning on azurerm_function_app. Expected whenever var.function_app is set. The legacy resource is removed in AzureRM v5.0 β€” migrate to linux_function_apps / windows_function_apps / flex_consumption_apps.
  • App won't start / "Functions Runtime is unreachable." Every app requires a working storage account. If the storage account is deleted, network-blocked, or its key was rotated without updating the app, the runtime fails. On Consumption you cannot use a network-secured storage account.
  • Host ID collision. In 4.x, function apps that share a storage account fail to start when their computed host IDs collide. Give each app its own storage account (or set a unique AzureWebJobsSecretStorageType/host ID). High-volume Durable/Event Hubs apps should always use a dedicated storage account.
  • Keys/blobs disappearing. Do not apply blob lifecycle-management policies to a Function App's storage account β€” Functions persists access keys in blobs prefixed azure-webjobs and scm. A policy that deletes them breaks key auth. Exclude those containers if a policy is unavoidable.
  • Slots fail to create. Deployment slots are not supported on Consumption or Flex Consumption; Premium allows 3 and Dedicated up to 20. Move the app to a slot-capable plan first.
  • Availability-zone apps. When the plan has AZ support enabled, the storage account must be zone-redundant (ZRS/GZRS) or app creation fails.
  • Identity-based storage (storage_uses_managed_identity = true). Also requires storage_account_access_key omitted, shared_access_key_enabled = false on the storage account, and a Storage Blob Data role assignment for the app identity β€” otherwise the app cannot read host storage.
  • cors perpetual diff (older 4.x). Fixed in azurerm 4.19.0 (site_config.cors no longer plans on the default value). Ensure your provider lock is β‰₯ 4.19.0.
  • WEBSITE_CONTENTSHARE / WEBSITE_CONTENTAZUREFILECONNECTIONSTRING drift. Correct update behavior in app_settings landed in azurerm 4.21.0.
  • Functions Proxies / AzureWebJobsDashboard. Removed in the v4 runtime. Use route rewrites and Application Insights instead β€” don't carry these forward in app_settings.
  • Linux Consumption retirement. The Linux Consumption plan is retiring (Sept 30, 2028); v3 runtime on Linux Consumption stops Sept 30, 2026. New serverless workloads should use Flex Consumption (flex_consumption_apps).

πŸ”— Related Docs

  • Terraform Registry β€” azurerm_linux_function_app, azurerm_windows_function_app, azurerm_function_app_flex_consumption, _slot, _function, _connection, _hybrid_connection, _active_slot
  • Microsoft Learn β€” Azure Functions hosting options (plan comparison)
  • Microsoft Learn β€” Storage considerations for Azure Functions (host ID, lifecycle policies)
  • Microsoft Learn β€” Migrate Azure Functions v3.x β†’ v4.x breaking changes
  • Microsoft Learn β€” Terraform AzureRM provider version history (4.x)
  • β€” tf-mod-azure-app-service-plan, tf-mod-azure-storage-account, tf-mod-azure-role-assignment, tf-mod-azure-private-endpoint

πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."

Packages

 
 
 

Contributors

Languages