Selenium3をAWS lambda python3.8以上で動かす[AWS SAM]

# AWS LambdaでSelenium

webスクレイピングで便利なseleniumですが、サーバーレスと組み合わせると、APIやFTPが無くてもデータ連携させたりできるので、重宝しています。

ただLambdaでseleniumを動かすにはheadlessブラウザのchromiumと対応するchromedriverが必要で、バージョンによってはうまく（ほとんど）動かなかったりします。

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

私も業務で運用フローに組み込んでしまっているので、死活問題。python3.8だっていつまで使えるのか。（EOLを見よう）

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

python3.8廃止は2024年10月14日、うん、すぐだね。:fearful:

ということで、今回は私の「AWS SAMを使って比較的ラクにpython3.8以上のLambdaでseleniumをつかう方法」を紹介するとともに、この先pythonのアプデでどこまで使えるのかについて書いてみたいと思います。

## 必要なツール

このあたりのツールを使います。

* AWS SAM cli
* Docker

## Githubレポジトリ

https://github.com/elephantnode/python-selenium

レポジトリにソースコードは上げておきました。先に見たい方はどうぞ。


## 各種バージョン

selenium 3.141.0
headless-chromium v1.0.0-37
chromedriver_linux64 2.37


## 構築

AWS SAMを使っていきます。

```bash
sam init --name python-selenium
```

```bash
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から。

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

```requirements.txt
requests
boto3>=1.34.11
selenium==3.141.0
```

seleniumは3.141.0を利用します。

お次は実行ファイルのapp.py

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

```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のみ変更しています。
```yml
Globals:
  Function:
    Timeout: 600
    MemorySize: 1024
```
## イメージをビルドして実行


ビルドします。

```bash
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
```

しばらく待つと、ビルドが完了したので、一旦ローカルで実行してみます。

```bash
❯ 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へデプロイします。

```bash
sam deploy --guided

<<省略>>

Successfully created/updated stack - python-selenium in ap-northeast-1
```

デプロイで来たので、コンソールでテストしてみます。


![python-selenium-HelloWorldFunction-KTMtRy6ovTw8___関数___Lambda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38388/d86cc1bb-a0e0-4524-ae8b-fd5a35ad851c.png)

出来ましたね。でもpython3.8ももうすぐEOLになるんですよね。

というわけで、Lambdaのpythonのバージョンを上げてビルドしていきます。

```bash
FROM public.ecr.aws/lambda/python:3.9
```

3.9にします。


```bash
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\"}"}
```

すんなりビルドして実行できました:hugging: 
よかったこれで、3.9まで生き残れます！

```bash
FROM public.ecr.aws/lambda/python:3.10
```
3.10にしたところ、ビルドは出来たものの、実行エラーが発生

```bash
{"errorMessage": "Timeout value connect was <object object at 0x4008ba0610>, but it must be an int, float or None."
```

このエラーをすでに解説してくださっている方がいました。

https://qiita.com/niimi_ToB/items/45e6dce80bf281069a09


seleniumのバージョンとurllib３のバージョンで問題が発生したようです。
これ、AWS SAMでデフォルトでrequirements.txtにあるrequestsでもurllib3のバージョンが問題でエラーが発生してるんですよね。

なので、こちらを追加します。

```ｔｘｔ
urllib3<2
```

もういちどビルドして実行してみます。

```bash
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

```bash
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
```

ついにビルドに失敗しました。どうもインストールしようとしているモジュールがプラットフォームに合わないと言われています。
それじゃアーキテクチャを変えてみよう。

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

アーキテクチャを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
```
```bash
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
```

ビルド成功。そして実行。


```bash
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まで通過できました:tada:

これでしばらくは業務フローも無事に運用出来そうです♪

# 本当にやりたかったことは、、、

今回のselenium3だと、単なる延命措置で、seleniumはversionが4になっていて、ブラウザとドライバの管理も刷新されているので、そちらを試してみるつもりでしたが、残念ながら手元のマシン（M2 MacBookPro)でlocal invokeが動作せず。どうもAppleシリコンのMacBookProではうまく動作しないのかも。

:::note info
追記）IntelベースのMacBookProではAWS SAMで動かしたdockerコンテナでselenium4が動作しました。でも通常開発環境ではないので今後の開発は考え中。
:::

そこで次はSeverless flameworkを使って本当にやってみたかったことを記事にしていこうと思います。

:::note info
記事にしました！ すみません、やっぱりSAMでやりました。
:::




https://qiita.com/murodon/items/f06ec50427185110c9ed



# 参考

https://github.com/umihico/docker-selenium-lambda/

このレポジトリのおかげで勤め先のシステム運用フローは無事にpython3.8を迎えられました。ありがとうございました。

https://create-it-myself.com/research/research-combination-of-chromedriver-and-headless-chromium-versions/

バージョン組み合わせについてはこちらがとても参考になります。

以上です。ありがとうございました。