Web App に Node.js アプリをデプロイする GitHub Actions を速くする

Web App の [デプロイ センター] ブレードで、ソースコード等が置いてある GitHub レポジトリを選択すると、その Web App にデプロイするための GitHub Actions ワークフローを作成してくれる。

Node.js アプリの場合、既定では下記の yaml ファイルが GitHub レポジトリの .github/workflows に作成される。

# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy Node.js app to Azure Web App - some-web-app-name

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Set up Node.js version
        uses: actions/setup-node@v1
        with:
          node-version: '16.x'

      - name: npm install, build, and test
        run: |
          npm install
          npm run build --if-present
          npm run test --if-present

      - name: Upload artifact for deployment job
        uses: actions/upload-artifact@v2
        with:
          name: node-app
          path: .

  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: 'production'
      url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

    steps:
      - name: Download artifact from build job
        uses: actions/download-artifact@v2
        with:
          name: node-app

      - name: 'Deploy to Azure Web App'
        id: deploy-to-webapp
        uses: azure/webapps-deploy@v2
        with:
          app-name: 'some-web-app-name'
          slot-name: 'production'
          publish-profile: ${{ secrets.AzureAppService_PublishProfile_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }}
          package: .

この既定のワークフローのままだと、ところどころ遅いところがあるので速くしてみる。

1. devDependencies なモジュールはデプロイしない

build ジョブの 3 つ目の step npm install, build, and test の中で npm run build でアプリをビルドしている。
その為に npm installpackage.jsondevDependencies で指定しているモジュールも全てインストールしている。

これ。

      - name: npm install, build, and test
        run: |
          npm install
          npm run build --if-present
          npm run test --if-present

このインストールはビルドに必要だからしょうがないが、以降 node_modules に触れず、次の step, job まで進んでデプロイしているので、devDependencies なモジュールも含めて Web App にデプロイしていることになる。
当然、これらは運用環境に不要なモジュールなので、本当はビルドが終わったら削除するのが適切。

なので、

      - name: npm install, build, and test
        run: |
          npm install
          npm run build --if-present
          npm run test --if-present
          npm prune --production # `devDependencies` なモジュールを削除

と最後に npm prune --production を追加する。
これで dependencies なモジュールは残しつつ devDependencies なモジュールを削除できる。

2. artifact を zip 化する

既定では、ビルドした後のプロジェクトを、特にまとめることもなく、そのまま artifact として保存している。

ここ。

      - name: Upload artifact for deployment job
        uses: actions/upload-artifact@v2
        with:
          name: node-app
          path: .

上の 1. で npm prune --production を実行してモジュールを減らしても、大抵node_modules 配下には大量のファイルがある。
なので、この artifact のアップロードをする actions/upload-artifact アクションと deploy ジョブ内のダウンロードをする actions/download-artifact アクションに時間がかかる。

こういう警告も出る。

そこで、build ジョブ内の actions/upload-artifact アクションの前に一度 zip ファイル化しておき、それを artifact としてアップロードする。
すると、アップロードするファイルが 1 つになるので速くなるし、release ジョブの actions/download-artifact アクションでも 1 つの zip ファイルのダウンロードになり、こっちも速くなる。 さらに azure/webapps-deploy アクションで Web App にデプロイする時も ダウンロードしてきた zip ファイルをそのまま使えば、デプロイするファイルが 1 つになるので速くなる。

