Github Actions で Azure Container Apps の B/G Deployを設定する
先日、現在開発をしているサービスのQueue workerの一部をAzure Container Appsに移行しました。とても使いやすいのでメインのAPIの移行準備として、Github Actions使用したB/Gデプロイの実験をしてみました。
サンプルコードの準備
今回実験用にデプロイするのは、クイック スタート: Azure Container Apps にコードをデプロイする で使用している、 Azure Container Apps Album APIを使いたいと思います。
このAPIはnodejsで実装されていて、静的に保持したデータを返すAPIが一つだけ定義されています。まずは手元の環境で実際に実行してみます。
自身の環境でGithub Actionsを実行したいので、 Azure Container Apps Album APIを fork
してpullします。
Dockerfileが用意されているので、以下のようにビルド・起動します。
$ cd containerapps-albumapi-javascript/src $ docker build -t kaz29/containerapps-albumapi-javascript . $ docker run -it --rm -d --name containerapps-albumapi-javascript -p 80:3500 kaz29/containerapps-albumapi-javascript
実際にAPIを叩くとこんな感じです
$ curl http://localhost/albums | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 751 100 751 0 0 35753 0 --:--:-- --:--:-- --:--:-- 57769 [ { "id": 1, "title": "You, Me and an App ID", "artist": "Daprize", "price": 56.99, "image_url": "https://aka.ms/albums-daprlogo" }, { "id": 2, "title": "Seven Revision Army", "artist": "The Blue-Green Stripes", "price": 17.99, "image_url": "https://aka.ms/albums-containerappslogo" }, { "id": 3, "title": "Scale It Up", "artist": "KEDA Club", "price": 39.99, "image_url": "https://aka.ms/albums-kedalogo" }, { "id": 4, "title": "Lost in Translation", "artist": "MegaDNS", "price": 39.99, "image_url": "https://aka.ms/albums-envoylogo" }, { "id": 5, "title": "Lock Down your Love", "artist": "V is for VNET", "price": 39.99, "image_url": "https://aka.ms/albums-vnetlogo" }, { "id": 6, "title": "Sweet Container O' Mine", "artist": "Guns N Probeses", "price": 39.99, "image_url": "https://aka.ms/albums-containerappslogo" } ]
src/models/Album.js
に配列で持っているデータを返すだけのシンプルなAPIです。
リソースの準備
基本的にはここの手順通りなのですが、まずはリソースグループとContainer Registryを作成します。
環境変数を設定
export GITHUB_USERNAME="<YOUR_GITHUB_USERNAME>" export RESOURCE_GROUP="album-containerapps" export LOCATION="japaneast" export ACR_NAME="acaalbums"$GITHUB_USERNAME export API_NAME="album-api"
リソースグループを作成
az group create \ --name $RESOURCE_GROUP \ --location "$LOCATION"
Container Registryを作成
az acr create \ --resource-group $RESOURCE_GROUP \ --name $ACR_NAME \ --sku Basic \ --admin-enabled true
Github actionの設定
下記で取得したユーザ名・パスワードをGithub Actions Secretに設定
az acr credential show --name $ACR_NAME -g $RESOURCE_GROUP --query "username" --out tsv az acr credential show --name $ACR_NAME -g $RESOURCE_GROUP --query "passwords[0].value" --out tsv
ユーザ名: CONTAINER_REGISTRY_USERNAME
パスワード: CONTAINER_REGISTRY_PASSWORD
今回は、github container registry ではなく、Azure Container Registryを使うのでGHAの設定を以下のように書き換えます。 また、tagのpushのみデプロイをしたいので main の pushトリガーを削除しています。
環境変数からtag名を取得して、ACRにpush時のtagとして使用しています。
name: Build and Push on: push: # Publish semver tags as releases. tags: ["v*.*.*"] workflow_dispatch: env: ACR_NAME: acaalbumskaz29 API_NAME: album-api jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Log in to container registry uses: docker/login-action@v1 with: registry: ${{ env.ACR_NAME }}.azurecr.io username: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} password: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - name: Set tag name to env run: | echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Build and push container image to registry uses: docker/build-push-action@v2 with: push: true tags: ${{ env.ACR_NAME }}.azurecr.io/${{ env.ACR_NAME }}:${{ env.TAG }} context: ./src - name: Upload artifact uses: actions/upload-artifact@v2 with: name: deploy-artifact path: bicep/*
修正をcommit/pushした上で、以下の様にタグをつけてpushするとACRにデプロイ用のイメージがpushされます
$ git tag v0.0.1 $ git push origin --tags
Container Apps環境を作成
以下のコマンドを実行し、Container Apps環境を作成します。
export CONTAINERAPPS_ENVIRONMENT="my-containerapps-env" az containerapp env create \ --name $CONTAINERAPPS_ENVIRONMENT \ --resource-group $RESOURCE_GROUP \ --location $LOCATION
アプリをデプロイ
デプロイの環境ができたので、アプリをデプロイする準備・設定を進めていきます。
サービスプリンシパルを作成
デプロイに使用するサービスプリンシパルを作成します。以下では、1年有効な設定をしていますがこの辺りは適宜修正してください。
$ export RESOURCE_GROUP_ID=$(az group show \ --name "$RESOURCE_GROUP" \ --query id --output tsv) $ az ad sp create-for-rbac \ --display-name "$RESOURCE_GROUP GHA deploy" \ --scope $RESOURCE_GROUP_ID \ --role Contributor \ --sdk-auth \ --years 1
上記azコマンドで出力されたjson文字列を、GHA Action Secretに保存します。
初回デプロイ
まずは、B/Gデプロイではなく先ほどビルドしたコンテナを単純にデプロイします。
azコマンドでもデプロイはできるのですが、細かな設定ができないので今回はbicepを使用してデプロイします。
この辺りの詳細は、トニー (@TonyTonyKun) / Twitter さんの ブログ - Azure Container Apps で Blue-Green Deployments を試してみたがとても参考になります。
bicep/api.bicep を作成
# bicep/api.bicep param containerAppName string param location string = resourceGroup().location param environmentId string param imageName string param tagName string param revisionSuffix string param oldRevisionSuffix string param isExternalIngress bool param acrUserName string @secure() param acrSecret string @allowed([ 'multiple' 'single' ]) param revisionMode string = 'single' resource containerApp 'Microsoft.App/containerApps@2022-03-01' = { name: containerAppName location: location properties: { managedEnvironmentId: environmentId configuration: { activeRevisionsMode: revisionMode ingress: { external: isExternalIngress targetPort: 3500 transport: 'auto' allowInsecure: false traffic: ((contains(revisionSuffix, oldRevisionSuffix)) ? [ { weight: 100 latestRevision: true } ] : [ { weight: 0 latestRevision: true } { weight: 100 revisionName: '${containerAppName}--${oldRevisionSuffix}' } ]) } dapr:{ enabled:false } secrets: [ { name: 'acr-secret' value: acrSecret } ] registries: [ { server: '${acrUserName}.azurecr.io' username: acrUserName passwordSecretRef: 'acr-secret' } ] } template: { revisionSuffix: revisionSuffix containers: [ { image: '${acrUserName}.azurecr.io/${imageName}:${tagName}' name: containerAppName resources: { cpu: any('0.25') memory: '0.5Gi' } } ] scale: { minReplicas: 1 maxReplicas: 10 rules: [ { name: 'http-scaling-rule' http: { metadata: { concurrentRequests: '10' } } } ] } } } } output fqdn string = containerApp.properties.configuration.ingress.fqdn
bicep/deploy.bicep を作成
#bicep/deploy.bicep param location string = resourceGroup().location param isExternalIngress bool = true param revisionMode string = 'multiple' param environmentName string param containerAppName string param imageName string param tagName string param revisionSuffix string param oldRevisionSuffix string param acrUserName string @secure() param acrSecret string resource environment 'Microsoft.App/managedEnvironments@2022-03-01' existing = { name: environmentName } module apps 'api.bicep' = { name: 'container-apps' params: { containerAppName: containerAppName location: location environmentId: environment.id imageName: imageName tagName: tagName revisionSuffix: revisionSuffix oldRevisionSuffix: oldRevisionSuffix revisionMode: revisionMode isExternalIngress: isExternalIngress acrUserName: acrUserName acrSecret: acrSecret } }
後ほど、B/Gデプロイを実現するために、以下のパラメータを定義しています。
- revisionSuffix: 新たにデプロイされるリビジョン
- oldRevisionSuffix: 現在デプロイされているリビジョン
また、初回デプロイ時にはまだ実行中のリビジョンが存在しないため、以下の様にrevisionSuffix / oldRevisionSuffixが同じ場合には最新版のリビジョンに100%トラフィックを流すように設定しています。
traffic: ((contains(revisionSuffix, oldRevisionSuffix)) ? [ { weight: 100 latestRevision: true } ] : [ { weight: 0 latestRevision: true } { weight: 100 revisionName: '${containerAppName}--${oldRevisionSuffix}' } ]) }
workflowを更新
.github//workflows/build-and-push.yaml に以下のjobを追加します。
deploy: runs-on: ubuntu-latest needs: build environment: name: build url: https://${{ steps.fqdn.outputs.fqdn }} outputs: revision_suffix: ${{ steps.revision_suffix.outputs.revision_suffix }} previous_revision_suffix: ${{ steps.previous_revision_suffix.outputs.previous_revision_suffix }} fqdn: ${{ steps.fqdn.outputs.fqdn }} steps: - name: Download artifact uses: actions/download-artifact@v2 with: name: deploy-artifact - name: Set tag name to env run: | echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV # タグ名から.(ドット)を除去する - name: Set revision suffix name to env id: revision_suffix run: | echo "REVISION_SUFFIX=${TAG//./}" >> $GITHUB_ENV echo "::set-output name=revision_suffix::${TAG//./}" - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Deploy to containerapp uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp az deployment group create \ -f ./deploy.bicep \ -g ${{ env.RESOURCE_GROUP_NAME }} \ --parameters \ environmentName=${{ env.CONTAINER_APPS_ENVIRONMENT }} \ containerAppName=${{ env.API_NAME }} \ imageName=${{ env.API_NAME }} \ tagName=${{ env.TAG }} \ revisionSuffix=${{ env.REVISION_SUFFIX }} \ oldRevisionSuffix=${{ env.REVISION_SUFFIX }} \ acrUserName=${{ secrets.CONTAINER_REGISTRY_USERNAME }} \ acrSecret=${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
リビジョン名には . (ドット)
は含められないため、 Set revision suffix name to env
stepでタグ名のドットを除去しています。
修正をcommit/pushした上で、以下の様にタグをつけてpushするとコンテナアプリが追加され、APIがデプロイされます。
$ git tag v0.0.2 $ git push origin --tags
デプロイが完了したら、Auzre Portalのコンテナアプリのページに表示されているURLをブラウザで開くとAPIの動作を確認できます。
B/Gデプロイの設定を追加
では今回の主目的の、B/Gデプロイを実現するための設定を追加していきます。まずは、workflowのdeploy stepを以下の様に修正します
deploy: runs-on: ubuntu-latest needs: build environment: name: build url: https://${{ steps.fqdn.outputs.fqdn }} outputs: revision_suffix: ${{ steps.revision_suffix.outputs.revision_suffix }} previous_revision_suffix: ${{ steps.previous_revision_suffix.outputs.previous_revision_suffix }} fqdn: ${{ steps.fqdn.outputs.fqdn }} steps: - name: Download artifact uses: actions/download-artifact@v2 with: name: deploy-artifact - name: Set tag name to env run: | echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV # タグ名から.(ドット)を除去する - name: Set revision suffix name to env id: revision_suffix run: | echo "REVISION_SUFFIX=${TAG//./}" >> $GITHUB_ENV echo "::set-output name=revision_suffix::${TAG//./}" - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Get Previous revision name id: previous_revision_suffix uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp export REVISIONS=`az containerapp revision list --resource-group ${{ env.RESOURCE_GROUP_NAME }} --name ${{ env.API_NAME }} --query '[].name' --out tsv` echo "REVISION_NUM=`az containerapp revision list --resource-group ${{ env.RESOURCE_GROUP_NAME }} --name ${{ env.API_NAME }} --query '[] | length(@)' --out tsv`" >> $GITHUB_ENV echo "PREVIOUS_REVISION_NAME=${REVISIONS##*--}" >> $GITHUB_ENV echo "::set-output name=previous_revision_suffix::${REVISIONS##*--}" - name: Active revision count check if: ${{ env.REVISION_NUM != 1 }} uses: actions/github-script@v3 with: script: | core.setFailed('Multiple revisions are active!') - name: Deploy to containerapp uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp az deployment group create \ -f ./deploy.bicep \ -g ${{ env.RESOURCE_GROUP_NAME }} \ --parameters \ environmentName=${{ env.CONTAINER_APPS_ENVIRONMENT }} \ containerAppName=${{ env.API_NAME }} \ imageName=${{ env.API_NAME }} \ tagName=${{ env.TAG }} \ revisionSuffix=${{ env.REVISION_SUFFIX }} \ oldRevisionSuffix=${{ env.PREVIOUS_REVISION_NAME }} \ acrUserName=${{ secrets.CONTAINER_REGISTRY_USERNAME }} \ acrSecret=${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - name: Get new revision's fqdn id: fqdn uses: azure/CLI@v1 with: inlineScript: | export FQDN=`az deployment group show \ -g ${{ env.RESOURCE_GROUP_NAME }} \ -n ${{ env.DEPLOYMENT_NAME }} \ --query properties.outputs.fqdn.value \ --out tsv` export BASE_NAME=${FQDN#*.} echo "::set-output name=fqdn::${{ env.API_NAME }}--${{ env.REVISION_SUFFIX }}.$BASE_NAME"
各ステップの概要
- Get Previous revision name
B/Gデプロイを実現する為に、現在実行中のリビジョン名を取得しています。
- Active revision count check
B/Gデプロイ時に、実行中のリビジョンが複数ある場合はどのようにトラフィックを割り当てるか判断できないので、複数リビジョンが稼働している場合は、エラーになるようにチェックしています。
- Get new revision's fqdn
次に定義するジョブで、新しいリビジョンのURLを表示するために新しいリビジョンのFQDNを生成しています。
このjobを実行すると、新しいリビジョン(green)がデプロイされますが、トラフィックの割り当ては 0%
に設定されているため実際には新しいリビジョンのAPIは呼び出されません。
リビジョン毎にURLが発行されるので、新しいリビジョンのURLを使用して、動作確認を実施しします。
今回は、確認が完了後に、新しいリビジョンに 100%
、古いリビジョンに 0%
のトラフィックの割り当て、Blue / Greenを入れ替えて最新版を反映します。
B / Gを入れ替える
今回は、B/Gの入れ替え時に、承認処理を挟むために Environments
機能 の Environment protection rules
を利用します。
残念ながら、現状、Environment protection rulesは publicリポジトリか、Github Enterpriseでのみ利用可能です。
Environmentを追加
リポジトリの Settings
- Environments
で、リビジョン入れ替え用のEnvironment flip
を作成します。
以下の様に Required reviewers
をチェックし、レビュアーのアカウントを選択します。
B / Gを入れ替えるJobを追加
flip: runs-on: ubuntu-latest needs: deploy environment: name: flip steps: - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Flip revisions uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp az containerapp ingress traffic set \ -g ${{ env.RESOURCE_GROUP_NAME }} \ -n ${{ env.API_NAME }} \ --revision-weight \ ${{ env.API_NAME }}--${{ needs.deploy.outputs.revision_suffix }}=100 \ ${{ env.API_NAME }}--${{ needs.deploy.outputs.previous_revision_suffix }}=0
古いリビジョンを非アクティブにする
Environmentを追加
リポジトリの Settings
- Environments
で、非アクティブ用のEnvironment deactivate
を作成します。
以下の様に Required reviewers
をチェックし、レビュアーのアカウントを選択します。
古いリビジョンを非アクティブにするJobを追加
deactivate: runs-on: ubuntu-latest needs: [flip, deploy] environment: name: deactivate steps: - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Deactivate previous revision uses: azure/CLI@v1 with: inlineScript: | az extension add --upgrade --name containerapp az containerapp revision deactivate \ -g ${{ env.RESOURCE_GROUP_NAME }} \ -n ${{ env.API_NAME }} \ --revision \ ${{ env.API_NAME }}--${{ needs.deploy.outputs.previous_revision_suffix }}
デプロイを実行
修正をcommit/pushした上で、以下の様にタグをつけてpushすると新しいリビジョンがデプロイされます。
$ git tag v0.0.3 $ git push origin --tags
flip
デプロイ完了後、flipの実行待ち状態でworkflowが止まります。 deploy
jobのボックスにはデプロイされた新しいリビジョンのAPIのURLが表示されています。
Review deployments
をクリックして、B / Gを入れ替え(flip)します。
flipが完了すると、トラフィック割り当てが変更され新しいリビジョンを利用する状態になります。
deactivate
この状態で問題がなければ、古いリビジョンを非アクティブにする必要があります。リビジョンがアクティブな状態だと課金対象になってしまいますので、早めに非アクティブ化したほうが良いでしょう。
workflowは deactivate
Jobの実行待ち状態で停止しています。
Review deployments
をクリックして、古いリビジョンを非アクティブ化(deactivate)します。
deactivate jobが完了すると古いリビジョンが非アクティブ化されます
無事、Github actionsを使用して、Container AppsのB/Gデプロイが実現できました。
まとめ
いかがでしたでしょうか?Container AppsでBlue / Green デプロイを実現するには、現在実行中のリビジョン名が必要なため若干複雑な流れになっています。やっていることは特に難しいことではないですが、調査に少し時間がかかりました。「もっといい方法があるよ!」とかあれば是非教えてほしいです。
現状、Environment protection rulesは publicリポジトリか、Github Enterprise以外では利用できないので、privateリポジトリで開発をしている現場で使うには一部見直しが必要かもしれません。
今回はBlue / Green デプロイを採用しましたが、ちょっと修正すればカナリーリリースとかも実現できると思うので、参考にしてもらえると嬉しいです。参考までに、私が試したforkしたAPIのリポジトリを残しておきます。
私が現在担当している現場でも、今後Container Appsに移行する予定があるので、いい感じで実現できて捗りそうです。