AWS LambdaでSeleniumとActiveRecordを扱う

気付いたらもう9月ですね。 最近、AWS Lambdaでいろいろと遊ぶ機会があったのでメモとして残します。

はじめに

とあるセキュリティゲームの運営用に、SeleniumでWebスクレイピングをやっているRubyのスクリプトをEC2で運用していたのですが、Headless Chromeを扱うため大量に起動するとメモリ食っちゃうし、スケールしようにもEC2インスタンスのAutoScaling組むのもちょっとなあ。とか、インスタンスの起動まで待ってられないからある程度多めにインスタンスを実行したりするのも余分にコストが。。。 1実行に15分もかからないスクリプトだし、ということでLambdaに移行することにしました。

Lambda Layerについて

Headless Chromeを扱う場合、単純にFunctionのデプロイパッケージにバイナリを含めると50MBを超えてしまうため、Lambdaのクォータ(制限)にかかります。
今ではLambda Layerという仕組みができていてFunctionとは別に、Functionの実行に必要なライブラリやバイナリファイルをデプロイパッケージとして作成できます。それをアップロードし、Layerとして追加することでコンテナ内の/optとして展開されます。ただし、Layerの展開後のサイズが250MBに収まっている必要があります。

デプロイパッケージの作成

Layerに追加するためのデプロイパッケージを作成します。
ここでは、Layerの再利用性を考慮し、3つのパッケージに分割します。

  • Headless Chrome
  • font
  • Ruby Gems

Headless Chromeのデプロイパッケージ作成

serverless chromeとchromedriverを用意。serverless-chromeとchroomedriverのバージョンは合わせる感じで、chromedriverは chromedriver_linux64.zip をダウンロード。

ディレクトリは以下のような感じで作成し、zipで圧縮する。

bin
├── chromedriver
└── headless-chromium
zip -r headless-chrome.zip bin

.fontsのデプロイパッケージ作成

日本語用フォントがインストールされてないと、日本語のページを表示した場合に文字化けしてしまうためフォントパッケージを作成します。

wget https://ipafont.ipa.go.jp/IPAexfont/IPAexfont00201.zip
unzip IPAexfont00201.zip
mkdir .fonts
cp IPAexfont00201/*.ttf .fonts
zip -r .fonts.zip .fonts

Ruby Gemsパッケージの作成

ActiveRecordなり、rubyでseleniumを扱うためにいろいろとライブラリが必要なのでパッケージを作成します。
ActiveRecordがmysql2を使用するため、mysql2のライブラリもまとめてパッケージにする必要があります。
作成方法は以下が参考になります。

上記参考資料からの変更点は、Gemfileのみ。以下のGemfileを作成しました。

source 'https://rubygems.org'

gem 'activerecord'
gem 'selenium-webdriver'

実行後のカレントディレクトリの構成は以下のようになっています。主に使うのは libvendor/bundle です。

.
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── bundle_install.sh
├── handler.rb
├── lib
│   ├── libmysqlclient.so.18 -> libmysqlclient.so.18.0.0
│   └── libmysqlclient.so.18.0.0
└── vendor
    └── bundle
        └── ruby
            └── 2.5.0
                ├── bin
                ├── build_info
                ├── cache
                ├── doc
                ├── extensions
                ├── gems
                └── specifications

libvendor/bundleをパッケージ化する必要がありますが、Lambda関数に追加したLayerは全て /opt に展開されます。
コンテナが参照するGEM_PATHは ruby/gems/2.5.0 になっており、このままパッケージ化するとうまく読み込まれません。

※ 上記サイトから引用

json.zip
└ ruby/gems/2.5.0/
               | build_info
               | cache
               | doc
               | extensions
               | gems
               | └ json-2.1.0
               └ specifications
                 └ json-2.1.0.gemspec

ちょっとパスを弄ってパッケージ化します。

mkdir -p ruby/gems
cp -r vendor/bundle/ruby/2.5.0 ruby/gems
zip -q -r ruby.zip lib ruby

※ これ今考えたら環境変数でGEM_PATH設定したら別にやらなくてよかったんじゃないかと思う。

あとは、出来上がった3つのパッケージをS3アップロードして、Layerを作成してLambda関数に追加します。

関数コードの作成

あとは以下のような関数作って、テスト完了。

ブラウザのタイムアウトがデフォルトで60秒なのですが、setup_driverでtimeout値を弄っています。
実行時間がどれぐらい必要かという見積もりは事前にしておくべきですが、アクセス先のページが何らかの理由でダウンしてる場合に60秒かかってしまうとその分コンテナの実行時間が長くなってしまう。
あとは、Lambda関数そのもののタイムアウトが40秒の場合に、サイトがダウンしてしまうと、タイムアウトエラーとなってしまうのでそれを回避するためでもあります。
実行時間、メモリ、ページ遷移時のエラー処理などなど考えることはたくさんありますが、テスト用なのでこの辺で。
大量実行時にコネクションプールの問題もありますが、この辺りはRDS Proxyで解決できます。

雑感

2017年ぐらいにLambda使ってた時は、Layerもなければカスタムラインタイムも実行できなかった状況からはだいぶ進化しているなあと実感しました。
それに、RDSProxyが使えるので、サーバレスであれこれやるための環境は整った感じがあります。
おしまい。