Skip to content

mildronize/actions-az-webapp-swap

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

Safely Swap Azure App Service Slot v2

build-test Coverage Status

Have you ever swapped Azure App Service Slots for more than 30 sites?

Swapping multiple Azure App Service Slots can cause some unwanted swapping of App Settings which might cause an incident in the production.

To prevent swapping the App Setting, we need to set SlotSetting parameters in the key/value of the App Settings, because default SlotSetting is false.

To be more clear,

⚠️ WARNING: Using this Actions requires set App Settings and Connection String for modifying SlotSetting value in settings, because the Azure API doesn't allow us to modifying only SlotSetting value without touching actual value of settings, please run this action is non-production first to be make sure everything works as you expected.

Features

  • Prevent unwanted swap app settings between two slots
  • Support multiple Azure App Services
    • Users can reviews changes all app services app settings before swap
    • Users can config which the app setting will be swapped or not.
    • Automatically fix the app setting to be sticked with desired slot following config
  • Leverage GitHub Features
    • GitHub Action Matrix for retryable steps
    • GitHub Pull Request review process for protecting unintentionally swap app service.

This GitHub Actions is required to composition multiple GitHub Actions events for using full workflows as see figure:

Workflow Composition

Award

Participant in the top 10 final round of Microsoft Virtual Hackathon 2022 June 28, 2022, GitHub Actions Theme

Example Usage

Write a full workflows of using this Actions, it requires to using job and specific events for using this GitHub Actions. You can see the example below:

name: Swap Slots
on:
  workflow_dispatch:
  pull_request:
    types: [opened, closed]
    branches:
      - appsettings
env:
  config_dir: ./.github/workflows/configs
jobs:
  get-matrix:
    runs-on: ubuntu-latest
    outputs:
      result: ${{ steps.get-matrix.outputs.deployment-matrix }}
    steps:
    - uses: actions/checkout@v3
    - name: Export deployment matrix
      id: get-matrix
      run: |
        node ./index.js test-get-deploy-slots.json
      working-directory: ${{ env.config_dir }}

  get-slot-settings:
    if: github.event_name == 'workflow_dispatch'
    name: ${{ format('βš™οΈ Get Slot | {0} - {1}', matrix.name, matrix.slot) }}
    runs-on: ubuntu-latest
    needs: [ get-matrix ]
    strategy:
      matrix:
        include: ${{ fromJson(needs.get-matrix.outputs.result) }}
    steps:
      - uses: actions/checkout@v2
      # SP: github action az webapp swap
      - uses: azure/login@v1
        with:
          creds: ${{ secrets.azure_credentials }}
      - uses: mildronize/[email protected]
        with:
          mode: get-deploy-slots
          config: ${{ toJson(matrix) }}
    
  create-swap-plan: 
    needs: [ get-slot-settings ]
    name: Create Swap Plan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Create Swap Plan
        uses: mildronize/[email protected]
        with: 
          mode: create-swap-plan
          token: ${{ secrets.PAT }}
          repo: mildronize/actions-az-webapp-swap-demo
          
  set-slot-settings:
    if: >- 
      github.event_name == 'pull_request' &&
      github.event.action != 'closed'
    name: ${{ format('βš™οΈ Set Slot | {0} - {1}', matrix.name, matrix.slot) }}
    needs: [ get-matrix  ]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include: ${{ fromJson(needs.get-matrix.outputs.result) }}
    steps:
      - uses: actions/checkout@v2
      # SP: github action az webapp swap
      - uses: azure/login@v1
        with:
          creds: ${{ secrets.azure_credentials }}
      
      - name: set-slot-settings
        uses: mildronize/[email protected]
        with: 
          mode: set-deploy-slots
          config: ${{ toJson(matrix) }}

  swap-slot:
    if: >- 
      github.event_name == 'pull_request' &&
      github.event.action == 'closed' && 
      github.event.pull_request.merged == true
    name: ${{ format('πŸš€ Swap Slot | {0} - {1}', matrix.name, matrix.slot) }}
    needs: [ get-matrix ]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include: ${{ fromJson(needs.get-matrix.outputs.result) }}
    steps:
      - uses: actions/checkout@v2
      # SP: github action az webapp swap
      - uses: azure/login@v1
        with:
          creds: ${{ secrets.azure_credentials }}
      
      - name: set-slot-settings
        uses: mildronize/[email protected]
        with: 
          mode: swap-slots
          config: ${{ toJson(matrix) }}

  clean:
    name: Clean
    needs: [ swap-slot ]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: clean
        uses: mildronize/[email protected]
        with: 
          mode: clean
          token: ${{ secrets.PAT }}
          repo: mildronize/actions-az-webapp-swap-demo

  close:
    if: >- 
      github.event_name == 'pull_request' &&
      github.event.action == 'closed' && 
      github.event.pull_request.merged == false
    name: close PR
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: clean
        uses: mildronize/[email protected]
        with: 
          mode: clean
          token: ${{ secrets.PAT }}
          repo: mildronize/actions-az-webapp-swap-demo
  

