Automation, Azure, Azure DevOps, DevOps, Professional, Terraform, YAML Pipelines

Terraform, Azure DevOps, App Services, and Slots

Background

This post is going to cover a lot achieving the swapping of slots via Azure DevOps tasks and YAML templates! Essentially wanted to go over how to deploy application code to an App Service slot, created by Terraform, then leverage Azure DevOps (ADO) tasks to perform the swap between the slot and production. And let’s do this all leveraging ADO YAML…..with templates to boot!

Slots

What is a slot? Hear is the Microsoft documentation on slots. Essentially a slot is a parallel version, in this case App Service. This second version shared such components as the App Service Plan just with the slot name appended to the URL. However, and this is important that everyone seems to forget, the slot has a different identity.

This means that if leveraging Role Based Access Control will need to be set up twice, once for production and once for the slot. I always recommend what is the justification for using slots. Are you looking at rolling out canary testing (sending x % of your traffic to a different site to test new features)? Perform testing in production (really don’t advise this, though there are valid reasons)? Or are you just wanting to ensure zero downtime (avoid the blip of the site being unavailable during code deployment? In our case it will be the later as it’s the simplest.

Terraform

The Terraform is going to be the easiest part of this equation. Here is the Hashicorp documentation on slots for Azure. Since we leveraging for zero downtime, we don’t need to get fancy with app settings, network configuration, nor identities.

You may have noticed that there is an option to have Terraform perform the promotion of a slot to production. We will not cover this as this complicates state management as we’d have to deploy Terraform to create the slot, deploy the App Service, deploy the second Terraform code to perform the swap. This really isn’t conducive to a CI/CD pipeline.

To create the slot just declare it like:

resource "azurerm_app_service_slot" "slot" {
  name                = "staging"
  app_service_name    = azurerm_app_service.app_service.name
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  app_service_plan_id = azurerm_app_service_plan.app_service_plan.id
  https_only = true
}

In the above code we are assuming the App Service, App Service Plan, and Resource Group all contained within the same Terraform project.

Code Deployment

For deploying the code we will be leveraging version 4 of the AzureRMWebAppDeployment task. In order to deploy to a slot there are a few settings which may different from how the AzureRMWebAppDeployment task works for regular App Service deployments.

We want to set DeployToSlotOrASEFlag to true. This indicates we are doing a slot deployment. Then, since this is a slot, we need to provide the task with a value for SlotName. Lastly for all slot deployments a ResourceGroupName needs to be provided. The end result in a highly templated task format will look like:

parameters:
    azureSubscription: ''
    webAppName: ''
    takeAppOfflineFlag: true
    packagePath: ''
    appSettings: ''
    slotName: 'staging'
    resourceGroupName: ''
steps:
- task: AzureRmWebAppDeployment@4
  inputs:
    azureSubscription: ${{ parameters.azureSubscription }}
    WebAppName: ${{parameters.webAppName }}
    deployToSlotOrASE: true
    SlotName: ${{ parameters.slotName }}
    Package: ${{ parameters.packagePath }}
    TakeAppOfflineFlag: ${{ parameters.takeAppOfflineFlag }}
    appSettings: ${{ parameters.appSettings }}
    ResourceGroupName: ${{ parameters.resourceGroupName}}

I’ve done some liberty on parameter handling and defaulting where appropriately.

Slot Promotion

Currently we have the infrastructure handled for slots and the code deploying to a slot; however, we are still missing the actual swapping of the slots. We will be handling this via the AzureAppServiceManage task version 0.

For this task since we are swapping with production we will not need to override the default swapWithProduction flag nor provide a value for the targetSlot. However; we will still need to provide a sourceSlot value and resourceGroupName. Again a heavily templatized version of this task may look like:

parameters:
    azureSubscription: ''
    webAppName: ''
    resourceGroupName: ''
    slotname: 'staging'
steps:
- task: AzureAppServiceManage@0
  displayName: 'Swap ${{parameters.webAppName }}'
  inputs:
    ConnectedServiceName: ${{ parameters.azureSubscription }}
    WebAppName: ${{parameters.webAppName }}
    ResourceGroupName: ${{parameters.resourceGroupName }}
    SourceSlot: ${{parameters.slotname}}

One More Thing….

We don’t have to; however, the thought occurred to stop the swap after it has been completed. This makes logical sense since we are leveraging it just for zero downtime so why not ensure nothing is running rogue and just turn it off after completion.

To achieve this we will use the same AzureAppServiceManage task version 0, just with a different action. This may look like:

parameters:
    azureSubscription: ''
    webAppName: ''
    resourceGroupName: ''
    specifySlotOrASE: true
    slotname: 'staging'
    action: 'Start Azure App Service'
steps:
- task: AzureAppServiceManage@0
  displayName: '${{ parameters.action }} ${{parameters.webAppName }}'
  inputs:
    ConnectedServiceName: ${{ parameters.azureSubscription }}
    Action: ${{parameters.action }}
    WebAppName: ${{parameters.webAppName }}
    SpecifySlotOrASE: ${{parameters.specifySlotOrASE }}
    ResourceGroupName: ${{parameters.resourceGroupName }}
    Slot: ${{parameters.slotname}}

Note the default on this on was ‘Start Azure App Service’. This is defaulted as least impact if something goes wrong or a the paremter isn’t properly passed. The net result will be an App Service running vs inadvertently stopping something in a production environment.

Stitching It Together

This is a lot, so how does it come together? I am going to gloss over the Terraform components. For that review my post of Azure DevOps Terraform Task. Also it may not hurt to review my thoughts on MultiStage YAML Pipelines with Templates

So the Tasks….

appservice-manage-task.yml
parameters:
    azureSubscription: ''
    webAppName: ''
    resourceGroupName: ''
    specifySlotOrASE: true
    slotname: 'staging'
    action: 'Start Azure App Service'
steps:
- task: AzureAppServiceManage@0
  displayName: '${{ parameters.action }} ${{parameters.webAppName }}'
  inputs:
    ConnectedServiceName: ${{ parameters.azureSubscription }}
    Action: ${{parameters.action }}
    WebAppName: ${{parameters.webAppName }}
    SpecifySlotOrASE: ${{parameters.specifySlotOrASE }}
    ResourceGroupName: ${{parameters.resourceGroupName }}
    Slot: ${{parameters.slotname}}
appservice-manage-swap-slots-task.yml
parameters:
    azureSubscription: ''
    webAppName: ''
    resourceGroupName: ''
    slotname: 'staging'
steps:
- task: AzureAppServiceManage@0
  displayName: 'Swap ${{parameters.webAppName }}'
  inputs:
    ConnectedServiceName: ${{ parameters.azureSubscription }}
    WebAppName: ${{parameters.webAppName }}
    ResourceGroupName: ${{parameters.resourceGroupName }}
    SourceSlot: ${{parameters.slotname}}
webapp-deploy-slot-task-.yml
parameters:
    azureSubscription: ''
    webAppName: ''
    takeAppOfflineFlag: true
    packagePath: ''
    appSettings: ''
    slotName: 'staging'
    resourceGroupName: ''
steps:
- task: AzureRmWebAppDeployment@4
  inputs:
    azureSubscription: ${{ parameters.azureSubscription }}
    WebAppName: ${{parameters.webAppName }}
    deployToSlotOrASE: true
    SlotName: ${{ parameters.slotName }}
    Package: ${{ parameters.packagePath }}
    TakeAppOfflineFlag: ${{ parameters.takeAppOfflineFlag }}
    appSettings: ${{ parameters.appSettings }}
    ResourceGroupName: ${{ parameters.resourceGroupName}}
appservice-deploy-swap-job.yml
parameters:
- name: environmentName
  type: string
  default: 'dev'
- name: webAppName
  type: string
- name: resourceGroupName
  type: string
  default: ''
- name: slotName
  type: string
  default: 'staging'
- name: dependsOn
  type: object
  default: []
- name: packagePath
  type: string
  default: ''


jobs:
- deployment: swap_${{ parameters.environmentName }}
  dependsOn: ${{ parameters.dependsOn }}
  displayName: 'Swap Slots for ${{parameters.webAppName }}'
  variables:
  - template: ../variables/azure.${{ parameters.environmentName }}.yml
  environment: ${{ parameters.environmentName }}
  strategy:
    runOnce:
        deploy:
            steps:
            - template: ../tasks/webapp-deploy-slot-task.yml
              parameters:
                azureSubscription: ${{ variables.AzureSubscriptionServiceConnectionName}}
                webAppName: ${{parameters.webAppName }}
                packagePath: ${{ parameters.packagePath }}
                resourceGroupName: ${{parameters.resourceGroupName}}
            - template: ../tasks/appservice-manage-task.yml@YAMLTemplates
              parameters:
                azureSubscription: ${{ variables.AzureSubscriptionServiceConnectionName}}
                action: Start Azure App Service
                webAppName: ${{parameters.webAppName }}
                resourceGroupName: ${{parameters.resourceGroupName }}
                slot: ${{parameters.slotName }}
            - template: ../tasks/appservice-manage-swap-slots-task.yml@YAMLTemplates
              parameters:
                azureSubscription: ${{ variables.AzureSubscriptionServiceConnectionName}}
                webAppName: ${{parameters.webAppName }}
                resourceGroupName: ${{parameters.resourceGroupName }}
                slot: ${{parameters.slotName }}
            - template: ../tasks/appservice-manage-task.yml@YAMLTemplates
              parameters:
                azureSubscription: ${{ variables.AzureSubscriptionServiceConnectionName}}
                action: Stop Azure App Service
                webAppName: ${{parameters.webAppName }}
                resourceGroupName: ${{parameters.resourceGroupName }}
                slot: ${{parameters.slotName }}
tf_appservice_deploy_swap_slot_stage.yml
parameters:
- name: serviceName
  type: string
- name: environmentName
  type: string
- name: terraformVersion
  type: string
- name: webAppName
  type: string
- name: resourceGroupName
  type: string
- name: projectName
  type: string

stages:
- stage: ${{ parameters.serviceName }}_${{ parameters.environmentName}}
  jobs:
  - template: ../jobs/tf-deploy-job.yml@YAMLTemplates
    parameters:
        environmentName: ${{ parameters.environmentName}}
        serviceName: ${{ parameters.serviceName}}
        terraformVersion: ${{ parameters.terraformVersion}}
  - template: ../jobs/appservice-deploy-swap-job.yml@YAMLTemplates
    parameters:
      environmentName: ${{ parameters.environmentName}}
      webAppName: ${{ parameters.webAppName}}
      resourceGroupName: ${{parameters.resourceGroupName}}
      dependsOn: ['terraformApply${{ parameters.environmentName }}']
      packagePath: '$(Pipeline.Workspace)/drop/${{ parameters.projectName }}.zip'

Conclusion

That’s it, just like a firehose! Realize I didn’t include every single step in this (mainly the Terraform, and the pipeline.ymladn the variable files ). Reason behind this is time as well as these are just the Lego blocks used to build new pipelines, thus optimizing reusability.

Hopefully this is enough to get you started on leveraging bits and pieces in your existing environments!