Promotes a function app deployment slot into Production (
azurerm_function_app_active_slot). An apply is a release, not convergent state. Targetshashicorp/azurerm ~> 4.0.
- 🔁 Promotes an existing deployment slot into
Productionas one keystone resource namedthis. ⚠️ An apply is a production swap. Changingslot_idperforms another one; destroying the resource does not swap back.- 🧊 Documents the function-app-specific consequence: in-flight executions on the outgoing instance are not drained.
- 🌐 Surfaces
overwrite_network_config— the setting that quietly decides whether a release also promotes staging's network posture. - 🕓 Emits
last_successful_swapas a release marker for downstream automation. - 🪞 Behaves identically to
terraform-azurerm-web-app-active-slot, including its two documented deviations.
💡 Why it matters: most modules in this library describe a desired state. This one performs an operation. The plan shows a small diff and the apply changes what is running in production — so the value here is in the documentation making that unmistakable, and in
overwrite_network_configbeing visible rather than buried.
If this module saves you time, please consider supporting its continued development:
- ⭐ Star the repository on GitHub.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
app["terraform-azurerm-function-app-linux or -windows"]
slot["terraform-azurerm-function-app-linux-slot or -windows-slot"]
cicd["a deployment pipeline puts code in the slot"]
this["terraform-azurerm-function-app-active-slot"]
sw["azurerm_function_app_active_slot"]
prod["the app Production slot goes live"]
peer["terraform-azurerm-web-app-active-slot"]
app -->|"hosts the slot"| slot
cicd -->|"deploys to"| slot
slot -->|"slot_id"| this
this -->|"creates"| sw
sw -->|"promotes the slot into"| prod
peer -->|"same design decisions, web app equivalent"| this
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class this me;
class sw keystone;
class app,slot,cicd,prod,peer sib;
flowchart TB
slotid["slot_id: changing it performs another swap"]
onc["overwrite_network_config: force-new, defaults true"]
warn["true promotes the slot network posture into Production"]
this["terraform-azurerm-function-app-active-slot"]
sw["azurerm_function_app_active_slot.this"]
drain["in-flight function executions are NOT drained"]
nodestroy["destroy removes the record, it does not swap back"]
out["outputs: id is the parent app id, slot_id, last_successful_swap"]
slotid -->|"which slot goes live"| this
onc -->|"release-time posture choice"| this
onc -->|"left true by design"| warn
this -->|"creates"| sw
sw -->|"during the swap"| drain
sw -->|"on terraform destroy"| nodestroy
sw -->|"emits"| out
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class this me;
class sw keystone;
class slotid,onc,warn,drain,nodestroy,out sib;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_function_app_active_slot.this |
1 | The keystone swap record, with its timeouts block. |
ℹ️ Shape note. This resource is deliberately its own module rather than a feature of the slot modules. Creating a slot and choosing which slot is live are different decisions made at different times — a slot exists for the life of the app, while an active-slot record changes at every release.
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm" { features {} }, auth, and subscription. |
Schema notes that bite (verified against the live provider schema):
- An apply is a production swap, not convergent state. Changing
slot_idperforms another swap. Destroying the resource removes Terraform's record and does not swap back — rollback is another swap, expressed as another apply. - The
idis the parent function app's ID, not a distinct resource ID, because the record tracks which slot is live for that app. One active-slot resource per function app; a second one for the same app fights the first. overwrite_network_configis force-new and defaults totrue, matching the service. Left true, a swap promotes the staging slot's network configuration into Production — so a staging slot with no VNet integration, or a laxer IP-restriction set, carries that laxness into production along with the code.- In-flight function executions on the outgoing instance are not drained. This is the difference from a web app swap: a long-running queue trigger or a Durable Functions orchestration mid-execution can be interrupted.
- Slot count depends on the hosting plan: Consumption gives 2 slots (production plus one), Premium 3, Dedicated 1-20 — and Flex Consumption does not support slots at all.
- A swap on a cold app takes noticeably longer than on a warm one, which is why
createandupdatedeserve headroom. - This resource type has no
tagssurface.
Website ContributororContributoron the function app, or a custom role coveringMicrosoft.Web/sites/slots/slotsswap/action.- Note that this is effectively release authority: whoever holds it can change what runs in production without touching any code repository or pipeline. Scope it to the app, not the subscription.
- An existing function app with at least one deployment slot, on a plan that supports slots. Consumption does (2 including production); Flex Consumption does not.
- Code already deployed to the slot being promoted; a swap of an empty slot promotes an empty app.
- A release window chosen deliberately — see the in-flight execution note above.
- The caller configures the
provider "azurerm" { features {} }block, auth, and subscription.
terraform-azurerm-function-app-active-slot/
├── providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf # slot_id, overwrite_network_config, timeouts tail
├── main.tf # keystone azurerm_function_app_active_slot.this; dynamic timeouts
├── outputs.tf # id (parent app), slot_id, last_successful_swap, overwrite_network_config
├── README.md # this document
├── SCOPE.md # cross-module contract
├── LICENSE # MIT
└── .gitignore # canonical library ignore set
provider "azurerm" {
features {}
}
module "orders_release" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-active-slot.git?ref=v1.0.0"
slot_id = module.orders_staging_slot.id
# Production carries VNet integration that staging does not reproduce.
overwrite_network_config = false
timeouts = { create = "45m", update = "45m" }
}ℹ️ The caller owns the provider, its authentication, and the mandatory
features {}block. This module never declares them.
Consumes
| Input | Type | Source module |
|---|---|---|
slot_id |
string |
terraform-azurerm-function-app-linux-slot (id) or terraform-azurerm-function-app-windows-slot (id) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The parent function app's Resource ID (first) | audit inventories |
slot_id |
The slot currently swapped into Production | release review |
last_successful_swap |
Timestamp of the last successful swap | release markers, downstream automation |
overwrite_network_config |
Whether the swap promotes the slot's network configuration | governance review |
The examples take deployment-slot IDs as inputs. slot_id is this module's only required argument, so the slot
is always something that already exists when the swap is configured.
variable "slot_ids" {
description = "Map of a stable key to the Resource ID of an existing Function App deployment slot."
type = map(string)
}1 · A minimal swap
module "release" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-active-slot.git?ref=v1.0.0"
slot_id = var.slot_ids["staging"]
}
⚠️ This applies the service defaultoverwrite_network_config = true, which promotes staging's network configuration into Production. Read example 3 before shipping it.
2 · No `function_app_id` input
# The app is inferred from the slot — there is deliberately no function_app_id.
slot_id = var.slot_ids["staging"]ℹ️ Adding a redundant app input would invite a mismatch the provider would then reject. Note the consequence for outputs:
idis the parent app's ID, since the record tracks which slot is live for that app.
3 · Preserving Production's network configuration
overwrite_network_config = false🔒 Set this to
falsewhenever the Production slot carries network settings the staging slot does not reproduce — VNet integration, private-endpoint wiring, or an IP-restriction set. Leaving ittruewith a laxer staging configuration promotes that laxness into production along with the code, and nothing in the plan flags it.
4 · Why the default is not inverted
# The module leaves this at the service default of true, deliberately.
# overwrite_network_config = trueℹ️ Inverting swap semantics would make Terraform behave differently from the portal and the Azure CLI — a difference you would discover during an incident, at the worst possible moment. Instead the field is documented in five places, emitted as an output, and set to
falsein every example where posture differs. This mirrors the identical decision interraform-azurerm-web-app-active-slot, so the two behave the same way.
5 · In-flight executions are not drained
# A queue-triggered or Durable Functions app mid-orchestration can be interrupted
# by the swap. Schedule the release around the workload.
module "release" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-active-slot.git?ref=v1.0.0"
slot_id = var.slot_ids["staging"]
}
⚠️ This is the real difference from a web app swap. A warm swap is not transparent for a function app: the outgoing instance is not given time to finish what it started. For a Durable Functions app, prefer a quiet window or a drain-and-pause step before the release.
6 · Rolling back
# Rollback is another swap, not a destroy.
slot_id = var.slot_ids["previous"]
⚠️ terraform destroydoes not swap back. It removes Terraform's record of which slot is live and leaves production exactly as it is. To revert, pointslot_idat the slot now holding the previous build and apply again.
7 · Using `last_successful_swap` as a release marker
output "orders_last_release" {
description = "Timestamp of the last successful production swap for the orders function app."
value = module.orders_release.last_successful_swap
}💡 This is genuinely useful in a pipeline: it is the service's own record of when production last changed, independent of your CI system's history.
8 · Timeout headroom for a cold app
timeouts = {
create = "45m"
read = "5m"
update = "45m"
delete = "30m"
}ℹ️ A swap on a warm, heavily loaded app takes several minutes; on a cold one it takes longer. The defaults are often enough and occasionally are not — give
createandupdateroom rather than discovering the limit mid-release.
9 · One active-slot resource per app
# ❌ Two active-slot resources against the same function app.
module "release_a" { slot_id = var.slot_ids["blue"] }
module "release_b" { slot_id = var.slot_ids["green"] }
⚠️ Both would resolve to the sameid— the parent app's — so they fight, and each apply would swap away the other's work. Model a blue/green release as one resource whoseslot_idchanges.
10 · Blue/green with a variable
variable "live_slot" {
description = "Which slot should be live: blue or green."
type = string
default = "blue"
validation {
condition = contains(["blue", "green"], var.live_slot)
error_message = "live_slot must be blue or green."
}
}
module "release" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-active-slot.git?ref=v1.0.0"
slot_id = var.live_slot == "blue" ? var.slot_ids["blue"] : var.slot_ids["green"]
overwrite_network_config = false
}💡 One resource, one variable, and the release becomes a reviewable one-line change. The plan will show
slot_idchanging — which is exactly the signal a release approver should be looking for.
11 · Gating the swap behind the deployment
module "release" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-active-slot.git?ref=v1.0.0"
slot_id = var.slot_ids["staging"]
# Ensure the slot exists and is configured before promoting it.
depends_on = [module.staging_slot]
}
⚠️ depends_onorders the slot's creation, not its deployment. If code reaches the slot from a CI pipeline rather than from Terraform, the swap can still promote an empty or stale slot — sequence that in the pipeline, not here.
12 · Reviewing release posture across apps
output "release_review" {
description = "Per-app release state. An overwrite_network_config of true means the last swap promoted staging's network posture."
value = {
for k, m in module.releases : k => {
live_slot = m.slot_id
last_swap = m.last_successful_swap
overwrote_network = m.overwrite_network_config
}
}
}💡
overwrite_network_configis emitted precisely so this table can be built from state. Atrueon an app whose production slot has private networking is the row to question.
13 · Slots are unavailable on Flex Consumption
# ❌ A Flex Consumption function app has no deployment slots at all.
# (The ordinary Consumption plan DOES have them -- 2, including production.)
⚠️ The failure arrives at apply, not at plan, because the plan tier is not an input to this module. Consumption gives 2 slots (production plus one), Premium 3, Dedicated 1-20 — and Flex Consumption does not support slots at all. Check the plan before wiring this module in.
14 · Importing an existing swap record
terraform import 'module.release.azurerm_function_app_active_slot.this' \
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-orders/providers/Microsoft.Web/sites/func-orders"ℹ️ Note the ID is the function app's, not a slot's or a swap's — consistent with what the resource emits.
15 · 🏗️ End-to-end composition
provider "azurerm" {
features {}
}
# 1 · The resource group.
module "orders_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-orders-eastus2"
location = "eastus2"
}
# 2 · A plan that supports slots -- anything except Flex Consumption.
module "orders_plan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-service-plan.git?ref=v1.0.0"
name = "asp-orders-eastus2"
resource_group_name = module.orders_rg.name
location = module.orders_rg.location
os_type = "Linux"
sku_name = "P1v3"
}
# 3 · The function app. Its production posture includes VNet integration.
module "orders_storage" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"
name = "storders0prod" # globally unique; choose your own
resource_group_name = module.orders_rg.name
location = module.orders_rg.location
}
module "orders_vnet" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-network.git?ref=v1.0.0"
name = "vnet-orders"
resource_group_name = module.orders_rg.name
location = module.orders_rg.location
address_space = ["10.70.0.0/16"]
subnets = {
functions = {
address_prefixes = ["10.70.1.0/24"]
delegations = [{
name = "serverFarms"
service_delegation = { name = "Microsoft.Web/serverFarms" }
}]
}
}
}
module "orders_app" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-linux.git?ref=v1.0.0"
name = "func-orders"
resource_group_name = module.orders_rg.name
location = module.orders_rg.location
service_plan_id = module.orders_plan.id
storage_account_name = module.orders_storage.name
virtual_network_subnet_id = module.orders_vnet.subnet_ids["functions"]
}
# 4 · The staging slot the pipeline deploys to.
module "orders_staging_slot" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-linux-slot.git?ref=v1.0.0"
name = "staging"
function_app_id = module.orders_app.id
storage_account_name = module.orders_storage.name
}
# 5 · The release — this module. One resource; the slot_id is the release lever.
module "orders_release" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-function-app-active-slot.git?ref=v1.0.0"
slot_id = module.orders_staging_slot.id
# Production has VNet integration that staging does not reproduce — do not
# promote staging's network configuration along with the code.
overwrite_network_config = false
# A cold swap on a loaded app takes minutes.
timeouts = { create = "45m", update = "45m" }
depends_on = [module.orders_staging_slot]
}
# 6 · The release marker, for the pipeline to record.
output "orders_last_release" {
description = "When production last changed, per the service's own record."
value = module.orders_release.last_successful_swap
}💡 This wiring shows the whole release path: resource group → a slot-capable plan → the app with its production network posture → the staging slot a pipeline deploys into → the swap. Step 5 carries the decision that matters:
overwrite_network_config = false, because step 3's VNet integration must survive the release. Note also thatdepends_onorders the slot's creation, not its deployment — getting code into the slot is the pipeline's job. Output names on sibling modules are illustrative; match them to the versions you pin.
Required: slot_id.
Release posture: overwrite_network_config (defaults to the service's true).
Universal tail: timeouts. This resource type does not support tags.
Full object() schemas
variable "slot_id" {
type = string # the slot to promote. Changing it performs ANOTHER swap.
# The parent app is inferred from this — there is no function_app_id input.
}
variable "overwrite_network_config" {
type = bool
default = true # the SERVICE default, left uninverted deliberately — see Design Principles
# force-new
}
variable "timeouts" {
type = object({ create = optional(string), read = optional(string), update = optional(string), delete = optional(string) })
default = null
}| Output | Description | Kind |
|---|---|---|
id |
The Azure Resource ID of the Function App Active Slot | Passthrough |
function_app_id |
The parent Function App's Resource ID, reconstructed from slot_id by dropping the trailing "/slots/" | Derived |
slot_id |
The Resource ID of the deployment slot currently swapped into Production, as the provider read it back | Passthrough |
slot_name |
The name of the deployment slot being promoted, parsed from the last segment of the slot_id input | Derived |
function_app_name |
The name of the parent Function App, parsed from slot_id | Derived |
resource_group_name |
The resource group containing the Function App and its slots, parsed from slot_id | Derived |
subscription_id |
The subscription containing the Function App, parsed from slot_id | Derived |
last_successful_swap |
The timestamp of the last successful swap with Production, as reported by the app's swap status | Passthrough |
manages_no_azure_resource |
Always true | Constant |
destroy_performs_no_swap_back |
Constant | |
update_performs_another_swap |
Always true | Constant |
is_singleton_per_function_app |
Always true | Constant |
serializes_on_parent_app_lock |
Always true | Constant |
swap_poll_interval_seconds |
The interval, in seconds, at which the provider polls the app for swap completion during create and update | Passthrough |
swap_completion_is_detected_by_comparing_the_source_slot_name |
Constant | |
swap_may_terminate_executing_functions |
Always true, and it is the substantive difference from a web app swap | Constant |
swap_resets_function_keys_when_secret_storage_is_files |
Constant | |
slots_available_per_hosting_plan |
Derived | |
shares_its_implementation_with_web_app_active_slot |
Constant | |
slot_id_is_read_from_the_last_swap_source |
Constant | |
read_fails_when_app_has_no_swap_history |
Always true | Constant |
overwrite_network_config |
The value of overwrite_network_config in force for the next swap, as configured | Passthrough |
overwrite_network_config_read_back_from_azure |
Always false | Constant |
overwrite_network_config_defaults_to_true_on_import |
Always true | Constant |
overwrite_network_config_maps_to_preserve_vnet |
Constant | |
force_new_arguments |
The arguments that force a new record when changed | Derived |
slot_id_is_not_force_new |
Always true, and it is the surprising half of the lifecycle | Constant |
arm_api_version |
The ARM API version the provider uses for this resource: 2023-12-01 | Derived |
timeout_defaults |
The provider's built-in timeouts for this resource, which appear nowhere in the schema | Derived |
effective_timeouts |
The timeouts actually in force: the caller's values where supplied, the provider's defaults otherwise | Derived |
No secret is accepted or emitted. A swap record carries a slot reference and a flag.
- This module performs an operation, not a state. An apply swaps production. Changing
slot_idswaps again. A destroy removes Terraform's record and leaves production untouched — so destroy is not rollback; rollback is another swap. There is no configuration that makes a swap idempotent, so the honest treatment is to say so plainly rather than model it as convergent. overwrite_network_configis left at the service default oftrue, against this suite's secure-by-default convention. Inverting swap semantics would make Terraform differ from the portal and CLI, and the moment you would discover that difference is during an incident. Instead the field is documented in the variable description, the schema notes, the design principles, and here; it is emitted as an output; and every example where production posture differs sets it tofalse. This is the identical decision taken forterraform-azurerm-web-app-active-slot, so the two modules behave the same way.- In-flight executions are not drained, and that is function-app-specific. A web app swap warms and shifts traffic; a function app's outgoing instance is not given time to finish a queue message or a Durable Functions orchestration. Releases should be scheduled around the workload rather than assumed transparent.
- The
idis the parent app's, which constrains the model. One active-slot resource per function app — a second resolves to the same ID and the two fight, each apply undoing the other. Blue/green is one resource with a changingslot_id, not two resources. - No
function_app_idinput. The app is inferred from the slot; a redundant input would only invite a mismatch the provider would reject. depends_onorders creation, not deployment. If code arrives in the slot from a CI pipeline, Terraform cannot know whether the slot is ready — sequencing the deploy before the swap is the pipeline's job.- The plan tier is invisible here. Consumption gives 2 slots (production plus one), Premium 3, Dedicated 1-20 — and Flex Consumption does not support slots at all — but the plan is not an input, so a slot that does not exist is only discovered at apply.
features {}dependence. The module carries noprovider {}block. If it appears not to initialize in isolation, the cause is a missing caller-sideprovider "azurerm" { features {} }.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Network posture on release | not inverted — left at the service's true, and emitted as an output |
set false to preserve Production's configuration |
| Posture visibility | overwrite_network_config and slot_id emitted |
— (no opt-out; that is the point) |
| Release semantics | documented as non-convergent; destroy is not rollback | — (no configuration changes this) |
| Redundant inputs | no function_app_id — inferred from the slot |
— (structural) |
| Release authority | documented as equivalent to production write access | — |
| Swap duration | timeouts exposed with guidance to raise create / update |
accept the defaults |
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0— never a branch. - This library is plan-only during authoring; a human runs
terraform plan/applyfrom CI against real credentials. ⚠️ For this module the human review is the release gate. Read theslot_iddiff and theoverwrite_network_configvalue before approving, and confirm the slot actually holds the intended build — Terraform cannot tell you that.
terraform validateproves the configuration is internally consistent and type-correct against the pinned provider schema. For this module that is a narrow claim — there are only two inputs and no enums to check.terraform fmt -checkenforces canonical formatting.- Neither command calls Azure. Only
terraform plan(run by a human, from CI) exercises the ARM API — the module ships without any cloud apply. Whether the plan tier supports slots, whether the slot holds the intended build, and what a swap will do to in-flight executions are all apply-time or operational facts that no static check reaches.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-orders-eastus2/providers/Microsoft.Web/sites/func-orders"
slot_id = "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-orders-eastus2/providers/Microsoft.Web/sites/func-orders/slots/staging"
last_successful_swap = "2026-07-28T14:22:11Z"
overwrite_network_config = false
| Symptom | Cause | Fix |
|---|---|---|
Provider configuration not present / features error |
No caller-side provider "azurerm" { features {} }. |
Add the provider block with features {} in the root module. |
| Apply fails: slots not supported | The function app is on Flex Consumption, the one plan with no slot support. | Move to Consumption, Premium or Dedicated, or use Flex Consumption's rolling site updates instead; the plan tier is not visible to this module. |
| Production lost its VNet integration after a release | overwrite_network_config was left true and staging had no VNet integration. |
Set it to false; re-apply the app's network configuration. |
terraform destroy did not roll back the release |
Destroy removes Terraform's record; it does not swap. | Roll back by pointing slot_id at the previous slot and applying. |
| Queue messages or orchestrations were interrupted | In-flight executions are not drained during a swap. | Schedule releases around the workload; drain or pause before swapping. |
| Two active-slot resources keep undoing each other | Both resolve to the same parent-app id. |
Use one resource whose slot_id changes. |
| The swap promoted an empty or stale app | depends_on ordered the slot's creation, not its deployment. |
Sequence the deploy before the swap in the pipeline. |
| The swap timed out | A cold or heavily loaded app takes longer than the defaults allow. | Raise timeouts.create / timeouts.update. |
| Plan wants to replace the resource after changing the flag | overwrite_network_config is force-new. |
Expected; confirm the replacement performs the swap you intend. |
| Unexpected production change with no code deploy | Someone with slotsswap/action swapped outside the pipeline. |
Scope that permission to the app and treat it as release authority. |
- azurerm provider —
azurerm_function_app_active_slot - Set up staging environments in Azure App Service
- Azure Functions deployment slots
- Sibling modules:
terraform-azurerm-function-app-linux,terraform-azurerm-function-app-windows,terraform-azurerm-function-app-linux-slot,terraform-azurerm-function-app-windows-slot,terraform-azurerm-web-app-active-slot,terraform-azurerm-service-plan. - This module's
SCOPE.md.
💙 "Infrastructure as Code should be standardized, consistent, and secure."