ほりひログ

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

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