【AWS SAM】コンテナイメージ版のLambdaでSeleniumによるスクレイピングを定期実行してみた

NeeNetです。

今回はAWS環境でLambdaを用いて、Seleniumによるスクレイピングを定期実行してみたいと思います。

以前AWS環境上でSeleniumを動かすのにECS (Fargate)を利用した方法をご紹介しましたが、今回はそのLambda版です。

あわせて読みたい
ECS (Fargate)でSeleniumによるスクレイピングを定期実行してみた NeeNetです。 今回はAWS環境でECS (Fargate)を用いて、Seleniumによるスクレイピングを定期実行してみたいと思います。 AWS環境で定期実行するため、スクレイピングをす...

1回のスクレイピング処理が15分以内に完了するのであればLambdaの方が起動が早くコストも安くてお得なので、以下ではLambdaを利用した方法をご紹介します。

アーキテクチャ

今回作成するアーキテクチャ図は以下の通りです。

アーキテクチャ図

概要説明

今回はLambdaの起動にStep Functionsを利用し、Lambdaはコンテナイメージを利用します。

SeleniumやPythonスクリプトなどはローカルでイメージ化を行ってECRにPushし、Lambda実行時にそのイメージを取得するイメージです。

EventBridgeスケジューラを用いて、指定時間に上記で記載したStep Functionsを起動するように設定します。

なお、今回デプロイには AWS SAM (Serverless Application Model) を利用します。

ECRの作成

まずAWSのマネジメントコンソールからAWS ECRのサービスページにアクセスし、プライベートレジストリの作成を行います。

今回は「python-app-ecr-lambda」という名前のレジストリを作成します。

作成が完了したら、『プッシュコマンドを表示』ボタンからPushに必要なコマンドのメモしておきます。

このコマンドは後でBashファイルとして記載します。

ファイルの準備

ディレクトリ構成

今回のディレクトリ構成は以下の通りです。

.
├── Dockerfile
├── ecr_push.sh
├── function
│   └── app.py
├── samconfig.toml
└── template.yaml

以下で、それぞれのファイルの記載内容について説明します。

Dockerfile

Dockerfileは以下の通りです。

FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.11 as build  

