ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

Azure App Service と GitHub Actions で Pull Request のプレビュー環境を自動で作成する

Pull Requestを作成したときにプレビュー用の環境を自動で作成してくれるGithub Actionsを作った。

PR作成時にプレビュー用のURL発行

こんな感じでプレビューURLを発行してくれて実際に動作確認ができるようになっている。
ローカルにブランチをpullしなくて済むので非常に便利である。

はじめに

Azure App Serviceではslotという概念があって、運用スロットから別環境を生成することができる。masterブランチからstagingブランチを切るイメージ。これを利用してPR作成時にプレビュースロットを作成してそこで動作確認をしようという試み。

https://levelup.gitconnected.com/dynamic-pull-request-previews-with-github-actions-and-azure-app-service-1f613986eab8

こちらを死ぬほど参考にさせていただきました。上記の記事では必要最低限のことが非常によくまとまっており、試すならまずこちらからいくのがわかりやすい。Buy me a coffeeで思わずお布施してしまうほど。

上記の記事の内容に

  • AppServiceのカスタムコンテナ(Docker)を利用した版
  • CIがコケたあとの再pushへの対応
  • actionsの共通化(Composite action)

をプラスしたものが今回の内容となる。

構成

.github
├── actions
│   ├── ci
│   │   └── action.yml
│   ├── create_slot
│   │   └── action.yml
│   ├── deploy_slot
│   │   └── action.yml
│   ├── image_push
│   │   └── action.yml
│   └── is_exist_slot
│       └── action.yml
└── workflows
    └── create_preview.yml

コード

.github/workflows/create_preview.yml これがメイン。PR作成時、更新時、削除時の3つの段落に分かれている。

name: Pull Request Preview

on:
  pull_request:
    branches: # masterとstageは除外
      - '**'
      - '!master'
      - '!stage'
    types: [opened, synchronize, closed]
  workflow_dispatch:

