コンテナイメージ版のAWS LambdaでSelenium (Chrome) を動かす

えらくハマったのでメモです。以下のようにするとできました。

Dockerfile

FROM public.ecr.aws/lambda/python:3.12

# Seleniumをインストールする。
RUN python3.12 -m pip install selenium -t .
# selenium-managerを使ってChromeとChromeDriverをダウンロードする。
RUN /var/task/selenium/webdriver/common/linux/selenium-manager --browser chrome --cache-path /var/task
# Chromeの依存関係をインストールする。
# 参考: https://qiita.com/hideki/items/d1ff83e7e82afc0c0502
RUN dnf install -y atk cups-libs gtk3 libXcomposite alsa-lib \
        libXcursor libXdamage libXext libXi libXrandr libXScrnSaver \
        libXtst pango at-spi2-atk libXt xorg-x11-server-Xvfb \
        xorg-x11-xauth dbus-glib dbus-glib-devel nss mesa-libgbm \
        libgbm libxkbcommon libdrm

# 以下、デフォルトのコード
COPY app.py requirements.txt ./
RUN python3.12 -m pip install -r requirements.txt -t .

# Command can be overwritten by providing a different command in the template directly.
CMD ["app.lambda_handler"]

Lambda側のコードは以下のような感じです。

app.py

from tempfile import mkdtemp
import glob

from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By

print("chrome起動中")
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless=new')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-dev-tools')
chrome_options.add_argument('--no-zygote')
chrome_options.add_argument('--window-size=1280x1696')
chrome_options.add_argument(f"--user-data-dir={mkdtemp()}")
chrome_options.add_argument(f"--data-path={mkdtemp()}")
chrome_options.add_argument(f"--disk-cache-dir={mkdtemp()}")
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--hide-scrollbars')
chrome_options.add_argument('--enable-logging')
chrome_options.add_argument('--log-level=0')
chrome_options.add_argument('--v=99')
chrome_options.add_argument('--single-process')
# chrome_options.add_argument('user-agent=' + ...)
# "/var/task/chrome/linux64/121.0.6167.85/chrome" のような場所にある実行ファイルを指定する。
chrome_options.binary_location = glob.glob("/var/task/chrome/linux64/*/chrome")[0]
service = ChromeService(glob.glob("/var/task/chromedriver/linux64/*/chromedriver")[0])
driver = webdriver.Chrome(service=service, options=chrome_options)
print("chrome起動完了")


def lambda_handler(event, context):
    driver.get("https://example.com/")
    body = WebDriverWait(driver, 10).until(lambda d: d.find_element(By.TAG_NAME, "body"))
    return body.text

Chromeの起動オプションは以下の記事を参考にしました。
https://qiita.com/hideki/items/d1ff83e7e82afc0c0502

上記の構成のCloudFormation(SAM)のサンプル
https://github.com/778a0a/docker-lambda-selenium-chrome-example

若干の補足

selenium-managerで良い感じのバージョンのChromeとChromeDriverをダウンロードしているところがポイントです。selenium-managerでダウンロードした実行ファイルはデフォルトだと/root/.cache/selenium/以下に保存されるのですが、Lambda環境だとこの場所にあるファイルは実行できないようなので、--cache-pathを指定してpythonファイルと同じ/var/task/以下に保存しています。(/opt以下でも問題ない?)

Lambda側では、ダウンロードした実行ファイルのパスを明示的に指定して実行します。バージョン番号は不定なので、globで良い感じに解決しています。

以上です。