# seleniumの用意
RUN yum install -y unzip && \  
    curl -Lo "/tmp/chromedriver-linux64.zip" "https://storage.googleapis.com/chrome-for-testing-public/122.0.6261.111/linux64/chromedriver-linux64.zip" && \  
    curl -Lo "/tmp/chrome-linux64.zip" "https://storage.googleapis.com/chrome-for-testing-public/122.0.6261.111/linux64/chrome-linux64.zip" && \
    unzip /tmp/chromedriver-linux64.zip -d /opt/ && \  
    unzip /tmp/chrome-linux64.zip -d /opt/ && \  
    rm -rf /tmp/*.zip && \  
    yum clean all && \  
    rm -rf /var/cache/yum  

FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.11  

# パッケージのインストール
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 ipa-pgothic-fonts.noarch && \  
    yum clean all && \  
    rm -rf /var/cache/yum  

# pythonモジュールのインストール
RUN pip install --no-cache-dir selenium pandas beautifulsoup4 boto3 line-bot-sdk==3.5.0  

COPY --from=build /opt/chrome-linux64 /opt/chrome  
COPY --from=build /opt/chromedriver-linux64 /opt/  
COPY ./function ./

CMD ["app.handler"]  

上記では、ビルドステージと最終ステージを分けて最終的なイメージのサイズを最適化しています。

ベースイメージはどちらのステージも公式のAWS Lambda用Python 3.11イメージです。

ビルドステージではChromeドライバーとヘッドレスブラウザをダウンロードして展開しています。

最終ステージではChrome実行に必要なグラフィック関連のライブラリや、日本語のページを正しく表示できるように日本語フォント(ipa-pgothic)をインストールした後、必要なPythonモジュールをインストールしています。

その後、ビルドステージからChrome関連ファイルのコピーし、アプリケーションコードを ./function ディレクトリからコンテナにコピーした後、エントリーポイントを app.handler に設定しています。

ecr_push.sh

ECR作成時にメモしたPushコマンドを、 ecr_push.sh に記載します。(※以下はmacOS/Linux用のコマンド)

#!/bin/bash
set -o pipefail

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com

docker build -t python-app-ecr-lambda .

docker tag python-app-ecr-lambda:latest <YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda:latest

docker push <YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda:latest

上記で <YOUR ACCOUNT ID> となっている部分については、ご自身のAWSアカウントIDが記載されているかと思います。

function/app.py

function ディレクトリの app.py には、実際にLambdaで動かすアプリケーションのコードを記載します。

今回は動作確認のため簡単な内容にしました。

import shutil
from tempfile import mkdtemp

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service


def setup_driver(tmp_dir):
    # Chromeオプションの設定
    chrome_options = Options()
    chrome_options.binary_location = "/opt/chrome/chrome"
    chrome_options.add_argument("--headless=new")
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-dev-shm-usage")
    chrome_options.add_argument("--window-size=1920,1080")
    chrome_options.add_argument("--disable-gpu")
    chrome_options.add_argument('--single-process')
    chrome_options.add_argument("--no-zygote")
    chrome_options.add_argument(f"--user-data-dir={mkdtemp(dir=tmp_dir)}")
    chrome_options.add_argument(f"--data-path={mkdtemp(dir=tmp_dir)}")
    chrome_options.add_argument(f"--disk-cache-dir={mkdtemp(dir=tmp_dir)}")
    chrome_options.add_argument("--disable-dev-tools")
    chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36')

    # WebDriverの設定
    service = Service("/opt/chromedriver")  # ChromeDriverのパスを指定
    driver = webdriver.Chrome(service=service, options=chrome_options)
    return driver


def handler(event, context):
    # 一時ディレクトリの作成
    tmp_dir = mkdtemp()

    # ドライバのセットアップ
    driver = setup_driver(tmp_dir)

    # python.org にアクセス
    driver.get("https://www.python.org/")

    # titleを取得・表示
    title = driver.title
    print(title)

    # ブラウザを閉じる
    driver.quit()

    # 後片付け
    shutil.rmtree(tmp_dir)

    return {"message": "success"}

samconfig.toml

AWS SAMの設定ファイルである samconfig.toml は以下のように記載します。

version = 0.1

[default]
region = "ap-northeast-1"

[default.build.parameters]
debug = true

[default.deploy.parameters]
stack_name = "<YOUR STACK NAME>"
s3_bucket = "<YOUR S3 BUCKET>"
s3_prefix = "sam-deploy"
capabilities = "CAPABILITY_NAMED_IAM"
confirm_changeset = true
resolve_image_repos = true

スタック名とデプロイ用のバケット名については、ご自身の環境に合わせて任意に設定して下さい。( <YOUR STACK NAME><YOUR S3 BUCKET> となっている部分)

template.yaml

AWS SAMのテンプレートを記載した template.yaml は以下の通りです。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: "Python SAM Application"

Parameters:
  EcrArn:
    Description: Please type the ARN of ECR 
    Type: String
    Default: "<YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda:latest"

Resources:
  # Lambda
  AppLambda:
    Type: AWS::Lambda::Function
    Properties:  
      FunctionName: "python-app-lambda"  
      Role: !GetAtt AppLambdaRole.Arn  
      PackageType: Image
      Code:
        ImageUri: !Sub ${EcrArn}
      Architectures:
        - x86_64
      MemorySize: 1024
      Timeout: 180
  
  # IAM Role for Lambda
  AppLambdaRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: "python-app-lambda-role"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com 
            Action: sts:AssumeRole
      Path: "/"
      Policies:
        - PolicyName: LambdaExecutionPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - "ecr:GetAuthorizationToken"
                  - "ecr:BatchCheckLayerAvailability"
                  - "ecr:GetDownloadUrlForLayer"
                  - "ecr:BatchGetImage"
                Resource: '*'
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 
        - "arn:aws:iam::aws:policy/AmazonS3FullAccess"

  # Step Functions
  AppSfnForLambda:
    Type: "AWS::Serverless::StateMachine"
    Properties:
      Name: "python-app-invoke-lambda-sf"
      Definition:
        Comment: State Machine to invoke Lambda Functions
        StartAt: InvokeLambda
        States:
          InvokeLambda:
            Type: Task
            Resource: !GetAtt AppLambda.Arn
            End: true
      Role: !GetAtt AppSfnForLambdaRole.Arn
      Events:
        Schedule:
          Type: ScheduleV2
          Properties:
            ScheduleExpressionTimezone: "Asia/Tokyo"
            ScheduleExpression: "cron(0/1 9-18 ? * MON-FRI *)" # 月-金の 9時~18時に1分毎に実行
            Input: "{}"
  
  # IAM Role for Step Funtions
  AppSfnForLambdaRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: "python-app-invoke-lambda-role"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: states.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaRole

作成しているリソースは冒頭で示したアーキテクチャ図の通りです。

パラメータとなっている EcrArn には、ご自身のECRのARNを設定してください。

例えばリポジトリ名を本記事と同様に python-app-ecr-lambda としているのであれば、ARNは <YOUR ACCOUNT ID>.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda:latest になっているかと思います。

スケジュールはCRON書式により、 cron(0/1 9-18 ? * MON-FRI *) という月曜から金曜の9時~18時に1分毎に実行するよう設定しています。

デプロイ

必要なファイルが準備できたので、デプロイを行います。

Dockerイメージのビルド・Push

DockerイメージのビルドとPushを行うために、先に作成したBashファイルを実行します。

$ bash ecr_push.sh

以下のような感じでログが出ていれば成功です。

...(省略)
The push refers to repository [123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/python-app-ecr-lambda]
b19907c367ef: Pushed 
c502836631a4: Pushed 
f344de6b3ad6: Pushed 
015c4cf5f4b8: Pushed 
cfaec26324a3: Pushed 
dddb3c2ac8c7: Pushed 
cc8cc690bb37: Pushed 
e1b8ef616f15: Pushed 
01bcef6ece3c: Pushed 
0cfe72a3568f: Pushed 
814345d22610: Pushed 
latest: digest: sha256:d0bf7f8ca97c317bd13487c4e004e9fb768c8affd0a0daa7d8a771227d133584 size: 2634

AWS SAMのデプロイ

以下のコマンドを実行し、AWS SAMのデプロイを行います。

$ sam deploy

デプロイが正常に完了すると、先に設定したスタック名でスタックが作成されていると思うので、AWSのマネジメントコンソールからCloudFormationのサービスページに遷移し、スタックが作成されていることを確認して下さい。

実行確認

設定したEventBridgeスケジューラの通り、Step Functionsが起動すると思います。

Step Functionsの実行を確認すると、以下のように正常に実行が完了していました。

Lambdaのログを確認すると、以下のようにSeleniumが正しく動き、python.orgのタイトルを取得できていることが確認できました。

最後に

今回はAWS環境でコンテナイメージ版のLambdaを用いて、Seleniumによるスクレイピングを定期実行してみました。

参考になりましたら幸いです。

ご依頼について

NeeNetではAWSを利用したインフラ環境の構築、およびその上で動くアプリケーションの開発のご依頼・ご相談をお引き受けしております。

個人・法人問わず、何かご相談事項がございましたら、一度ご連絡いただければと思います。

  • URLをコピーしました!