Azure Functions + Chromium をフレックス従量課金プランで使おうとしてハマった話

## 結論だけ知りたい！って方向け
Azure Functions をフレックス従量課金プランで作成すると、依存ライブラリが足りずに Chromium を使うことができません。
つまり、Chromium に依存した **Puppeteer や Playwright, Selenium などが使えません**。
これにより、スクリーンショット機能を用いた PNG への変換などができないなどの問題があります。

Azure Functions を使って Chromium を動かしたい場合は、**従量課金プラン (not flex)** を使うようにしましょう。

## はじめに
この記事は、SVG 画像を PNG 化する際に Azure Functions のフレックス従量課金プランを使おうとして2週間ハマった筆者が、今後同じようなハマり方をする人が減るよう願いを込めたものです。

本記事は 2025/05/22 現在の情報をもとにしたものであり、もしかしたら将来的に仕様が変わってフレックス従量課金プランでも Chromium が利用可能になるかもしれません。

## 前提条件
- Azure Functions Flex Consumption Plan (Linux)
    - Runtime version: 4.1038.400.1
- Python 3.12
- Selenium 4.32.0

## Azure Functions とは
サーバーレスで関数を実行することができ、API 化して外部公開することもできます。
データベースやストレージなどとの連携も可能なので、データを溜めたり取り出したりすることもできます。
似たサービスには AWS Lambda などがあります。

https://azure.microsoft.com/ja-jp/products/functions

## Headless Chromium
GUI が存在せず、プログラム上でも起動・実行できるのが特徴。
Chrome を使う必要があるが、制約の多い状況では厳しい…といったときに有効です。

より詳しくは公式リファレンスから↓

https://developer.chrome.com/docs/chromium/headless?hl=ja

## フレックス従量課金制とは
2024年に新しく追加されたプラン。
従来の従量課金制に比べ、起動時間の高速化（コールドスタートの軽減）やスケーラビリティの向上が実現されています。

より詳しくは、すでによく説明されている Qiita 記事があるためそちらを参照。

https://qiita.com/mym/items/4cdf20f7af1a995ff3e3

https://learn.microsoft.com/ja-jp/azure/azure-functions/flex-consumption-plan

## 従量課金制と Chromium
2020年時点で、従量課金制プランにて Headless Chromium が利用可能になったというお知らせが公式よりありました。

参考記事：

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

## やりたいこと
Azure Functions と Selenium を使って、ECharts を PNG 画像に変換して表示したい。

## 試すコード
今回は以下の Python コードを使います。
このコードによって、ECharts を PNG 画像にしたうえで表示することができます。

```py
import logging
import os

import azure.functions as func
from pyecharts import options as opts
from pyecharts.charts import Bar
from selenium.webdriver.chrome.options import Options
from selenium import webdriver
import time
import urllib.parse

app = func.FunctionApp()

@app.route(route="create_image", auth_level=func.AuthLevel.ANONYMOUS)
def create_image(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("Python HTTP trigger function processed a request.")

    try:
        bar = (
            Bar()
            .add_xaxis(["A", "B", "C"])
            .add_yaxis("例", [10, 20, 30])
            .set_global_opts(title_opts=opts.TitleOpts(title="Bar Example"))
        )

        html_template = f"""
        <html>
        <head>
        <meta charset="utf-8">
        <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
        </head>
        <body>
        <div id="main" style="width:600px;height:400px;"></div>
        <script>
            var chart = echarts.init(document.getElementById('main'));
            var option = {bar.dump_options_with_quotes()};
            chart.setOption(option);
        </script>
        </body>
        </html>
        """

        chrome_options = Options()
        chrome_options.add_argument("--headless=new")
        chrome_options.add_argument("--disable-gpu")
        chrome_options.add_argument("--window-size=960,960")

        driver = webdriver.Chrome(options=chrome_options)
        encoded_html = urllib.parse.quote(html_template)
        driver.get("data:text/html;charset=utf-8," + encoded_html)
        time.sleep(1)
        png = driver.get_screenshot_as_png()

        driver.quit()

        try:
            # PNG画像をレスポンスとして直接返す
            return func.HttpResponse(
                body=png,
                mimetype="image/png",
                status_code=200
            )
        except Exception as e:
            logging.error(f"Failed to return image: {e}")

            return func.HttpResponse(
                body=f"Failed to render bar_chart.png: {e}",
                status_code=500,
            )
    except Exception as e:
        logging.error(f"Error creating bar chart: {e}")

        return func.HttpResponse(
            body=f"Failed to render bar_chart.png: {e}",
            status_code=500,
        )

```

## フレックス従量課金制で Selenium を試した結果
エラーメッセージが表示されます。

|　　　　　　　　  　　　　　　　　   　　　　　　　　　　　　　　　　　　　　　|
|:-:|
|![error_message.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833431/7a69ef58-fac5-4e82-b901-cb5aff0713dd.png)|
|ChromeDriver が落ちてる…？|


より詳しくは、以下の通りになります。
```text
Failed to render bar_chart.png: Message: Service /home/.cache/selenium/chromedriver/linux64/136.0.7103.113/chromedriver unexpectedly exited. Status code was: 127
```

ChromeDriver が何らかの理由により落ちているようです。
Selenium 以外にも Puppeteer などで試しましたが、いずれも Chromium の起動に失敗します。

Puppeteer で試したところ以下のエラーが発生したため、依存ライブラリが入っていない可能性が高いです。

```text
/tmp/chromium: error while loading shared libraries: libnss3.so: cannot open shared object file: No such file or directory
```

## 通常の従量課金制で Selenium を試した結果
何事もなく成功しました。

|　　　　　　　　  　　　　　　　　   　　　　　　　　　　　　　　　　　　　　　|
|:-:|
|![success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833431/91ba66f2-2c23-4b69-aefd-67712fb54ba8.png)|
|棒グラフが表示されている。|

## フレックス従量課金制でできること、できないこと
Chromium を使うことが主目的になる場合は諦めて通常の従量課金制を使うしかなさそうですが、たとえば単に "グラフの PNG を出力したい" という目的を達成するためなら、他の手法も考えられます。
例えば、Python で使える `matplotlib` はフレックス従量課金制でも使うことができます。

画像の生成のために JavaScript で Canvas を使おうとした際には、関数の起動にそもそも失敗してしまったため難しそうです。
一貫して、画像の表示を伴うライブラリは使えない可能性が高く、直接画像を生成する方法しかとれない可能性があります。
（OpenCV や libvips などのライブラリは未確認です。）

また、あとから apt-get などで依存ライブラリをインストールすることは難しそうです。
もしやりたい場合は、Docker コンテナを使う必要がありそうです。

## おわりに
こんなん知らんやん…

フレックス従量課金は従来の従量課金プランに比べ、拡張性に優れる利点があります。
ただし、従来の従量課金プランで入っていたライブラリが存在しないことがあります。
今回の例なら、Headless Chromium を起動するために必要なライブラリが入っておらず一部の機能を使うことができませんでした。
そのため、従来の従量課金制の完全上位互換ではないようです。

欲しい機能がないこともあるため、使い方によってプランは正しく選択しましょう。