Write a JSON config file:

[
  {
    "name": "my-swap-app-test-01",
    "resourceGroup": "rg-swap-app-test",
    "slot": "staging",
    "targetSlot": "production",
    "defaultSlotSetting": "true",
    "defaultSensitive": "false",
    "appSettings": [
      {
        "name": "data1",
        "sensitive": false,
        "slotSetting": true
      },
      {
        "name": "data2",
        "sensitive": false,
        "slotSetting": true
      },
      {
        "name": "data3",
        "sensitive": true,
        "slotSetting": true
      }
    ],
    "connectionStrings": [
      {
        "name": "data4",
        "sensitive": true,
        "slotSetting": true
      }
    ]
  }
]

GitHub Tokens

  • Assign permission read:org, repo

Create a Service Principle to Access the Azure Resources

To handle with service principle, I've suggestion 2 ways based on scenarios:

  1. Create a Service Principle and assign Role & Scope at the same time

    az ad sp create-for-rbac --name "my-test-app" --role contributor --scopes /subscriptions/9eac942-xxxxxxxxx --sdk-auth

    Note: using azure/login GitHub Actions require flag --sdk-auth, even this flag is deprecated.

    To login, using azure/login Actions,

      - uses: azure/login@v1
        with:
          creds: ${{ secrets.azure_credentials }}

    Use Case: This will suite with a few resource to handle

  2. Create a Service Principle without assigning any role assignment

    az ad sp create-for-rbac -n my-test-app --skip-assignment --sdk-auth

    Note: using azure/login GitHub Actions require flag --sdk-auth, even this flag is deprecated.

    To support this service principle, the azure/login support Support for using allow-no-subscriptions

    To login, using azure/login Actions,

      - uses: azure/login@v1
        with:
          creds: ${{ secrets.azure_credentials }}
          allow-no-subscriptions: true

    Next step, you can assign this Service Principle in a Azure AD group, and assign role assignment (Access Control (IAM)) to the resource that you want to get access:

    For example,

    • Assign role assignment (Access Control (IAM)) to the resource that you want to get access:

    • Assign this Service Principle in a Azure AD group:

Azure Permission

To provide Least Privilege for Azure Resources:

Read more:

Create Azure Custom Role for this Actions

Create a file role.json and save the JSON content below:

{
    "type": "Microsoft.Authorization/roleDefinitions",
    "roleName": "prod-swap-slot",
    "description": "Prod Swap Slot",
    "assignableScopes": [
        "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    ],
    "name": "prod-swap-slot",
    "roleType": "CustomRole",
    "permissions": [
        {
            "actions": [
                "Microsoft.Web/sites/slots/slotsswap/action",
                "Microsoft.Web/sites/slots/config/list/Action",
                "Microsoft.Web/sites/config/Read",
                "Microsoft.Web/sites/config/list/Action",
                "Microsoft.Web/sites/config/Write",
                "Microsoft.Web/sites/slots/config/Write",
                "microsoft.web/sites/slots/operationresults/read"
            ],
            "notActions": [],
            "dataActions": [],
            "notDataActions": []
        }
    ]
}

Run this command to create a role.

az role definition create --role-definition role.json

get-deploy-slot

Microsoft.Web/sites/slots/config/list/Action
Microsoft.Web/sites/config/Read
Microsoft.Web/sites/config/list/Action

set-deploy-slot

Microsoft.Web/sites/config/Write
Microsoft.Web/sites/slots/config/Write

swap-slot

Microsoft.Web/sites/slots/slotsswap/action
Microsoft.Web/sites/slots/operationresults/read

Authors

Sponsors

T.T. Software Solution
WRM Software

TODO

  • Mask sensitive in log