playwright on container imageをAWS Lambdaで動かす

dockerコンテナ上のアプリをAWS Lambda上で呼び出すには、awslambdaricというのが必要なようです。 dockerイメージは以下のようになります。

# Define custom function directory
ARG FUNCTION_DIR="/function"

FROM --platform=linux/amd64 python:3.10-slim-bullseye
ENV TZ="Asia/Tokyo"

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

# Install aws-lambda-cpp build dependencies
RUN apt-get update -y && \
  apt-get install -y \
  g++ \
  make \
  cmake \
  unzip \
  libcurl4-openssl-dev \
  oathtool

# Copy function code
RUN mkdir -p ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

# Install the function's dependencies
RUN pip install \
    --target ${FUNCTION_DIR} \
        awslambdaric

ENV PLAYWRIGHT_BROWSERS_PATH=0

RUN python -m playwright install --with-deps chromium

ENTRYPOINT [ "python", "-m", "awslambdaric" ]
CMD [ "app.lambda_handler" ]

なぜ、わざわざ、dockerイメージを使用しているかって? それは、oathtoolコマンドを使用してOTPを生成し、多要素認証を突破したかったからです。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import asyncio
from playwright.async_api import async_playwright
import subprocess

URL = ""
MAILADDRESS = ""
PASSWORD = ""
SECRET = ""

async def main():
    async with async_playwright() as p:
        try:
            browser = await p.chromium.launch(
                args=[
                    '--autoplay-policy=user-gesture-required',
                    '--disable-background-networking',
                    '--disable-background-timer-throttling',
                    '--disable-backgrounding-occluded-windows',
                    '--disable-breakpad',
                    '--disable-client-side-phishing-detection',
                    '--disable-component-update',
                    '--disable-default-apps',
                    '--disable-dev-shm-usage',
                    '--disable-domain-reliability',
                    '--disable-extensions',
                    '--disable-features=AudioServiceOutOfProcess',
                    '--disable-hang-monitor',
                    '--disable-ipc-flooding-protection',
                    '--disable-notifications',
                    '--disable-offer-store-unmasked-wallet-cards',
                    '--disable-popup-blocking',
                    '--disable-print-preview',
                    '--disable-prompt-on-repost',
                    '--disable-renderer-backgrounding',
                    '--disable-setuid-sandbox',
                    '--disable-speech-api',
                    '--disable-sync',
                    '--disk-cache-size=33554432',
                    '--hide-scrollbars',
                    '--ignore-gpu-blacklist',
                    '--metrics-recording-only',
                    '--mute-audio',
                    '--no-default-browser-check',
                    '--no-first-run',
                    '--no-pings',
                    '--no-sandbox',
                    '--no-zygote',
                    '--password-store=basic',
                    '--use-gl=swiftshader',
                    '--use-mock-keychain',
                    '--disable-gpu',
                    '--single-process',
                    '--headless=new'
                ],
                slow_mo=1000)

            context = await browser.new_context()
            page = await context.new_page()
            await page.goto(URL)

            async with page.expect_navigation():
                # ログイン
                await page.type("input[name=loginfmt]", MAILADDRESS)
                await page.click("input[type=submit]")

                await page.type("input[name=username]", MAILADDRESS)
                await page.click("button[type=submit]")


                await page.type("input[name=password]", PASSWORD)
                await page.click("button[type=submit]")

                token = await generate_token(SECRET)

                await page.type("input[id=security-code]", token)
                await page.click("button[type=submit]")
                await page.wait_for_timeout(10000)

                # 以下は省略

        finally:
            await browser.close()


async def generate_token(secret: str) -> str:
    cmd = ['oathtool', '--totp', '--base32', secret]
    out = subprocess.run(cmd, stdout=subprocess.PIPE)
    fa_string = out.stdout.decode()
    # 取得する文字列の最後に改行があるため、6文字で切る処理
    string_2fa = fa_string[:6]
    return string_2fa



def lambda_handler(event, context):
    asyncio.run(main())


if __name__ == "__main__":
    pass

特殊な環境だったため、chromiumが、うまく動かなくて非常にはまりました。 chromiumは、複数プロセスを立ち上げるので、それがどうも相性がわるかったようです。 シングルプロセスで起動するオプションをchromium起動時に指定する必要があります。 また、--disable-gpuも付与しないとクラッシュします。