Azure Functions on Linux で Puppeteer を動かす

# はじめに

Azure Functions Linux Consumption plan では、Chromium の依存ライブラリが事前に用意された環境で実行されるため、[Puppeteer](https://pptr.dev/) や [Playwright](https://playwright.dev/)といった、ヘッドレスブラウザを実行することができます。
スクレイピングや E2E テストなどの実行環境として Azure Functions を利用できるというわけです。

なお、依存ライブラリが予め用意されている環境は Azure Functions Linux Consumption plan のみとなり本記事でも他の環境については確認していません。
また、ほとんど後述の参考記事の内容と変わらないですが、2022 年版 Tips として書いておきます。

コードはこちら

https://github.com/georgeOsdDev/azure-function-headless-browser


# Tips 1. リモートビルドを有効にする

> Since we're deploying to a Linux environment, we have to make sure we run npm install in Linux so it downloads a version of Chromium that matches the deployment target. 

https://anthonychu.ca/post/azure-functions-headless-chromium-puppeteer-playwright より

VS Code で開発/デプロイしている場合は、.vscode/setting.json に `azureFunctions.scmDoBuildDuringDeployment` を設定する。

```json:.vscode/setting.json
{
  "azureFunctions.deploySubpath": ".",
  "azureFunctions.projectLanguage": "JavaScript",
  "azureFunctions.projectRuntime": "~4",
  "debug.internalConsoleOptions": "neverOpen",
  "azureFunctions.scmDoBuildDuringDeployment": true
}
```

Azure Functions Core Tools でデプロイするときは以下のコマンド

```bash
func azure functionapp <appName> --build remote
```


# Tips 2. ダウンロードされるブラウザの保存先について

## Puppeteer の場合

> Starting in v19.0.0, Puppeteer stores browsers in ~/.cache/puppeteer to globally cache browsers between installation.

https://pptr.dev/guides/configuration#examples　より

とのことで、デフォルトだと Azure 上でのビルド実行時(`npm install`)で保存されるバイナリがアプリケーション実行時に見えない状態となる。詳しい解説は[こちら](https://uncaughtexception.hatenablog.com/entry/2022/12/09/191520#1-Puppeteer-%E3%81%AE%E4%BB%95%E6%A7%98%E5%A4%89%E6%9B%B4)

これを回避するため、サンプル通りカレントディレクトリ以下に保存するような設定を加える

```js:.puppeteerrc.cjs
const {join} = require('path');

/**
 * @type {import("puppeteer").Configuration}
 */
module.exports = {
  // Changes the cache location for Puppeteer.
  cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};
```

あるいは実行時にダウンロードすることでも回避できるがあんまりやる意味はなさそう。
```js
const puppeteer = require("puppeteer");
const BrowserFetcher = require("puppeteer").BrowserFetcher
const browserFetcher = new BrowserFetcher({path: '/home/site/'});

module.exports = async function (context, req) {
    const url = req.query.url || "https://google.com/";
    const revisionInfo = await browserFetcher.download('1069273');
    const browser = await puppeteer.launch({
        executablePath: revisionInfo.executablePath,
    });
    const page = await browser.newPage();
    await page.goto(url);
    const screenshotBuffer = await page.screenshot({ fullPage: true });
    await browser.close();

    context.res = {
        body: screenshotBuffer,
        headers: {
            "content-type": "image/png"
        }
    };
};
```

## Playwright の場合

環境変数 `PLAYWRIGHT_BROWSERS_PATH` は、アプリケーション設定から設定可能なため、アプリケーション設定 PLAYWRIGHT_BROWSERS_PATH として `0` を設定しておく

https://playwright.dev/docs/browsers#hermetic-install

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4914/e6a3fbfe-2402-09ce-b758-94ade116bd7b.png)

# Tips 3. フォントファイルの追加

デフォルトでは Azure Functions の実行環境のシステムフォントに日本語フォントが存在しないため豆腐が表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4914/4c794f90-08b6-4344-5fc5-25a5a969f099.png)

この事象は Puppeteer、Playwright 両方で発生します。
また、フォント問題については以下のスレッドで議論されています。
https://github.com/Azure/azure-functions-host/issues/4883#issuecomment-683375972

上記スレッド内では実行時にフォントをコピーする方法が紹介されていますが、以下の方法でも回避可能でした。

## 任意のフォントファイルを追加する方法

後述の方法で `/etc/fonts/fonts.conf`を見ると以下のような設定となっている。

```xml
<!-- Font directory list -->
        <dir>/usr/share/fonts</dir>
        <dir>/usr/local/share/fonts</dir>
        <dir prefix="xdg">fonts</dir>
        <!-- the following element will be removed in the future -->
        <dir>~/.fonts</dir>
```


<details><summary>実行環境の/etc/fonts/fonts.conf 全文</summary>