jobs:
  ######## PR作成時 #########
  create-deployment-preview:
    if: github.event.action == 'opened'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout GitHub Action
        uses: actions/checkout@main

      # プレビュー環境作成中のコメント
      - name: Initial Deployment Preview Comment
        uses: peter-evans/create-or-update-comment@v1.4.5
        id: pr-preview-comment
        with:
          issue-number: ${{ github.event.pull_request.number }}
          body: |
            ### Deployment Preview
            A preview of this Pull Request is being created. Hold tight while it's building ⚒️
            This comment will be automatically updated when the preview is ready.

      # lint, test
      - name: CI
        uses: ./.github/actions/ci

      # Dockerイメージをビルド、デプロイ
      - name: Docker Build and Push
        uses: ./.github/actions/image_push
        with:
          AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
          AZURE_ACR_ID: ${{ secrets.AZURE_ACR_ID }}
          AZURE_ACR_PASSWORD: ${{ secrets.AZURE_ACR_PASSWORD }}
          DOCKER_IMAGE_TAG: '${{ secrets.AZURE_ACR_ID }}.azurecr.io/demo/webapp:preview-pr-${{ github.event.pull_request.number }}'

      # プレビューslot作成
      - name: Create Preview Slot
        uses: ./.github/actions/create_slot
        with:
          AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
          AZURE_APP_NAME: '${{ secrets.AZURE_APP_NAME_STAGING }}'
          AZURE_RESOURCE_GROUP: '${{ secrets.AZURE_RESOURCE_GROUP_STAGING }}'
          SLOT_NAME: 'preview-pr-${{ github.event.pull_request.number }}'
          AZURE_ACR_ID: ${{ secrets.AZURE_ACR_ID }}
          AZURE_ACR_PASSWORD: ${{ secrets.AZURE_ACR_PASSWORD }}

      # slotにイメージ反映
      - name: Deploy Slot
        uses: ./.github/actions/deploy_slot
        with:
          AZURE_ACR_ID: ${{ secrets.AZURE_ACR_ID }}
          AZURE_APP_NAME: ${{ secrets.AZURE_APP_NAME_STAGING }}

      # コメント更新
      - name: Update PR Preview Comment
        uses: peter-evans/create-or-update-comment@v1.4.5
        with:
          comment-id: ${{ steps.pr-preview-comment.outputs.comment-id }}
          edit-mode: replace
          body: |
            ### Deployment Preview
            😎 Preview this PR: https://${{ secrets.AZURE_APP_NAME_STAGING }}-preview-pr-${{ github.event.pull_request.number }}.azurewebsites.net
            👶 Commit SHA: ${{ github.sha }}
          reactions: 'rocket'

  ######## PR更新時(pushなど) #########
  update-deployment-preview:
    if: github.event.action == 'synchronize'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout GitHub Action
        uses: actions/checkout@main

      - name: Find PR Preview Comment
        uses: peter-evans/find-comment@v1
        id: deploy-preview-comment
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-author: 'github-actions[bot]'
          body-includes: Deployment Preview

      - name: Update PR Preview Comment
        if: steps.deploy-preview-comment.outputs.comment-id != ''
        uses: peter-evans/create-or-update-comment@v1.4.5
        with:
          comment-id: ${{ steps.deploy-preview-comment.outputs.comment-id }}
          edit-mode: replace
          body: |
            ### Deployment Preview
            The Pull Request preview is being updated. Hold tight while it's building ⚒️
            This comment will be automatically updated when the new version is ready.

      # lint, test
      - name: CI
        uses: ./.github/actions/ci

      # Dockerイメージをビルド、デプロイ
      - name: Docker Build and Push
        uses: ./.github/actions/image_push
        with:
          AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
          AZURE_ACR_ID: ${{ secrets.AZURE_ACR_ID }}
          AZURE_ACR_PASSWORD: ${{ secrets.AZURE_ACR_PASSWORD }}
          DOCKER_IMAGE_TAG: '${{ secrets.AZURE_ACR_ID }}.azurecr.io/demo/webapp:preview-pr-${{ github.event.pull_request.number }}'

      # プレビューslotが作成されているかを確認
      - name: Check Preview Slot Exists
        id: check-slot-exists
        uses: ./.github/actions/is_exist_slot
        with:
          AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
          AZURE_APP_NAME: '${{ secrets.AZURE_APP_NAME_STAGING }}'
          AZURE_RESOURCE_GROUP: '${{ secrets.AZURE_RESOURCE_GROUP_STAGING }}'
          SLOT_NAME: 'preview-pr-${{ github.event.pull_request.number }}'

      # プレビューslotが作成されていない場合、slotを作成する
      - name: Create Slot If Not Exists
        if: steps.check-slot-exists.outputs.exists_slot == '0'
        uses: ./.github/actions/create_slot
        with:
          AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
          AZURE_APP_NAME: '${{ secrets.AZURE_APP_NAME_STAGING }}'
          AZURE_RESOURCE_GROUP: '${{ secrets.AZURE_RESOURCE_GROUP_STAGING }}'
          SLOT_NAME: 'preview-pr-${{ github.event.pull_request.number }}'
          AZURE_ACR_ID: ${{ secrets.AZURE_ACR_ID }}
          AZURE_ACR_PASSWORD: ${{ secrets.AZURE_ACR_PASSWORD }}

      # slotにイメージ反映
      - name: Deploy Slot
        uses: ./.github/actions/deploy_slot
        with:
          AZURE_ACR_ID: ${{ secrets.AZURE_ACR_ID }}
          AZURE_APP_NAME: ${{ secrets.AZURE_APP_NAME_STAGING }}

      - name: Update PR Preview Comment
        uses: peter-evans/create-or-update-comment@v1.4.5
        with:
          comment-id: ${{ steps.deploy-preview-comment.outputs.comment-id }}
          edit-mode: replace
          body: |
            ### Deployment Preview
            😎 Preview this PR: https://${{ secrets.AZURE_APP_NAME_STAGING }}-preview-pr-${{ github.event.pull_request.number }}.azurewebsites.net
            👶 Commit SHA: ${{ github.sha }}

  ######## PR削除時 #########
  delete-deployment-preview:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout GitHub Action
        uses: actions/checkout@main

      - name: Azure Login
        uses: Azure/login@v1.4.3
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Login to Azure Container Registry
        uses: azure/docker-login@v1
        with:
          login-server: ${{ secrets.AZURE_ACR_ID }}.azurecr.io
          username: ${{ secrets.AZURE_ACR_ID }}
          password: ${{ secrets.AZURE_ACR_PASSWORD }}

      # Dockerイメージの削除
      - name: Delete Docker Image
        uses: Azure/cli@v1
        with:
          inlineScript: az acr repository delete --name ${{ secrets.AZURE_ACR_ID }} --image demo/webapp:preview-pr-${{ github.event.pull_request.number }} --username ${{ secrets.AZURE_ACR_ID }} --password ${{ secrets.AZURE_ACR_PASSWORD }} --yes

      # slotの削除
      - name: Delete PR Deployment Slot
        uses: Azure/cli@v1
        with:
          inlineScript: az webapp deployment slot delete --name ${{ secrets.AZURE_APP_NAME_STAGING }} --resource-group ${{ secrets.AZURE_RESOURCE_GROUP_STAGING }} --slot preview-pr-${{ github.event.pull_request.number }}

      - name: Find PR Preview Comment
        uses: peter-evans/find-comment@v1
        id: deploy-preview-comment
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-author: 'github-actions[bot]'
          body-includes: Deployment Preview

      - name: Update PR Preview Comment
        if: steps.deploy-preview-comment.outputs.comment-id != ''
        uses: peter-evans/create-or-update-comment@v1.4.5
        with:
          comment-id: ${{ steps.deploy-preview-comment.outputs.comment-id }}
          edit-mode: replace
          body: |
            🏁 This PR has been closed. No deployment preview is available.
          reactions: 'hooray'

