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_eachmap collections. Built for AzureRM v4.x.
- π¦ 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_appas the single primarythisresource (optional β set tonulland 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_idexposed 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 markedsensitive.
π‘ 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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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
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.
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
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.
| 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 validateemits aDeprecated Resourcewarning whenevervar.function_appis set. Preferlinux_function_apps/windows_function_apps/flex_consumption_appsfor 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 useservice_plan_id/client_certificate_mode/builtin_logging_enabled/functions_extension_version. Flex Consumption introducesruntime_name+runtime_versionand thestorage_container_*/storage_authentication_typetriple.
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
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" }
}| 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 |
| 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 |
βΉοΈ
*_namesoutputs for the secret-bearing collections (function_app_name,linux_names,windows_names,flex_names, slot*_names,connection_names) are markedsensitive = truebecause the name is supplied through asensitiveinput. IDs, hostnames, and principal IDs are provider-computed and non-sensitive.
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) andshared_access_key_enabled = falseon 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,notifyare 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"]
}Module-level
resource_group_name(string, required) β default RG for every app; per-app override available.location(string, defaultnull) β default region for every app; per-app override available.tags(map(string), default{}) β merged into every resource; per-apptagsmerged on top.
App families (each a map(object), default {}, all sensitive)
function_app(singleobject, defaultnull, sensitive) β the legacy primarythis.linux_function_apps,windows_function_apps,flex_consumption_appsfunction_app_slots,linux_function_app_slots,windows_function_app_slotsconnections
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
.tffiles are the authoritative schema.
| 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 |
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.
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 withfor_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 forfor_eachover a sensitive map.
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.
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.
- π 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
objectschemas 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 usesnonsensitive(keys(...))so values never leak throughfor_each. - π
for_each, nevercountβ stable keys, surgical lifecycle, no positional drift. - π§© One boundary, one contract β every Functions shape behind a single
SCOPE.md.
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.
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 passlinux_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"
}Deprecated Resourcewarning onazurerm_function_app. Expected whenevervar.function_appis set. The legacy resource is removed in AzureRM v5.0 β migrate tolinux_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-webjobsandscm. 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 requiresstorage_account_access_keyomitted,shared_access_key_enabled = falseon the storage account, and a Storage Blob Data role assignment for the app identity β otherwise the app cannot read host storage. corsperpetual diff (older 4.x). Fixed in azurerm 4.19.0 (site_config.corsno longer plans on the default value). Ensure your provider lock is β₯ 4.19.0.WEBSITE_CONTENTSHARE/WEBSITE_CONTENTAZUREFILECONNECTIONSTRINGdrift. Correct update behavior inapp_settingslanded 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 inapp_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).
- 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."