ほりひログ

所属組織の製品 (Azure とか) に関連する内容が多めだけど、個人の見解であって、所属組織を代表する公式情報ではないです。

Azure ポータルの favicon をリソースのアイコンに置き換える

このツイートの話。

困りごと

お仕事の関係上、Azure ポータルを頻繁に開く。
ただいろんなリソースを一つのブラウザー タブで行ったり来たりするのは苦痛。
なので、Azure のリソースごとにそれぞれ別タブとして開くこともしばしば。

すると、こんな感じ👇で、同じ favicon が並んでしまい、各タブがどのリソースを開いているのか迷う。

Cloud Adaption Framework に掲載されている各リソースの省略形*1を使ってリソース名を命名するよう運用すれば、タブ タイトルから類推はできる。

learn.microsoft.com

ただこれも、タブをピン止めしてしまうと、、、

タブ タイトルが見えなくなるので手がかりを失う。

そりゅーしょん

なので、最初のツイートに戻って、Azure ポータルで開いているリソースに合わせて、タブの favicon を強制上書きする UserScript*2 を書いてみた。

github.com

実質自分専用なので Edge (Dev) と Tampermonkey の組み合わせしか試していない。
まだまだバグってそうなので、勇気のある人柱の方だけ募集中。

favicon はどこから?

基本的に [概要] ブレード左のアイコンをそのまま favicon に転用している。

例えば、App Service リソースを開いていたらこう。

App Service Environment を開いたらこう。

リソース グループ を開いたらこう。

リソースのリスト表示の時は [概要] の部分がない。 なので、ある 1 種類のリソースのリストを開いた時は、リソース名の左のアイコンが全部同じはずなので、そのアイコンを拝借。

1 つもなければ、真ん中にうっすら出ているリソース アイコンから。

例外として、すべてのリソース のようにいろんなリソースが含まれるリスト表示はデフォルト favicon のまま。

並べてみる

それぞれ、どのリソース タイプなのか、馴染みのあるアイコンから判別可能。

ピン止めしたって一目瞭然。

うーん、なかなかいいんじゃない?直感的になった。

ちなみに

この UserScript を使って App Service を開いた Azure ポータル (下図左) と、App Service Team Blob (同右) を並べると判別不可、というね😅

あと、コチラもよろしく。

uncaughtexception.hatenablog.com

*1:ぜひご覧あれ

*2:User Script? Userscript?

puppeteer を Azure Web Apps で動かす試み 2022

Microsoft Azure Tech Advent Calendar 、9 日目の記事。

以前書いた puppeteer を Docker コンテナーを使わずに Azure Web Apps で動かすネタ、3 年もたてばさすがにうまくいかないらしい。

「だったら原因と対策をアドベント カレンダーのネタにでも。」とのんびりまとめてたら、のんびり過ぎて先を越されてしまった。

qiita.com

かといって他のネタを思いつかないので、背景的な話、使い方的な話は👆の Qiita のエントリーを見てもらって、こっちはもう少し技術的に突っ込んだ話を。
# 結果、Microsoft Azure Tech Advent Calendar なのに Puppeteer 色が強い内容になってしまった。

原因

以前の方法では動かなくなった原因は主に 2 つ。

  1. Puppeteer の仕様変更
  2. Web Apps 側のベース イメージ更新

それぞれを詳しく書いていく。

1. Puppeteer の仕様変更

どうやら今年になって、npm install puppeteer を実行した時に Chromium のバイナリー ファイルをダウンロード/インストールする場所が変わっている。
デフォルトでは $HOME/.cache/puppeteer の下にインストールされる。

Puppeteer のソースコード的にはこのあたり。

https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer/src/getConfiguration.ts#L38

インストール後は👇のようなファイル構造になる。

/
├── PATH/TO/PROJECT          <-- ここで `npm i puppeteer` したのに
│   ├── app.js
│   └── node_modules
│       ├── puppeteer    
:       :
└── $HOME/.cache/puppeteer   <-- ここに Chromium が入っちゃう
    └── chrome
         └── linux-XXXXXXX
              └── ... 

# /PATH/TO/PROJECT は適宜実際のプロジェクトに置き換える。

以前は npm install を実行したディレクトリの node_modules/puppeteer (上の例だと /PATH/TO/PROJECT/node_modules/puppeteer/) の中に Chromium がダウンロードされていた。

でも今回 Chromium がインストールされる場所が ~/.cache/puppeteer という npm install を実行ディレクトリとは全く関係のない所に変わったことで、普通に Web Apps へデプロイしようとすると Chromium 関連のバイナリー ファイルが含まれない *1

