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

# はじめに

AWS SAMを使ってAWS LAMDA上でselenium3を使う方法をご紹介いたしましたが、当初目的が別にありました。

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

https://778a0a.hatenablog.com/entry/2024/02/13/224050

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/elephantnode/python-selenium4

## 手順

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

## SAMでテンプレートを作成

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

```shell
sam --init --name python-selenium4
```

```shell
❯ 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

```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を編集します。

```requirements.txt
requests
boto3>=1.34.11
urllib3<2
selenium==4.21.0
```

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


### Dockerfile

hello_world > Dockerfileを編集します。

```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の実行ファイルを編集します。

```app.py
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のある階層で次を実行します。

```bash
sam build --use-container
```

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

```shell
❯ 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ディレクトリが出力されました。

### ローカルテスト

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

```shell
sam local invoke
```

実行結果

```shell
❯ 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のチュートリアルにも同様の手順がありますのでそちらをご覧ください。

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38388/3e7b2187-8f09-8a13-320c-af8a91ca6bb0.png)

無事テスト成功です!! :wink:

## 手元のマシン（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も両方サポートしているものの、エミュレート結果に若干の違いがあるようです。

https://www.sria.co.jp/blog/2023/06/7308/

ベースイメージが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を利用して、コンテナに収めるのかがつかめました。

https://778a0a.hatenablog.com/entry/2024/02/13/224050




https://qiita.com/hideki/items/d1ff83e7e82afc0c0502