AWS Lambda Ruby2.7でヘッドレスChromeを動かす

ネットで調べてもあまり正解に辿り着けなかったので。
書き殴りなので、すみませんが動作保証はしないです。

## やりたいこと

タイトル通りですが、AWS LambdaでWebスクレイピングをしたい。

諸事情あって言語はRuby2.7になります。
RubyでのスクレイピングにはCapybara + selenium-webdriver を利用します。

Lambdaのデプロイには[Serverless Framework](https://www.serverless.com/)を使うので、この記事はServerlessについての知識を事前条件としています。

Capybaraやselenium-webdriverの説明はしないので、ググってくだしあ。

## 注意

Ruby2.5はサポート終了が宣告されているので今からやるにはRuby2.7一択です。

実は今現在(この記事が公開された日)ネット上に溢れている「Lambda(Ruby)でスクレイピングする」系の記事はほとんどがRuby2.5だったりするので、注意しましょう。

後述しますが、Ruby2.5とRuby2.7ではランタイムのOSが異なるので、Chromeを動かすために必要な条件が大分異なります。

## ディレクトリ構成
こんな感じの構成で、Lambda Layerをデプロイするディレクトリを作成します。
それぞれのファイルを個別に説明していきます。
また、package.jsonとか .dockerignoreなどは省略していますが、実際には勿論あります。

```
chromedriver/
├── Dockerfile
├── build
│   ├── .fonts
│   │   ├── fonts.conf
│   │   └── ipaexg.ttf
│   ├── bin
│   ├── lib
│   └── ruby
│       └── lib
│           └── setup_cabybara.rb
└── serverless.yml
```

## serverless.yml
こんな感じで。

```yaml
service: chromedriver

plugins:
  - serverless-hooks-plugin

custom:
  hooks:
    before:deploy:deploy:
      - docker build -t chromedriver .
      - docker run --rm -v "$PWD"/build:/opt chromedriver

provider:
  name: aws
  runtime: ruby2.7
  region: ap-northeast-1
  timeout: 900

layers:
  chromedriver:
    path: build
    compatibleRuntimes:
      - ruby2.7
```

functionsがないですが、これだけもLambda Layerだけをデプロイして他のサービスから参照することができます。
serverless-hooks-pluginはserverlessコマンドの前後に任意の処理を挟むことができるプラグインです。
ここでは `sls deploy` コマンドを実行した時に、後述のDockerfileを使ってchromeの動作に必要なバイナリやライブラリを `/build/bin` と `/build/lib` に出力しています。

## Dockerfile

Dockerのプロではないので、変なところがあるかもしれませんがお許しを。

```docker
FROM lambci/lambda:build-ruby2.7

WORKDIR /tmp

RUN yum install -y unzip && \
    curl -SL https://chromedriver.storage.googleapis.com/2.43/chromedriver_linux64.zip > chromedriver.zip && \
    curl -SL https://github.com/adieuadieu/serverless-chrome/releases/download/v1.0.0-55/stable-headless-chromium-amazonlinux-2017-03.zip > headless-chromium.zip && \
    unzip chromedriver.zip && \
    unzip headless-chromium.zip

RUN yum install -y libX11

CMD cp /tmp/chromedriver /opt/bin/ && \
    cp /tmp/headless-chromium /opt/bin/ && \
    # chromium
    cp /usr/lib64/libexpat.so.1 /opt/lib/ && \
    cp /usr/lib64/libuuid.so.1 /opt/lib/ && \
    # chromedriver
    cp /usr/lib64/libglib-2.0.so.0 /opt/lib/ && \
    cp /usr/lib64/libX11.so.6 /opt/lib/ && \
    cp /usr/lib64/libxcb.so.1 /opt/lib/ && \
    cp /usr/lib64/libXau.so.6 /opt/lib/
```

ここでは、以下２点を行っています。

- ChromeのバイナリとChromeの操作に必要なchromedriverをダウンロード
- Lambda Ruby2.7で動かすために不足しているネイティブのライブラリをローカルにコピー

#### chromeとchromedriverのバイナリをダウンロード

当然Lambdaの環境にはChromeはインストールされていないので、自分で実行バイナリをパッケージしてデプロイしないといけないわけですが、公式のバイナリだとサイズがデカすぎてLambda Layerのサイズ制限(250MB)に引っかかってデプロイできません。
そこで、サイズを小さくしてビルドしたものを配布してくれている方を見つけたので、そちらを利用させてもらっています。（感謝。。。！）

なお、releasesを見ると[v1.0.0-57](https://github.com/adieuadieu/serverless-chrome/releases/tag/v1.0.0-57) が最新で、Ruby2.7のOSであるAmazon Linux 2向けにビルドされているようですが、自分が試したところ何故か動かなかったので少し前の v1.0.0-55 を利用しています。

また、rubyからChromeを操作するためにchromedriverもダウンロードしています。(こちらは公式から)
chromedriverのバージョンはChromeのバージョンに合わせたものを使う必要があるので注意しましょう。(参考：https://chromedriver.chromium.org/downloads/version-selection)


#### ネイティブのライブラリをローカルにコピー

Ruby2.5はAmazon Linuxで動くのですが、Ruby2.7からはAmazon Linux 2 になるため、インストール済みのパッケージやそれの共有ライブラリが大きく異なります。というか大分少ないです。
なので、ネイティブライブラリの依存を自分で解決して必要なものをバイナリと一緒にパッケージしないといけないわけです。
これがネットで探してもなかなか出てこなくて、lddコマンド打ったりしてハマったところですが、経緯はともかくとして、上記のライブラリたちがあればなんとか動作しました。


また、ネイティブライブラリの取得のためにlambci/lambda:build-ruby2.7のイメージを利用していますが、今は[公式のイメージ](https://hub.docker.com/r/amazon/aws-sam-cli-build-image-ruby2.7)が公開されているらしいのでそちらの方がベターかもしれません。（未検証）




## fonts.conf と ipaexg.ttf

ここまで動くだろうと思いきや、まだ足りないものがあります。フォントファイルです。
ネット上だとこれがないと文字化けするという記事も見ましたが、自分の場合何故かそもそもChromeが起動しなかったです。
フォントの設定はよくわかってないですが、とりあえずLambda Layerがデプロイされる `/opt`に .fontsを置いておきます。

fonts.conf

```xml
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/opt/.fonts/</dir>
<cachedir>/tmp/fonts-cache/</cachedir>
<config></config>
</fontconfig>
```

ipaexg.ttfは[こちらの文字情報技術促進協議会さん](https://moji.or.jp/ipafont/ipaex00401/)からダウンロードしてます。

### setup_cabybara.rb

これはLambda Layerに含める必要はないですが、Layerにしておけば利用するLambda側で `require 'setup_cabybara'` するだけで、Capybaraの設定が完了します。 

```rb
require 'capybara'
require 'selenium-webdriver'

ENV['FONTCONFIG_FILE'] = '/opt/.fonts/fonts.conf'

Capybara.register_driver :chrome_headless do |app|
  version = Capybara::Selenium::Driver.load_selenium
  options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
  browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |options|
    options.add_argument('--headless')
    options.add_argument('--no-sandbox')
    options.add_argument("--disable-gpu")
    options.add_argument("--window-size=1280x1696")
    options.add_argument("--single-process")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--homedir=/tmp")
  end

  browser_options.binary = '/opt/bin/headless-chromium'
  driver_path = '/opt/bin/chromedriver'
  service = Selenium::WebDriver::Service.chrome(path: driver_path)

  Capybara::Selenium::Driver.new(app,
                                 browser: :chrome,
                                 service: service,
                                 options_key => browser_options)
end
Capybara.javascript_driver = :chrome_headless
```

Lambda Layerは実行環境の`/opt` に配置されるので、これまで用意してきたchrome、chromedriver、フォントそれぞれのパスを設定しています。


## デプロイ

`sls deploy` すればDockerから出力した諸々と一緒にデプロイされます。

##  Lambda Layer利用側

疲れたので、Lambda Layerの利用方法自体は[公式ドキュメント参照](https://www.serverless.com/framework/docs/providers/aws/guide/layers#using-your-layers)で。。。

rubyコードはこんな感じで使えます。
capybara と selenium-webdriverのgemはインストール前提です。

```rb
require 'setup_cabybara'
session = Capybara::Session.new(:chrome_headless)
```


## 終わりに

書き殴ったので、動作保証はしないです。(2度目)

余裕ができたら動作確認できたサンプルをgithubで公開したいですね。