Microsoft Azure Tech Advent Calendar 、9 日目の記事。
以前書いた puppeteer を Docker コンテナーを使わずに Azure Web Apps で動かすネタ、3 年もたてばさすがにうまくいかないらしい。
だいぶ変わってそう (@n0bisuke さんが奮闘中) なので、 2022 年版をまとめるか。。。 https://t.co/jNHd8xP9DT
— ほりひろ loves <⚡> (@hori__hiro) 2022年11月29日
「だったら原因と対策をアドベント カレンダーのネタにでも。」とのんびりまとめてたら、のんびり過ぎて先を越されてしまった。
かといって他のネタを思いつかないので、背景的な話、使い方的な話は👆の Qiita のエントリーを見てもらって、こっちはもう少し技術的に突っ込んだ話を。
# 結果、Microsoft Azure Tech Advent Calendar なのに Puppeteer 色が強い内容になってしまった。
原因
以前の方法では動かなくなった原因は主に 2 つ。
- Puppeteer の仕様変更
- 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. から鑑みて以下の対策を実施。
- アプリのデプロイ時のパッケージに Chromium のバイナリーを含める。
さらに、実行時にデプロイ パッケージ内の Chromium を使うよう設定する。 - ランタイム環境に 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_DIR
に Chromium のインストール先として /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/puppeteer
に Chromium を探しに行ってしまう。
当然そこには 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 install
は GitHub Actions のワークフローの中で実行しているので、Puppeteer インストール時の PUPPETEER_CACHE_DIR
はここで設定。
コマンドの前に書くのが好きじゃないなら env
を使ってもいい。
(以下余談)
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。
- まず Web Apps が使っている Node.js のコンテナ イメージを特定する
一度立ち上げてみて、/home/LogFiles
あたりのログにdocker run ~
とログが残っているので、そこから特定する*7
-> この場合mcr.microsoft.com/appsvc/node:18-lts_20221007.3.tuxprod
になる - ローカルの Docker を使って 1. で見つけたコンテナーのシェルを立ち上げる
👆 に列挙したパッケージを入れずに、
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 のプロセスが立ち上がらない」と言っている。- https://packages.debian.org/ で
libgobject-2.0.so.0
を含むパッケージを探す。
->libglib2.0-0
に入っていることがわかる apt install libglib2.0-0
を実行してlibgobject-2.0.so.0
を入れる- エラーが出なくなるまで 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