さらに Web Apps 上での Puppeteer 実行時にも ~/.cache/puppeteer (Web Apps 上だと /root/.cache/puppeteer) を探しに行くので、当然「Chromium が見つからない」というエラーが出る。

Puppeteer 側としては ~/.cache に集約することで、複数のプロジェクトで使いまわしが簡単になるので、ディスク容量の削減とかを狙っているのかもしれない。

これがまず Puppeteer 側の変更の影響。

2. Web Apps 側のベース イメージ更新

2 つ目。
こっちは Web Apps の話。

Web Apps の Node.js のベース イメージが 2022 年 12 月現在 Debian 11 になっている。

Debian 11 の初版リリースは 2021 年 *2 なので、前回書いた 2020 年 1 月時点で Debian 11 が存在しないのは間違いない*3

ベース OS のメジャー バージョンが変わっているなら Chromium が依存するライブラリー (.so ファイル群)も変わっていてもおかしくない。

対策

原因 1. と 2. から鑑みて以下の対策を実施。

  1. アプリのデプロイ時のパッケージに Chromium のバイナリーを含める。
    さらに、実行時にデプロイ パッケージ内の Chromium を使うよう設定する。
  2. ランタイム環境に Chromium が依存する .so を入れる。

a. アプリのデプロイ パッケージに Chromium のバイナリーを含める

Puppeteer のインストール時/実行時に Chromium を入れる場所/探す場所は環境変数 PUPPETEER_CACHE_DIR で指定できるので、ローカルの開発環境や CI など npm install を実行するタイミングで、

PUPPETEER_CACHE_DIR=/PATH/TO/PROJECT/node_modules/puppeteer npm install puppeteer

のように環境変数 PUPPETEER_CACHE_DIRChromium のインストール先として /PATH/TO/PROJECT/node_modules/puppeteer を渡すと、/PATH/TO/PROJECT/node_modules/puppeteer/chrome/linux-XXXXXXX/... といい感じに Chromium をインストールしてくれる。

/PATH/TO/PROJECT       <-- ここで `PUPPETEER_CACHE_DIR=/PATH/TO/PROJECT/node_modules/puppeteer npm i puppeteer` とすると
├── app.js
└── node_modules
    ├── puppeteer      <-- ここの下に Chromium が入る
    :    └── chrome
    :        └── linux-XXXXXXX
    :            └── ... 

これで、 Chromium も他の依存モジュールと一緒にデプロイ パッケージに含めることができる。

こうして作ったデプロイ パッケージを Web App 上にデプロイすると、Chromium/home/site/wwwroot/node_modules/puppeteer に展開される。

/home/site/wwwroot       <-- ここにデプロイ パッケージが展開されるので、
├── app.js
└── node_modules
     ├── puppeteer       <-- ここに Chromium があるはず
     :    └── chrome
     :        └── linux-XXXXXXX
     :            └── ... 

一方で Web App 上にデプロイした後の Puppeteer 実行時も、デフォルト (環境変数 PUPPETEER_CACHE_DIR が未設定) だと /root/.cache/puppeteerChromium を探しに行ってしまう。
当然そこには Chromium はなく、実行時エラーが出る。

なので、これも環境変数 PUPPETEER_CACHE_DIR/home/site/wwwroot/node_modules/puppeteer と設定しておくことで、Web Apps の内部で展開されたデプロイ パッケージ内の Puppeteer を探しに行くよう変更でき、適切に Chromium を見つけることができる。

PUPPETEER_CACHE_DIR が未設定の時は process.env['npm_package_config_puppeteer_cache_dir'] なども参照しているので、pacakge.json に書いてもいいかもしれない*4

デプロイ パッケージに含めない&環境変数を設定しない場合でも、実行時のスタートアップ スクリプト等で node node_modues/puppeteer/install.js を実行すると Chromium をインストールすることも可能。
でもサイトのウォームアップで Chromium のダウンロードとインストールが実行されるので、当然その分時間がかかりサイトの立ち上げ時間にも影響するのであまりお勧めしない。

b. ランタイム環境に Chromium が依存する .so を入れる

どのパッケージが必要か色々調べたが、結論から言うと以下のパッケージをがあればいい。

  • libasound2
  • libatk1.0-0
  • libatk-bridge2.0-0
  • libcairo2
  • libcups2
  • libdrm2
  • libgbm1
  • libglib2.0-0
  • libnss3
  • libpango-1.0-0
  • libxcomposite1
  • libxdamage1
  • libxfixes3
  • libxkbcommon0
  • libxrandr2

2020 の時と同様、これらをスタートアップ スクリプトなどを駆使して入れるしかない。
別途、日本語フォントのインストール等は必要。

#!/bin/sh