actionsの共通化

PR作成時、更新時、削除時でいくつか共通のactionsがあるので共通化している。Composite actionと呼ばれているやつ。

今回共通化したactionsは下記

  • ci
  • create_slot
  • deploy_slot
  • image_push
  • is_exist_slot

.github/actions/ci/action.yml

CIの設定

name: 'CI'
runs:
  using: 'Composite'
  steps:
    - name: Set up Node.js version
      uses: actions/setup-node@v1
      with:
        node-version: 14.x

    - name: yarn lint, test
      shell: bash
      run: |
        yarn install
        yarn lint
        yarn test

.github/actions/create_slot/action.yml

slotの作成。ポイントはACR認証の環境変数をセットしないとイメージのpullに失敗してしまうため、ここでセットする。

name: 'Create Slot'
description: 'slot作成'
inputs:
  AZURE_CREDENTIALS:
    required: true
  AZURE_APP_NAME:
    required: true
  AZURE_RESOURCE_GROUP:
    required: true
  SLOT_NAME:
    required: true
  AZURE_ACR_ID:
    required: true
  AZURE_ACR_PASSWORD:
    required: true

runs:
  using: 'Composite'
  steps:
    - name: Azure Login
      uses: azure/login@v1
      with:
        creds: ${{ inputs.AZURE_CREDENTIALS }}

    - name: Create PR Deployment Slot
      uses: Azure/cli@v1
      with:
        inlineScript: |
          az webapp deployment slot create \
          --name ${{ inputs.AZURE_APP_NAME }} \
          --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} \
          --configuration-source ${{ inputs.AZURE_APP_NAME }} \
          --slot ${{ inputs.SLOT_NAME }}

    # AppServiceの環境変数をセット。ここでACR認証の環境変数をセットしないとImageのpullに失敗するため。
    - name: Set Web App ACR authentication
      uses: Azure/appservice-settings@v1
      with:
        app-name: '${{ inputs.AZURE_APP_NAME }}'
        slot-name: '${{ inputs.SLOT_NAME }}'
        app-settings-json: |
          [
              {
                  "name": "DOCKER_REGISTRY_SERVER_URL",
                  "value": "${{ inputs.AZURE_ACR_ID }}.azurecr.io",
                  "slotSetting": false
              },
              {
                  "name": "DOCKER_REGISTRY_SERVER_USERNAME",
                  "value": "${{ inputs.AZURE_ACR_ID }}",
                  "slotSetting": false
              },
              {
                  "name": "DOCKER_REGISTRY_SERVER_PASSWORD",
                  "value": "${{ inputs.AZURE_ACR_PASSWORD }}",
                  "slotSetting": false
              },
          ]

