新機能で Lambda を採用するために検討したこと

この記事は「弁護士ドットコム Advent Calendar 2022」の 14日目の記事です。 昨日は @shellme さんでした。

はじめに

こんにちは。 弁護士ドットコム株式会社のクラウドサイン事業本部で、スクラムマスターをしながらバックエンドの開発に携わっている @enkdsn です。

クラウドサインではアプリケーションの実行に多くの ECS on Fagate を使っており、メインとなるサービスのほかにバッチ処理を行うサービスが多く存在しています。 しかし、Fagate のコストは地味に高く、マネージドであることのコストを差し引いてもそれなりに痛いものになります。 そこで「ECS on Fagate ではなく Lambda を使うことでコストを節約していけるか」ということで、新しい機能開発で Lambda を採用するに至った話をできればと思います。

この記事で言及すること

  • Lambda vs ECS を検討するに至った背景
  • Lambda を選ぶために考慮したこと
  • Lambda の開発環境

この記事で言及しないこと

  • Runtime Interface Emulator(RIE) の使い方
  • 新機能のアーキテクチャ
  • 具体的な開発環境の構築方法

Lambda vs ECS を検討するに至った背景

クラウドサインでは ECS のクラスタ内で起動するタスクが多く存在しており、新機能で worker や cron が必要になった場合は ECS のクラスタに追加されていきます。それによりコスト面での問題が発生しつつあります。

伸びゆく ECS のコスト

直近の ECS のコスト

画像の通り、クラウドサイン内での ECS における vCPU の課金額が直近右肩上がりで上昇しています。 この要因として、クラウドサイン内で動くタスク数が増えてきているということが挙げられます。タスク数増加の背景は、クラウドサインが社会インフラとしての責任を果たすにあたり高可用性が求められるようになることへの備えをはじめたことにあります。これによって疎結合アーキテクチャを取り入れるようになり、各所で非同期のワーカーサービス等が立つようになりました。

ECS クラスタに worker や cron の多くが追加される背景

これは、クラウドサイン内での Lambda での開発事例の少なさと ECS での worker, cron の実績の多さにあると個人的に感じています。

クラウドサインでは、Lambda を採用した機能の事例があまり多くありませんでした。採用事例が少ない背景について明確な理由は見いだせていないですが、おそらく Lambda でのローカル開発の体験がコンテナのそれに劣っていたからということがあるかと個人的に考えています。私自身も SAM に慣れておらず、Lambda での開発は若干敬遠しているところがありました。

ECS であれば既存の worker や cron の Dockerfile や docker-compose.yml、.tf を使い回せていたため、わざわざ Lambda での開発環境を整えるよりもアプリケーションに集中しやすい状態でした。

しかし、そろそろ Faragate のコストも無視できなくなってきており、適切なユースケースがあれば Lambda を採用する必要が出てきました。このコストの問題はクラウドサインの SRE チームから掲示され、アプリケーション開発者もコスト面を意識するようになりました。これをきっかけに、私が参加しているチームではその流れに乗っかろうということで Lambda 採用を検討するに至りました。

Lambda を選ぶために考慮したこと

実際に Lambda を採用するためにいくつかの検討を行いました。 具体的には以下 3 点の確認・検討を行いました。

  • ユースケースの特性の確認
  • コンテナと Lambda の比較による検討
  • Lambda を採用するための追加検討

ユースケースの特性の確認

まず、新機能でのユースケースの確認をしました。新機能の特性は以下のようなものです。

  • この機能の使用頻度はそこまで多くない(四半期に1度この機能を複数回利用することを想定)
  • この機能のレスポンスタイムは短くなくてもよい(20秒くらいでも問題ないと想定)
  • この機能は一つのフロー上で完結させたい。つまり、"見かけ"は同期的である必要がある。*1

以上を踏まえると、

  • ECS で worker を動かす場合はコスパが悪い
  • 多少コンピューティングリソースが少なくてもよい
  • ユーザーフローは同期的にしたいので cron は採用しづらい

ということが考えられます。

すでに Lambda を採用すべきように見えますが検討を続けます。

AWS で提案されているディシジョンツリーの確認

ユースケースの確認によって、Lambda でいけそうというアタリをつけることができました。 次に、AWS で提案されているコンテナと Lambda のディシジョンツリーの確認をしました。

Lambda or コンテナのディシジョンツリー
https://d1.awsstatic.com/webinars/jp/pdf/services/202107_AWS_Black_Belt_Container350-Container_and_Serverless.pdf

ここでは以下の観点でディシジョンツリーを下っていきます。

  1. アプリケーションまたはプラットフォームのランタイム管理を望んでいますか?
  2. 短い実行タスク(15分未満)または非同期処理ですか?
  3. 使用するメモリは 10 GB 以下ですか?
  4. 特殊なハードウェアは不要ですか?
  5. ステートレス処理ですか?
  6. Lambdaバースト制限内ですか?

