コンテナ環境の Azure Functions 上で Playwright (Python) を動かす

定期的にブラウザー操作を自動化して処理するため、Azure Functions の Timer トリガーで Playwright を動かす方法を紹介します。

当初は Azure Functions の Flex Consumption もしくは Consumption ホスティングを利用して、Python で Playwright を動かす方法を検討しましたが、Playwright をインストールして動かすのがうまくいきませんでした。

今回の用途としては、1 日 1 回の実行で、動作時間も数分程度しかかからないため、待機時間にもコストがかかる Premium プランや App Service プランはオーバースペックでした。また、コンテナーイメージに Playwright をインストールすれば動作可能だろうという理由から、Azure Functions の Container Apps 環境でのホスティングを選択しました。


https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-scale


## Azure リソースの作成

Azure Portal から Function app を作成します。

その際ホスティングオプションで、「Container Apps environment」を選択します。


![function_app_hosting_option.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231348/409f7484-c955-2102-50f9-cafdb325862f.png)


配置するリソースグループとリソースの名称を決めて作成します。

以下の構成が生成されます

![function_app_container_resources.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231348/6bab713e-7456-1324-6f1b-d98e5197ab2d.png)


続いて、この後作成する Azure Functions の Python プロジェクトのコンテナーイメージをデプロイするために必要な Container Registry を作成しておきます。

![container_registry.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231348/92e9a30b-06d9-6b1f-cb43-f8232f0d1f4d.png)


Container Registry が作成されました。

![container_registry_resource.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231348/8227e6c8-702f-131e-9c07-91f65f8067d2.png)


## Azure Functions プロジェクトの作成

Visual Studio Code で Azure Functions プロジェクトを作成します。

事前に Azure CLI と Azure Functions Core Tools をインストールしておきます。

