Azure App Service is a service used to create and deploy scalable, mission-critical web apps.
Hi @George Flourentzos ,
You want one idempotent Bicep deployment that pins the container image (linuxFxVersion with a digest) and moves it into production through a staging slot with warm-up, using buildVersion / targetBuildVersion instead of az webapp deployment slot swap. The documentation does not cover containers explicitly for this mechanism, so I reproduced the pattern below on an S1 Linux plan (API version 2024-04-01, nginx images pinned by digest as A, B and C).
1. Does the declarative swap work for containers, and is it the full swap?
Yes. The staging slots documentation says a differing targetBuildVersion triggers "the swap operation" and restricts only auto swap, a different feature, on Linux and containers. In the test, every successful swap appeared in the activity log as Apply Web App Slot Configuration, StartSlotWarmup, EndSlotWarmup and Swap Web App Slots, the same multi-phase sequence as a regular swap; failed swaps showed no EndSlotWarmup and ended with Swap Web App Slots: Failed.
The warm-up is an HTTP request to the container, controlled by WEBSITE_SWAP_WARMUP_PING_PATH and WEBSITE_SWAP_WARMUP_PING_STATUSES; the documentation states that warm-up and swap stop when the status code is not in the list. A container that crashes or never answers fails the swap with "did not respond to http ping" (Azure OSS Developer Support). This warm-up is independent of the Health check feature (healthCheckPath); to gate the swap on your health endpoint, point the ping path at it and restrict the statuses, for example to 200.
The failure does reach the ARM deployment. With a ping path returning 404 and statuses set to 200, the deployment failed with BadRequest: Cannot swap slots ... application initialization in 'staging' slot either took too long or failed, and production stayed on the previous image. After fixing the path, the next run completed the swap. The documentation warns that a failed swap can leave the slot blocked with "The slot cannot be changed because its configuration settings have been prepared for swap"; this did not happen in my single test, but a pipeline check for that state is a sensible precaution.
2. Do linuxFxVersion and buildVersion move with the content?
Yes. After swapping B into production, production had B's linuxFxVersion and buildVersion and staging had A's; the kind values in the resource JSON were exchanged as well, although kind is not on the documented list of swapped settings. This matches the documentation: language framework settings, which linuxFxVersion holds on Linux, are listed as swapped, and the ARM sample can only be idempotent if production carries the matching buildVersion after the swap.
3. Holding the image versions in an idempotent template
Do not declare linuxFxVersion on the production site; otherwise ARM updates production in place, without warm-up, and the swap has nothing left to do. ARM normally resets omitted properties on redeployment (deployment modes article), but the article names web app site configuration as an exception: it lives in the child resource Microsoft.Web/sites/config, which an empty siteConfig object leaves untouched. That makes siteConfig: {} a suitable way to leave the production configuration untouched in main.bicep; the swap module follows the documented swap sample, which sends no site configuration at all. In the test, neither PUT changed production's linuxFxVersion in any run without a swap, with and without the empty siteConfig object.
Mind the kind: when the slot is created, it must equal the production site's stored kind, otherwise the deployment fails with The slot 'kind' property 'app,linux,container' must match the Production site 'kind' property 'app,linux'. A new site without linuxFxVersion was stored as app,linux although the template declared app,linux,container. Your existing non-container app should also be app,linux; check the kind in its resource JSON before the first run. After creation, a differing declared kind was accepted.
The pattern uses two files because it writes the production site twice in deployment order, once with its normal configuration and once with targetBuildVersion after the slot. Declaring the same resource name twice in one Bicep file triggers BCP121, so the second write sits in a module; the module boundary also resolves the dependency cycle described in section 4.
// main.bicep
param siteName string
// must equal the production site's stored kind when the slot is created;
// a non-container Linux site is stored as 'app,linux'
param siteKind string = 'app,linux'
param location string = resourceGroup().location
param planId string
@description('Full image reference including the digest')
param image string
var buildVersion = uniqueString(image)
resource site 'Microsoft.Web/sites@2024-04-01' = {
name: siteName
location: location
kind: siteKind
properties: {
serverFarmId: planId
// empty object: sites/config is left untouched, production gets its image only through the swap
siteConfig: {}
}
}
resource staging 'Microsoft.Web/sites/slots@2024-04-01' = {
parent: site
name: 'staging'
location: location
kind: siteKind
properties: {
serverFarmId: planId
#disable-next-line BCP037
buildVersion: buildVersion
siteConfig: {
linuxFxVersion: 'DOCKER|${image}'
}
}
}
module swap 'swap.bicep' = {
name: 'swap-${buildVersion}'
params: {
siteName: siteName
siteKind: siteKind
location: location
buildVersion: buildVersion
}
dependsOn: [
staging
]
}
// swap.bicep
param siteName string
param siteKind string
param location string
param buildVersion string
resource site 'Microsoft.Web/sites@2024-04-01' = {
name: siteName
location: location
kind: siteKind
properties: {
#disable-next-line BCP037
targetBuildVersion: buildVersion
}
}
With image B, the first run deploys B to staging and swaps it in, leaving A in staging. The second run writes B to staging again (which should restart only staging) and finds production at the target version, so no swap happens; from the third run on, nothing changes, and the container logs showed no image pull. After the first successful swap, staging holds whatever production ran before, in your case the current code-based deployment.
This convergence costs you the quick rollback: after the second unchanged run, staging no longer holds A, so an immediate reverse swap only works until then, and it leaves production out of line with the template, which the next run swaps forward again. A rollback consistent with the template is a run with the previous image parameter, including warm-up.
4. Same deployment or two?
One deployment, as in the documented sample. In Bicep, Azure/bicep#8712 describes two obstacles:
- A
dependsOnfrom the site to its child slot is a cycle. A maintainer suggested a segmented slot name instead ofparent, which the reporter confirmed for an existing site; withoutparentthere is no implicit dependency on the site. The module above avoids both issues: it is a nested deployment within the sameaz deployment group createand runs after the slot. -
buildVersionandtargetBuildVersionare missing from the Microsoft.Web REST API specification behind the Bicep types, so Bicep reports BCP037. The properties are still sent; the directives only suppress the warning.
References
- Set up staging environments in Azure App Service
- Troubleshooting failed slot swaps on App Service Linux
- Azure Resource Manager deployment modes
- Bicep reports cycle for app service slot swapping (Azure/bicep#8712)
- Microsoft.Web REST API specification
- Bicep core diagnostics
- Bicep diagnostic code BCP037
Drafted with help from Claude, disclosed per the Q&A AI usage policy. Documented: the ARM rule for redeclared resources and its site configuration exception, the swap steps, swapped settings, warm-up settings and the ARM swap sample, the Bicep cycle and type warnings (Azure/bicep#8712, REST API specification), and the Bicep rule against declaring the same resource twice in one file (BCP121). Reproduced on an S1 Linux plan with API version 2024-04-01, starting from both a new site and an existing Linux code app: the multi-phase swap via targetBuildVersion for a container app, the failed deployment on a failed warm-up with production unchanged, linuxFxVersion, buildVersion and kind moving with the swap, linuxFxVersion staying unchanged on production across repeated runs, the convergence including the loss of the rollback copy, the previous code deployment landing in staging after the first swap, and the kind requirement at slot creation. The absence of the prepared-for-swap error is a single observation.