【名古屋Ruby会議04】Ruby × AWS Lambda × SAM の開発・テスト方法の紹介 ~TUNAGデータ処理基盤を例に ~

スタメン エンジニアの松谷(@uuushiro)です。6/8に開催された名古屋Ruby会議04Ruby×AWS Lambdaの内容について発表してきました。発表時に口頭で補足していた内容も含めて今回記事にしました。こちらのスライドは図を多めに説明をしているので参考にしていただければ幸いです。

https://speakerdeck.com/uuushiro/ruby-x-aws-lambdade-sabaresufalsedao-ru-tunagfen-xi-ji-pan-falseshi-li-womotoni

TL;DR

スタメンが運営しているサービス「TUNAG」におけるデータ処理基盤にAWS Lambdaを導入することで、簡単に並列処理を実現しデータ処理時間を大幅に短縮することができ、サーバー運用コストから解放されました。今回はRuby × AWS Lambda × SAM を利用したアプリケーションの作成方法、自動テストについて共有します。

AWS Lambda活用の背景

スタメンが運営しているサービス、「TUNAG」は会社と社員・社員同士の
エンゲージメント(信頼関係)向上を
目的とした企業向けSNSです。TUNAGで発生したコミュニケーションなどのデータは、会社への関心や社員同士の関係性が反映されています。そのデータを分析することで、社内のエンゲージメント状況を定量的に測定することができます。このデータという根拠に基づいた社内活性化施策のPDCAを回すことができるというところが TUNAG の強みの1つです。そのため、データ処理基盤はこれからのTUNAGのビジネス上重要な鍵になっています。 しかし、エンゲージメントのような定性的なものを数値化するということはまだまだ不確実性が高いです。そのため頻繁に分析指標の見直しをすることがあるのですが、過去全期間分の指標の再集計ともなると既存のデータ処理基盤では8時間ほどの時間がかかってしまい、試行錯誤の回数に制限がありました。そこでAWS Lambdaのオートスケールを活用することで、複数のEC2インスタンスの管理をせずに並列処理を実現でき、集計時間を短縮することができました。また、スタメンではRubyに慣れているエンジニアが多いという理由から、AWS LambdaではRuby ランタイムを利用しました。この記事では、Ruby × AWS Lambda × SAM を利用したアプリケーションの作成及び自動テストの方法について共有します。

管理フレームワーク SAMを利用した開発

データ処理基盤のように一定の規模以上のシステムをサーバーレスで構築する場合、複数のAWS Lambda関数を管理する必要があり煩雑になりがちです。そしてRubyのコードをLambdaにデプロイする際にも、S3へファイルをアップロードしAWS Lambdaと紐付ける作業が必要で時間と手間がかかってしまいます。このような悩みを解決してくれるのが、AWS SAM(Serverless Application Model)です。

AWS SAM(Serverless Application Model)

SAMは以下の特徴を持つ、サーバーレスアプリケーションを構築するためのフレームワークです。 * サーバーレスアプリケーションの管理フレームワーク * CloudFormationのサーバーレスリソース特化の拡張 * アプリケーションのビルド・パッケージング・デプロイコマンドを提供 * ローカルでAWS Lambdaの構築・テスト・デバッグが可能

このため、複数のLambda関数をCloudFormationのようにコードで管理することができ、またCLIでデプロイを簡単に実行できます。具体的なSAMの使い方を見ていきます。

SAMとは

SAMの開発フロー

以下のように、sam init --runtime ruby2.5 とすることでランタイムがRubyに指定されたアプリケーションのサンプルの雛形が作成されます。ここで生成された template.yml というファイルにはAWSのサーバーレスリソースが記述されており、以下の AWS::Serverless::Function はAWS Lambdaの関数を表しています。

以下のSAMコマンド package を実行すると、対象関数のフォルダ(CodeUriプロパティ)の .zip ファイルを作成しS3へアップロードし、さらにローカルの生成物への参照をアップロードしたS3のURLに置き換えた packaged.yamlというファイルが作成されます。

$ sam package --s3-bucket tunag-sam-temlate-store

そして以下の deployコマンドを実行すると、テンプレートに記述されている通りのリソースがAWS上に構築されます。

$ sam deploy \
--template-file packaged.yaml \
--stack-name sample-functions \
--capabilities CAPABILITY_NAMED_IAM

Gemの扱いについて

RubyAWS Lambdaのアプリケーションを作成する際にも、通常のRubyアプリケーション開発と同じように便利なGemで開発効率を上げたいです。下記のsam buildコマンドは、アプリケーション内の関数を繰り返し処理し、Gemfileをもとに依存関係を解決し、Lambdaにデプロイできる成果物を.aws-sam / buildフォルダーに書き込みます。

