NeeNetです。
今回はAWS環境でLambdaを用いて、Seleniumによるスクレイピングを定期実行してみたいと思います。
以前AWS環境上でSeleniumを動かすのにECS (Fargate)を利用した方法をご紹介しましたが、今回はそのLambda版です。

1回のスクレイピング処理が15分以内に完了するのであればLambdaの方が起動が早くコストも安くてお得なので、以下ではLambdaを利用した方法をご紹介します。
アーキテクチャ
今回作成するアーキテクチャ図は以下の通りです。

概要説明
今回はLambdaの起動にStep Functionsを利用し、Lambdaはコンテナイメージを利用します。
SeleniumやPythonスクリプトなどはローカルでイメージ化を行ってECRにPushし、Lambda実行時にそのイメージを取得するイメージです。
EventBridgeスケジューラを用いて、指定時間に上記で記載したStep Functionsを起動するように設定します。
なお、今回デプロイには AWS SAM (Serverless Application Model) を利用します。
ECRの作成
まずAWSのマネジメントコンソールからAWS ECRのサービスページにアクセスし、プライベートレジストリの作成を行います。
今回は「python-app-ecr-lambda」という名前のレジストリを作成します。

作成が完了したら、『プッシュコマンドを表示』ボタンからPushに必要なコマンドのメモしておきます。