.github/actions/deploy_slot/action.yml

slotにDockerイメージを反映

name: 'Deploy Slot'
description: 'slotにイメージ反映'
inputs:
  AZURE_APP_NAME:
    required: true
  AZURE_ACR_ID:
    required: true

runs:
  using: 'Composite'
  steps:
    - name: Slot Update
      uses: azure/webapps-deploy@v2
      with:
        app-name: '${{ inputs.AZURE_APP_NAME }}'
        images: ${{ inputs.AZURE_ACR_ID }}.azurecr.io/demo/webapp:preview-pr-${{ github.event.pull_request.number }}
        slot-name: preview-pr-${{ github.event.pull_request.number }}

.github/actions/image_push/action.yml

コンテナレジストリにイメージをpush

name: 'Docker Image Push'
inputs:
  AZURE_CREDENTIALS:
    required: true
  AZURE_ACR_ID:
    required: true
  AZURE_ACR_PASSWORD:
    required: true
  DOCKER_IMAGE_TAG:
    required: true

runs:
  using: 'Composite'
  steps:
    - name: Azure Login
      uses: azure/login@v1
      with:
        creds: ${{ inputs.AZURE_CREDENTIALS }}

    - name: Login to Azure Container Registry
      uses: azure/docker-login@v1
      with:
        login-server: ${{ inputs.AZURE_ACR_ID }}.azurecr.io
        username: ${{ inputs.AZURE_ACR_ID }}
        password: ${{ inputs.AZURE_ACR_PASSWORD }}

    # Dockerイメージをビルド、デプロイ
    - name: Docker Build and Push
      shell: bash
      run: |
        docker build -t demo-docker -f Dockerfile . \
        && docker tag demo-docker:latest ${{ inputs.DOCKER_IMAGE_TAG }} \
        && docker push ${{ inputs.DOCKER_IMAGE_TAG }}

.github/actions/is_exist_slot/action.yml

slotが作成済みかをチェック。

name: 'Exists Slot'
description: 'slotの存在確認'
inputs:
  AZURE_CREDENTIALS:
    required: true
  AZURE_APP_NAME:
    required: true
  AZURE_RESOURCE_GROUP:
    required: true
  SLOT_NAME:
    required: true

outputs:
  exists_slot:
    description: "slotが存在している場合は1、存在していない場合は0を返す"
    value: ${{ steps.check-slot-exists.outputs.exists_slot }}

runs:
  using: 'Composite'
  steps:
    - name: Azure Login
      uses: Azure/login@v1.4.3
      with:
        creds: ${{ inputs.AZURE_CREDENTIALS }}

    # プレビューslotが作成されているかを確認
    - name: Check Preview Slot Exists
      uses: Azure/cli@v1
      id: check-slot-exists
      with:
        inlineScript: |
          az webapp config container show \
          --resource-group ${{ inputs.AZURE_RESOURCE_GROUP }} \
          --name ${{ inputs.AZURE_APP_NAME }} \
          --slot ${{ inputs.SLOT_NAME }} \
          > /dev/null 2>&1 && echo '::set-output name=exists_slot::1' || echo '::set-output name=exists_slot::0'

プレビュー環境の作成時間

大体5~6分で作成が完了する。ただここはアプリのbuild時間にもよるので各々。

slotの作成自体は2分ぐらい。

終わり

フロントエンドのPRレビューするときはプレビュー環境ないとやる気にならない身体になってしまった。