$ sam build

Gemfileに応じて下記のコマンドが実行されるため、成果物のディレクトリ構成は以下のようになります。

$ bundle install --deployment --without development test

AWS Lambdaでは、以下のように vendor/bundleがgemの探索ディレクトリ対象(GEM_PATH)であるため、sam build コマンドで生成された成果物をデプロイするだけでAWS LambdaからGemを利用することができます。

# GEM_PATH
$LAMBDA_TASK_ROOT/vendor/bundle/ruby/2.5.0
/opt/ruby/gems/2.5.0

ネイティブ拡張があるGemの扱いについて

ネイティブ拡張があるGemは、Lambdaの実行環境と同等環境で成果物を生成する必要があります。 例えば、nokogiri が依存している libxml2 と libxslt はLambdaのイメージに含まれているので、以下のコマンドでビルド可能です。 buildコマンドに --use-containerオプションをつけると、Lambdaの実行環境と同等環境のコンテナ内ででビルドを実行してくれます。

$ sam build --use-container

それに対して、mysql2 が依存している mysql-devel はAWS Lambdaのイメージに含まれていないため、Lambdaイメージコンテナで必要なパッケージをインストールした上でbundle installをする必要があります。

$ docker run `pwd`:/var/task -it lambci/lambda:build-ruby2.5 \
/bin/bash -c "yum -y install mysql-devel; bundle install --path=ruby/gems;"

このとき、必要な共有ライブラリ(libmysqlclient)は、 LD_LIBRARY_PATHに配置することでLambdaから読み込むことができます。

$ LD_LIBRARY_PATH: /var/task:/var/task/lib:/opt/lib

AWS Lambda Layers

複数のAWS Lambdaの関数を組み合わせてシステムを構築していると、複数の関数の間で共通のロジックコードがでてきます。 AWS Lambdaでは、それぞれの関数に共通するコードをそれぞれの関数に対しアップロードする必要があります。また、ロジックを更新するたびに activesupport や mysql2 のGemのファイル群など、変更がないファイルをアップロードする必要があります。その結果、アップロードするファイルの容量が無駄に増え、容量の制限やデプロイ時間の遅延など問題が出てきます。そこで AWS Lambda Layers という機能が役に立ちます。AWS Lambda Layersは以下の特徴があります。

  • 複数のAWS Lambdaでコードを共有できる仕組み
  • Lambdaを呼び出すとLayersがコンテナの/opt配下にマウントされる
  • sam build は現時点で非対応

AWS Lambda Layers は、共有コードを管理したり、コードの変更頻度が少ないライブラリやGemを配布したりする場合に特に便利です。Layersのパッケージを複数のLambda関数に添付して使用することができるため、AWS Lambda関数のビジネスロジックが単純化され、依存関係管理が容易になり、デプロイパッケージのサイズを小さくできます。 また、AWS Lambdaのデプロイパッケージの最大サイズは50 MBですが、最大5つのLayersをアタッチすることで、250MBまで上限を増やすことができます。また、Gemや共有コードなどの依存関係とビジネスロジックの間で、関心事の分離を強制できることがLayersを利用することの副次的なメリットでもあると思います。

AWS Lambda Layers 参考URL

共通コード用をLayersで扱う方法は、以下のように template.yamlAWS::Serverless::LayerVersionというリソースを定義し、AWS::Serverless::FunctionのLayersプロパティから参照するだけです。

LayersにおけるRubyライブラリの探索パス(RUBY_LIB)は ruby/lib なので、以下のような構成でコードを配置すれば、LambdaからLayersを通して読み込むことができます。

LayersにおけるGemの探索パス(GEM_PATH)は ruby/gems/2.5.0 なので、以下のような構成でGemを配置すれば、LambdaからLayersを通して読み込むことができます。

img2lambdaを使ったLayersのデプロイ(おまけ)

Layersを作成する際に、依存関係の管理などが多少煩雑になっていました。この管理をもう少し簡単にする方法は無いかと色々探していた中で、AWSが提供している img2lambda というライブラリがあったので紹介します。 これは、DockerイメージをLambdaやLayersに変換しデプロイできるツールで、これを利用することで依存関係をイメージに閉じ込め
変更があれば再ビルドという形で簡単に管理できるようになります。とても便利なので使ってみたかったのですが、後述するSAMのAWS Lambda Layersのローカル実行機能が利用できなくなってしまうので、今回は採用を見送りました。