このコマンドは後でBashファイルとして記載します。
ファイルの準備
ディレクトリ構成
今回のディレクトリ構成は以下の通りです。
.
├── Dockerfile
├── ecr_push.sh
├── function
│ └── app.py
├── samconfig.toml
└── template.yaml
以下で、それぞれのファイルの記載内容について説明します。
Dockerfile
Dockerfileは以下の通りです。
FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.11 as build
# seleniumの用意
RUN yum install -y unzip && \
curl -Lo "/tmp/chromedriver-linux64.zip" "https://storage.googleapis.com/chrome-for-testing-public/122.0.6261.111/linux64/chromedriver-linux64.zip" && \
curl -Lo "/tmp/chrome-linux64.zip" "https://storage.googleapis.com/chrome-for-testing-public/122.0.6261.111/linux64/chrome-linux64.zip" && \
unzip /tmp/chromedriver-linux64.zip -d /opt/ && \
unzip /tmp/chrome-linux64.zip -d /opt/ && \
rm -rf /tmp/*.zip && \
yum clean all && \
rm -rf /var/cache/yum
FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.11
# パッケージのインストール
RUN yum 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 ipa-pgothic-fonts.noarch && \
yum clean all && \
rm -rf /var/cache/yum
# pythonモジュールのインストール
RUN pip install --no-cache-dir selenium pandas beautifulsoup4 boto3 line-bot-sdk==3.5.0
COPY --from=build /opt/chrome-linux64 /opt/chrome
COPY --from=build /opt/chromedriver-linux64 /opt/
COPY ./function ./
CMD ["app.handler"]
上記では、ビルドステージと最終ステージを分けて最終的なイメージのサイズを最適化しています。
ベースイメージはどちらのステージも公式のAWS Lambda用Python 3.11イメージです。
ビルドステージではChromeドライバーとヘッドレスブラウザをダウンロードして展開しています。
最終ステージではChrome実行に必要なグラフィック関連のライブラリや、日本語のページを正しく表示できるように日本語フォント(ipa-pgothic)をインストールした後、必要なPythonモジュールをインストールしています。
その後、ビルドステージからChrome関連ファイルのコピーし、アプリケーションコードを ./function
ディレクトリからコンテナにコピーした後、エントリーポイントを app.handler
に設定しています。
ecr_push.sh
ECR作成時にメモしたPushコマンドを、 ecr_push.sh
に記載します。(※以下はmacOS/Linux用のコマンド)
#!/bin/bash
set -o pipefail
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t python-app-ecr-lambda .
docker tag python-app-ecr-lambda:latest <YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda:latest
docker push <YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda:latest
上記で <YOUR ACCOUNT ID>
となっている部分については、ご自身のAWSアカウントIDが記載されているかと思います。
function/app.py
function
ディレクトリの app.py
には、実際にLambdaで動かすアプリケーションのコードを記載します。
今回は動作確認のため簡単な内容にしました。
import shutil
from tempfile import mkdtemp
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
def setup_driver(tmp_dir):
# Chromeオプションの設定
chrome_options = Options()
chrome_options.binary_location = "/opt/chrome/chrome"
chrome_options.add_argument("--headless=new")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument('--single-process')
chrome_options.add_argument("--no-zygote")
chrome_options.add_argument(f"--user-data-dir={mkdtemp(dir=tmp_dir)}")
chrome_options.add_argument(f"--data-path={mkdtemp(dir=tmp_dir)}")
chrome_options.add_argument(f"--disk-cache-dir={mkdtemp(dir=tmp_dir)}")
chrome_options.add_argument("--disable-dev-tools")
chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36')
# WebDriverの設定
service = Service("/opt/chromedriver") # ChromeDriverのパスを指定
driver = webdriver.Chrome(service=service, options=chrome_options)
return driver
def handler(event, context):
# 一時ディレクトリの作成
tmp_dir = mkdtemp()
# ドライバのセットアップ
driver = setup_driver(tmp_dir)
# python.org にアクセス
driver.get("https://www.python.org/")
# titleを取得・表示
title = driver.title
print(title)
# ブラウザを閉じる
driver.quit()
# 後片付け
shutil.rmtree(tmp_dir)
return {"message": "success"}
samconfig.toml
AWS SAMの設定ファイルである samconfig.toml
は以下のように記載します。
version = 0.1
[default]
region = "ap-northeast-1"
[default.build.parameters]
debug = true
[default.deploy.parameters]
stack_name = "<YOUR STACK NAME>"
s3_bucket = "<YOUR S3 BUCKET>"
s3_prefix = "sam-deploy"
capabilities = "CAPABILITY_NAMED_IAM"
confirm_changeset = true
resolve_image_repos = true
スタック名とデプロイ用のバケット名については、ご自身の環境に合わせて任意に設定して下さい。( <YOUR STACK NAME>
と <YOUR S3 BUCKET>
となっている部分)
template.yaml
AWS SAMのテンプレートを記載した template.yaml
は以下の通りです。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: "Python SAM Application"
Parameters:
EcrArn:
Description: Please type the ARN of ECR
Type: String
Default: "<YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda:latest"
Resources:
# Lambda
AppLambda:
Type: AWS::Lambda::Function
Properties:
FunctionName: "python-app-lambda"
Role: !GetAtt AppLambdaRole.Arn
PackageType: Image
Code:
ImageUri: !Sub ${EcrArn}
Architectures:
- x86_64
MemorySize: 1024
Timeout: 180
# IAM Role for Lambda
AppLambdaRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: "python-app-lambda-role"
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Path: "/"
Policies:
- PolicyName: LambdaExecutionPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- "ecr:GetAuthorizationToken"
- "ecr:BatchCheckLayerAvailability"
- "ecr:GetDownloadUrlForLayer"
- "ecr:BatchGetImage"
Resource: '*'
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
- "arn:aws:iam::aws:policy/AmazonS3FullAccess"
# Step Functions
AppSfnForLambda:
Type: "AWS::Serverless::StateMachine"
Properties:
Name: "python-app-invoke-lambda-sf"
Definition:
Comment: State Machine to invoke Lambda Functions
StartAt: InvokeLambda
States:
InvokeLambda:
Type: Task
Resource: !GetAtt AppLambda.Arn
End: true
Role: !GetAtt AppSfnForLambdaRole.Arn
Events:
Schedule:
Type: ScheduleV2
Properties:
ScheduleExpressionTimezone: "Asia/Tokyo"
ScheduleExpression: "cron(0/1 9-18 ? * MON-FRI *)" # 月-金の 9時~18時に1分毎に実行
Input: "{}"
# IAM Role for Step Funtions
AppSfnForLambdaRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: "python-app-invoke-lambda-role"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: states.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaRole
作成しているリソースは冒頭で示したアーキテクチャ図の通りです。
パラメータとなっている EcrArn
には、ご自身のECRのARNを設定してください。
例えばリポジトリ名を本記事と同様に python-app-ecr-lambda
としているのであれば、ARNは <YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda:latest
になっているかと思います。
スケジュールはCRON書式により、 cron(0/1 9-18 ? * MON-FRI *)
という月曜から金曜の9時~18時に1分毎に実行するよう設定しています。
デプロイ
必要なファイルが準備できたので、デプロイを行います。
Dockerイメージのビルド・Push
DockerイメージのビルドとPushを行うために、先に作成したBashファイルを実行します。
$ bash ecr_push.sh
以下のような感じでログが出ていれば成功です。
...(省略)
The push refers to repository [123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda]
b19907c367ef: Pushed
c502836631a4: Pushed
f344de6b3ad6: Pushed
015c4cf5f4b8: Pushed
cfaec26324a3: Pushed
dddb3c2ac8c7: Pushed
cc8cc690bb37: Pushed
e1b8ef616f15: Pushed
01bcef6ece3c: Pushed
0cfe72a3568f: Pushed
814345d22610: Pushed
latest: digest: sha256:d0bf7f8ca97c317bd13487c4e004e9fb768c8affd0a0daa7d8a771227d133584 size: 2634
AWS SAMのデプロイ
以下のコマンドを実行し、AWS SAMのデプロイを行います。
$ sam deploy
デプロイが正常に完了すると、先に設定したスタック名でスタックが作成されていると思うので、AWSのマネジメントコンソールからCloudFormationのサービスページに遷移し、スタックが作成されていることを確認して下さい。

実行確認
設定したEventBridgeスケジューラの通り、Step Functionsが起動すると思います。
Step Functionsの実行を確認すると、以下のように正常に実行が完了していました。

Lambdaのログを確認すると、以下のようにSeleniumが正しく動き、python.orgのタイトルを取得できていることが確認できました。

最後に
今回はAWS環境でコンテナイメージ版のLambdaを用いて、Seleniumによるスクレイピングを定期実行してみました。
参考になりましたら幸いです。
ご依頼について

NeeNetではAWSを利用したインフラ環境の構築、およびその上で動くアプリケーションの開発のご依頼・ご相談をお引き受けしております。
個人・法人問わず、何かご相談事項がございましたら、一度ご連絡いただければと思います。