Playwright PythonをAWS Lambdaで動かす

たまにPlaywrightをLambdaで動かしたくなりますよね。

すでにいろいろな方が挑戦されているようで、簡単に使えるLambdaレイヤーが公開されているようです。

https://developer.mamezou-tech.com/blogs/2024/07/19/lambda-playwright-container-tips/


ただ、そういうことじゃないんです。自分でやることに意味があるんです。なんとか動作しましたので、ご紹介。

## きっかけ

先日投稿したこちらの記事で、LangChainの「AsyncChromiumLoader」を紹介しました。

https://qiita.com/moritalous/items/44c0cff860f7b5697822

これをLambda化したいなぁと思ったのがきっかけです。（そしてBedrock Agentsから呼び出したい）


AsyncChromiumLoaderは内部でPlaywrightが使われているのですが、先ほど解説した「`chromium.launch`」の呼び出し部分のargsを外部から指定する方法がなかったので、素のPlaywrightを使って検証しました。

Lambda化ができたので、あとはBedrock Agentsのアクショングループとして登録してやれば、Web検索ができるエージェントが作成できる！

**はずでした。。。**

Bedrock Agentsのクオータ制限で、「Lambda レスポンスペイロードサイズは25KBまで」というものがあり、この制限に引っかかってしまうサイトが多く、実用には至りませんでした。

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/quotas.html


## Playwrightを動作させる条件を確認

### システム要件

* Python 3.8 or higher.
* Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
macOS 13 Ventura, or macOS 14 Sonoma.
* Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.

https://playwright.dev/python/docs/intro

### インストール手順

Playwrightのインストールは`pip install playwright`だけではなく、ブラウザのダウンロードなどの手順が必要です。

https://playwright.dev/python/docs/intro

https://playwright.dev/python/docs/browsers

```shell:Shell
pip install playwright
playwright install --with-deps chromium
```

:::note
`--with-deps`はシステムの依存ライブラリをあわせて行うオプションです
:::

Lambdaで同様のことを行う必要があるため、zip形式ではなくコンテナイメージ形式で作成します。

## Dockerfileを作成

AWSが提供するベースイメージ（public.ecr.aws/lambda/python:3.12）はOSがAmazon Linux 2023のため、Playwrightのシステム要件に合致しません。そのため、インストール手順を実行してもエラーになってしまいます。

```
BEWARE: your OS is not officially supported by Playwright; installing dependencies for ubuntu20.04-x64 as a fallback.
Installing dependencies...
sh: line 1: apt-get: command not found
Failed to install browsers
Error: Installation process exited with code: 127
```

「[ランタイムインターフェイスクライアントで代替ベースイメージを使用する](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-image.html#python-image-clients)」を参考に、AWS提供ではないベースイメージを使用する方法を採用します。

参考サイトを少しカスタマイズし、以下のようなDockerファイルを作成します。

:::note
ベースイメージはpython:3.12-slimです。
:::

```docker:Dockerfile
# Define custom function directory
ARG FUNCTION_DIR="/function"

# Use a slim version of the base Python image to reduce the final image size
FROM python:3.12-slim

# Include global arg in this stage of the build
ARG FUNCTION_DIR

ENV PLAYWRIGHT_BROWSERS_PATH=/playwright

# Copy function code
RUN mkdir -p ${FUNCTION_DIR}
COPY requirements.txt ${FUNCTION_DIR}

# Set working directory to function root directory
WORKDIR ${FUNCTION_DIR}

# Install the function's dependencies
RUN pip install -r requirements.txt && \
    playwright install --with-deps chromium

COPY app.py ${FUNCTION_DIR}

# Set runtime interface client as default command for the container runtime
ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ]
# Pass the name of the function handler as an argument to the runtime
CMD [ "app.lambda_handler" ]

```

「PLAYWRIGHT_BROWSERS_PATH」を指定しない場合、chromiumが「/root/.cache/ms-playwright」配下にダウンロードされます。Lambdaの実行ユーザーはrootではないため、「PLAYWRIGHT_BROWSERS_PATH」を明示的に指定します。

:::note
PLAYWRIGHT_BROWSERS_PATHは、Lambdaの環境変数にもセットする必要があります。

```yaml:template.yaml
Resources:
  PlaywrightFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      PackageType: Image
      Architectures:
        - x86_64
      Environment:
        Variables:
          PLAYWRIGHT_BROWSERS_PATH: /playwright     # これ！
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./hello_world
      DockerTag: python3.12-v1
```

:::


## その他のコードを作成

requirements.txtを作成します。

```txt:requirements.txt
awslambdaric
playwright
```

awslambdaricは、「[AWS Lambda Python Runtime Interface Client](https://pypi.org/project/awslambdaric/)」です。これをインストールすると、AWS提供ではないベースイメージでも簡単にLambdaで動作させることができます。

最後にPythonコードを作成します。

```python:app.py
from playwright.sync_api import sync_playwright

with sync_playwright() as p:

    browser = p.chromium.launch(
        args=[
            "--single-process",
            "--no-zygote",
            "--no-sandbox",
            "--disable-gpu",
            "--disable-dev-shm-usage",
            "--headless=new",
        ]
    )

    context = browser.new_context(user_agent=user_agent)
    page = context.new_page()

    response = page.goto(url=url)

    content = page.content()
    title = page.title()

    browser.close()
```

argsオプションを指定します。

| オプション | 説明 |
| --- | --- |
| --single-process | Runs the renderer and plugins in the same process as the browser |
| --no-zygote | Disables the use of a zygote process for forking child processes. Instead, child processes will be forked and exec'd directly. Note that --no-sandbox should also be used together with this flag because the sandbox needs the zygote to work. |
| --no-sandbox | Disables the sandbox for all process types that are normally sandboxed. Meant to be used as a browser-level switch for testing purposes only. |
| --disable-gpu | Disables GPU hardware acceleration. If software renderer is not in place, then the GPU process won't launch. | The /dev/shm partition is too small in certain VM environments, causing Chrome to fail or crash (see http://crbug.com/715363). Use this flag to work-around this issue (a temporary directory will always be used to create anonymous shared memory files). |
| --disable-dev-shm-usage | 	The /dev/shm partition is too small in certain VM environments, causing Chrome to fail or crash (see http://crbug.com/715363). Use this flag to work-around this issue (a temporary directory will always be used to create anonymous shared memory files). |
| --headless=new | Run in headless mode, i.e., without a UI or display server dependencies. |

:::note
それぞれの意味はよくわかりませんが、以下を参考に設定したところうまく動作しました。
引用元：https://peter.sh/experiments/chromium-command-line-switches/
:::


これでうまくいくと思います。