img2lambda

自動テスト

AWS Lambdaのコードについても普通のアプリケーションと同様に自動テストを書いていきます。ただ、AWS Lambdaという環境の特性を考慮し、以下のような方針で自動テストを行うことにしました。

  • クラウド上での検証はデプロイなどを含め時間が掛かり過ぎるのでなるべくローカルでテストしたい
  • AWS Lambdaは他のAWSサービスとの連携が多く統合テストが重要になる
  • IAM権限などの、クラウドでしか検証できないものに限りクラウド上でテストを行う

今回はAWS Lambdaをローカルでテストをする方法についてフォーカスします。AWS Lambdaのローカルテストにおいて、以下3点のポイントを考慮しテストを作成しました。

  • AWS Lambda特有の依存を排除し単体テストを行いやすいコードにする
  • AWSサービスに依存するロジックはスタブを利用する
  • ローカルにLambdaのエンドポイントを起動し、AWS Lambda・Layersの組み合わせでテストする

実際に、TUNAGのデータ処理基盤で想定されるテストケースとして、Athenaへのクエリが失敗した場合を考えてみます。テストの手順として、以下の1~3をコードと共に示します。

① Athenaへのクエリ実行APIを叩く ② クエリは非同期的に処理されるため
実行IDをもとに実行状況を問い合わせる ③ クエリの結果を取得し状態が
 “FAILED”なら例外を投げる

まずここで1つ目のポイント、AWS Lambda特有の依存を排除し単体テストを行いやすくするを考慮し、以下のようにLambdaに依存するハンドラーの引数などの箇所からロジックを切り離します。

そうすることで以下のように、Lambdaに依存しないコードになり、通常のRubyのクラスとしてテストが可能になりました。

続いて、Athenaのような他AWSサービスに依存するロジックをどのようにテストをすればいいでしょうか。ここで、2つめのポイントの AWSサービスに依存するロジックはスタブを利用 を実践します。AWSが提供する、AWS SDK for Ruby には 便利なスタブ機能があり、APIアクセスのレスポンス・エラーを簡単にスタブすることができ、AWS APIクライアントを実行するように振る舞います。

クライアントをスタブする方法は、以下のように、クライアントオブジェクト作成時に stub_responsesパラメータにtrueを設定します。そして、stub_dateメソッドでスタブデータを作成し、stub_responsesメソッドの引数に設定することで、特定のAPI呼び出しメソッドのレスポンスにスタブデータを設定することができます。 ここで作成したスタブオブジェクトを引数でテスト対象に渡すことで、ローカルでAWSサービス依存のテストができるようになりました。

AWS Lambdaのテストについては @t_wada さんの資料Testable Lambdaがとても参考になったので、興味のある方は是非見てみてください。この資料の中で利用されている LocalStackという、ローカルにAWSサービスと同等に振る舞うエンドポイントを提供してくれるサービスを利用してテストをしても良かったのですが、今回データ処理基盤に必要なAWS Athena・Glueは
非サポートだったため、AWS SDK for Ruby のスタブ機能を利用しました。

そして3つ目のポイント、ローカルにLambdaのエンドポイントを起動し、AWS Lambda・Layersの組み合わせでテストをします。ローカルでAWS Lambda・Layers を組み合わせてテストをする方法として、SAMにはローカルでLambdaのエンドポイントを起動するコマンドが用意されています。

$ sam local start-lambda

上記のコマンドを実行し、以下のようにエンドポイントをローカルに向け、テスト対象のLambdaをinvokeすることで、Layersを含めたAWS Lambdaのテストがローカルで実行可能になります。

まとめ

以上、Ruby × AWS Lambda × SAMにおけるアプリケーション作成・自動テストについて説明しました。通常のRubyアプリケーション開発と比べると考慮するべき事項が多く、決して簡単ではありませんでした。しかし、今回のデータ処理基盤において、AWS Lambdaの「オートスケール」、「従量課金性によるコストパフォーマンスの向上」、「サーバー運用コストからの解放」というメリットは、そのようなコストを優に上回ったのではないかと考えています。

また今回紹介した開発・自動テスト方法がベストだとは思いませんので例えば、SAMではなくServerless Frameworkならもっと簡単にできるよ、このツール便利だよ、とかありましたら是非フィードバックいただけると嬉しいです。

スタメンでは、Railsアプリケーションの開発及び、基盤システムをAWSで構築するエンジニアを大募集中です!興味をもたれた方は是非こちらのエンジニア採用サイトから気楽にご連絡ください。