最近、 aws づいてます。ここしばらく、下記などをやってきました。
- Python で動的ページのスクレイピングをやりました - プログラマーのメモ書き
- SAM Lambda + SNS + EventBridge Scheduler で定期実行する - プログラマーのメモ書き
- EventBridge Scheduler のデッドレターキューを設定 - プログラマーのメモ書き
察しのいい方はもうお分かりかと思いますが、最終的にやりたいのは、 Lambda で Selenium を動かしてスクレイピングするのを定期的に実行するアプリケーションを SAM で管理したいということです。
で、ここまで試したので、あとは簡単に行けるだろうと思ってやってみると、あれれ?うまくいきません。まずはローカルデバッグするとエラーで Selenium が起動しません。
以前、こちらの記事で Selenium の NoSuchDriverException が出た話をおまけ的に書きましたが、またこいつがでてきてます。
ということで調べてみると、 Lambda で Selenium を動かすにはちょっと工夫がいったようです。ということで、この部分を試したので、メモっておきます。
Layer とコンテナイメージ
Selenium はブラウザを動かすので、 Lambda だとそれがなくて動かないということのようです。で、 Lambda で Selenium を動かす方法を調べると、 Layer を使う方法
- SeleniumをLambdaで実行する(快適な)環境を作る #Python - Qiita
- AWS Lambda PythonでSeleniumを使える環境を構築する | DevelopersIO
- AWS Lambdaでselenium×chromeを動かす時の AWS Lambda Layers のつくり方 #Python - Qiita
とコンテナを使う方法
- [AWS SAM] Selenium4をAWS lambda Python3.12で動かす #Docker - Qiita
- SeleniumをAWS Lambdaでサーバーレスに動かしてみる #AWS - Qiita
- 【AWS】Lambdaを利用したスクレイピング【Container Support】
の2種類が見つかります。どちらの方法でも出きるっぽいです。
Lambda でコンテナを使えるようになったのが 2020年12月とのことで、
AWS Lambda の新機能 – コンテナイメージのサポート | Amazon Web Services ブログ
こちらの方法の方が新しいようです。また、両者のメリットデメリットについて調べてみると、いろいろと出てきます。中でも、コンテナイメージを使ったほうが複雑な依存関係をまとめて管理できる、とかが挙げられてます。
なので、当初は Layer でやってみた・・・のですが、なかなかどうして、うまくいきませんでした。
これ、調べてみると、 Python 3.8 以降で Lambda のランタイムが Amazon Linux から Amazon Linux2 に変更になり、その影響で Selenium および Chrome の動作に影響が出るとのことです。下記の記事の書き始め当たりに、いろいろと書かれています。
- Selenium3をAWS lambda python3.8以上で動かす[AWS SAM] #Docker - Qiita
- AWS Lambdaでseleniumを使う方法 #lambda - Qiita
そういわれて、 Layer を使った方法についての記事を見てみると、 Python 3.7 以前のランタイムのものばかりです。 Layer だと必要なバイナリをまとめておけば動くイメージなのでわかりやすそうだったですがそうも問屋が卸させてくれないようです。
もっとも、頑張れば Python 3.8 以降で Layer を使う方法もなくはないようなのですが、今回はそこに時間をかけるよりも、まずは動かしたいので、コンテナを使う方法でやってみました。
コンテナイメージの作成
さきほどいくつか挙げた記事のうち、新しめだったこちらの
[AWS SAM] Selenium4をAWS lambda Python3.12で動かす #Docker - Qiita
を元に作業をやってみました。
以前にやったこちらの記事で作成したもの(gist に置いてます)をベースにして、 まずは template.yaml を変更します。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > periodic-lambda Sample SAM Template for periodic-lambda Globals: Function: Timeout: 60 MemorySize: 512 Resources: # Lambda 関数の作成 PeriodicLambdaFunction: Type: AWS::Serverless::Function Properties: PackageType: Image Architectures: - x86_64 Events: # EventBridge Scheduler からの定期実行 (略) Metadata: Dockerfile: Dockerfile DockerContext: ./src DockerTag: python3.13-v1
主な変更点は、
- Globals で、タイムアウト時間とメモリサイズを変更
- Lambda の定義で、 CodeUri, Handler, Runtime を削除
- MetaData を追加
した形になります。
Lambda のハンドラーを書いたファイルは、
app.py
import glob from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from util import email_util, page_util, util def debug_environment(): import os import sys print(f"current dir: {os.getcwd()}") print(f"target file: {__file__}") print(f"sys.path: {sys.path}") print(f"LAMBDA_TASK_ROOT= {os.environ['LAMBDA_TASK_ROOT']}") print(f"PATH= {os.environ['PATH']}") print(f"PYTHONPATH= {os.environ['PYTHONPATH']}") def create_driver(): options = webdriver.ChromeOptions() options.add_argument("--headless=new") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--incognito") options.add_argument("--blink-settings=imagesEnabled=false") options.add_argument("--enable-logging") options.add_argument("--log-level=0") options.add_argument("--single-process") options.binary_location = glob.glob("/opt/chrome/linux64/*/chrome")[0] service = Service(glob.glob("/opt/chromedriver/linux64/*/chromedriver")[0]) driver = webdriver.Chrome(service=service, options=options) return driver def lambda_handler(event, context): # for debug debug_environment() # webdriverの作成 driver = create_driver() try: # スクレイピング開始 (略)
のような形にしました。
なお、 ChromeDriver のオプションは下記などを参考にしました。
- chrome-launcher/docs/chrome-flags-for-tools.md at main · GoogleChrome/chrome-launcher · GitHub
- 【Python】SeleniumのChromeOptionsのオプション設定はこれだけで大丈夫
requirements.txt には必要なものを書いておきます。
boto3 urllib3 selenium
Dockerfile は
FROM public.ecr.aws/lambda/python:3.13 COPY app.py requirements.txt ${LAMBDA_TASK_ROOT}/ COPY util ${LAMBDA_TASK_ROOT}/util RUN python3.13 -m pip install -r requirements.txt -t ${LAMBDA_TASK_ROOT} RUN ${LAMBDA_TASK_ROOT}/selenium/webdriver/common/linux/selenium-manager --browser chrome --cache-path /opt # Chrome の依存関係をインストール 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 # Command can be overwritten by providing a different command in the template directly. CMD ["app.lambda_handler"]
としました。なお、参考にした記事はコピー先のパスがカレントディレクトリ
./
となっていたのですが、これが実際にはどこを指しているのかわかりませんでした。Lambda のドキュメントを調べると、
コンテナイメージで Python Lambda 関数をデプロイする - AWS Lambda
${LAMBDA_TASK_ROOT}
という指定のされ方だったので、これに切り替えました。なお、これを実行時に python で出力させると
/var/task
でした。
あと、 util 以下に自分で作成した関数群を置いていたので、それもコピーするようにしています。
なお、当初は dockerfile の書き方でこの util のコピーの指定方法につまづいていて、なかなか自分で作った関数が読み込めずにはまりました。
ビルド
さて、準備ができたらビルドです。ここが、残念だったのですが、 VSCode からイメージタイプの Lambda のデバッグ実行(およびビルド)を行うことができませんでした。たぶん .vscode/launch.json の内容がまずかったんでしょうね。この構成次第だと思うので、また試してみようと思います。
(2025/2/7 追記)VSCode でもデバッグ実行できました。詳しくは下記をご覧ください。
SAM コンテナイメージを使用した Lambda のデバッグ - プログラマーのメモ書き
ということで、今回は仕方ないので、コマンドラインからビルドします。
(.venv)mor@DESKTOP-DE7IL4F:~/tmp/sam_samples/periodic-lambda$ sam build --use-container Starting Build inside a container (略) Build Succeeded Built Artifacts : .aws-sam/build Built Template : .aws-sam/build/template.yaml Commands you can use next ========================= [*] Validate SAM template: sam validate [*] Invoke Function: sam local invoke [*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch [*] Deploy: sam deploy --guided (.venv)mor@DESKTOP-DE7IL4F:~/tmp/sam_samples/periodic-lambda$
こんな感じにビルドされます。中で、 docker を呼び出しているっぽいですね。
ローカルテスト
ローカルテストもコマンドラインで実行します。
(.venv)mor@DESKTOP-DE7IL4F:~/tmp/sam_samples/periodic-lambda$ sam local invoke No current session found, using default AWS::AccountId Invoking Container created from periodic-lambda:python3.13-v1 Building image................. Using local image: periodic-lambda:rapid-x86_64. START RequestId: 49fdc119-ed45-4c2a-b18b-5dcffb70cf6d Version: $LATEST (略)
こんな感じに実行されていきます。最終的には、 SNS トピックへの発行を呼び出したところでエラーとなって終わりますが、それまでが正しく動作していることが確認できました。
なお、ローカルテストでエラーが見つかり、修正してビルド、を何度か繰り返していると、実行時に最新のビルドが反映されていないことがありました。
どうも SAM では実行時には、ビルドしたイメージから実行用のイメージ(rapid-x86_64 というタグがつていました)を作成してそれを実行しているようなのですが、その rapid のイメージが更新されていない場合に起きているようです。
aws のドキュメントは見つからなかったのですが、下記にそれっぽい動作のことを書いています。
もし、こうなった場合は、
(.venv)mor@DESKTOP-DE7IL4F:~/tmp/sam_samples/periodic-lambda$ sam local invoke --force-image-build
と --force-image-build オプションをつけると、実行用のイメージを再作成してくれます。
(おまけ)chromedriver のオプションについて
最初 chromedrive のオプションとして --no-sandbox をつけていなかったら、
[ERROR] SessionNotCreatedException: Message: session not created: probably user data directory is already in use, please specify a unique value for --user-data-dir argument, or don't use --user-data-dir
とエラーが表示されてしまいました。
当初は、このエラーについて調べてみると、
SeleniumでChromeのユーザープロファイルを指定しつつ同時に自分もChromeを使う方法 #Chrome - Qiita
という記事がでてきていたので、元の参考記事のように --user-data-dir をつけたりもしたのですが、思うように動作しませんでした。
で、 ChatGPT に聞いてみると、 --no-sandbox をつけると動くかもとのことだったので、試すと問題なく動作しました。ご参考までに。
デプロイ
ここまでできたら、デプロイします。前回の記事で作った template.yaml をベースにしているので、コンテナ部分で問題が無かったため、すんなりビルドできました。
ただ、前回の記事と異なるところは、
こんな感じに CloudFormation スタックが2つできていました。
無事にデプロイできたら次は、 Lambda 関数を開いて、テストを行って動作確認だけしておきます。ちゃんと、スクレイピング後、 SNS トピックからメールも届いてました。
ただし、コンテナを利用した Lambda 関数の場合、 ECR にリポジトリを自動で作成し、そこにイメージをアップロードしていました。
なので、デプロイを繰り返すとこんな感じにイメージが溜まっていき、当然料金もかかるので、その点ご注意ください。
まとめ
Lambda をコンテナイメージから作成する部分が、不慣れなこともあり手間取りましたが、様子がわかってくればなかなかよさそうです。なんといっても、 SAM を使うおかげで、ローカルでテストができるのは魅力的ですね。
とはいえ、コンテナから Lambda 関数を作った場合、 aws のコンソール上でコードを追いかけられないんですよね。当たり前といえば当たり前ですが、その場で確認できないのって、ちょっと不便かも?と思っています。ま、慣れかな?
(おまけ) Layer でやる場合
ECR の料金も気になるのでコンテナは嫌だ、やっぱり Layer でやりたい、という時になったための備忘録です。
簡単なのは、 Python 3.8 以降の Amazon Linux2 で動作する Layer を作っているリポジトリがあるみたいなので、これを自分の Lambda の Layer として組み込むのが早そうです。
- GitHub - marekfejda/aws-lambda-selenium-layer: Precompiled layer of selenium for headless enviroments in aws lambda. Supports python 3.8 / 3.11.
- GitHub - diegoparrilla/headless-chrome-aws-lambda-layer: Headless Chrome prepared to run as a layer in AWS lambda with Python >=3.8 and Amazon Linux 2
試していないですが、ご参考までに。