PR Slot Deployments¶
Overview¶
PR slot deployments let you preview changes from a pull request in a live environment without affecting other users. When a PR pipeline runs with slot deployment enabled, it deploys your code to an Azure App Service deployment slot. You activate routing to that slot via a cookie — only your browser sees the PR code while everyone else continues hitting the production slot.
Automated tests and CI pipelines can target a specific slot directly by sending the x-saif-slot-routing request header (e.g. x-saif-slot-routing: my-project-id=pr-1234), without needing a browser or cookie.
Quick navigation
New project created with saif new? Start at Step 1 — Enable on the main pipeline.
Existing project adding slot deployments? Jump to Enabling on an Existing Project.
Prerequisites¶
- Pipeline templates v3 — your pipeline must reference
azure-dotnet-api-pr-v3.ymlorazure-react-web-pr-v3.yml saif-apiservicemodule version>= 3.8.0
Enabling PR Slot Deployments¶
Step 1 — Enable enableSlotDeployment on the main pipeline¶
The main (production) pipeline must opt in first. This triggers the setup_pr_slot_cleanup deployment which creates the service connection and service hook needed for automatic slot cleanup.
Important
Your Azure infrastructure (App Service, resource group, APIM API) must already exist before slots can be created. If this is a new project, run the main pipeline at least once without enableSlotDeployment to deploy the base infrastructure, then add the parameter and run again.
In your main pipeline (azure-pipelines-api.yml / azure-pipelines-web.yml):
extends:
template: azure-dotnet-api-v3.yml@templates
parameters:
applicationName: ${{ variables.ApplicationName }}
projectId: ${{ variables.ProjectId }}
enableSlotDeployment: true
# ... other parameters
Commit this to main and run the pipeline. The setup_pr_slot_cleanup deployment stage will create:
- An incoming webhook service connection (
PRClosedWebhook-{projectId}) - A service hook subscription that fires on
git.pullrequest.updatedwithStatusUpdateNotification
These resources are created idempotently — re-running the pipeline is safe.
Step 2 — Create the slot cleanup pipeline¶
Add an azure-pipelines-pr-slot-cleanup.yml file to your .azdo/ folder:
variables:
- template: vars/base.yml
- template: saif-vars.yml@templates
- group: TerraformCloud
trigger: none
pool:
vmImage: 'ubuntu-latest'
resources:
repositories:
- repository: templates
type: git
name: SAIF/pipeline-templates
ref: refs/heads/releases/v3
webhooks:
- webhook: PRClosed-{YourProjectId}
connection: PRClosedWebhook-{YourProjectId}
stages:
- template: azure-pr-slot-cleanup.yml@templates
parameters:
projectId: ${{ variables.ProjectId }}
applicationName: ${{ variables.ApplicationName }}
prNumber: ${{ parameters['PRClosed-{YourProjectId}'].resource.pullRequestId }}
prStatus: ${{ parameters['PRClosed-{YourProjectId}'].resource.status }}
Replace {YourProjectId} with your actual project ID (e.g. saif-my-service).
If your project has a frontend, also include the frontEndProjectId parameter:
Then create a pipeline in Azure DevOps pointing to this file. The webhook trigger name must match the service connection created in Step 1.
New projects from saif new
The saif-feature-api and saif-feature-web-standalone templates already include the cleanup pipeline file. You only need to create this manually for existing projects.
Step 3 — Enable enableSlotDeployment on the PR pipeline¶
In your PR pipeline (azure-pipelines-api-pr.yml / azure-pipelines-web-pr.yml):
extends:
template: azure-dotnet-api-pr-v3.yml@templates
parameters:
applicationName: ${{ variables.ApplicationName }}
projectId: ${{ variables.ProjectId }}
enableSlotDeployment: true
# ... other parameters
This can be pushed to the branch that will create a PR (enables for that PR only) or to main (enables for all future PRs).
If your project has a separate web (frontend) pipeline, enable enableSlotDeployment: true there as well.
Step 4 — Develop and create a PR¶
Develop using the standard workflow. When ready, create a pull request and allow the API and/or web validation stages to complete.
Step 5 — Activate the slot¶
Note
Slot activation is only available in the test environment. It is intentionally not available in QA, UAT, or production.
When deployment completes, a comment is posted on your PR:
🔀 PR Slot Deployment — test¶
A backend deployment slot
pr-1234has been created for this PR.
Action Link Activate slot routing 🟢 Activate Deactivate slot routing 🔴 Deactivate Slot routing value:
my-project-id=pr-1234Clicking Activate sets a cookie that routes your requests to the PR slot. Other users and environments are not affected.
If frontend slot deployment is also enabled, a second comment will appear for the web component.
Click Activate — this opens a confirmation page and sets the x-saif-slot-routing cookie in your browser. APIM matches your project ID in the cookie and routes subsequent requests to the PR slot.
For web/frontend activation, an additional x-ms-routing-name cookie is set scoped to your app's path. This is because frontend requests go directly from Front Door to App Service (bypassing APIM), so Azure's built-in ARR routing is used instead.
Step 6 — Deactivate when done¶
Click Deactivate when validation is complete. This removes only the designated project ID from the cookie value — other activated project IDs remain intact. Your requests resume going to the production slot.
Tip
If you don't deactivate, the cookie value persists for that project ID. The alternative is to clear the entire x-saif-slot-routing cookie in your browser, but that deactivates all project IDs at once.
Step 7 — Slot cleanup¶
When the PR is completed or abandoned, the slot cleanup pipeline is triggered automatically via the service hook to remove the deployment slot associated with that PR. The cleanup pipeline:
- Receives the webhook payload containing the PR number and status
- Deletes the
pr-{number}App Service slot - Removes the traffic routing rule for that slot
Enabling on an Existing Project¶
For projects not created from the latest saif new templates, follow this checklist:
1. Update saif-apiservice module version¶
In your infra/api/main.tf (or infra/web/main.tf for frontend-only):
module "webapp" {
source = "app.terraform.io/saif-corp/saif-apiservice/azurerm"
version = "~> 3.8.0"
# ...
}
Run the main pipeline to deploy the updated APIM policy fragments.
2. Add enableSlotDeployment: true to the main pipeline¶
Edit .azdo/azure-pipelines-api.yml:
extends:
template: azure-dotnet-api-v3.yml@templates
parameters:
applicationName: ${{ variables.ApplicationName }}
projectId: ${{ variables.ProjectId }}
enableSlotDeployment: true
# ... existing parameters
Push to main and run the pipeline. This creates the service connection and service hook.
3. Create the cleanup pipeline file¶
Add .azdo/azure-pipelines-pr-slot-cleanup.yml:
variables:
- template: vars/base.yml
- template: saif-vars.yml@templates
- group: TerraformCloud
trigger: none
pool:
vmImage: 'ubuntu-latest'
resources:
repositories:
- repository: templates
type: git
name: SAIF/pipeline-templates
ref: refs/heads/releases/v3
webhooks:
- webhook: PRClosed-{YourProjectId}
connection: PRClosedWebhook-{YourProjectId}
stages:
- template: azure-pr-slot-cleanup.yml@templates
parameters:
projectId: ${{ variables.ProjectId }}
applicationName: ${{ variables.ApplicationName }}
prNumber: ${{ parameters['PRClosed-{YourProjectId}'].resource.pullRequestId }}
prStatus: ${{ parameters['PRClosed-{YourProjectId}'].resource.status }}
Replace {YourProjectId} with your project ID. If you have a frontend, add:
4. Create the ADO pipeline¶
In Azure DevOps, create a new pipeline:
- Go to Pipelines → New Pipeline → Azure Repos Git
- Select your repository
- Choose Existing Azure Pipelines YAML file
- Set path to
/.azdo/azure-pipelines-pr-slot-cleanup.yml - Name it
{YourProjectId} - PR Slot Cleanup - Save (do not run manually — it is webhook-triggered)
Note
On the first webhook-triggered run, ADO will prompt for pipeline authorization to the service connection. Approve it once and subsequent runs will proceed automatically.
5. Enable enableSlotDeployment on the PR pipeline¶
Edit .azdo/azure-pipelines-api-pr.yml:
extends:
template: azure-dotnet-api-pr-v3.yml@templates
parameters:
applicationName: ${{ variables.ApplicationName }}
projectId: ${{ variables.ProjectId }}
enableSlotDeployment: true
# ... existing parameters
6. Verify¶
- Create a test PR with a trivial change
- Confirm the PR pipeline creates a slot and posts the activation comment
- Click Activate and verify routing works
- Complete the PR and confirm the cleanup pipeline triggers and deletes the slot
Multi-Service Chains¶
Slot routing propagates across service-to-service calls automatically. You do not have to deploy the entire chain to PR slots — any combination works:
- Experience only — useful when only the frontend-facing layer has changes; downstream services run on production
- Process or System only — the upstream caller (running on production) propagates the routing header automatically to the PR slot
- Process → System — both inner layers on PR slots; the experience layer runs on production
- Experience + Process or any other subset
When the entry point is a browser request through an experience API, the x-saif-slot-routing cookie set by Activate is enough — the Forge middleware propagates it downstream as a header automatically. When you are testing deeper layers without a frontend (e.g. hitting a process API directly, or running automated tests), send the header explicitly:
This works because Forge's SlotRoutingMiddleware reads x-saif-slot-routing from the inbound Cookie (browser) or header (service-to-service or direct test), and SlotRoutingDelegatingHandler attaches it as a header on all outgoing HttpClient requests.
Slot routing is registered automatically when you call AddJwtBearerServices() or AddOpenIdConnectServices(). If your service does not use either (e.g. a purely internal API that authenticates via a different mechanism), register it explicitly:
This registers SlotRoutingMiddleware and SlotRoutingDelegatingHandler via ConfigureHttpClientDefaults, so all HttpClient instances propagate the routing header automatically.
sequenceDiagram
participant Browser
participant APIM as APIM Gateway
participant Exp as Exp API (PR slot)
participant Proc as Proc API (PR slot)
Browser->>APIM: Request with cookie<br/>x-saif-slot-routing=exp=pr-100,proc=pr-200
APIM->>Exp: Route to exp PR slot
Note over Exp: SlotRoutingMiddleware reads value<br/>DelegatingHandler propagates on outbound call
Exp->>APIM: Outbound call with header<br/>x-saif-slot-routing=exp=pr-100,proc=pr-200
APIM->>Proc: Route to proc PR slot
Note
The middleware reads from the cookie (browser requests) or header (service-to-service). APIM's inbound-set-slot-backend fragment supports both — cookie for the first hop from the browser, header for subsequent hops between services.
Staging slots also benefit
Because inbound-set-slot-backend runs on every API in the environment, the blue-green staging slot is also routable via cookie — set x-saif-slot-routing to {project-id}=staging to direct your browser to the staging slot during a blue-green validation window. This works without any extra configuration.
Troubleshooting¶
Slot shows production code after activation¶
- Check cookie: Open DevTools → Application → Cookies and verify
x-saif-slot-routingis present with the correct project ID and slot name - Check module version: Ensure your
saif-apiserviceTerraform module is on>= 3.8.0— older versions don't include theinbound-set-slot-backendpolicy fragment - Check environment: Slot activation only works in
test - Run infrastructure pipeline: The APIM policy fragment is deployed by Terraform on
main— if this is the first time enabling slots, ensure the pipeline has run after the module version update
Downstream API hits production despite upstream being on a slot¶
- Verify the downstream API also has module version
>= 3.8.0so theinbound-set-slot-backendfragment is included in its APIM policy - Confirm your service calls
AddJwtBearerServices(),AddOpenIdConnectServices(), or the standaloneAddSlotRouting()— one of these must be present for theSlotRoutingDelegatingHandlerto be registered - If using a custom
HttpClientsetup that bypassesConfigureHttpClientDefaults, manually addSlotRoutingDelegatingHandlerto that client's pipeline
403 on downstream API calls¶
- Verify your authentication configuration is set up correctly between the calling and downstream APIs
- Check that the downstream API's
infra/auth/corp/config.ymlgrants the appropriate scopes and roles for the calling application - Confirm the downstream API has an
authorized_appsentry for the calling API's project ID
Cookie not being set¶
- The activation endpoint returns 404 in environments where slot deployment is not enabled (QA, UAT, production)
- Verify you're hitting the correct Front Door hostname for your environment
Limitations¶
Concurrent open PRs¶
Azure App Service limits deployment slots per app based on the plan tier. The production slot occupies one, leaving a maximum of 19 simultaneously open PR slots per app on Premium (v1–v4) and Isolated plans. Each app has its own independent quota — an API and its web front end each get 19, not 19 combined. See Azure App Service limits for the full tier breakdown.