```
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<!-- /etc/fonts/fonts.conf file to configure system font access -->
<fontconfig>
        <its:rules xmlns:its="http://www.w3.org/2005/11/its" version="1.0">
                <its:translateRule translate="no" selector="/fontconfig/*[not(self::description)]"/>
        </its:rules>

        <description>Default configuration file</description>

<!--
        DO NOT EDIT THIS FILE.
        IT WILL BE REPLACED WHEN FONTCONFIG IS UPDATED.
        LOCAL CHANGES BELONG IN 'local.conf'.

        The intent of this standard configuration file is to be adequate for
        most environments.  If you have a reasonably normal environment and
        have found problems with this configuration, they are probably
        things that others will also want fixed.  Please submit any
        problems to the fontconfig bugzilla system located at fontconfig.org

        Note that the normal 'make install' procedure for fontconfig is to
        replace any existing fonts.conf file with the new version.  Place
        any local customizations in local.conf which this file references.

        Keith Packard
-->

<!-- Font directory list -->

        <dir>/usr/share/fonts</dir>
        <dir>/usr/local/share/fonts</dir>
        <dir prefix="xdg">fonts</dir>
        <!-- the following element will be removed in the future -->
        <dir>~/.fonts</dir>

<!--
  Accept deprecated 'mono' alias, replacing it with 'monospace'
-->
        <match target="pattern">
                <test qual="any" name="family">
                        <string>mono</string>
                </test>
                <edit name="family" mode="assign" binding="same">
                        <string>monospace</string>
                </edit>
        </match>

<!--
  Accept alternate 'sans serif' spelling, replacing it with 'sans-serif'
-->
        <match target="pattern">
                <test qual="any" name="family">
                        <string>sans serif</string>
                </test>
                <edit name="family" mode="assign" binding="same">
                        <string>sans-serif</string>
                </edit>
        </match>

<!--
  Accept deprecated 'sans' alias, replacing it with 'sans-serif'
-->
        <match target="pattern">
                <test qual="any" name="family">
                        <string>sans</string>
                </test>
                <edit name="family" mode="assign" binding="same">
                        <string>sans-serif</string>
                </edit>
        </match>

<!--
  Ignore dpkg temporary files created in fonts directories
-->
        <selectfont>
                <rejectfont>
                        <glob>*.dpkg-tmp</glob>
                </rejectfont>
        </selectfont>
        <selectfont>
                <rejectfont>
                        <glob>*.dpkg-new</glob>
                </rejectfont>
        </selectfont>

<!--
  Load local system customization file
-->
        <include ignore_missing="yes">conf.d</include>

<!-- Font cache directory list -->

        <cachedir>/var/cache/fontconfig</cachedir>
        <cachedir prefix="xdg">fontconfig</cachedir>
        <!-- the following element will be removed in the future -->
        <cachedir>~/.fontconfig</cachedir>

        <config>
<!--
  Rescan configuration every 30 seconds when FcFontSetList is called
 -->
                <rescan>
                        <int>30</int>
                </rescan>
        </config>

</fontconfig>
```
</details>



> If 'prefix' is set to "default" or "cwd", the current working directory will be added as the path prefix prior to the value. If 'prefix' is set to "xdg", the value in the XDG_DATA_HOME environment variable will be added as the path prefix. please see XDG Base Directory Specification for more details. If 'prefix' is set to "relative", the path of current file will be added prior to the value.

とのことなので、環境変数 `XDG_DATA_HOME` で指定したパス以下にある `font` ディレクトリにフォントファイルを配置しておけば読み込まれそうです。

アプリケーション設定 `XDG_DATA_HOME` として `/home/site/wwwroot/` を設定しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4914/90f82813-3166-6bef-7acc-a9cb52c5e225.png)

この設定をしたうえで、プロジェクトのルートディレクトリに `fonts` ディレクトリを作成し、[Noto](https://fonts.google.com/noto/fonts) などから必要なフォントをダウンロードして配置されるようにしておけば、
`/home/site/wwwroot/fonts/NotoSansJP-Regular.otf` など追加のシステムフォントとして読み込まれることになります。

予めリポジトリに含めてもよさそうですが、`postinstall` で動かしてみました。
```bash:install_font.sh
#!/bin/bash
curl -L 'https://fonts.google.com/download?family=Noto%20Sans%20JP' -o font.zip
unzip font.zip -d ./fonts
rm -rf font.zip
```

本当はプラットフォーム側で `/etc/fonts/conf.d` 配下に Azure Functions 用の設定を追加してくれるといい気がしています。
どのフォントを追加するかはユーザーに任せれば、実行環境側でフォントを決める必要はなく、イメージサイズにも影響しないはずです。

当然追加したフォントが不足していれば豆腐は発生します。
日本語フォントだけを追加した状態で、タイ語のサイトを見ると以下のような感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4914/da2a9a50-4d06-419b-f45b-2edaf9253249.png)


## Tips 4. 実行環境の情報を得る方法

Azure Functions Linux Consumption plan では、実行環境に対して Kudu 経由での WebSSH 接続ができないので構成情報の取得や、デバッグ、トラシューがはかどらない。そのため HTTP Trigger 経由で任意のコマンドを実行するインターフェイスを用意すると便利。
詳細については以下の記事で書きました。

https://qiita.com/georgeOsdDev@github/items/092b6510fc96d53df879

# 参考記事


https://uncaughtexception.hatenablog.com/entry/2020/08/30/091852

https://uncaughtexception.hatenablog.com/entry/2022/12/09/191520

https://anthonychu.ca/post/azure-functions-headless-chromium-puppeteer-playwright/

https://dotnetthoughts.net/running-playwright-on-azure-functions/