これは YAMAP エンジニア Advent Calendar 2022 5日目の記事です。
はじめに
以前に Heroku で Selenium を動かすということをやっていたのですが、諸般の事情により Heroku から AWS Lambda への移行が必要となりました。簡単にできるだろうと高をくくっていたのですが、いくつかハマりポイントがあったのでここに記しておきます。
やりたいこと
とある Web サイトを表示して、スクリーンショットを AWS S3 に保存します。
結論
最終的には以下の構成となりました。
環境/バージョン情報
- serverless
- Framework Core: 3.24.1
- Plugin: 6.2.2
- SDK: 4.3.2
- ChromeDriver 2.40
- Headless Chromium v1.0.0-45
- AWS Lambda
- Amazon Linux
- Python 3.7
ディレクトリ構成
lambda: Selenium を実行する Lambda 関数
selenium-layer: Selenium 関連と font ファイルをアップロードする Lambda Layer
$ tree -L 2
.
├── lambda
│ ├── handler.py
│ └── serverless.yml
├── node_modules
└── selenium-layer
├── driver
├── fonts
├── selenium
└── serverless.yml
Selenium と font を Lambda Layer にデプロイする
- serverless framework をインストール
$ npm install -g serverless
- headless-chrome をインストール
$ CHROMEVERSION="v1.0.0-45" \
&& CHROMEFILE=https://github.com/adieuadieu/serverless-chrome/releases/download/${CHROMEVERSION}/stable-headless-chromium-amazonlinux-2017-03.zip \
&& curl -SL $CHROMEFILE > headless-chromium.zip \
&& unzip headless-chromium.zip \
&& rm headless-chromium.zip \
&& mv headless-chromium selenium-layer/driver/
- chromedriver をインストール
$ DRIVERVERSION="2.40" \
&& CHROMEDRIVER=https://chromedriver.storage.googleapis.com/${DRIVERVERSION}/chromedriver_linux64.zip \
&& curl -SL $CHROMEDRIVER > chromedriver.zip \
&& unzip chromedriver.zip \
&& rm -rf chromedriver.zip \
&& mv chromedriver selenium-layer/driver/
- Selenium をインストール
$ pip install -t selenium/python/lib/python3.7/site-packages 'selenium<4'
- selenium-layer/fonts 配下にフォントファイルを配置
フォントは IPAex フォント を利用しました。
.
└── selenium-layer
└── fonts
└── .fonts
├── ipaexg.ttf
└── ipaexm.ttf
- selenium-layer/serverless.ymlを編集
service: selenium-layer
custom:
pythonVer: python3.7
stage: dev
region: ap-northeast-1
provider:
name: aws
runtime: ${self:custom.pythonVer}
stage: ${self:custom.stage}
region: ${self:custom.region}
layers:
selenium:
path: selenium
description: selenium layer
compatibleRuntimes:
- ${self:custom.pythonVer}
chromedriver:
path: driver
description: chrome driver layer
compatibleRuntimes:
- ${self:custom.pythonVer}
fonts:
path: fonts
description: fonts for selenium
resources:
Outputs:
SeleniumLayerExport:
Value:
Ref: SeleniumLambdaLayer
Export:
Name: SeleniumLambdaLayer
ChromedriverLayerExport:
Value:
Ref: ChromedriverLambdaLayer
Export:
Name: ChromedriverLambdaLayer
FontsLayerExport:
Value:
Ref: FontsLambdaLayer
Export:
Name: FontsLambdaLayer
- デプロイする
$ sls deploy
Lambda 関数をデプロイする
- lambda/serverless.ymlを編集
service: example
custom:
pythonVer: python3.7
timeout: 30
memorySize: 1024
stage: dev
region: ap-northeast-1
seleniumLayer: selenium-layer
frameworkVersion: '3'
useDotenv: true
provider:
name: aws
runtime: ${self:custom.pythonVer}
timeout: ${self:custom.timeout}
memorySize: ${self:custom.memorySize}
stage: ${self:custom.stage}
region: ${self:custom.region}
iamRoleStatements:
- Effect: "Allow"
Action:
- "s3:ListBucket"
- "s3:GetObject"
- "s3:PutObject"
- "s3:DeleteObject"
Resource:
Fn::Join:
- ""
- - "arn:aws:s3:::"
- ${env:AWS_S3_BUCKET_NAME}
- "/*"
environment:
AWS_S3_BUCKET_NAME: ${env:AWS_S3_BUCKET_NAME}
REGION: ${self:custom.region}
package:
include:
- ".fonts/**"
functions:
hello:
handler: handler.hello
reservedConcurrency: 1
layers:
- ${cf:${self:custom.seleniumLayer}-${self:custom.stage}.SeleniumLayerExport}
- ${cf:${self:custom.seleniumLayer}-${self:custom.stage}.ChromedriverLayerExport}
- ${cf:${self:custom.seleniumLayer}-${self:custom.stage}.FontsLayerExport}
resources:
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${env:AWS_S3_BUCKET_NAME}
- .env を編集する
AWS_S3_BUCKET_NAME=xxxxxx
- handler.py を編集する
import traceback
import requests
import json
import time
import datetime
import boto3
import os
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# .fonts を参照するため
os.environ["HOME"] = "/opt/"
def create_driver():
options = Options()
options.binary_location = "/opt/headless-chromium"
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--window-size=1280,1024")
options.add_argument("--single-process")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--homedir=/tmp")
try:
driver = webdriver.Chrome(executable_path="/opt/chromedriver", chrome_options=options)
except Exception as e:
print(traceback.format_exc())
return driver
def upload_s3(file_path):
s3 = boto3.client("s3")
bucket_name = os.environ["AWS_S3_BUCKET_NAME"]
s3.upload_file(file_path, bucket_name, file_path)
def hello(event, context):
try:
driver = create_driver()
wait = WebDriverWait(driver, 10)
driver.get("https://example.com")
page_width = driver.execute_script("return document.body.scrollWidth")
page_height = driver.execute_script("return document.body.scrollHeight")
driver.set_window_size(page_width, page_height)
file_path = "/tmp/example.png"
driver.save_screenshot(file_path)
upload_s3(file_path)
os.remove(saved_file_path)
except Exception as e:
print(traceback.format_exc())
return error_response()
- デプロイする
$ sls deploy
つまずいたこと
Selenium 起動時に Timeout エラーになる
エラーログ上は Timeout しか出力されないので調査が難航しました。
こちらは以下のバージョンを使用することで解決しました。
(もっと新しいバージョンの組み合わせでも動作するかもしれませんが、時間が無く未調査です。。)
- Python のバージョンを 3.8 -> 3.7 に変更
- Selenium のバージョンを 4系 -> 3系に変更
- headless-chrome のバージョンを v1.0.0-45 に変更
- chromedriver のバージョンを 2.40 に変更
日本語が文字化けする
Heroku で実行したときも同じ問題が発生しました。 .fonts ディレクトリにフォントファイルを配置したら良いのですが、Lambda Layer を利用する際は参照先の設定に注意が必要です。上記のパス指定で解決しました。