apt update && apt install -y \
  libasound2 \
  libatk1.0-0 \
  libatk-bridge2.0-0 \
  libcairo2 \
  libcups2 \
  libdrm2 \
  libgbm1 \
  libglib2.0-0 \
  libnss3 \
  libpango-1.0-0 \
  libxcomposite1 \
  libxdamage1 \
  libxfixes3 \
  libxkbcommon0 \
  libxrandr2 \
#    :  以下、フォントのインストール等の処理

npm start # 等、Web アプリの起動コマンド

サンプル

シンプルながら、Puppeteer の動作確認だけしたリポジトリがこちら。 github.com Chromium のインストール先は、👆に合わせて /PATH/TO/PROJECT/node_modules/puppeteer にしている。

npm installGitHub Actions のワークフローの中で実行しているので、Puppeteer インストール時の PUPPETEER_CACHE_DIR はここで設定。
コマンドの前に書くのが好きじゃないなら env を使ってもいい。

20221129-puppeteer-on-webapp/main_app-puppeteer.yml at main · horihiro/20221129-puppeteer-on-webapp · GitHub

(以下余談)
Chromium をデプロイ パッケージに含めるようとすると、それだけで 150MB はありファイル数も膨大になる。
このファイルを GitHub Actions のワークフローのジョブ間で artifact として受け渡すとかなり遅いので、事前に 1 つの zip ファイルに固めてから artifact として受け渡した方がいい。

というのを以前つらつら書いたのがこちら。

uncaughtexception.hatenablog.com

(余談終わり)

うまいことデプロイ パッケージの中に Chromium を含めることができたら、次は Puppeteer 実行時の設定。
このサンプルでは PUPPETEER_CACHE_DIR の設定はスタートアップ スクリプトの中で設定している。

20221129-puppeteer-on-webapp/start.sh at main · horihiro/20221129-puppeteer-on-webapp · GitHub

この時も環境変数としてアプリケーションに渡せさえすればいいので、アプリケーション設定を使ってもいい。

Chromium の実行に必要なパッケージのインストールも、同じスタートアップ スクリプトの中で実施。

20221129-puppeteer-on-webapp/start.sh at main · horihiro/20221129-puppeteer-on-webapp · GitHub

動作結果自体は特に前回と変わらないので省略。

結論

古い「試してみた」系エントリーには outdated つけておこう*5

そしてやっぱり Azure 色 / Web App 色が薄かった。


(おまけ) 依存パッケージの探し方

一応 Puppeteer が依存しているパッケージの探し方も残しておくが、これがだいぶ泥臭い*6

  1. まず Web Apps が使っている Node.js のコンテナ イメージを特定する
    一度立ち上げてみて、/home/LogFiles あたりのログに docker run ~ とログが残っているので、そこから特定する*7

    -> この場合 mcr.microsoft.com/appsvc/node:18-lts_20221007.3.tuxprod になる
  2. ローカルの Docker を使って 1. で見つけたコンテナーのシェルを立ち上げる
  3. 👆 に列挙したパッケージを入れずにnpm install puppeteer をインストールして、以下の Node.js のコマンドを実行する

    node -e "require('puppeteer').launch()" 2>&1 | grep " error while loading shared libraries"
    

    -> 以下のような error while loading shared libraries と書かれたエラーを見ることになる。

    /root/.cache/puppeteer/chrome/linux-1056772/chrome-linux/chrome: error while loading shared libraries: libgobject-2.0.so.0: cannot open shared object file: No such file or directory

    この場合「libgobject-2.0.so.0 が見つからないので Chromium のプロセスが立ち上がらない」と言っている。

  4. https://packages.debian.org/libgobject-2.0.so.0 を含むパッケージを探す。

    -> libglib2.0-0 に入っていることがわかる
  5. apt install libglib2.0-0 を実行して libgobject-2.0.so.0 を入れる
  6. エラーが出なくなるまで 3. に戻って繰り返す

(おまけ終わり)

*1:シンボリック リンクとか、やりようはあるかもしれない

*2:https://www.debian.org/News/2021/20210814

*3:正直なところ、当時の正確なバージョンは覚えていないけども。

*4:パッケージ作成時と実行時で、相対パスをうまく調整する必要があるかも

*5:Qiita なら自動で付けてくれるのに。。。

*6:スクリプト化できそうな気もするが、そこまで頻繁にやることじゃない

*7:詳細は https://qiita.com/georgeOsdDev@github/items/abdadd35248b98c4ef88#%E5%AE%9F%E9%9A%9B%E3%81%AB%E5%88%A9%E7%94%A8%E3%81%95%E3%82%8C%E3%82%8B%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8blessed-image

Azure Container Apps のプレビュー用リビジョンを作る GitHub Actions