build ジョブ

      - name: Zip artifact for deployment
        run: zip release.zip ./* -qr # 除外したいディレクトリがあるなら -x オプションで指定

      - name: Upload artifact for deployment job
        uses: actions/upload-artifact@v2
        with:
          name: node-app
          path: release.zip

release ジョブ

      - name: 'Deploy to Azure Web App'
        id: deploy-to-webapp
        uses: azure/webapps-deploy@v2
        with:
          app-name: 'some-web-app-name'
          slot-name: 'production'
          publish-profile: ${{ secrets.AzureAppService_PublishProfile_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }}
          package: release.zip

完成形

以上、2 点を反映させた完成形がこちら。

# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy Node.js app to Azure Web App - some-web-app-name

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Set up Node.js version
        uses: actions/setup-node@v1
        with:
          node-version: '16.x'

      - name: npm install, build, and test
        run: |
          npm install
          npm run build --if-present
          npm run test --if-present
          npm prune --production # `devDependencies` なモジュールを削除

      - name: Zip artifact for deployment
        run: zip release.zip ./* -qr # 除外したいディレクトリがあるなら -x オプションで指定する

      - name: Upload artifact for deployment job
        uses: actions/upload-artifact@v2
        with:
          name: node-app
          path: release.zip

  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: 'production'
      url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

    steps:
      - name: Download artifact from build job
        uses: actions/download-artifact@v2
        with:
          name: node-app

      - name: 'Deploy to Azure Web App'
        id: deploy-to-webapp
        uses: azure/webapps-deploy@v2
        with:
          app-name: 'some-web-app-name'
          slot-name: 'production'
          publish-profile: ${{ secrets.AzureAppService_PublishProfile_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }}
          package: release.zip

Azure Functions in Javascript (Node.js) で SQL binding を試した

C# だけに来てた Azure Functions の SQL binding が、Javascript/Typescript (Node.js) でも動くようになったらしい*1ので試してみた。


https://github.com/Azure/azure-functions-sql-extension/releases/tag/v.0.1.304-preview

何ができるのか

Input/Output binding ができる。トリガーはできない。でもこれは C# も一緒。
トリガーはできなくても、これまでは tedious*2*3 とかを使って自前で接続を書いていたはずなので、それよりずっと楽になる。

が、肝心の JS での設定例が main ブランチに見当たらないので発掘してみると、開発用ブランチで発見。

https://github.com/Azure/azure-functions-sql-extension/tree/maddy/jsSamples/samples/samples-js

これを元に*4サンプルを作ってみた。

github.com


環境構築 (2022/05/04 追記)

まずは SQL binding 用の拡張機能をインストール。

func extensions install --package Microsoft.Azure.WebJobs.Extensions.Sql --version 0.1.304-preview

binding を使うために拡張機能が必要なので、これはわかる。

次がわからない。かつ必須。
上のコマンドでできたであろう ./bin/extensions.deps.json を同じディレクトリーの function.deps.json としてコピーする。

cp ./bin/extensions.deps.json ./bin/function.deps.json

もしかしたら cp ではなく mv でもいいのかもしれないが、念のため元のファイルも残している。

今まで拡張機能のインストールでこの手順を踏んだことがないが、この ./bin/function.deps.json がないと動かないので、今は必要なんだろう。
どこからどういう経緯で参照されているのか、 extensions.deps.json のままじゃダメなのか、など諸々気になるが、まだプレビューということで優しく見守る。

作ったサンプルでは、vscodetasks.json でこの手の処理を定義しておいたので、F5 デバッグで自動的にやってくれるはず。

(追記ここまで)


Binding 設定

サンプル/リファレンスから読み解いた設定方法は以下の通り。

Input binding

    {
      "type": "sql",
      "direction": "in",
      "name": "employees",
      "CommandType": "Text",
      "CommandText": "select * from [dbo].[Employees]",
      "ConnectionStringSetting": "SqlConnectionString"
    },

ConnectionStringSetting に指定している SqlConnectionStringSQL サーバーの接続文字列を収めたアプリケーション設定名*5
CommandText は Binding でデータを取る時に使われる SQL 文。

https://github.com/Azure/azure-functions-sql-extension#input-binding

Output binding

    {
      "type": "sql",
      "direction": "out",
      "name": "employees",
      "CommandText": "dbo.Employees",
      "ConnectionStringSetting": "SqlConnectionString"
    },

ConnectionStringSetting は Input Binding と同じ。
CommandText は Output binding でデータを突っ込むテーブル名。

https://github.com/Azure/azure-functions-sql-extension#output-binding

Output Binding でハマったポイント

注意: 0.1.311-preview で解消済み

最初、Output binding で例外*6が出ていたので issue を立てたら、「カラム名を全部小文字にしてみて」と。
回避策がサクッと出てきたので、問題は把握していていたんだと思う。なので、そう時間はかからずに直るだろう。

OK:

全部小文字だと成功する。

    context.bindings.employees = JSON.stringify([{
      "employeeid": 1,
      "firstname": "Hello",
      "lastname": "World",
      "company": "Microsoft",
      "team": "Functions"
    }])

NG:

DB のテーブルに合わせる感じで大文字を使っちゃうとダメ。

    context.bindings.employees = JSON.stringify([{
      "EmployeeId": 1,
      "FirstName": "Hello",
      "LastName": "World",
      "Company": "Microsoft",
      "Team": "Functions"
    }])

※ テーブル設計

まだプレビューなので、早期の GA を期待しつつ、温かく見守ろう。

*1:と言っても、C# でもプレビュー。

*2:https://www.npmjs.com/package/tedious

*3:なぜこの名前なのか。意味を知ってびっくりした。

*4:ほぼコピー。

*5:ローカル開発なら local.settings.json に書く。

*6:曰く「Primary Key が null だぞ」という内容。

PHP8/Laravel8 アプリを App Service on Linux で動かす方法 (2022/04 暫定版)

どうやら App Service on Linux で PHP8 / Laravel8 のアプリを動かす時に、いろいろ気を付けることがあるようなので、回避策含め備忘録としてまとめておく。

主な注意点

大きく分けて二つ。

  1. PHP を動かす Web サーバーの設定
  2. ビルド方法

1. Web サーバー設定

App Service on LinuxPHP のコンテナー イメージは、PHP7 までは apache だったのが nginx に変更になった。
それに伴って、PHP を動かす設定も変えないといけないらしい。
# ちなみに、OS は Debian 10 (buster)

具体的には、Startup Command として、nginx へのリクエストを PHP アプリへルーティングする?ための設定を変更する処理を指定する。

まずは nginx の設定変更。
App Service on Linux 内の nginx は /etc/nginx/sites-available/default を見ているので、これをコピーして PHP8/Laravel8 の構成に合わせて変更する。

これをアプリケーション プロジェクトの scripts/default として保存しておく。

scripts/default

server {
    #proxy_cache cache;
        #proxy_cache_valid 200 1s;
    listen 8080;
    listen [::]:8080;

    # 変更 1) index.php があるディレクトリを指定。これは Laravel8 の場合。
    root /home/site/wwwroot/public;  
    index  index.php index.html index.htm;
    server_name  example.com www.example.com; 

    location / {            
        index  index.php index.html index.htm hostingstart.html;
        # 変更 2) 詳細は https://docs.microsoft.com/en-us/answers/questions/542749/deploying-an-app-service-with-laravel-8-and-php-8.html
        try_files $uri $uri/ /index.php?$args;
    }

    # 以下デフォルトのまま
}

次に scripts/default/etc/nginx/sites-available/default に戻して、nginx を再起動し、変更した設定を反映するスクリプトを書く。
同じく scripts/startup.sh としておく。

scripts/startup.sh

#!/bin/bash

cp /home/site/wwwroot/scripts/default /etc/nginx/sites-available/default
service nginx reload

最後に App Service 側の Startup Command を使って、起動時に scripts/starup.sh を実行するよう設定する。

これで nginx と PHP との間の設定は解決。

2. ビルド設定

2022 年 4 月現在、PHP8 のアプリは既定の設定のままだと App Service のビルド エンジンである Oryx でのビルド (compser install コマンドの実行) ができない。

下記 Oryx の GitHub リポジトリの issue にある通り、Kudu コンテナー*1上でのビルド時に必要なファイル libonig.so.4 が見つからずエラーになってしまう。

github.com

追えるとこまで頑張ってみた感じ、これは、

  1. Kudu コンテナー上での composer install 実行に使用される PHP バイナリーが libong.so.4 とリンクしている
  2. リンクされている libonig.so.4 は、Debian 9 (コードネーム stretch) 向けのライブラリーである
    https://packages.debian.org/search?keywords=libonig
  3. でも Kudu コンテナーは Debian 10 (コードネーム buster) ベースのコンテナーである

    だから libonig.so.4 は入っていないし入れられない。

なので、この PHP バイナリーは今の Kudu コンテナーでは実行できない、という話。
たぶん PHP バイナリーをビルドしている環境が stretch だったのではないか、という疑惑。

一応、修正済みの Oryx が GitHub 上にはリリースされているので、Azure 上への次のリリース*2を待てばビルドできるようになるはず。

github.com


(以下 4/30 追記)

上記修正が適用されているかどうかは、Kudu コンソール上で oryx --version を実行してみて、ReleaseTagName20220427.1 かそれ以降になっているかを見たらいいっぽい。

(追記ここまで)


それが待てない人は以下の回避策をお試しあれ。

回避策 1. Oryx でビルドしない

最初はシンプルな策、Oryx でのビルドをやめる。
Oryx でビルドできないんだから、ローカルや CI 環境でビルドして、ビルド済みのものを App Service にデプロイしてしまえばいい。

App Service のアプリケーション設定で SCM_DO_BUILD_DURING_DEPLOYMENTfalse を指定すると、デプロイ時のビルドは無効になる。

回避策 2. Kudu を stretch ベースにする

issue で Corp. のエンジニアから出ている方法で、アプリケーション設定で SCM_DISABLE_BUSTER_KUDUtrue を指定する。
すると、Kudu のコンテナーが stretch ベースのものになるらしい。

試してみる。

既定の状態は buster なのは上のスクショの通り。

次に SCM_DISABLE_BUSTER_KUDUtrue を指定した状態。

おー確かに stretch になった。マジかよ知らなかった*3

これで libonig.so.4 が使えるので、composer install はできるはず。

ただし、ビルドは stretch ベースのコンテナー上で行われる一方で、実際のアプリケーションは buster ベースのランタイム コンテナー上で動くことになるので、ライブラリーによってはこの違いで動かなくなる(かもしれない)。

回避策 3. PHP バイナリー自体を置き換える

Kudu コンテナー内での composer install に使われている PHP バイナリーは、上記の通り stretch 上でビルドされたものと推察されるが、これは Oryx を起動した時にここからダウンロードして Kudu コンテナー内に展開したもの。
これを buster 上でビルドされた PHP バイナリーとに置き換えてしまえばいい。

そうすると

  • buster ベースの KutuLite コンテナー
  • buster 向けの PHP バイナリーを使って composer install を実行してビルド
  • ランタイムも buster

という感じで全て buster で統一できてハッピー。

buster 向け PHP バイナリーは自前でビルドしてデプロイ パッケージに入れることもできるし、実は stretch 向けの PHP バイナリーと同じところにあるのでそっちも使える。

「置き換え」処理そのものは、Oryx が持っている「ビルド前後に任意のスクリプトを実行する」仕組みを使う。

github.com

PHP バイナリーの置き換えはビルドの前にやらないといけないので、 PRE_BUILD_SCRIPT_PATHPRE_BUILD_COMMAND を使う。
単発コマンドでいい場合、またはワンライナーで書ける場合は PRE_BUILD_COMMAND がいいが、スクリプト ファイルを書く場合は PRE_BUILD_SCRIPT_PATH でファイルを指定する。

例えば、下記のようなスクリプトscripts/prebuild.sh として配置するなら、PRE_BUILD_SCRIPT_PATHscripts/prebuild.sh と指定する。

scripts/prebuild.sh

#!/bin/bash

PHP_VERSION=8.0.17

cd /tmp
mkdir -p /tmp/oryx/platforms/php/${PHP_VERSION}
curl -O https://oryx-cdn.microsoft.io/php/php-buster-${PHP_VERSION}.tar.gz
tar -xzf /tmp/php-buster-${PHP_VERSION}.tar.gz -C /tmp/oryx/platforms/php/${PHP_VERSION}

これで、composer install の直前に buster 向けの PHP バイナリーに差し替えができる。

懸念点は Corp. のエンジニアから「そっちのバイナリーは、依存モジュールが変わっているから、何が起こるか。。。」(超意訳)と言われたこと*4だけど、試した限り composer install くらいなら動く。
もちろん自前でビルドして持ち込む場合は自己責任で。

startup.sh など scripts 配下もまとめて GitHub に公開してみた。

github.com

一応、修正リリースと同じアプローチのはず。

ビルド エラー回避策のまとめ

ここまで書いたビルド設定に関する 4 種類の回避策のまとめ。

# 回避策 設定方法 メリット デメリット
1 Oryx でのビルドをやめる SCM_DO_BUILD_DURING_DEPLOYMENT = false ビルド環境の自由度が高い CI 等のビルド環境が別途必要
2 stretch ベースの Kudu コンテナー SCM_DISABLE_BUSTER_KUDU = true 設定簡単 ビルド/ランタイム環境でベース OS が異なり、アプリ実行時に不整合が起こるかも。
3 ビルド用 PHP バイナリーの変更 PRE_BUILD_SCRIPT_PATH = <script_path> ビルド/ランタイム環境の統一感 動くことは動くがエンジニアが前向きじゃない。自己責任。
4 次リリースを待つ - 公式方法。 「いつから使える」ってはっきりしたことは言えない

あなたはどれを選びます?

*1:KuduLite と言われるコンテナー イメージ。1. の nginx が動くアプリ コンテナーとは別コンテナー

*2:2022 年 5 月か 6 月かな?

*3:App Service にはこういう知らない設定がいっぱいある

*4:なんで公開しているんですかねぇ。。。

Teams 会議で AfterShokz のマイクをミュートすると、ビープ音が鳴りませんか?

はじめに

今まで何ともなかったのに、ホントについ最近、AfterShokz OpenComm を使って参加した Teams 会議中にミュートにすると、ビープ音が鳴るようになってしまった。

よくよく思いかえせば、1 年位前に同僚から似たような症状を聞いていたけど、自環境では発生してなかったのですっかり他人事で忘れてた。

なぜ突然この症状が出たのかはわからないが、AfterShokz がマイク ミュートの制御を PC から受け取るとビープ音が鳴るのは、どうやら元々そういう仕様らしい。
で、何の拍子かで(Teams のアップデート?)、自分の Teams がミュートの指示を出すようになったんだろう。

AfterShokz はファームウェア アップデートの仕組みは持ってないだろうから、本来はこれが正しい状態なのかもしれない。

そうはいってもビープ音はうるさい。他の人の発言を遮らないためにマイク ミュートしているのに、ビープ音が邪魔で集中できない。

解決策

VB-Audio Virtual Cable という仮想オーディオデバイスを使おう。

こんな感じに、Teams のスピーカーは、AfterShokz を直接指定し、マイクは仮想オーディオ出力に指定する。AfterShokz のマイクは仮想オーディオ入力につなぐ。

こうすると、Teams からのマイク ミュートの制御が(なぜか)AfterShokz まで伝搬しないので、AfterShokz はミュートに気づかず、結果ビープ音もならない。

手順

ググってみるものの、「仮想オーディオ デバイスを使えばいいよ!」という情報は見つかるが、どう使うのかいまいち書いていない。
あっているかわからないが、とりあえず動いている設定を書いておく。

1. インストール

まずインストーラをダウンロードする。

Mac は持っていないので、Windowsインストーラが入った zip ファイルをダウンロードする。
2015 年のものらしいが、Windows 11 でも問題なし。互換性バンザイ。

vb-audio.com

zip ファイルを展開すると、その中のインストーラを実行する。
今どきは 64 bit OS なので、VBCABLE_Setup_x64.exe で。

インストールが完了したら、指示に従って再起動。

2. デバイス設定

設定 アプリを開いて システム > サウンド と辿り、その一番下 サウンドの詳細設定 を開く。

サウンド のプロパティ シートが開くので、AfterShokz のデバイスを選択して、その中の 聴く タブの このデバイスを聴く にチェックを入れ、このデバイスを使用して再生するCABLE Input を選択する。

3. Teams 設定

上で書いた通り、Teams では、スピーカーはヘッドセットのものを使って、マイクは CABLE Output を選択する。

これで Teams でミュートしてもビープ音に悩まされずに済む。

おわりに

もしかしたら 仮想オーディオ デバイスを挟むことで、AfterShokz ならではの機能がつぶれてるかもしれないけど、今のところ困ってないので、そんな機能があっとしても使ってなかったんだろう。

Bicep メモ

f:id:horihiro:20220402143937p:plain

都度検索するのが大変なので、メモとして。

1. 子リソースの書き方

例えば Microsoft.Web/sites/sourcecontrols

1-1. 入れ子にする。

親リソースの中に resource で子リソースを宣言する。

resource appService 'Microsoft.Web/sites@2021-02-01' = {
  name: webSiteName
  location: location
  kind: 'app'
  properties: {
    :
  }
  resource web 'sourcecontrols@2021-02-01' = {  // ここから子リソース
    name: 'web'
    properties: {
      :
    }
  }
}

1-2. 子から親を参照する。

子リソースは親リソースと同じレベルで宣言する。
加えて parent で親リソースを参照する。

resource appService 'Microsoft.Web/sites@2021-02-01' = {
  name: webSiteName
  location: location
  kind: 'app'
  properties: {
    :
  }
}

resource web 'Microsoft.web/sites/sourcecontrols@2021-02-01' = {
  parent: appService // ここで親リソースを参照する
  name: 'web'
  properties: {
    :
  }
}

2. ロール割り当て

デプロイ時にマネージド ID を有効にして、同時にそれを他のリソースのロールをhttps://www.google.com/url?sa=i&url=https%3A%2F%2Fdevblogs.microsoft.com%2Fdevops%2Fazurefunbytes-intro-to-bicep%2F&psig=AOvVaw0zWLc6KaUdbfEbb_8IGnfO&ust=1648963806189000&source=images&cd=vfe&ved=0CAgQjRxqFwoTCLjLpcrU9PYCFQAAAAAdAAAAABAD割り当てる。

Web App とストレージ アカウントのデプロイタイミングで、Web App のマネージド ID に ストレージ アカウント共同作成者 を割り当てる例。

param roleNameGuid string = newGuid()

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

resource appService 'Microsoft.Web/sites@2021-02-01' = {
  name: webSiteName
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  kind: 'app'
  properties: {
    :
  }
}

// https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/bicep/scenarios-rbac#role-assignments
resource assignRole 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = {
  name: roleNameGuid
  scope: storageAccount
  properties: {
    principalId: appService.identity.principalId
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab') // "ストレージ アカウント共同作成者" のロール定義 ID
    // https://docs.microsoft.com/ja-jp/azure/role-based-access-control/role-assignments-template#new-service-principal
    principalType: 'ServicePrincipal'
  }
}

GitHub Actions w/ OIDC で ACR に docker push する

f:id:horihiro:20220321154822p:plain

TL; DR

azure/docker-login など docker login 系の Action ではなく az acr login コマンドでログインする。

name: Run Login to ACR w/ OpenID Connect

on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  ACRJob:
    runs-on: ubuntu-18.04

    steps:
      - uses: actions/checkout@main
      - uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 

      - name: docker login, build and push
        run: |
          az acr login -n ${{ secrets.ACR }}  # <--- **これ**
          docker build ./target -t ${{ secrets.ACR }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.run_number }}
          docker push ${{ secrets.ACR }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ github.run_number }}
        env:
          IMAGE_NAME: minimalserver

あ、Azure AD アプリケーションに AcrPull のロールを割り当てることを忘れずに。

以下、背景的な何か

GitHub Actions が OpenID Connect に対応してた

もう半年近く前、GitHub Actions が OpenID Connect に対応していた。
これで azure/login のアクションも資格情報として Azure AD アプリケーションの clientSecret を使わなくて済むようになった。

詳細はここに全て書いてある。

blog.haniyama.com

ただし Azure Container Regisitry & GitHub Actions のドキュメントを見ると、azure/docker-login で昔ながら?の clientSecret を使って ACR にログインする方法で書かれている。

ここと、

f:id:horihiro:20220319200022p:plain

ここ。

f:id:horihiro:20220319195916p:plain

できることなら clientSecret はあまり管理したくないので、何とか OIDC の仕組みを使って、GitHub Actions 内で clientSecret を使わない方法がないかにチャレンジ。

(おさらい) ACR にログインする

ACR は Docker の API (でいいの?)と互換性を持っているので、

docker login -u <ユーザー名> -p <パスワード> <ACR ログイン サーバー>

でログインできる。<ユーザー名> / パスワード は、

  1. Azure AD アプリケーションの場合は、
    • ユーザー名: アプリケーション ID (ClientID)
    • パスワード: clientSecret
  2. ACR の管理者ユーザーを有効にした場合は、
    • ユーザー名: ACR リソース名
    • パスワード: [アクセス キー] ブレード内の password or password2
      f:id:horihiro:20220319195524p:plain

のどちらかのペアを使うことになる。

今回は、OIDC を使って azure/login アクションで取得した認証情報を、ここのユーザー名/パスワードとして使う方法がないか調べたら、そこまでしなくていい方法がここに書いてあった。

github.com

曰く、

az acr login -n <ACR リソース名>

を使えばいい、と。

これを実行すると、

  • ユーザ名は null GUID 00000000-0000-0000-0000-000000000000
  • パスワードは、azure/login からの認証情報を元に(なんやかんやで)取得してきた ACR の refresh token

を使って docker login を実行してくれるらしい。
azure/docker-login アクションすら必要ないので、冒頭yaml になる。

サンプル リポジトリー。

github.com

ちなみに azure/docker-login がやっていること

azure/docker-loginwith で与えられた clientSecret などを使って何しているか見てみると、

github.com f:id:horihiro:20220320092328p:plain

なんと直接 config.json に認証情報を追記している。docker login すら実行していないとは。

act を使ってローカル プロジェクトを Azure Static Web Apps にデプロイしてみた


Note!
あくまで「やってみたらできた」的エントリーです。公式の方法ではありません。試すときは壊れてもいいリソースで。


act

github.com

f:id:horihiro:20220213091853p:plain なるものです。スゴイ。
同僚 (一度だけ実物見たことある) の YouTube 配信で「GitHub Actions のデバッグって大変ですよねー」と愚痴コメントしたら、その場で調べて教えてくれました。

Go 製のワン バイナリー ツールなので、(いつでも消せそうなので)気軽に入れられます。
各アクションの実行に Docker を使っているのでこれも必要です。
インストールは公式リポを参照ください。

act <event> <options>

みたいに実行すると、GitHub Actions の yaml ファイルを探して (既定では ./.github/workflows 以下)、<event> *1 にあった action を実行するみたいです。

Runner は Linux 限定、など制限はあるみたいですね。

Azure Static Web Apps

一方で Azure Static Web Apps (以下、SWA) 。 docs.microsoft.com

Azure Static Web Apps は、コード リポジトリから Azure にフル スタックの Web アプリを自動的にビルドしてデプロイするサービスです。

とあるとおり、GitHub Actions や Azure Pipeline を使ってGitHub (or Azure DevOps) のリポジトリにある静的サイトのコードをビルドして、Azure 上の SWA リソースにデプロイまでしてくれます。

SWA デプロイ用の GitHub Actions

この SWA デプロイ用の GitHub Actions を act でローカル実行し、Azure 上の SWA にデプロイできるか試してみました。

ぶっちゃけ、SWA デプロイ用の GitHub Actions は自動生成されるものなので、GitHub Actions 自体をデバッグすることはあまりないでしょう。それはうすうす気づいています。
あくまで、act を試してみた、という実験です。

以下、手順。

0. 事前準備

まず、SWA 自体を Azure にデプロイしておきます。GitHub と連携する必要はないです。

f:id:horihiro:20220213110401p:plain

次にプロジェクトの用意 SWA にデプロイする予定のプロジェクトを GitHub 等に作成して、ローカルに git clone しておきます。

1. GitHub Actions の用意

./.github/workflows に下記 yaml ファイルに置きます。ほぼ自動生成されるものそのままです*2

name: Azure Static Web Apps CI/CD

on:
  push:

jobs:
  build_and_deploy_job:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true
      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: "upload"
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: "/" # App source code path
          api_location: "" # Api source code path - optional
          output_location: "dist" # Built app content directory - optional
          ###### End of Repository/Build Configurations ######

2. デプロイ トークンのコピー

Azure ポータルを開いてデプロイ トークンをコピーします。
yaml ファイルに書いた secrets.AZURE_STATIC_WEB_APPS_API_TOKEN として使います。

[概要] ブレードの 「デプロイ トークンの管理」からコピーできます。

f:id:horihiro:20220213110216p:plain

3. .actrc の準備

act の実行設定として下記の内容を .actrc としてプロジェクトのルートに保存します。

-s AZURE_STATIC_WEB_APPS_API_TOKEN=<2. でコピーしてきたデプロイ トークン>
-P ubuntu-latest=node:14.18.3-buster-slim
-P ubuntu-20.04=node:14.18.3-buster-slim
-P ubuntu-18.04=node:14.18.3-buster-slim

ここに書いてしておくと、act 実行時のオプションとして自動で読み込まれます。

最初の行は 2. でコピーした SWA のデプロイ トークンを貼り付けます。
-s FOO_BAR=... と書くと GitHub Actions 内で ${{ secrets.FOO_BAR }} で参照できます。

残りの行は runs-on: で指定するプラットフォームと使うコンテナー イメージの対応です。
ubuntu-*** と書いてあるのに Debian のイメージだったりしますが、細かいことはいいのかもしれません。気にしない。

ポイントは Node.js のバージョンをパッチ レベルまで指定していること。
Oryx (SWA のビルド エンジンである) は Node.js のサポート バージョンとしてパッチ レベルまで指定しています。
なので、マイナー/パッチを省略したバージョンを使うと、コンテナー レジストリ (Docker Hub) 内のイメージ更新で、いつか動かなくなるかもしれません。

f:id:horihiro:20220213121501p:plain

ちなみに act を始めて実行する時、既定のプラットフォームのサイズを Micro とすると、ホーム ディレクトリ配下に .actrc を作りますが、node:12-buster-slim*3 が使われます。
一方で Node.js V12 の 2022 年 2 月現在の最新バージョンは v12.22.10 なので、上のスクショのように「バージョンが合わん」とエラーになります。

4. act を実行する

プロジェクト ルートはこうなっているはず。

$ tree -a
.
├── .actrc
├── .github
│   └── workflows
│       └── azure-static-web-apps.yml
├── .gitignore
├── src
│   :
:
└── staticwebapp.config.json

ここで act push を実行 (push イベントを発行) すると GitHub Actions が実行され、SWA 上にデプロイされるはずです。

まとめ

SWA の GitHub Actions を題材にしたのは反省しているが、act は使えそう。

*1: push とか pull_request とか

*2:静的サイト構築に使うフレームワークによってビルド設定等は変わります

*3:node:16-buster-slim の場合も