- [Azure CLI のインストール](https://learn.microsoft.com/ja-jp/cli/azure/install-azure-cli)
- [Azure Functions Core Tools のインストール](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-run-local?tabs=windows%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-csharp)


今回は WSL2、Python 3.12.8 の環境で作業を行いました。

新規ディレクトリを作成し、移動します。

```bash
mkdir containerfunc01
cd containerfunc01
```

venv 環境を作成し Activate します。

```bash
python -m venv venv
source venv/bin/activate
```

そのディレクトリで Visual Studio Code を開きます。

```bash
code .
```

Visual Studio Code を開いたら、コマンドパレットで、`Azure Functions: Create New Containerized Project...` を選択します。

指示に従って、言語 `Python`、Programming Model を `Model V2 (Recommended)` で作成します。

最初のトリガーを作成するかも聞かれるため、テストのため HTTP トリガーの Function を `http_trigger` という名前で作成します。

以下のファイルが作成されます。

![function_initial_project.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231348/f8e11989-d2e8-4f3f-c1f6-3e40aeacb118.png)


## 作成直後の状態でローカルで動作確認

作成直後の `function_app.py` と、`Dockerfile`、`requirements.txt` は以下のようになっていました。

```python:function_app.py
import azure.functions as func
import logging

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

@app.route(route="http_trigger")
def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
    else:
        return func.HttpResponse(
             "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
             status_code=200
        )
```

```Dockerfile:Dockerfile
# To enable ssh & remote debugging on app service change the base image to the one below
# FROM mcr.microsoft.com/azure-functions/python:4-python3.12-appservice
FROM mcr.microsoft.com/azure-functions/python:4-python3.12

ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

COPY requirements.txt /
RUN pip install -r /requirements.txt

COPY . /home/site/wwwroot
```


```text:requirements.txt
# DO NOT include azure-functions-worker in this file
# The Python Worker is managed by Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues

azure-functions
```


ローカルでの動作確認のため、`function_app.py` の承認レベルを anonymous に変更します。

```diff_python:function_app.py
- app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
```

Docker イメージをビルドします。

```bash
docker build --tag containerfunc01 .
```

Docker イメージを実行します。

```bash
docker run -p 8080:80 -it containerfunc01
```

ブラウザーで、 `http://localhost:8080/api/http_trigger?name=test` にアクセスし、`Hello, test. This HTTP triggered function executed successfully.` と表示されれば成功です。


https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-create-container-registry?tabs=acr%2Cbash&pivots=programming-language-python


## 動作確認用の Playwright の処理を作成

ローカルで Playwright が動くことを確認するための処理を作成します。

venv 環境に Playwright をインストールします。

```bash
pip install playwright
python -m playwright install --with-deps
```

次のコマンドで現在の環境で `requirements.txt` を更新します。

```bash
pip freeze > requirements.txt
```


Dockerfile に Playwright をインストールするための `python -m playwright install --with-deps` を追加します。

```Dockerfile:Dockerfile
# To enable ssh & remote debugging on app service change the base image to the one below
# FROM mcr.microsoft.com/azure-functions/python:4-python3.12-appservice
FROM mcr.microsoft.com/azure-functions/python:4-python3.12

ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

COPY requirements.txt /
RUN pip install -r /requirements.txt && \
    python -m playwright install --with-deps

COPY . /home/site/wwwroot
```


指定した URL のページの内容を取得しログに出力する関数を作成します。

```python:exec_playwright.py
from playwright.sync_api import sync_playwright
import logging


def exec_playwright(url: str):
    try:
        with sync_playwright() as p:
            browser = p.chromium.launch()
            page = browser.new_page()
            page.goto(url)

            logging.info(f"URL: {page.url}")
            logging.info(f"Title: {page.title()}")
            logging.info(f"Content: {page.content()}")

            browser.close()
    except Exception as e:
        logging.error(f"Error crawling {url}: {e}")
```


その関数を HTTP トリガーの Function から呼び出します。

```python:function_app.py
import azure.functions as func
import logging

from exec_playwright import exec_playwright

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)


@app.route(route="http_trigger")
def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    exec_playwright("https://www.example.com")

    return func.HttpResponse("This HTTP triggered function executed successfully.")
```


## ローカルで Playwright の動作確認

ローカルで Playwright の動作確認を行います。

先ほどと同様に Docker イメージをビルドして実行します。

```bash
docker build --tag containerfunc01 .
```

Docker イメージを実行します。

```bash
docker run -p 8080:80 -it containerfunc01
```

ブラウザーで、 `http://localhost:8080/api/http_trigger?name=test` にアクセスし、ログに取得したページの内容が表示されれば成功です。


## Azure 環境にデプロイ

承認レベルを function に戻します。

```diff_python:function_app.py
- app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+ app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
```

コンテナーイメージをビルドし Azure Container Registry に登録します。

```bash
az acr build --registry containerfunc01cr --image containerfunc01:v0.0.1 .
```

次に、Function App で上記イメージを使用するように構成します。

ただし、その前に次の設定をしないと登録したイメージを指定できませんでした。

Container Registry の管理者権限が有効か確認します。

```bash
az acr show --name containerfunc01cr --query "adminUserEnabled"
```

無効（false）になっている場合は、次のコマンドで管理者権限を有効にします。

```bash
az acr update --name containerfunc01cr --query "adminUserEnabled" --admin-enabled true
```


Function App にて、登録したコンテナーイメージを指定します。

Function App の Settings > Configuration にて、登録したイメージを指定します。

- **Image source**: Azure Container Registry
- **Registry**: containerfunc01cr
- **Image**: containerfunc01
- **Image tag**: v0.0.1

![function_app_container_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231348/3eef54fe-5964-5df0-cafe-087059da4677.png)


Azure Portal 上から HTTP トリガーの Test/Run を実行するため、CORS を有効にし、Allowed Origin に `https://portal.azure.com` を追加します。

![function_app_cors.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231348/aea900cf-c482-5dee-952f-c964c981a3e5.png)


:::note info
Azure Portal 上から HTTP トリガーの Test/Run を利用しない場合は、CORS の設定は不要です。
::::


Function App の Overview にて、作成した Function が表示されていることを確認します。


![function_app_overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231348/25bb1631-4f6c-3d05-634b-4aac7d05c4a0.png)


http_trigger にアクセスし、 Test/Run から実行します。

![function_app_testrun.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231348/ecfcec7b-db35-f588-d4f4-1d3f721d1385.png)


結果に `This HTTP triggered function executed successfully.` が表示され、ログに取得したページの内容が表示されれば成功です。


これで、コンテナ環境の Azure Functions 上で Playwright が動くことが確認できました。

あとは、必要な処理を追加したり、Timer トリガーで定期実行するように設定することで、自動化処理をスケジュール化できます。