社内のハッカソン イベントで、Azure Container Apps 関連の GitHub Actions を作った。

Marketplace でも公開中。

github.com

実は、ハッカソン イベントへの参加とは別に、GitHub 上にリポジトリがあることは知っていてウォッチするつもりだったけど、ハッカソン イベントのプロジェクトになっていたので、メンバーに追加してもらった*1

できること

この Action の機能としては 2 つ。
Container App リソースに対して、

  1. コンテナー イメージ名やリビジョン名のサフィックスを指定して、Traffic が 0 のリビジョンを 作成する
  2. 指定したサフィックスを持ったリビジョンを非アクティブにする

ということができる。

Container App のリビジョンについてはコチラのドキュメントをご覧あれ *2

learn.microsoft.com

使い方

想定しているユースケースは、Container App リソースのコンテナーを更新する時に、稼働中のコンテナー/アプリはそのままにして、新しいコンテナーの動作を Container App 上で確認したいケース。

この Action で作ったリビジョンは、イングレスでの Traffic が 0 に設定しておりそのリビジョン専用 URL (Action の出力として取得可能) 経由でのみアクセスができる。
なので、稼働中のアプリへのアクセスには影響を与えずに、プレビュー用途として作ったリビジョンの確認ができる。

例えば Pull Request をトリガーにしたワークフローの中で、新しいコンテナー イメージのビルドとコンテナー レジストリーへの push をした後、この Action で新しいプレビュー用リビジョンを作り、作ったプレビュー用リビジョンの専用 URL を使って、実際の動作を確認する。

Pull Request を閉じた時のワークフローとして、作ったプレビュー用リビジョンを非アクティブにすることもできる。

プレビュー用リビジョンのコンテナーをどうやって本番用リビジョンとするかはお任せ。
リビジョンの動作を確認した後に Traffic を 100 に変えてもいいし、リリース用のブランチへのマージの際に (プレビュー用のリビジョンを非アクティブにした後に) 別のワークフローで改めて Traffic が 100 の本番用リビジョンをリリースしてもいい。

ワークフロー サンプル

この Action を使ったサンプルのワークフローはこちら。

github.com

以下は動作概要。

PR を Open した時

  1. コンテナー イメージをビルド
  2. ビルドしたコンテナー イメージを Azure Container Registry へ docker push
  3. この Action で Azure Container Apps へプレビュー用リビジョンを作成
  4. プレビュー用 URL を PR にコメント

PR を Close/Merge した時

  1. この Action で Azure Container Apps へプレビュー用リビジョンを非アクティブ
  2. PR へのコメントからプレビュー用 URL を削除

検討しないといけないこと

まだリリースほやほやなので、いくつか残課題も。

  • コンテナー イメージ名以外のパラメータ (env, cmd, etc)
  • 複数コンテナー対応
     :
    etc.

オープンソースなので、ご意見いただきながら改善していければ。

あと現在 45 stars★ (2022/10/31 現在) 。

こちらもまだまだ絶賛募集中。

余談

あとハッカソン イベントの参加賞 (自己申告制) として T シャツ👕貰った。

余談でもすらない戯言

このブログへのアクセスが 10/28 だけスパイクしてた。

まぁ、あの記事へのアクセスだろう。
また Shokz のビープ音で検索している人が増えたか。

*1:ただ実際に自分がやったことは、Dev Container 作ったり、バグの再現/検証のお手伝いだったりで、メインのコードはあまり書いてない。

*2:最近ドメイン名が docs.microsoft.com から learn.microsoft.com に変わったみたい

Azure Functions Node.js の新しいプログラミング モデル

ここの issue で議論されてた新しいプログラミング モデル、情報がまとまって試せるものが出てきたのでローカル PC 環境で動かしてみた。

github.com

注意 : 2022 年 9 月現在、まだ "internal testing" phase (パブリック プレビューですらない) で議論中なので、変更の可能性は大いにあり。

変更ポイント ~ function.json の廃止

上述のリンク先でも Say goodbye 👋 to "function.json" files! って言ってる。一番影響でかそう。

function.json が廃止されると、function.json 内で設定していた

  1. エントリー ポイント
  2. トリガー / バインディング

を他で定義することになるので、簡単にその説明を。

1. エントリー ポイント

今のプログラミング モデルでは function.jsonscriptFile フィールドで定義していた (関数の) エントリー ポイントは、(モジュール全体のエントリー ポイントとして) package.jsonmain フィールドに移動、ロードしたい js ファイルのパスを指定することに。

package.json

{
    "name": "azure-functions-prototype",
    "version": "1.0.0",
    "description": "",
      :
    "main": "/PATH/TO/ENTRYPOINT.js",
      :
}

