2020/12 に AWS Lambda の新機能として Container Image (Docker/OCI) がサポート [1] されました。最大 10 GB のコンテナイメージをデプロイすることができ, AWS Lamdba の可能性がさらに広がりました。ワークロードをサーバレスに移行する流れは加速していきそうです。
今回は, AWS Lambda の Container Image Support を使い Selenium を動かす手順の備忘録です。環境は以下です。
- macOS Catalina
- Python 3.7
- aws-cli/2.0.50
- selenium-3.141.0
- ChromeDriver 87.0.4280.88
- Headless Chromium-87.0.4280.27
Headless Chrome & ChromeDriver
準備として, Headless Chrome (Chromium) と Chrome 用の WebDriver である ChromeDriver を以下から DL し unzip する。
- Headless Chromium-87.0.4280.27: adieuadieu/serverless-chrome/releases
- ChromeDriver 87.0.4280.88: ChromeDriver
Headless Chrome は GUI を持たない軽量版 Chrome である。また, WebDriver は Web ブラウザを操作し主に Web アプリを自動テストするための OSS ツールで, JavaScript 実行をサポートする。
Headless Chrome と ChromeDriver の互換性に注意する必要がある。 Chrome のバージョンは MAJOR.MINOR.BUILD.PATCH の形式で表されており, MAJOR.MINOR.BUILD が一致する Chrome を ChromeDriver がサポートしている。 その他のバージョン選択の指針は Version Selection を確認する。
Amazon ECR
次に, AWS 管理のコンテナのレジストリである Amazon ECR (Amazon Elastic Container Registry) にリポジトリを作成する。 事前に IAM ユーザに必要な権限のポリシーがアタッチされていることを確認する。
$ REGION=ap-northeast-1
$ AWS_ACCOUNT_ID=xxxxxxxxxxxx
$ aws ecr create-repository \
--repository-name sample \
--image-scanning-configuration scanOnPush=true \
--region $REGION \
--profile t2sy
リポジトリの作成は, AWS CLI でなく ECR コンソールからも可能。
Dockerfile & Lamnda handler
基本的な手順は AWS 公式ドキュメントの Deploy Python Lambda functions with container images や Creating Lambda container images を参照する。
AWS から Lambda の実行に必要な言語ランタイムとコンポーネントがプリロードされた AWS base images for Lambda が提供されている。
今回は python:3.7 base image を元に Dockerfile を書く。(OS は Amazon Linux 2 でなく Amazon Linux を用いるため Python 3.7 を選択)
FROM public.ecr.aws/lambda/python:3.7
RUN pip install --upgrade pip && \
pip install -t ./ selenium
COPY app.py ./
COPY bin/chromedriver /var/task/bin/
COPY bin/headless-chromium /var/task/bin/
CMD ["app.handler"]
次に, Python handler (app.py) を書く。 ChromeDriver 経由で Headless Chrome を起動し Event で渡されたクエリを Google 検索し, 検索件数の結果を返すコードである。
import sys
import os
import time
import shutil
from selenium import webdriver
def move_bin(
fname: str, src_dir: str = "/var/task/bin", dest_dir: str = "/tmp/bin"
) -> None:
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
dest_file = os.path.join(dest_dir, fname)
shutil.copy2(os.path.join(src_dir, fname), dest_file)
os.chmod(dest_file, 0o775)
def create_driver(
options: webdriver.chrome.options.Options,
) -> webdriver.chrome.webdriver:
driver = webdriver.Chrome(
executable_path="/tmp/bin/chromedriver", chrome_options=options
)
return driver
def handler(event, context):
query = event["query"]
move_bin("headless-chromium")
move_bin("chromedriver")
options = webdriver.ChromeOptions()
options.binary_location = "/tmp/bin/headless-chromium"
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--single-process")
options.add_argument("--disable-gpu")
options.add_argument("--window-size=1280x1696")
options.add_argument("--disable-application-cache")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-infobars")
options.add_argument("--hide-scrollbars")
options.add_argument("--enable-logging")
options.add_argument("--log-level=0")
options.add_argument("--ignore-certificate-errors")
options.add_argument("--homedir=/tmp")
driver = create_driver(options)
driver.get("https://www.google.co.jp")
time.sleep(5)
search_box = driver.find_element_by_name("q")
search_box.send_keys(query)
search_box.submit()
time.sleep(5)
stats_elem = driver.find_elements_by_css_selector("#result-stats")
search_count = stats_elem[0].text
driver.quit()
return {"statusCode": 200, "body": {"query": query, "result": search_count}}
Headless Chrome の配置先には注意が必要である。Lambda requirements for container imagesに以下の記述がある。
The container image must be able to run on a read-only file system. Your function code can access a writable /tmp directory with 512 MB of storage. If you are using an image that requires a writable directory outside of /tmp, configure it to write to a directory under the /tmp directory.
Lambda 関数から書き込み可能な領域は /tmp (512 MB) のみで, /var/task や /opt への配置を試したところ Headless Chrome の起動に失敗した。このエラーは [2] でも報告されており, [2] を参考に Lambda 関数実行時に Headless Chrome を /tmp/bin に移動する move_bin() を追加した。
この方法は記事執筆時点 (2021-01-10) のため, 今後変更やより良いプラクティスが出てくるかもしれない。
次に, 以下のディレクトリ構成で Dockerfile からコンテナイメージを構築する。
$ tree
.
├── Dockerfile
├── app.py
└── bin
├── chromedriver
└── headless-chromium
$ docker build -t hello-world .
docker login コマンドで Amazon ECR にログインする。認証に失敗した場合は, ~/.aws/credential などを再確認する。
$ aws ecr get-login-password --region $REGION --profile t2sy | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
Login Succeeded
docker tag コマンドでコンテナイメージにタグ付けし, docker push コマンドで Amazon ECR にコンテナイメージをデプロイする。
$ docker tag hello-world:latest $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/hello-world:latest
$ docker push $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/hello-world:latest
AWS Lambda Runtime Interface Emulator (RIE) でローカルテスト
イメージの変更の度に Amazon ECR にデプロイし Lambda 関数をテストするのは大変なため, AWS は AWS Lambda Runtime Interface Emulator (RIE) というローカルで Lambda 関数をテスト/デバッグするためのエミュレータを提供している。
macOS の場合, 以下のコマンドで RIE をインストールできる。
$ mkdir -p ~/.aws-lambda-rie && curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && chmod +x ~/.aws-lambda-rie/aws-lambda-rie
docker run コマンドでコンテナイメージをローカルで起動する。
docker run -p 9000:8080 hello-world:latest
curl コマンドで以下のエンドポイントに HTTP POST リクエストを投げると Lamdba 関数が呼び出される。
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"query": "Python"}'
{"statusCode": 200, "body": {"query": "Python", "result": "\u7d04 395,000,000 \u4ef6 \uff080.43 \u79d2\uff09 "}}
Python の Google 検索結果の件数は約 395,000,000 件であった。
AWS Lmabda コンソールからテスト
ローカルで動作確認できたため, AWS Lmabda コンソールから Lamdba 関数を作成しテストする。
AWS Lmabda コンソールから 「関数」->「関数の作成」を選択, 「コンテナイメージ」のオプションを選択する。 関数名とコンテナイメージURLを入力する。コンテナイメージURLは Amazon ECR コンソールからも確認できる。
テストイベントに以下を設定する。
{
"query": "Scala"
}
AWS Lmabda コンソールからテストを実行する。
Scala の Google 検索結果の件数は約 161,000,000 件であった。
[1] AWS Lambda の新機能 – コンテナイメージのサポート
[2] AWS Lambda Container Running Selenium With Headless Chrome Works Locally But Not In AWS Lambda
[3] コンテナイメージ内でLambda レイヤーと拡張機能を動作させる