雑に回答を作るとこんな感じでしょうか

Q A
1 アプリケーションまたはプラットフォームのランタイム管理を望んでいますか? ランタイム管理は不要
2 短い実行タスクまたは非同期処理? Yes - 非同期であり、想定実行時間は長くて1分
3 10 GB 以下のメモリ? Yes - サービス仕様としてデータ数上限がある
4 特殊なハードウェアは不要? Yes
5 ステートレス? Yes
6 Lambdaバースト制限内? Yes - 同時実行クォータが 1000 (ap-northeast-1) あるので問題なし。ただし、VPC Lambda の予定なので ENI クォータ 250 を使い切らないことにも注意。

ディシジョンツリーを踏まえても Lambda で問題ないことがわかりました。

Lambda を採用するための追加検討

先述した検討を踏まえて、概ね Lambda でいいだろうという方針になりました。 ここから、技術的な要件に問題はないかについて検討をしました。

冪等性・排他制御

冪等性を担保することができそうか

Lambda は自動でリトライがありこれが大変便利なので、冪等性を担保できるか検討しました。 新機能ではマスターデータを元にデータベースを更新するということが求められていました。これは「何度実行してもデータベースをマスターデータが想定している状態にする」と考えることもでき、このようにアプリケーションを実装することで冪等性を担保できそうです。 経験上 Lambda のエラーでよく遭遇したのは一時的な DB のコネクションタイムアウトで、このエラーはリトライするとたいてい成功しますので、障害要因が一時的なものについては自動リトライで済ますという運用ができてかなり楽ができます。

排他制御はどうするか

先に述べたとおり「マスターデータを元にデータベースを更新する」ということを想定していたわけですが、同時実行による問題があるかについても検討しました。 たとえば、ユーザーAとユーザーBが、内容が部分一致するマスターデータを同時に一括処理に投げたらというケースなどです。 「レコードが作られたかどうか」だけに着目すれば、ユーザーAが投げたデータとユーザーBが投げたデータの和集合を正とすることで問題ないですが、期待値と実行結果がプレビューと異なるようなことはなるべく避けたいという要望もあり、同時実行をさせないようにする方針にしました。

これは、以下2つの方針をあわせることで実現することにしました。 * SQS の MessageGroupID を使用して同時実行を制限するようにする * 一括処理を管理するテーブルにて複合ユニークキーを貼ることで同時実行を防ぐ*2

厳密な排他制御は、後者の複合ユニーク制約によって実現されます。Lambda 起動後にまずレコードを更新し、その際に複合ユニークキーを貼ったカラムを更新します。もし別の Lambda が起動した場合はユニーク制約に引っかかり起動できません。加えて MessageGroupID による重複排除を行うことで、特定のチームに対する更新処理で Lambda の同時実行が発生しないようにしました。

RDS Proxy

Lambda を論じるにあたりよく言及される RDS Proxy ですが、今回は採用しなくてもよいだろうということになりました。 採用を見送った理由は以下です。

接続プーリングをあまり必要としていなかった

そもそも Lambda * RDS の相性が悪いとされる原因として、Lambda がデータベースのコネクションを使い潰してしまうということが言われていました。それを解消すべく提案されたのが RDS Proxy ですが、今回開発している新機能は高頻度で使われることを想定していません。したがって、Lambda の同時実行数が急激に伸びることでコネクションを使い潰してしまうことを想定しなくてもよいということで RDS Proxy の恩恵を受けづらいということがわかりました。*3

RDS Proxy が高い

安ければ接続プーリングができるし採用してもいいかなと思いました。 しかし、RDS Proxy を採用すると Lambda にしたコストメリットを潰してしまいかねないくらいに高くなりそうという見込みになりました。 Amazon RDS プロキシの料金 | 高可用性データベースプロキシ | Amazon Web Services

料金例にある通り、「RDS プロキシの料金は、Aurora クラスターの各データベースインスタンスの vCPU 数に相関します。」となっているので、大規模なサービスだと相当な額になります。今回の機能開発では Writer インスタンスにも Reader インスタンスにも接続する可能性があったため、例にあるとおり 43.20 USD かかる可能性もありました。これでは Lambda にすることで節約したコストを大きく上回る可能性があり、RDS Proxy を採用しないことにしました。

EFS

クラウドサインでは EFS を利用する必要がある場面があり、今回の新機能開発においても EFS が利用できる必要がありました。 実際はマウントは問題なくできることが分かったため、これについてもクリアになりました。

マウントにかかる時間や IOPS などについて細かく調べることはしませんでした。調べても出てこなかったのと、マウントポイントを介してアクセスするところは ECS on Fargate も共通であり、Lambda であることで大きなデメリットを抱えることはないという予想のもとです。*4