ちなみに、この main フィールドのパスの解析、globby を使って独自に行っているので、ワイルド カード (** , *) 指定での複数ファイルの読み込みが可能。

2. トリガー / バインディング

現状 function.jsonbindings フィールドで定義していたトリガー / バインディング設定は、新しいモデルでは上述のエントリー ポイントとして読み込まれたファイル内で、@azure\functions からインポートできる app にトリガーに対応するメソッド (get, timer, http, storageBlob etc) で登録していく。

以下、サンプルコード。

import { app, HttpRequest, InvocationContext} from "@azure/functions";

app.get('helloWorld1', async (context: InvocationContext, request: HttpRequest) => {
    context.log(`Http function processed request for url "${request.url}"`);

    const name = request.query.get('name') || await request.text() || 'world';

    return { body: `Hello, ${name}!` };
});

app.timer('timerTrigger1', {
    schedule: '0 */5 * * * *',
    handler: (context: InvocationContext, myTimer: Timer) => {
        const timeStamp = new Date().toISOString();
        context.log('The current time is: ', timeStamp);
    }
});

バインディングもコードで追加する。
以下、HTTP トリガーで実行して、その時に Blob コンテナーから特定のデータを入力バインディングで取得して、その内容を出力バインディングでキューに保存する、というサンプル。

import { app, HttpRequest, HttpResponse, input, InvocationContext, output } from "@azure/functions";

const queueOutput = output.storageQueue({
  queueName: 'testqueue',
  connection: 'storage_APPSETTING'
});
const blobInput = input.storageBlob({
  connection: 'storage_APPSETTING',
  path: 'testcontainer/testblob',
})
async function helloWorldWithExtraOutputs(context: InvocationContext, request: HttpRequest): Promise<HttpResponse> {
  context.log(`Http function processed request for url "${request.url}"`);

  const name = request.query.get('name') || await request.text() || 'world';

  if (process.env.storage_APPSETTING) {
    const message = context.extraInputs.get(blobInput);
    context.extraOutputs.set(queueOutput, message);
  }

  return { body: `Hello, ${name}!` };
}

const extraOutputs = [];
if (process.env.storage_APPSETTING) {
  extraOutputs.push(queueOutput);
}

const extraInputs = [];
if (process.env.storage_APPSETTING) {
  extraInputs.push(blobInput);
}

app.http('helloWorldWithExtraInputsOutputs', {
  authLevel: "anonymous",
  methods: ['GET', 'POST'],
  extraOutputs,
  extraInputs,
  handler: helloWorldWithExtraOutputs
});

outputinput@azure/functions からインポートして、そこから取得したバインディング定義のオブジェクトを http トリガーの関数 (handler) と一緒に extraOutputs, extraInputs として登録、という流れ。

その他諸々は上述のリンクをご参照くださいませ。

プロトタイプ

このリポジトリで公開中。

github.com

手順通りにやれば特に問題な動くことが確認できる。

注意 : Azure Functions 上にデプロイしてもまだ動かない

func コマンドの注意点

通常のインストール方法で入れた Azure Functions Core Tools の func コマンドから直接実行しちゃいけない。

WikiSetup の項目に

"func-cli-nodejs-v4": "4.0.4764": A preview build of the func cli that contains support for the new framework on the worker/host side of things

と書いてある通り、このプログラミング モデルでの実行には dev 版の Node.js Lauguage Worker が必要。
通常のインストール方法で入れた Azure Functions Core Tools 同梱の func を実行してしまうと、いつも通り function.json を探そうとして、当然見つからないので、結果「No job functions found.」となるだけ。

上記リポジトリの場合、この dev 版 Worker は package.jsondevDependencies にある func-cli-nodejs-v4@4.0.4764 に含まれているので、npm start 経由で func start を実行することで、dev 版 Worker を起動できる。

感想

だいぶ変わった印象だけど、関数登録処理自体を自前のコードでやるので、その処理の前(恐らく Worker が起動してエントリー ポイントがロードされた直後の状態)にオリジナルの処理を挟みやすくなる*1

まだ議論中の部分もありそうなので、いつ Public Preview、GA になるか、その時にはこの通り動くのかは不透明だけど、大きく変えようとしていることだけでも知っておいた方がよさそう。
他の言語も function.json 廃止の方向になるんだろうか。

*1:Top-level await はまだ使えないかも

Visual Studio Code の Compound launch configurations が割と便利

今更だけど、最近この便利さを知ったので。

Compound launch configurations とは。

.vscode/launch.jsonconfigurations に書いてあるデバッグ設定を、複数まとめて実行 (& 終了) してくれる機能。

code.visualstudio.com

