AWS LambdaでSelenium
webスクレイピングで便利なseleniumですが、サーバーレスと組み合わせると、APIやFTPが無くてもデータ連携させたりできるので、重宝しています。
ただLambdaでseleniumを動かすにはheadlessブラウザのchromiumと対応するchromedriverが必要で、バージョンによってはうまく(ほとんど)動かなかったりします。
とくにpython3.8以上ではLambdaのOSがAmazon Linux2で稼働するようになったため、seleniumがモジュール不足で動かなくなってしまい、従来のようにコード一式zipであげてといった方法が使えなくなりました。
私も業務で運用フローに組み込んでしまっているので、死活問題。python3.8だっていつまで使えるのか。(EOLを見よう)
python3.8廃止は2024年10月14日、うん、すぐだね。
ということで、今回は私の「AWS SAMを使って比較的ラクにpython3.8以上のLambdaでseleniumをつかう方法」を紹介するとともに、この先pythonのアプデでどこまで使えるのかについて書いてみたいと思います。
必要なツール
このあたりのツールを使います。
- AWS SAM cli
- Docker
Githubレポジトリ
レポジトリにソースコードは上げておきました。先に見たい方はどうぞ。
各種バージョン
selenium 3.141.0
headless-chromium v1.0.0-37
chromedriver_linux64 2.37
構築
AWS SAMを使っていきます。
sam init --name python-selenium
You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.
Which template source would you like to use?
1 - AWS Quick Start Templates
2 - Custom Template Location
Choice: 1
Choose an AWS Quick Start application template
1 - Hello World Example
2 - Data processing
3 - Hello World Example with Powertools for AWS Lambda
4 - Multi-step workflow
5 - Scheduled task
6 - Standalone function
7 - Serverless API
8 - Infrastructure event management
9 - Lambda Response Streaming
10 - Serverless Connector Hello World Example
11 - Multi-step workflow with Connectors
12 - GraphQLApi Hello World Example
13 - Full Stack
14 - Lambda EFS example
15 - Hello World Example With Powertools for AWS Lambda
16 - DynamoDB Example
17 - Machine Learning
Template: 1
Use the most popular runtime and package type? (Python and zip) [y/N]: n
Which runtime would you like to use?
1 - aot.dotnet7 (provided.al2)
2 - dotnet6
3 - go1.x
4 - go (provided.al2)
5 - graalvm.java11 (provided.al2)
6 - graalvm.java17 (provided.al2)
7 - java17
8 - java11
9 - java8.al2
10 - java8
11 - nodejs18.x
12 - nodejs16.x
13 - nodejs14.x
14 - python3.9
15 - python3.8
16 - python3.7
17 - python3.11
18 - python3.10
19 - ruby3.2
20 - ruby2.7
21 - rust (provided.al2)
Runtime: 15
What package type would you like to use?
1 - Zip
2 - Image
Package type: 2
Based on your selections, the only dependency manager available is pip.
We will proceed copying the template using pip.
Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]: n
Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: n
-----------------------
Generating application:
-----------------------
Name: python-selenium
Base Image: amazon/python3.8-base
Architectures: x86_64
Dependency Manager: pip
Output Directory: .
Configuration file: python-selenium/samconfig.toml
Next steps can be found in the README file at python-selenium/README.md
Commands you can use next
=========================
[*] Create pipeline: cd python-selenium && sam pipeline init --bootstrap
[*] Validate SAM template: cd python-selenium && sam validate
[*] Test Function in the Cloud: cd python-selenium && sam sync --stack-name {stack-name} --watch
クイックスタートテンプレートのHello Worldを選んで、pythonは3.8を、パッケージタイプは2のimageを選びました。
❯ tree
.
├── README.md
├── __init__.py
├── events
│ └── event.json
├── hello_world
│ ├── Dockerfile
│ ├── __init__.py
│ ├── app.py
│ └── requirements.txt
├── samconfig.toml
├── template.yaml
└── tests
├── __init__.py
└── unit
├── __init__.py
└── test_handler.py
このような構造になりました。
中身を作ります。
hello_worldフォルダのDockerfileから。
FROM public.ecr.aws/lambda/python:3.8
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 nss mesa-libgbm
# headless-chromium v1.0.0-37 & chromedriver_linux64 2.37
RUN yum install -y unzip && \
curl -Lo "/tmp/chromedriver.zip" "https://chromedriver.storage.googleapis.com/2.37/chromedriver_linux64.zip" && \
curl -Lo "/tmp/headless-chromium.zip" "https://github.com/adieuadieu/serverless-chrome/releases/download/v1.0.0-37/stable-headless-chromium-amazonlinux-2017-03.zip" && \
unzip /tmp/chromedriver.zip -d /opt/ && \
unzip /tmp/headless-chromium.zip -d /opt/
RUN rm -rf /tmp/chromedriver.zip && rm -rf /tmp/headless-chromium.zip
COPY requirements.txt ./
RUN python3.8 -m pip install -r requirements.txt -t .
COPY app.py ./
# Command can be overwritten by providing a different command in the template directly.
CMD ["app.lambda_handler"]
headless-chromium v1.0.0-37 と chromedriver_linux64 2.37をダウンロードして/opt/に展開しています。seleniumを動かすための依存関連もインストール。
次は/hello_world/requirements.txt
requests
boto3>=1.34.11
selenium==3.141.0
seleniumは3.141.0を利用します。
お次は実行ファイルのapp.py
import json
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
class ChromeInstance:
def __init__(self):
self.options = webdriver.ChromeOptions()
self.options.binary_location = '/opt/headless-chromium'
self.options.add_argument("--headless")
self.options.add_argument("--no-sandbox")
self.options.add_argument("--single-process")
self.options.add_argument("--disable-gpu")
self.options.add_argument("--window-size=1280x1696")
self.options.add_argument("--disable-application-cache")
self.options.add_argument("--disable-infobars")
self.options.add_argument("--hide-scrollbars")
self.options.add_argument("--enable-logging")
self.options.add_argument("--log-level=0")
self.options.add_argument("--ignore-certificate-errors")
self.options.add_argument("--homedir=/tmp")
self.driver = webdriver.Chrome(
executable_path="/opt/chromedriver",
options = self.options
)
def lambda_handler(event, context):
# call chrome_headless instance.
chrome = ChromeInstance()
try:
if chrome.driver:
# Googleにアクセスして検索
chrome.driver.get("https://www.google.com")
search_box = chrome.driver.find_element(By.NAME, "q")
search_box.send_keys('Selenium')
search_box.submit()
title = chrome.driver.title
print(title)
return {
"statusCode": 200,
"body": json.dumps({
"message": "title: " + title
}),
}
except Exception as e:
print(f"処理中にエラーが発生しました: {e}")
finally:
if chrome.driver:
chrome.driver.quit()
Googleにアクセスして、検索結果のタイトルを返すだけの簡単なスクレイピングです。
最後にtemplate.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
python3.8
Sample SAM Template for python-selenium
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 600
MemorySize: 1024
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
PackageType: Image
Architectures:
- x86_64
Events:
HelloWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /hello
Method: get
Metadata:
Dockerfile: Dockerfile
DockerContext: ./hello_world
DockerTag: python3.8-v1
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
HelloWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn
ほとんどそのままですが、TimeoutとMemorySizeのみ変更しています。
Globals:
Function:
Timeout: 600
MemorySize: 1024
イメージをビルドして実行
ビルドします。
sam build --use-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
しばらく待つと、ビルドが完了したので、一旦ローカルで実行してみます。
❯ sam local invoke
Invoking Container created from helloworldfunction:python3.8-v1
Building image..................
Using local image: helloworldfunction:rapid-x86_64.
START RequestId: 974080aa-9de4-4b1f-9759-1b9470e2693f Version: $LATEST
Selenium - Google 検索
END RequestId: 974080aa-9de4-4b1f-9759-1b9470e2693f
REPORT RequestId: 974080aa-9de4-4b1f-9759-1b9470e2693f Init Duration: 1.43 ms Duration: 13534.78 ms Billed Duration: 13535 ms Memory Size: 1024 MB Max Memory Used: 1024 MB
{"statusCode": 200, "body": "{\"message\": \"title: Selenium - Google \\u691c\\u7d22\"}"}
ちょっとわかりにくいのですが、「Selenium - Google 検索」と見えるので、googleで検索できたようです。
最後にAWSへデプロイします。
sam deploy --guided
<<省略>>
Successfully created/updated stack - python-selenium in ap-northeast-1
デプロイで来たので、コンソールでテストしてみます。
出来ましたね。でもpython3.8ももうすぐEOLになるんですよね。
というわけで、Lambdaのpythonのバージョンを上げてビルドしていきます。
FROM public.ecr.aws/lambda/python:3.9
3.9にします。
START RequestId: cef9bbb4-33b9-4967-8835-312893012fa6 Version: $LATEST
Selenium - Google 検索
END RequestId: cef9bbb4-33b9-4967-8835-312893012fa6
REPORT RequestId: cef9bbb4-33b9-4967-8835-312893012fa6 Init Duration: 1.30 ms Duration: 11618.55 ms Billed Duration: 11619 ms Memory Size: 1024 MB Max Memory Used: 1024 MB
{"statusCode": 200, "body": "{\"message\": \"title: Selenium - Google \\u691c\\u7d22\"}"}
すんなりビルドして実行できました
よかったこれで、3.9まで生き残れます!
FROM public.ecr.aws/lambda/python:3.10
3.10にしたところ、ビルドは出来たものの、実行エラーが発生
{"errorMessage": "Timeout value connect was <object object at 0x4008ba0610>, but it must be an int, float or None."
このエラーをすでに解説してくださっている方がいました。
seleniumのバージョンとurllib3のバージョンで問題が発生したようです。
これ、AWS SAMでデフォルトでrequirements.txtにあるrequestsでもurllib3のバージョンが問題でエラーが発生してるんですよね。
なので、こちらを追加します。
urllib3<2
もういちどビルドして実行してみます。
START RequestId: 49c5726f-1b65-45c8-ba50-658c2f16aa29 Version: $LATEST
Selenium - Google 検索
END RequestId: 49c5726f-1b65-45c8-ba50-658c2f16aa29
REPORT RequestId: 49c5726f-1b65-45c8-ba50-658c2f16aa29 Init Duration: 1.91 ms Duration: 10625.49 ms Billed Duration: 10626 ms Memory Size: 1024 MB Max Memory Used: 1024 MB
{"statusCode": 200, "body": "{\"message\": \"title: Selenium - Google \\u691c\\u7d22\"}"}
3.10も行けました!
さぁ次は3.11
FROM public.ecr.aws/lambda/python:3.11
Build Failed
Error: failed to get destination image "sha256:7a1931ac7df46d1ff759cf12e772899aeb763230004891bb1b815a59d86911c4": image with reference sha256:7a1931ac7df46d1ff759cf12e772899aeb763230004891bb1b815a59d86911c4 was found but does not match the specified platform: wanted linux/amd64, actual: linux/arm64/v8
ついにビルドに失敗しました。どうもインストールしようとしているモジュールがプラットフォームに合わないと言われています。
それじゃアーキテクチャを変えてみよう。
アーキテクチャをLinuxからx86_64へ。一部モジュールも削除します。
FROM public.ecr.aws/lambda/python:3.11-x86_64
Successfully tagged helloworldfunction:python3.11-v1
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
お、
START RequestId: 9426d7dd-4617-41f3-8e16-73c215004d5c Version: $LATEST
Selenium - Google 検索
END RequestId: 9426d7dd-4617-41f3-8e16-73c215004d5c
REPORT RequestId: 9426d7dd-4617-41f3-8e16-73c215004d5c Init Duration: 1.75 ms Duration: 17410.94 ms Billed Duration: 17411 ms Memory Size: 1024 MB Max Memory Used: 1024 MB
{"statusCode": 200, "body": "{\"message\": \"title: Selenium - Google \\u691c\\u7d22\"}"}
3.11も通過しました!
このまま、最後、
3.12!!
FROM public.ecr.aws/lambda/python:3.12-x86_64
Build Failed
Error: The command '/bin/sh -c 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 nss mesa-libgbm' returned a non-zero code: 127
失敗かー。yumが失敗しているので、umihico様のgithubにあったようにdnfにしてみます。
RUN yum
↓
RUN dnf
Successfully tagged helloworldfunction:python3.12-v1
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
ビルド成功。そして実行。
sam local invoke
Invoking Container created from helloworldfunction:python3.12-v1
Building image..................
Using local image: helloworldfunction:rapid-x86_64.
START RequestId: b802c9a2-8e50-48f2-ad7c-2914269e1e68 Version: $LATEST
Selenium - Google 検索
END RequestId: b802c9a2-8e50-48f2-ad7c-2914269e1e68
REPORT RequestId: b802c9a2-8e50-48f2-ad7c-2914269e1e68 Init Duration: 1.96 ms Duration: 12340.42 ms Billed Duration: 12341 ms Memory Size: 1024 MB Max Memory Used: 1024 MB
{"statusCode": 200, "body": "{\"message\": \"title: Selenium - Google \\u691c\\u7d22\"}"}
おお、3.12まで通過できました
これでしばらくは業務フローも無事に運用出来そうです♪
本当にやりたかったことは、、、
今回のselenium3だと、単なる延命措置で、seleniumはversionが4になっていて、ブラウザとドライバの管理も刷新されているので、そちらを試してみるつもりでしたが、残念ながら手元のマシン(M2 MacBookPro)でlocal invokeが動作せず。どうもAppleシリコンのMacBookProではうまく動作しないのかも。
追記)IntelベースのMacBookProではAWS SAMで動かしたdockerコンテナでselenium4が動作しました。でも通常開発環境ではないので今後の開発は考え中。
そこで次はSeverless flameworkを使って本当にやってみたかったことを記事にしていこうと思います。
記事にしました! すみません、やっぱりSAMでやりました。
参考
このレポジトリのおかげで勤め先のシステム運用フローは無事にpython3.8を迎えられました。ありがとうございました。
バージョン組み合わせについてはこちらがとても参考になります。
以上です。ありがとうございました。