内容をスキップ
Image Description

[AWS SAM] Selenium4をAWS lambda Python3.12で動かす

By Murodon

はじめに

WEBのテストなどに便利なSelenium4ですが、サーバレスで動かせば、いろんな可能性があります。APIやFTPが無くてもデータ連携させたりできるので、重宝しています。

サーバレスなAWS LambdaでSeleniumを動かすにはheadlessブラウザのchromiumと対応するchromedriverが必要で、バージョンによってはうまく(ほとんど)動かなかったりします。

とくにpython3.8以上ではLambdaのOSがAmazon Linux2で稼働するようになったため、seleniumがモジュール不足で動かなくなってしまい、従来のようにコード一式zipであげてといった方法が使えなくなりました。

きっかけはこちらの方の記事でした。

selenium4の最新版を利用すれば、新しく追加されたselenium-managerという機能があり、簡易的なChromeブラウザと、そのバージョンにあったChromDriverを自動でインストールしてくれます。

また古いHeadless-Chromiumはselenium4に対応していないため、最新のchromeに対応させたかったのですが、なぜか手元のマシン(M2 Macbookpro)で動かず、手詰まりになっていました。

この記事は動作するようになったコードの紹介と、その解決方法についてのまとめです。

こんな方におすすめ

  • AWS Lambda python3.8以上でselenium4を使いたい方
  • AWS SAMを使っている方
  • Apple Silicon製のMac(M1,M2チップ)でAWS Lambda seleniumのローカルテストがうまくいかない方

つくったもの

  • AWS SAMを利用して、AWS Lambdaで動作するpython版selenium4
  • selenium4のselenium-managerを利用して、chromeブラウザとchromedriverを自動選定でインストール
  • AWS SAMなので、ローカル実行でテストができる
  • Apple Silicon製のMacでも動作

動作検証環境

  • MacBookPro (Apple M2 Pro)
  • Docker Desktop 4.31.0 (153195)
  • SAM CLI, version 1.100.0

レポジトリ

ソースコードはこちらから

https://github.com/murodon/python-selenium4

手順

操作の流れです。ローカルに構築し、ローカルテスト後にデプロイ。デプロイしたものをコンソールでテストしました。
テストコードは書いていませんので、実行テストのみです。

SAMでテンプレートを作成

SAM CLIでフォルダにアプリケーションの雛形を作ります。

sam --init --name python-selenium4
❯ sam init --name sam-python-selenium4

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: 17

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: sam-python-selenium4
    Base Image: amazon/python3.11-base
    Architectures: x86_64
    Dependency Manager: pip
    Output Directory: .
    Configuration file: sam-python-selenium4/samconfig.toml

    Next steps can be found in the README file at sam-python-selenium4/README.md


Commands you can use next
=========================
[*] Create pipeline: cd sam-python-selenium4 && sam pipeline init --bootstrap
[*] Validate SAM template: cd sam-python-selenium4 && sam validate
[*] Test Function in the Cloud: cd sam-python-selenium4 && sam sync --stack-name {stack-name} --watch

ランタイムは現時点ではpythonが3.11までしか選択できないので、3.11を、パッケージタイプは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

雛形を変更していきます。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  python3.12

  Sample SAM Template for sam-python-selenium4

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 60
    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.11-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

GlobalsでMemorySizeとTimeoutを変更しています。

requirements.txt

hello_world > requirements.txtを編集します。

requests
boto3>=1.34.11
urllib3<2
selenium==4.21.0

Dockerfileでインストールするpythonのモジュールです。seleniumは4.21.0をインストールしました。

Dockerfile

hello_world > Dockerfileを編集します。

FROM public.ecr.aws/lambda/python:3.12-x86_64

COPY app.py requirements.txt ./
RUN python3.12 -m pip install -r requirements.txt -t .

# selenium-managerを使ってChromeとChromeDriverをダウンロードする。
RUN /var/task/selenium/webdriver/common/linux/selenium-manager --browser chrome --cache-path /opt
# Chromeの依存関係をインストールする。
# 参考: https://qiita.com/hideki/items/d1ff83e7e82afc0c0502
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"]

ポイントとなるところに、sam initでDockerfileに出力されるAWS Lambdaのイメージ「public.ecr.aws/lambda/python:3.12」ではなく、x86-64用のAWS Lambdaイメージ「public.ecr.aws/lambda/python:3.12-x86_64」に変更しています。後ほど理由を後述します。

requirements.txtのseleniumをインストール後、同梱しているselenium-managerを使い、chromeとchromeDriverをコンテナ内部の/optフォルダにダウンロードします。

このほかChromeの実行に必要な依存関係をインストールします。

app.py

hello_world > pythonの実行ファイルを編集します。

import json

from tempfile import mkdtemp
import glob