デバッグしたいアプリが、例えばフロントエンドの Javascript とバックエンドのサービス (BFF や各マイクロ サービスたち)、のように複数で構成されていて一斉にデバッグしたい時に便利。

設定方法

1. 単一フォルダーの場合

VS Code で一つのフォルダーを開いている場合は、通常のデバッグ設定と同様に .vscode/launch.json で設定する。

  1. まず一般的?な configurations (version と同じレベル) に単体でのデバッグ設定を書く。
  2. 次に 1. の configurations と同じレベルに compounds と、その中にも configurations を書く。 name もつけておく。
  3. 最後に 1. の configurations 内の設定のうち、一括デバッグしたいデバッグ設定の name の値を、2. の configurations の中に書いていく。

既定では一括で実行したデバッグも止める時は個別になる。止める時も一括処理で止めたい時は stopAlltrue に設定しておく。

以下は .vscode/launch.json の具体例。

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    // 単体デバッグの設定
    {
      "name": "Launch Frontend scripts",  // <-- ①の定義
      "request": "launch",
      "type": "msedge",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/public"
    },
    {
      "name": "Launch Backend API",  // <-- ②の定義
      "type": "node",
      "request": "launch",
      "preLaunchTask": "build",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "npm",
      "runtimeArgs": [
        "run",
        "start"
      ],
      "skipFiles": [
        "<node_internals>/**"
      ],
      "console": "integratedTerminal"
    }
  ],
  "compounds": [
    {
      "name": "Compound",
      "configurations": [
        "Launch Frontend scripts",  // --> ①への参照
        "Launch Backend API",  // --> ②への参照
      ],
      "stopAll": true // デバッグ終了時に一斉に止めるフラグ
    }
  ]
}

この設定の場合、VSCodeデバッグ メニューに Compound というデバッグ設定が表示され、これを実行すると Launch Frontend scriptsLaunch Backend API が同時に実行される。

stopAlltrue なので、デバッグを終了した時は一斉に止まる。

2. ワークスペース ファイルの場合

VS Code には、1 つのウィンドウで複数のフォルダー、例えばマイクロ サービスの個々のサービスを開くことができる。
その場合、ワークスペース構成を記述する、拡張子 .code-workspace のファイルにフォルダーのパス情報が 1 ないし複数記載されている。

Multi-root Workspaces *1 というらしい。

code.visualstudio.com

通常は個々のフォルダーには .vscode/launch.json があり、それぞれを一つのウィンドウで開いた時は、1. のデバッグ設定が実行できるはず。

それぞれのフォルダーで定義された個々のデバッグ設定を一つにまとめたい時は、.code-workspacelaunch 要素を追加して、.vscode/launch.json と同じように compounds を記載する。

foovar.code-workspace

{
  // 参照フォルダーのパス
  "folders": [
    {
      "path": "PATH/TO/SERVICE_A"
    },
    {
      "name": "service2",  // SERVICE_B の alias みたいなもん。フォルダー名が被った時に付ける?
      "path": "PATH/TO/SERVICE_B"
    }
  ],
  "launch": {
    "version": "0.2.0",
    "compounds": [
      {
        "name": "Compound",
        "configurations": [
          "Launch Client",  // --> ①への参照
          {
            // --> ②への参照
            "folder": "SERVICE_A", // デバッグ設定名が被った時に必要
            "Launch API Server"
          },
          {
            // --> ③への参照
            "folder": "service2",
            "Launch API Server"
          }
        ],
        "stopAll": true
      }
    ]
  }
}

中で参照しているデバッグ設定 Launch Client, Launch API Server は、それぞれ SERVICE_A, SERVICE_B.vscode/launch.json で定義している設定。

PATH/TO/SERVICE_A/.vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Client",  // <-- ①の定義
        :
    },
    {
      "name": "Launch API Server",  // <-- ②の定義
        :
    }
  ]
}

PATH/TO/SERVICE_B/.vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch API Server",  // <-- ③の定義
        :
    }
  ]
}

こうやって分離しておくと、それぞれのフォルダーを開いた時は単体のデバッグができて、ワークスペースとしてまとめて開いた時はそれぞれのデバッグを一括で実行できる。

もしそれぞれのフォルダーで定義したデバッグ設定名は被った時は、

          {
            // --> ③への参照
            "folder": "service2",
            "Launch API Server"
          }

みたいに、folders に書いたフォルダー名、またはそれの alias name を使って folder で指定することで、どこの Launch API Server なのかを判別する。苗字みたいなもんか *2

注意事項

いくつか注意事項を。

その 1 - デバッグの実行順序

