ServerlessFrameworkでスクレイピング用Lambda-Layerを作る

# 前書き
最近AWSの勉強をしていてLambda Layerの存在を知りました。
スクレイピングもLambdaで楽にできるのでは？と思いチュートリアル的な記事を色々やってみましたがハマったので備忘録を残します。

チュートリアル？は[こちら](https://hacknote.jp/archives/49974/)を参考にしました。
最終的にIaCにしたかったのでServerlessFrameworkも使ってみました。
作成したコードは→[touka9029/selenium-lambda-layer](https://github.com/touka9029/selenium-lambda-layer)

DockerやAWS Cloud9、AWS CloudFormationなど周辺機能については解説しません。

# 環境やライブラリバージョン
- AWS Cloud9, AMI: ami-081330c4becd75920
- [adieuadieu/serverless-chrome](https://github.com/adieuadieu/serverless-chrome), v1.0.0-55, chromium 69.0.3497.81 (stable channel) for amazonlinux:2017.03
- [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) 2.41
- Lambda Runtime: python3.7
- selenium: 3.141.0

# ハマったポイント
## その1: Cloud9のpythonバージョンとpipバージョンが異なる
Cloud9のPreferencesからpythonのバージョンはpython3に変更できるのですが、pipのバージョンは2のままでした。
[AWS Cloud9 でPython3を使うための設定](https://qiita.com/acecrc/items/fb34a12b265122816d4b)を参考に、pipのバージョンもpython3を向くようにしました。

## その2: serverless-chromeとChromeDriverのバージョン
https://sites.google.com/a/chromium.org/chromedriver/downloads で確認しましょう。
chromium 69.0なので`Supports Chrome version`が69を含むバージョンにします。
いろいろ試した結果、ChromeDriver 2.41に落ち着きました。
メジャーバージョンなら2.44まで動くはずなのですが、タイムアウトしてしまいました。
Chromeとchromiumって違うのでしょうか？（しっかり把握していない）

## その3: chrome_optionsが足りていなかった
これが一番大きいハマりポイント。
チュートリアルのオプションだけでは（1年以上前ですが）最新バージョンでは動かず、最終的にはIssueで動いたと報告のあったオプションと見比べて解決しました。

```python:lambda_handler.py
    # このオプションが必要だった
    options.add_argument('--disable-dev-shm-usage')
```

## その4: Cloud9のEBS容量
ServerlessFrameworkでIaC化している最中にEBSの容量が足りなくなりました。
10GBしかない上、デフォルトでlambci/lambdaのDockerイメージが入っていたり（合計で2～3GBぐらい？）、seleniumのライブラリを作るために使った[lambci/lambda:build-python3.7イメージ](https://hub.docker.com/r/lambci/lambda)もそこそこ容量を食うのでほとんど消しました（使う際に再取得すればいいと思います）。
Lambdaのローカルデバッグに使うらしいです~~がLayerは現時点（2020/01/20）では対応していないみたいです。残念~~。
dockerhubのドキュメントを見たらを確認したらlayerをマウントできるみたいです。

※後から確認したらCloud9とlambci/lambda:build-python3.7のOSは同じみたいなので直接`pip install`でも良かったかも。とはいえCloud9以外でも使えるようにするならDockerでしょうか。

# 実装
- IaC化にあたり、[AWS Lambdaで動的サイトのwebスクレイピングをしてtwitterに投稿するbotを作った(続)](https://qiita.com/kihoair/items/4545f5a0f46f24ef9c3e) の実装を参考にさせていただきました。

## chrome driverを取得
- まずはheadless-chromiumとChromeDriverを取得します。
- 最後に容量節約のためDockerイメージを削除しています。

```sh:get-binaries.sh
#!/bin/bash -x

SERVERLESS_CHROME_URL=https://github.com/adieuadieu/serverless-chrome/releases/download/v1.0.0-55/stable-headless-chromium-amazonlinux-2017-03.zip
CHROME_DRIVER_URL=https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip

# get headless_chrome
rm -r ./headless_chrome
mkdir -p ./headless_chrome

wget $SERVERLESS_CHROME_URL
unzip stable-headless-chromium-amazonlinux-2017-03.zip -d ./headless_chrome/bin/
rm stable-headless-chromium-amazonlinux-2017-03.zip

wget $CHROME_DRIVER_URL
unzip chromedriver_linux64.zip -d ./headless_chrome/bin/
rm chromedriver_linux64.zip

# get selenium
#  /opt/python, または /opt/python/lib/python3.7/site-packages に展開されるように配置する
PYTHON_LIB=python/lib/python3.7/site-packages
rm -r ./selenium
mkdir -p ./selenium/${PYTHON_LIB}
docker run --rm \
  -u=`id -u ${USER}`:`id -g ${USER}` \
  -v ${PWD}/selenium/${PYTHON_LIB}:/site-packages \
  lambci/lambda:build-python3.7 \
    pip install selenium -t /site-packages
docker image rm lambci/lambda:build-python3.7
```

## Serverless FrameworkでLambda Layerを作成
- `npm install -g serverless` で Serverless Framework をインストール、以下のYAMLを作成して`sls deploy`します。
- 何度かAWS CloudFormationを触っていたのでとっつきやすかったです。裏でzipに固めてS3にアップロード、CloudFromationのjsonを自動生成してスタック作成しているようでした。

```yaml
# template docs: https://serverless.com/framework/docs/providers/aws/guide/layers/
service: selenium-lambda-layer

provider:
  name: aws
  stage: dev
  region: ap-northeast-1

layers:
  selenium:
    path: selenium
    description: selenium layer, Runtime python3.7
    compatibleRuntimes:
      - python3.7
  headlessChrome:
    path: headless_chrome
    description: serverless-chrome v1.0.0-55, ChromeDriver2.41
    compatibleRuntimes:
      - python3.6
      - python3.7
      - python3.8

# docs: The name of your layer in the CloudFormation template will be your layer name TitleCased (without spaces) and have LambdaLayer appended to the end.
#  別のCloudFormationスタックから参照させるためにエクスポートする。
resources:
  Outputs:
    SeleniumLambdaLayerArn:
      Description: The ARN for the SeleniumLambdaLayer
      Value:
        Ref: SeleniumLambdaLayer
      Export:
        Name: SeleniumLambdaLayer
    HeadlessChromeLambdaLayerArn:
      Description: The ARN for the HeadlessChromeLambdaLayer
      Value:
        Ref: HeadlessChromeLambdaLayer
      Export:
        Name: HeadlessChromeLambdaLayer
```

# マネジメントコンソールで試す
- ここからは結果だけ。

```py:lambda_function.py
from selenium import webdriver


class Chrome:
    def headless_lambda(self):
        options = webdriver.ChromeOptions()
        options.binary_location = "/opt/bin/headless-chromium"
        options.add_argument("--headless")
        options.add_argument("--no-sandbox")
        options.add_argument("--single-process")
        options.add_argument("--disable-gpu")
        options.add_argument("--window-size=1280x1696")
        options.add_argument("--disable-application-cache")
        options.add_argument("--disable-infobars")
        options.add_argument("--hide-scrollbars")
        options.add_argument("--enable-logging")
        options.add_argument("--log-level=0")
        options.add_argument("--ignore-certificate-errors")
        options.add_argument("--homedir=/tmp")

        # 参考に無かった以下のオプションが必要だった
        options.add_argument('--disable-dev-shm-usage')
    
        driver = webdriver.Chrome(
            executable_path="/opt/bin/chromedriver",
            chrome_options=options
        )
        return driver


def lambda_handler(event, context):
    chrome=Chrome()
    driver=chrome.headless_lambda()
    driver.get('https://www.google.com')
    return driver.title
    driver.quit()
```

- ロール（適当）とメモリ（320MB）、タイムアウト（30秒）を設定してCloudWatchテンプレートでテストしました。
  - これだけでも12秒かかるんですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215540/884561f0-fc79-5af1-adca-9f786df1c266.png)

# 終わりに
ハマりポイントが誰かのためになれば幸いです。
完成してみれば Lambda Layer も Serverless Framework も便利ですね。
CloudWatchEventsで定期処理させたり通知飛ばしたりAPI Gatewayと連携させたり色々できそうです。

# その他参考記事
- [AWS Lambda上のheadless chromeをPythonで動かす](https://qiita.com/nabehide/items/754eb7b7e9fff9a1047d)
- [Lambda Layerの基本的な仕組みを確認する #reinvent](https://dev.classmethod.jp/cloud/aws/lambda-layer-basics-how-it-works/)