from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
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.add_argument('--headless=new')
        self.options.add_argument('--disable-gpu')
        self.options.add_argument('--disable-dev-shm-usage')
        self.options.add_argument('--disable-dev-tools')
        self.options.add_argument('--no-zygote')
        self.options.add_argument('--window-size=1280x1696')
        self.options.add_argument(f"--user-data-dir={mkdtemp()}")
        self.options.add_argument(f"--data-path={mkdtemp()}")
        self.options.add_argument(f"--disk-cache-dir={mkdtemp()}")
        self.options.add_argument('--no-sandbox')
        self.options.add_argument('--hide-scrollbars')
        self.options.add_argument('--enable-logging')
        self.options.add_argument('--log-level=0')
        self.options.add_argument('--v=99')
        self.options.add_argument('--single-process')

        self.options.binary_location = glob.glob("/opt/chrome/linux64/*/chrome")[0]
        service = ChromeService(glob.glob("/opt/chromedriver/linux64/*/chromedriver")[0])
        self.driver = webdriver.Chrome(service=service, 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()

globを使って/linux64以下にある/optフォルダ内へインストールされた、chromeとchromeDriverを検索して、Chromeを起動します。

/*の部分はselenium-managerが選定するバージョンによって変わります。

その後はドライバーをインスタンスとして呼び出して、googleを開いて「selenim」と検索し
、検索結果のタイトルを出力します。

アプリケーション構築

AWS SAM CLIでアプリケーションを構築します。
プロジェクトディレクトリのtemplate.yamlのある階層で次を実行します。

sam build --use-container

Dockerコンテナを利用して依存関係をインストールするので、--use-containerをつけています。

❯ sam build --use-container
Starting Build inside a container

# 省略

Successfully built 4fb36f1704ed
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

ダウンロードなどもあるので、構築にしばらく時間がかかります。プロジェクトディレクトリに.aws-samディレクトリが出力されました。

ローカルテスト

イベントを必要しないので、プロジェクトディレクトリで次のコマンドで実行してテストします。

sam local invoke

実行結果

❯ sam local invoke
Invoking Container created from helloworldfunction:python3.12-v1
Building image.................
Using local image: helloworldfunction:rapid-x86_64.

START RequestId: f23f3ebc-720f-4762-9306-aa689cb2ceab Version: $LATEST
Selenium - Google 検索
END RequestId: f23f3ebc-720f-4762-9306-aa689cb2ceab
REPORT RequestId: f23f3ebc-720f-4762-9306-aa689cb2ceab    Init Duration: 1.13 ms  Duration: 6459.17 ms    Billed Duration: 6460 ms    Memory Size: 1024 MB    Max Memory Used: 1024 MB
{"statusCode": 200, "body": "{\"message\": \"title: Selenium - Google \\u691c\\u7d22\"}"}

検索結果のタイトル、Selenium - Google 検索と無事出力されました。

アプリケーションをAWSにデプロイ

プロジェクトディレクトリでSAM CLIを使い、次のコマンドを実行してデプロイします。

sam deploy --guided

対話形式でデプロイ設定を進めますが、コマンドの詳細はSAM CLIのチュートリアルにも同様の手順がありますのでそちらをご覧ください。

■ チュートリアル: Hello World アプリケーションをデプロイする – AWS Serverless Application Model
https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html

❯ sam deploy --guided

省略

Successfully created/updated stack - python-selenium4 in ap-northeast-1

デプロイ完了まで待ちます。

AWS コンソールで実行

コンソール実行の手順は省略いたします。

python-selenium4-HelloWorldFunction-ma7BPTMPV3v9___関数___Lambda.png

無事テスト成功です!! 😉

手元のマシン(M2 Macbookpro)で動かなかった理由とその解決方法

AWS SAMのpython用コンテナベースイメージはLinux, x86-64, ARM 64の3種類のCPUアーキテクチャが用意されています。

https://gallery.ecr.aws/lambda/python

標準でインストールされるイメージはLinuxとなっており、ビルド時に出力された詳細だとlinux/amd64と出力されます。

Apple Silicon(ARM64)用のdocker-desktopでは、amd64もx86-64も両方サポートしているものの、エミュレート結果に若干の違いがあるようです。

ベースイメージがLinuxでもx86-64でもAWSのECRにアップロードしてしまえば、スクレイピング用のChromeは動作しますが、AWS SAMのようにローカルにDocker imageを作成して、Apple Silicon製Macの環境で立ち上げたコンテナでは、アーキテクチャがAmd64の場合にchromeがクラッシュしてしまうようです。

x86-64に変更したところ、すんなりとテストができるようになったので、もしApple Silicon Macでdockerを使ったサンプルがうまく動かない場合は、Dockerのコンテナに使われているアーキテクチャがなにかもチェックしたほうがよさそうです。

ちなみにIntelベースのmacbookproを引っ張り出して試したところ、Amd64でもローカルで実行できました。

またこのタイミングでdocker-desktopも最新版にしました。Rosettaも念の為にインストールしています。

https://www.publickey1.jp/blog/23/docker_desktopapplex86-64rosetta_for_linux.html

SAMにこだわった理由

AWS SAMはDockerを使ったfunctionのローカルテストがとても簡単にできます。
serverless flameworkは一旦imageを作成して、そこからdocker runで起動し、別のターミナルでコマンド送信して試す方法になります。cdkでの実装もこのテストパターンのようです。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-test.html

立ち上げてテストして、落としてといった工程になってしまうので、Lambdaに関しては、AWS SAMのほうがテストは楽だと思いました。(プラグインなどを使えば楽にできるようですが)

最後に

思わずアーキテクチャの勉強にもなってしまいましたが、ようやく心に引っかかっていたselenium4をサーバレスで動かせました。

サーバレスでスクレイピングをしたい方の一助になれば幸いです。

参考

この記事のお陰で、どうやってselenium-managerを利用して、コンテナに収めるのかがつかめました。

■ SeleniumをAWS Lambdaでサーバーレスに動かしてみる #AWS – Qiita
https://qiita.com/hideki/items/d1ff83e7e82afc0c0502

このエントリーをはてなブックマークに追加

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です