どうやら compounds を使って「API サーバー A が起動した後に、API サーバー B を起動して、最後に Web ブラウザーを、、、」みたいなデバッグ設定の順序を制御する方法はなさそう。
しばらく調べたけど、公式の方法は見当たらなかった。

ドキュメントにも in parallel と明記されているから、そういうもんなんだと思うことにした *3

制御したければ何か無理やりな hack が必要なんだと思う*4

その 2 - compounds はネスト出来ない

あるフォルダーの .vscode/launch.json で定義した compounds の設定を、別の compounds 設定から参照することができない。

これは、同じ .vscode/launch.json にかかれた compounds だろうが、.code-workspace から参照しようが関係なく、とにかくできないっぽい。

意外だったのが、2 つのフォルダーを 1 つのワークスペース .code-workspace に入れておくと、一方のフォルダーの .vscode/launch.jsoncompounds から、もう片方の .vscode/launch.jsonデバッグ設定が参照 & 実行できたこと。
もちろん、フォルダーとして開いた時は、もう片方のフォルダーとの関係性はどこにも書かれてないので、実行はできなくなる。なので、あまり使いどころは見えてない。

まだまだ知らない便利な設定 / 機能が多い。

*1:単数形の workspace はフォルダーと同じ?

*2:"namespace" という単語が スッと出せない悲しさ

*3:現実、マイクロサービス内の個々のアプリが行儀よく順序を守って立ち上がることは期待しない方がいい。依存先が立ち上がってなかったら、それなりのエラー処理を書くべきなんだろう

*4:今のところそれを追いかける情熱がない

Pre release 版 Azure Functions Runtime を動かす

以前から Pre release 版の Azure Functions Runtime を試そうとしては失敗してたが、ここにヒント(というかそのものずばり)があったので、その構築手順をまとめる。

Pre release 版?

まだ Azure Functions 上にリリースされていないランタイム*1
なので、ご利用は自己責任で。

構築手順

といっても、zip をダウンロードして展開するだけ。

まじめに書くと、

  1. Pre release 版の zip ファイルをダウンロードする
  2. ダウンロードしてきた zip ファイルをを好きなところに展開する
  3. func コマンドにパスを通す
  4. (Linux/Mac のみ) func コマンドに実行パーミッションを付与する

とこれだけ。

func コマンドを切り替える

上の手順で Pre release 版の func コマンドと Azure Functions Runtime が使えるようになるが、「展開してパスを通す」を素直にやってしまうと、以降も Pre release 版を使うことになるので、いざ正式リリース版を使う時にまたパスから直さないといけなくなる。

それはツライなので、Azure Functions Core Tools のバージョン マネージャー funcvm で Pre release 版も管理してみることにする。

github.com

funcvm 自体は npm i @anthonychu/funcvm -g でインストールする。

インストールされた funcvm は、 ~/.funcvm/download の下で Azure Functions Core Tools (つまり func コマンド) の各バージョンを管理している。
なので、この ~/.funcvm/download に適当な名前でディレクトリーを作って、その下に Pre release 版のパッケージを展開したらいいだけ。

~/.funcvm/download/
├── 3.0.4585
├── 4.0.4544
├── 4.x.xxxx-prerelease
└── funcvm-core-tools-version.txt

みたいな。

また funcvm では、

いつもはこのバージョンの func だけど、このディレクトリ配下ではこのバージョンの func を使う

みたいなことが、対象のディレクトリに .func-version ファイルを作ってその中にバージョン番号を書くことでできる。

この機能を使って、Pre release 版を試したいプロジェクトにだけ .func-version ファイルを作っておき、その中に上で作ったディレクトリ名 ( 上の例なら 4.x.xxxx-prerelease ) を書いておくと、そのディレクトリで実行した func コマンドは ~/.funcvm/download/4.x.xxxx-prerelease の下にある func を使ってくれる。
便利。

最後に funcvm で管理するためのバージョン番号に関する注意点。

まずディレクトリ名が正規表現 /^\d+\.\d+\.\d+/ にマッチしない場合 funcvm list に出てこない。
正直 ls ~/.funcvm/downloads とやれば出てくるディレクトリ一覧と同じなので、funcvm list で出せないから困るもの、って程でもないが、どうしても funcvm list で出力させたい場合は 4.9.9999 とかにしておけばいい。

また正規表現にマッチしていたとしても、そのバージョン番号が Azure Functions Core Tools の GitHub Releases に載っていない場合、funcvm use での .func-version の作成に失敗する。
これも、バージョン番号が書かれているだけのファイルなので、echo 4.9.9999 > .func-version で作ったとしてもその後の挙動に特に違いはない。

*1:Azure Functions 上のランタイムは、2022年6月3日現在、v4 系で v4.3.2、v3 系で 3.7.1