Lambda のトリガー

今回はマスターデータを S3 に保存するので、S3 の Put イベントをトリガーにするという方針も取れましたが、以下を理由に SQS にしました。

  • マスターデータの他に必要なメタデータが存在する
  • MessageGroupID による同時実行制御の必要性

前者は SQS のメッセージにメタデータを埋めることで解決させる必要がありました。また、排他制御の項で言及したMessageGroupID同時実行の制限もできれば欲しかったので、今回は SQS のメッセージングをトリガーにすることにしました。

Lambda 採用を後押ししてくれたもの

「Lambda を採用するための追加検討」の部分で技術的な要件を満たせることも確認できたのですが、根本の問題だった「開発環境」が解決していませんでした。納期や開発者体験を優先すれば ECS でもよかったのですが、Lambda のコンテナイメージのサポートや同時に出た Runtime Interface Emulater を使用することで開発者体験を損なわないことがわかりました。

コンテナイメージのサポート

もう2年前にはなりますが、Lambda はコンテナイメージをサポートするようになりました。 余談ですが、サポートした背景として、コンテナツールに投資した場合の Lambda アプリケーションの構築について言及されており、先述した Lambda が採用されづらかった背景に通ずるものがあるなと感じました。

AWS Lambda では、サーバーについて気にすることなくコードをアップロードして実行できます。多くのお客様に Lambda のこの仕組みをご活用いただいていますが、開発ワークフローのためにコンテナツールに投資した場合は、Lambda でのアプリケーションの構築に同じアプローチを使用することが難しくなります。

https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/

コンテナイメージのサポートによって docker compose のエコシステムを使って開発できるようになり、アプリケーションの構築方法の差を気にしなくてよくなりました。 Dockerfile を書いて、docker-compose.yml を書いて、Lambda をエミュレートするコンテナを立ち上げたらそこへ curl をすれば Lambda が起動するといった流れは、いままでのコンテナによる開発と遜色ありません。 実際の開発では、nats に流れてきたメッセージを吸って curl するデーモンを起動しておくようにしているので、デーモンの起動と Lambda のコンテナを docker compose up すれば動作するようになっています。Localstack を使うという選択肢もありますが、SQS をトリガーにしている都合で Localstack は Pro にしないといけなくなり、加えて不便さを感じていないのでこの形に落ち着いています。

まとめ

以上の検討を経て、実際に Lambda を採用し実際に新機能開発を進めています。

まだリリースされていない機能であるため、実際にコストがどれくらい軽減されたのか等は今後判明することになりますが、きっと安くなるでしょう。(それは来年のアドベントカレンダーに書かれるかもしれないし書かれないかもしれない。)

開発者体験の問題についても、Lambda がコンテナイメージに対応したおかげで今までのコンテナによる開発と遜色なく、キャッチアップのコストがかなり少なく済んだと実感しています。

Lambda やそのまわりのエコシステムの進化によって「コンテナ開発は ECS/EKS」という認識はとうに過去のものになっていると感じさせられました。サービス数が増えつつあるなかで、アーキテクチャを検討する上で Lambda vs ECS を考慮する機会は多く存在すると思います。少しでも参考になったら嬉しいです。 それではみなさま、よきサーバーレスライフを。

明日は @dskymd さんです。お楽しみに。

参考文献

AWS Lambda の新機能 – コンテナイメージのサポート | Amazon Web Services ブログ

Lambda 関数のスケーリング - AWS Lambda

Amazon SQSメッセージ重複排除ID の使用 - Amazon Simple Queue Service

サーバーレスが気になる開発者に捧ぐ「べき等性」ことはじめ 第一回〜べき等性 (冪等性/idempotency) ってなんだ!? - builders.flash☆ - 変化を求めるデベロッパーを応援するウェブマガジン | AWS

Lambda で Amazon EFS を使用する - AWS Lambda

*1:ここでいう”見かけ"は同期的という表現ですが、画像アップロードでいう「アップロード→ローディング→完了」のように、ローディングをはさみつつユーザーへの操作を継続させたいという意図を持ったフローを意味しています

*2:ここで複合ユニークキーとしているのは、チーム内での排他制御を実現するためです。もし全体で一つの処理しかしないのであれば単一のユニークキーで問題ないです。

*3:もし RDS Proxy を"使わない"場合、コネクションの切断はコード側から明示的に行う必要があります。VM が落ちるまでは DB とつながりっぱなしになり、一定期間コネクションが右肩上がりになって上限に近づいていくモニタを見ると震えます。単一の DB でサービスを動かしている場合は、Lambda でコネクションを切らなかったばかりにメインとなるサービスを落としてしまうこともあるので十分に注意してください。

*4:あくまで予想のですので、もし問題があった場合は追記します。