Deploy to Azure ボタンで Bicep ファイルからデプロイする (っぽく見せる powered by GitHub Actions )。

Deploy to Azure ボタン

GitHub リポジトリーの README.md などで見かけるこのボタン。

https://aka.ms/deploytoazurebutton

これ自体は単なる画像でクリックしても何も起きないが、Markdown 中に以下の書式を書いておくと、クリック一つで Azure ポータルでデプロイ画面が開くボタンになる。

[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/<URLEncoded ARM Template URL>)

肝は <URLEncoded ARM template URL> で、Azure プラットフォームがこの <URLEncoded ARM template URL> で指定された URL エンコード済み URL から ARM テンプレート (カスタム デプロイ テンプレート) を取ってきて、それを基にデプロイ画面を作るらしい。

docs.microsoft.com

Deploy to Azure ボタンの弱点

弱点はこの ARM テンプレートが JSON 形式のみ対応なこと。Bicep 形式の ARM テンプレートは、別途 JSON 形式にビルドしておいて、その URL を使う必要がある。
ドキュメントにもハッキリと。

多分、Azure プラットフォーム側に Bicep をビルドする仕組みがないのでは*1

なので、このボタンがあるリポジトリーには、Bicep ファイルがあったとしても、大抵 JSON もある。
編集は Bicep でして、ローカルでこのデプロイ ボタンのために JSON にビルドして、Bicep と JSON をセットでリポジトリーにあげているんだろう。

同じ内容なのに形式が違うものを一緒に管理する、というのは個人的に気持ち悪いものを感じる。

GitHub Actions で克服してみる

この気持ち悪さを解消する。

といっても、Bicep のビルド自体は GitHub Actions の中で bicep build (az bicep build) してしまえばいいので難しくない。

悩み

ただ問題は出来上がった JSON ファイルをどこに置くか。

GitHub Actions から Artifacts として保存してしまうと、zip ファイル形式なるし、ワークフローごとに Artifacts の URL も変わってしまう。
特に URL が変わってしまうと Deploy to Azure ボタンに指定する URL も変えないといけないので、README.md を都度編集するのか、となる。
そんなことしたくない。有効期限もある。

なので、

  1. JSON 形式のまま保存できる場所
  2. 繰り返し実行しても URL が変わらないプロセス (上書きしてくれる)

を満足する必要がある。

解決

ヒントは GitHub Pages の仕組みに。

GitHub Pages は開発用ブランチにある Markdown 等のコンテンツを、 HTML 等の Web ブラウザーで表示可能な形式に変換して別ブランチに配置し、それに *.github.io な URL を割り当てている。
コンテンツを更新しても URL は基本変わらない。

この仕組みをマルっとパクって、開発用ブランチでの管理は Bicep で行い、GitHub Actions で JSON にビルドした後、その JSON を別ブランチに置く。
Markdown 内の Deploy to Azure ボタンからは、別ブランチ上の JSON ファイルの URL を参照する。

おまけに、保存先のブランチ名と保存対象のディレクトリを指定するだけで、ブランチに保存してくれる便利な GitHub Pages 用の GitHub Actions がある。
先人に感謝。

github.com

ワークフロー完成

ということで、出来上がったワークフローがこちら。

on:
  push:
    branches:
      - main  # Set a branch name to trigger deployment
    paths:
      - '**.bicep'
  workflow_dispatch:

env:
  out_dir: public
  main_bicep_file: bicep/main_parent.bicep
  publish_branch: json_template

jobs:
  deploy:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2

      - name: Build bicep
        run: |
          mkdir ./${{ env.out_dir }}
          az bicep build --file ${{ env.main_bicep_file }} --outdir ${{ env.out_dir }}
      - name: Publish to other branch
        uses: peaceiris/actions-gh-pages@v3
        if: ${{ github.ref == 'refs/heads/main' }}
        with:
          personal_token: ${{ secrets.PERSONAL_TOKEN }}
          publish_dir: ./${{ env.out_dir }}
          publish_branch: ${{ env.publish_branch }}

注意点として peaceiris/actions-gh-pages アクションで別ブランチにアクセスするには、 public_repo のスコープを持った Personal Access Token が必要。理由はわかってない。

あとは Markdown 内の Deploy to Azure ボタンに渡す URL を、JSON を保存したブランチ名とパスに合わせて指定するだけ。

https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2F<org>%2F<repo>%2F<branch>%2Fpath%2Fto%2Ftemplate.json

これでリポジトリーで管理する ARM テンプレートは、一見 Bicep ファイルだけになって、Bicep からデプロイしているように見える、かもしれない。

めでたしめでたし。

*1:あくまでも想像。ホントのところは知りません。