ちょっと話題の記事

[レポート]サーバレスアプリケーションのコツ総ざらえ!(SVS401-R Optimizing your serverless applications) #reinvent

2020.02.10

「サーバーレスアーキテクチャのフルパワーを解き放つ便利なガイド、欲しくない?」
って言われたらそりゃ欲しいですよね!これはこのセッションの紹介文にあった一節です。この言葉に釣られて参加したセッションのレポートをお届けします。「まだre:Inventの話してるのかよ!」とツッコまれたあなた、おっしゃるとおりです。ですが二ヶ月ほど経った今でも十分有用な情報ばかりなのでぜひご一読いただければと思います。紹介文に偽りはなかったです!

セッションタイトル

SVS401-R1 - [REPEAT 1] Optimizing your serverless applications

セッション概要

あなたは経験豊富なサーバーレス開発者ですか?本番ワークロード用のサーバーレスアーキテクチャのフルパワーを解き放つ便利なガイドはご入用ですか?イベントソースとしてストリームかAPIどちらを選ぶべきか疑問に思ってる?一関数にするかたくさんにするかも?
このセッションでは、アーキテクチャのベストプラクティス、最適化、そしてあなたがセキュアで高スケール、高パフォーマンスなサーバレスアプリケーションを構築する際に使える便利なチートコードについて話します。実際の顧客シナリオを使用して、メリットを説明します。

翻訳元

スピーカー

CHRISTOPHER MUNNS Sr Manager / Principal Developer Advocate – Serverless

セッションレポート

目次 兼セッション要約

※各項目にはページ内リンクを貼っていますが、粒度や話す順がこれ通りになっているわけではないです

あなたの関数について

実行環境について

Lambdaサービスについて


以下、セッション内容

本日のテーマはサーバーレスアプリケーションの最適化です。アプリケーションがどういった要素で構成されているのかの理解が重要です。Lambda関数には以下のようなスタックが存在します。

本日はこの5層のうち以下の3層にフォーカスします。

関数自体=あなたが書くコードと、実行環境、つまりインフラやイベントなども含め関数をどう設定するのかと、最後にLambda サービス自体、re:Invent中に発表されたより良いパフォーマンスを手に入れるための微調整方法についてお話しします。

それではあなたの関数についてから始めましょう。

あなたの関数 (Your Function)

Lambda関数には以下の基本的な概念があります。

handler関数

必須。イベントが発生した際にコード内のこの関数を実行します。

イベントオブジェクト

Lambda関数呼び出し時に送られるデータ。JSON。

例えばS3で発生した呼び出しの場合、バケット名、キー名、オブジェクトに関する情報などがこの中に含まれます。

コンテキストオブジェクト

ランタイム情報とやりとりするためのメソッド


ここから擬似コードを使って説明していきます。

Lambda関数の中には必ずhandlerがあります。

Lambda関数に持たせうる別の機能の一つが、handlerが実行される前にコードを実行することです。

パッケージをインポートしたり、変数を宣言したり、他のサービスと通信したりDB接続するような他の関数をコールすることもできます。

つまり、このエリアは依存関係や設定情報、共通ヘルパー関数などを記述します。

この部分についてのベストプラクティスがあります。

pre-handler、依存関係、変数のベストプラクティス

必要なものだけインポートする

Lambdaのパフォーマンスはそのコード内容と非常に密接に関わっています。必要ではないパッケージをインポートしたり、開発環境では使うけど本番環境では必要ないものを入れるのはパフォーマンスに悪影響を与えます。

pre-handlerで接続確立するのはオススメだが、再接続処理を準備する必要あり

pre-handler内でDBとの接続処理を行ったり、機密情報を別のリソースから取得したりする方法はよく見られる手法です。しかし、再度handler内でその処理を行なう準備をしたほうが良いです。 pre-handler内の処理はコールドスタート時にしか実行されないので、ウォームスタートで関数実行された際にはDB接続がタイムアウトしていたり、機密情報値が更新されていたりする可能性があります。

※この部分についての詳細は、以下エントリもご一読ください。

グローバルスコープの変数の遅延ロード

例えば80%パスA、20%パスBに行く分岐処理がコード内あった場合、(pre-handler内で)初期化すべきはパスAについての処理のみで、パスBはその処理実行時に初期化、インポートなどをしたほうがよいでしょう。

必要ないものは読み込まない

pre-handlerの処理はすべてcold start(の処理時間)に影響を与えます。

使用した変数を空にする(初期化する)

Lambda実行環境は再利用される可能性があります。 処理実行後の後掃除をちゃんとしておかないと、前回の処理実行後の状態に、次の処理が引きずられる可能性があります。


pre-handler内で実行する、けれどもコード内(handler内)でも実行するかもしれない処理群があります。構成情報の取得であったり、DB接続処理だったり、、

こういった処理群を Common helper functionsと呼ぶことにします。

構成情報に関して、その取得方法は二つの主要な方法があります。コード内にハードコードするのはその二つに含まれませんよ。一つ目はLambda環境変数です。

Lambda環境変数

  • 動的に関数に渡せるKey-Value値
  • 言語の標準環境変数として利用可能。Node.jsではprocess.env、Pythonではos.environ
  • 開発、テスト、本番など複数環境作成する際に便利
  • 欠点
    • 関数ごとに設定しないといけない。関数間で共通のパラメータもあるはず
  • KMS経由の暗号化も可能
    • 欠点
      • 関数にKMSキーへのアクセス権を付与する必要がある
      • 暗号化・複合化に少し時間がかかる

System Manager-Parameter Store

より一元化できる方法が二つあります。一つがParameter Store、もう一つはSecrets Managerです。

Parameter Storeには以下の特長があります。

  • 階層化できる
  • プレーンテキスト、もしくはKMSで暗号化したものも使える
  • CloudTrailでロギング SNS/Lambdaに変更通知
  • IAM policyできめ細やかに権限制御できる
  • APIやSDKから使用可能

Secrets Manager

  • 上記Parameter Storeの特長も持つ
  • RDS含むたくさんのデータベースサービスと統合されている
  • 機密情報のローテーション機能
    • パスワードローテーションポリシー、保管ポリシーなどを自動実現できる

Parameter Store・Secrets Managerの使用により

  • Lambda関数間でシークレットを共有できる
  • コンテナ、EC2インスタンスとも
  • APIを叩ければAWS外のリソースとも共有できる

Lambda関数内でよく見る最後のものは、ビジネスロジックです。

ビジネスロジックはhandlerから切り離すのをお勧めします。別の場所に持っていったり、シェアしたりしやすくなるからです。

ここまで見てきたように、プログラム言語に関わらずLambda関数内にはこれらの共通の4タイプのコードがあります。


ここからはAPIベースのワークロードを想定してお話しします。API Gatewayがあり、単一のAPI関数を単一のLambda関数にマッピングするプラクティスに従っていくつかのAPI関数があり、各Lambda関数はDB接続があり、Secrets ManagerやParameter Storeも使っています。図にすると以下のようになります。

Lambda関数内に、たくさんの重複コードが発生します。各関数には先ほどご説明したような4タイプのコードがあります。handlerと、ビジネスロジックのいくつかは一意かもしれません。が、 pre-handlerコード、helper functionのコードは大抵重複します。

どうすればこういったコードを減らせるでしょうか。Lambda Layerを使いましょう。

Lambda Layerを使えば、共通コードを束ねて、各関数で利用できます。関数毎に最大5つのLayerを参照可能です。

Lambda Layerの良い点

イミュータブル

Layerを更新した場合、更新したものにversion IDが発行されます。Layerを使う関数側はどのversion を使うかIDを指定する必要があります。そのため関数のライフサイクル全体を通じて基本的に静的であると言えます。

関数のサイズを小さくできる

Layerに共通処理コードを移すと、(Layerを呼び出す側の)関数のデプロイパッケージサイズを小さくできます。
※デプロイパッケージサイズの上限は、Layer含めての合計値に対してかかります。(解凍後250 MB以下)
(参考)AWS Lambda の制限

Lambda Layer 使い方のコツ

関数間で共通の依存関係やパッケージをLayer内で扱うのは便利です。アップデートが必要な時はその中の一つのファイルに対して行えばよいだけです。そのファイルを含む別のレイヤーを追加すれば、その(アップデートが必要な)ファイルをオーバーライトできます。cherry-pick的なパッチ当てやアップデートに役立ちます。

というわけで、Lambda Layerによって、先ほどご紹介した4タイプのコードいずれも関数からLayerに移動させ、共通化できます。

Layer化にパフォーマンスペナルティはありません。パフォーマンス改善もありません。ですのでLambda Layerは目立たない存在ではありますが、開発の観点から見れば生活を少し楽にするのに役立ちます。


再びこの図にもどります。

この図の中で私が選択したもの(技術)の一つがDynamoDBです。サーバレスの世界では多くの場合DynamoDBやリレーショナルでないデータベースが好まれています。私たちはお客様からこのような声をよくお聞きします。「RDBの場合はどうすれば良いですか?」「(RDBの場合)どうやって接続や再接続を制御すれば?」

Amazon RDS Proxy

そういった声にお応えするために、新サービスAmazon RDS Proxy をリリースしました。

  • リレーショナルデータベース向けのプロキシサービスです。
  • 現在はプレビュー版です。
  • MySQLとAurora MySQLをサポートしています。
  • Lambda関数はプロキシに接続し、プロキシはDBに接続します。
  • Secrets Managerとも統合しています。ですので機密情報の管理の類について心配する必要はありません。
  • プロキシに対して、IAM認証も使えます。
  • これまで、Lambda関数のスケールに対処するためRDSインスタンスのインスタンスタイプを余裕の持った(スペックの)ものにしている、との声をよく聞きました。が、もはや必要なくなります。プロキシのおかげでもっともっと少ない接続数で済むようになります。
  • HAフェイルオーバーの制御するのにも役立ちます。

Less code > more code

次にお話しするのは、少ないコード量は多いコード量より良い、というわかりやすいコンセプトです。Lambdaとサーバーレスに関しては我々はいつも「less is more」に立ち返ります。

Lambda関数にはコード量を減らす方法がたくさんあります。ひとつ鍵となるのは、Lambda関数をTransformのために使い、Transportのために使わない、という考え方です。どういう意味でしょうか?

Transformのために使い、Transportのために使わない

特定のサービス、例えばS3、Kinesisとか、あるいは何かのAPIからデータを取得する状況はよくあります。そして、別の場所に移したい。RDB、S3、Elasticsearch、DynamoDBなど。これらはLambdaのあまりよくないユースケースです、何かしらのデータのtransform(変換)を行う場合を除いて。

AWSにはたくさんの専用サービスがあります。これらを使うほうが良いでしょう。

  • Kinesis firehoseがあります。これを使えば自動的にデータをS3やDBサービスに格納することができます。
  • GlueでETLワークフローを実現できます。Lambda関数内で処理する必要はありません。
  • などなど

可能な限り、2つの場所間でデータを移すような処理は避けましょう。

別の良い例はAPI Gatewayです。API Gatewayに直接別のAWSサービスを接続できます。例えばDynamoDBなどです。API GatewayとDynamoDB間にLambda関数を設置し、関数内でCRUDインターフェースを実装する代わりに、API Gatewayから直接DynamoDBにアクセスできます。

再試行とエラー処理をサービスに任せる

次に鍵となるのは再試行とエラー処理をサービスに任せる ことです。この点について、後ほどいくつか新機能の紹介をしますが、それらの新機能によって関数内からたくさんのコードを取り除くことができます。

必要なデータのみ扱う

関数内で扱うデータを可能な限り減らすことも重要です。DBやデータストアからデータを取得する際、全情報を取得するのを避け、フィルタリングを行いましょう。でないと過剰コストになり非効率的です。例えば以下のような方法があります。

  • SNSでメッセージフィルタリング
  • EventBridgeのきめ細やかな(フィルタリング)ルール
  • Auroraでのクエリフィルタ
  • S3 Selectを使う
  • 適切にインデックスが張られたDB
業務ワークフローが一本道であることはめったに無い

別の観点のお話をします。業務ワークフローが一本道であることはめったにありません。単にAからBに遷移するだけのワークフローはほとんど存在しません。決定木のようなものが必要になったり、処理失敗時のハンドリングやリトライ、Exponential Backoff のリトライが必要だったりします。 そこで、Step Functionsをおすすめします。

Step Functionsで業務ワークフローを取り扱うことができます。Aの処理の後、その結果によってBの処理かCの処理どちらを行うか制御したり、BとCどちらも実行してその結果をつなぎ合わせたり、Bの処理が失敗した場合に特定の処理を実行するなど、すべての関数内に記述していた決定木ロジックをStepFunctionsに持ってくることができます。

Express workflowという新機能をお知らせしました。Step Functionsをより早く、より軽量にそしてより低価格にします。

Step Functionsが最初にローンチされた際には、その主たる統合先はLambda関数でした。が今はたくさんのAWSサービスと統合されています。

  • SQS
  • SNS
  • ECS
  • Fargete
  • SageMaker
  • AWS Batch
  • Glue
  • EMR [NEW!]

これにより、あるサービスから別のサービスにデータを移す処理を自分で書く代わりに、その処理を行なうStep Functionsを置くことができます。

以下図がその例です。

バッチワークフローの例です。バッチをキックし、処理が終わるのを待ち、結果に応じてSNSで送るメッセージを変えます。

左がLambdaでの実装例です。複雑ですね。一方で右の図を見てください。Step Functionsの実装例です。AWS Batchを呼んでいます。AWS Batchの結果に基づきSNSに成功のメッセージ、失敗のメッセージどちらを送るか分岐させることができます。Lambda関数は不要です。

新機能 Lambda Event Destinations

re:Inventの2週間前に広報された新機能、Lambda Event Destinationsをご紹介します。

デッドレターキューという機能があります。Lambda関数実行に失敗した際に、リトライポリシーに沿って失敗イベントを保存し、再実行や状況把握に利用することができます。とても便利です。が、お客様から、特に非同期ベースのワークフローをお持ちのお客様から「関数実行成功も把握したい」との声をいただいています。

例をあげます。S3に画像や文書をアップロードするワークフローがあるとします。Lambdaがそのファイルを処理します。ログを吐いたり、別のAPIをコールしたり、どこかにメッセージを残したり…これらは情報をtransportするコードで、transformではありません。

今後はDestinationsを使えます。処理の成功/失敗に応じて伝搬先を変えることができます。伝搬先になるのは以下の4つです。

  • Lambda関数
  • SNS
  • SQS
  • EventBridge

最高のパフォーマンスを出すLambdaは…

ここまでお話したことのまとめです。最高のパフォーマンスを出すLambda関数はコードが無いものです。(=任せることができる箇所はコードを書かずに各種サービスに任せることでパフォーマンスアップができます。)StepFunctionsをご紹介しました。Event Destinations等々も。これらを活用することによりコード量を減らすことができます。私は100%Lambdaでできたアプリケーションが好きですが、別のサービスを活用することでLambda関数の数をずっと少なくて済むようにすべきです。

「あなたの関数」についてのまとめ

  • 依存関係を最小化する
  • 控えめに、かつ戦略的にpre-handlerロジックを使う
  • アプリのスコープに基づいてシークレットを共有する
    • 単一関数:環境変数
    • 複数関数/共通環境:パラメーターストア
  • 変数、接続、依存関係の再利用が与える影響を考慮する
  • Layer(Lambda Layer)はコードの重複を避けることができ、関数間の標準化の実現に役立つ
  • RDS ProxyはLambdaとRDBの組み合わせシンプルにする
  • 簡潔なロジック
  • オーケストレーションをStep Functionsに任せる
  • Lambda destinationsは非同期ワークフローの単純化、改良に役立つ

実行環境

つづいてLambdaの実行環境についての話にまいりましょう。

こちらはLambda関数のライフサイクルを表した図です。左側の赤い箱の箇所がフルコールドスタート時のみに処理される部分です。直近使われた実行環境が存在している場合は、セミコールドスタート(パーシャルコールドスタート)やウォームスタートとなります。左半分がAWSにより最適化が行われる部分、右半分があなたが最適化を行なう部分です。


X-Ray

計測なしではパフォーマンスの改善はできません。X-RayはLambda、API Gateway、他のサーバレスサービスと密接に関わるサービスです。X-Rayを使えば簡単に関数のパフォーマンスを見ることができます。

この図はX-Rayの例です。それぞれの円を深堀りすることで、アーキテクチャ内部で何が起きているか、より詳細な情報を取得できます。

個々の関数については、関数内で何が起こっているかウォーターフォール図を見ることができます。SDKでカスタムセグメントやカスタムアノテーションというものを作成できるので、計測したい任意のコードをそれらで囲って特定のブロックのコードのパフォーマンスの計測もできます。

X-Ray Trace Maps

re:Invent 一週間前にアナウンスされた新機能です。単一のリクエストについて、分散システム内のトレースを可能にします。先程お見せした図は収集されたトレースの集計です。が、Trace Mapsで1リクエストのLambdaや他サービスを通った実行結果トレース結果を確認することができます。


関数のコンピューティングパワーを微調整する

Lambda関数のパフォーマンスを上げるために、関数に割り当てられるメモリ量を調整することができます。メモリ量を128MBから増やした場合、同時にその増加量に比例したCPUリソースやネットワーク帯域も増えます。

あなたのコードがCPUバウンド、ネットワークバウンド、もしくはメモリバウンドである場合、メモリ量を増やしたほうがより安くなる可能性があります。

例を挙げます。百万以下の全素数を算出する処理を1000回実行する関数があるとします。これは典型的なCPUバウンドな関数です。この関数を、様々な異なるメモリ設定で実行し測定しました。以下がその結果です。

処理時間(=パフォーマンス)については、メモリを増やすほどに改善が見られました。一方でコストに目を向けると「ゆらぎ」があることがわかります。どれが最善かと一概に言うことは難しいです。しかし、128MBと1024MBだけでくらべてみましょう。

1024MBで関数実行した場合、128MBの場合と比べて10.25秒、処理時間を短縮できています。これは128MBでの実行時の処理時間の約90%にあたります。コスト増加は1実行あたりたった$0.00001です。 $0.00001のコスト増で10.25秒の処理時間を削減することが良いことかお考えください。

マルチスレッド?

  • 最大のメモリ割り当ては3GB
  • 1.8GB以上のメモリでCPUのコアは2になる
  • それ未満はシングルコア

VPC Lambdaのパフォーマンス

過去、Lambda関数のパフォーマンスで気になることといえば、VPCへの接続処理でした。が、9月にVPC接続とLambda関数に関する新しいモデルをアナウンスしました。ENIをLambda関数のためにあなたのVPC内に作成し、Lambda関数はhyperplaneと呼ばれる技術を、いわゆるVPCからVPCへのNATを作成するために使います。

  • 劇的に効率的になった
  • 劇的にネットワークリソース使用量が減った
  • 劇的に早くなった(例:14.8秒かかっていた処理が933ミリ秒になった)

この改良によって、VPCLambdaのコールドスタートが1秒以内、大抵は500ミリ秒以内で完了するようになりました。これは劇的な改善です。現在すべてのパブリックリージョンで利用できます。


Events and you

ここからイベントに関してお話します。

Lambdaのユニークな点の一つは、ポートやソケットを通じて接続できない点です。基本的にAPIを通じてLambda関数とやり取りします。APIがコンピューティングリソースにアクセスする唯一の方法という意味ではありません。各種SDKやCLIにAPIは組み込まれています。

Lambda 実行モデル

SDKでサポートしている二種類の実行モデルがあります。

一つは同期モデルです。例はAPI Gatewayです。API GatewayがLambda関数を呼び出し、Lambda関数がレスポンスを返します。

S3やSNSといった非同期モデルもあります。メッセージやオブジェクトがLambda関数に渡されますが、S3やSNS対してレスポンスを返しません。これは新機能Event Destinationsが役立つ部分です。

3つ目の選択肢はいわゆるストリームベースのモデルです。これは非同期モデルの組み合わせです。が、すこしやり取りの仕方が異なります。ストリームベースのモデルはKinesisやDynamoDB Streams、SQSで使われます。ポーリングをして、Lambda関数を呼び出し処理します。2週間前、SQS FIFOサポートをアナウンスしました。これは非同期イベントソースに関して最もリクエストが多かった機能の一つです。

お客様が作ったサーバレスアプリケーションは、同期的なワークフローにかなり依存していることが多いです。そしてその同期的なワークフローで問題を抱えている場合が多いです。何がいけないのでしょうか?

上記のようなシンプルなAPIがあったとします。バックエンドには1リソースだけ存在し、それが同期的にレスポンスを返します。

エラーが起きた場合もシンプルです。エラー後にクライアントは再度サービスを呼び出します。

今度はもう少し複雑な例です。クライアントは最初のサービスを呼び出し、そのリクエスト処理の中で、最初のサービスは次のサービスを呼び出します。この場合、エラーが発生しうる箇所は以下になります。

こうなってくると、以下のようなことを考えなくてはなりません…

  • 誰がリトライを行なうのか?
  • 誰がエラーをリポートするのか?
  • クライアントからはどう見えているべきなのか?
  • どれだけの時間リトライを待つべきか?

などなど。たくさん失敗パターンがあるせいで、仕様が複雑になっていき、コード量が増えていきます。これは先程お伝えした「Less code > more code」に反しますね。

ですので、非同期モデルへの移行を検討していただきたいです。

  • 最初のサービスが2つ目のサービスを呼び出す
  • 最初のサービスが(2つ目のサービスのレスポンスを待たずに)クライアントにレスポンスを返す

先程の例でいうと、以下のように作り変えましょう。

  • クライアントが最初のサービスを呼び出す
  • 最初のサービスが(2つ目のサービスのレスポンスを待たずに)クライアントにレスポンスを返す
  • クライアントが2つ目のサービスの情報(データ登録情報など)を知りたい場合は、クライアントから2つ目のサービスを直接呼び出す

「こんなことしたらUXが損なわれるよ!」という声をよく耳にしますが、amazon.comでも非同期処理がいたるところで活用されています。多くのケースで採用できるはずです。

また、レスポンスが不要な場合も非同期処理を採用しましょう。LambdaのSDKで実現できます。


非同期コミュニケーションに役立つサービスが4つあります。

  • SNS
  • SQS
  • EventBridge
  • Kinesis Data Streams

どれがあなたのワークロードにフィットするでしょうか。そこには様々な観点があります。全部お伝えしようとすると半日かかるかもしれませんので、6つの鍵となる要素についてご説明します。

  • スケール/並行性 のコントロール
  • 耐久性
  • 永続性
  • 消費モデル
  • リトライ
  • 価格

並行性についての例をご紹介します。

  • SNSは、データが渡ってくるとすぐに次のサービス(Lambda関数など)にデータを渡します。つまりSNSは非常に高速なスループットを持ちます。
  • SQSはスタンダード/FIFOどちらでも、キューからオブジェクトを一括で取得します。つまり、あなたのLambda関数は一括処理を行ないます。
  • Kinesisには2つのメカニズムがあります。ストリーム内のシャードの数と、各シャード内で一回の処理で受け取るイベントの最大数です。非常に多くのイベントを並列に、順番に処理することができます。

以下は最近発表された非同期イベントソースに関するアップデート情報です。

直接的 vs オブザーバブル(観測可能な)イベント

我々がEventBridgeとしてローンチした別のモデルがあります。違いは、直接的なイベントとオブザーバル(観測可能な)イベントにあります。SNS、SQS、Kinesis、これらは直接的に次のリソースにデータを送ります。イベントを停止するフィルタ機能はあまりありません。

しかしメッセージを複数箇所に送りたい場合があります。そういった場合にイベントをよりオブザーバブルにできます。イベントはブロードキャストされ、送信対象それぞれに送信する必要はありません。

EventBridgeでたくさんのサービスにわたるファンアウトを実現できます。StepFunctionとも連携できるので、複雑なワークフローとも連携可能です。

Schema RegistryとSchema Discoveryいう新機能を発表しました。(プレビュー)
イベントのJSONスキーマをここに登録でき、検索できます。それらのスキーマをEventBridge内で使用できます。コードバインディングという機能を使うと、そのイベントをパースするコードを自動生成してくれます。各種IDEと統合していますし、SAMとも統合しています。EventBridge経由のイベントをパースするのはより容易になります。

Lambda関数ごとの同時実行数制御

インフラ内でイベントを処理する方法を制御するという点での別の機能は、関数の同時実行制御と呼ばれる機能です。特定の関数を実行できる実行環境の数に制限を設けることができます。

  • 劇的かつ急速にスケールアップするワークロードがダウンストリームのリソース(RDSやほかサービスのAPIなど)に対して実質的にDDoS攻撃をしてしまうことを抑止するのに役立ちます。
  • kill switchと呼ばれる用途にも使えます。ダウンストリームのサービスが壊れた、もしくはメンテナンスウインドウのような状態になっている場合に、一時的に同時実行数をゼロにして、普及次第もとに戻すというようなことができます。

Lambda デッドレターキュー

Lambdaで非同期ワークフローを扱っているのなら、デッドレターキューもしくはEvent Destinationsをイベントの失敗に備えて有効化するのを激しくおすすめします。イベントの失敗が起きなければ、コストは発生しません。イベントの失敗が起きれば、それをキャプチャーし反応することができます。あなたの関数に渡されるイベントもしくはデータをセーブするのに非常に役立ちます。

Lambdaのpermission policy

  • Function Policy
    誰がLambda関数を実行できるか

  • 実行ロール
    Lambda関数がAWSクレデンシャルを使って何ができるか

ただし、IAMの機能を全て理解するの非常に困難で、多くの時間が必要です。この点を平易にしてくれるツールもしくはサービスとして、AWS SAMを作りました。

AWS Serverless Application Model (AWS SAM)

AWS SAMはサーバレスアプリを作成、管理、更新するためのテンプレート駆動のサービスで、CloudFormationを拡張したものです。そのためCloudFormationの全シンタックスを利用することができます。さらに、プロセスを非常にシンプルにするサーバレスアプリ固有のリソースが追加されています。

SAMのテンプレート例です。約20行です。これだけで、Lambda関数、API Gateway、DynamoDBテーブル、そしてこのアプリケーションを動作させるために必要なIAMポリシー が作成されます。もし同等のリソースを作成するCloudFormationテンプレートを書いたとしたら、100行程度になってしまうでしょう。

この例の中間ほどにあるPoliciesセクションに注目してみます。

DynamoDBReadPolicyという名のポリシーを使っています。TableName という属性を持っています。ご想像の通り、これは特定のDynamoDBテーブルの読み取りをLambda関数に許可するポリシーです。これはポリシーテンプレートと呼ばれるもので、SAMで我々が行ったことは、サーバーレスアプリケーションに関するもっとも普遍的な50超のユースケースを単純化し、参照オブジェクトを動的に渡すことができるIAMポリシーを作成することです。先程の DynamoDBReadPolicy は以下のCloudFormationテンプレートになります。

"DynamoDBReadPolicy": {
  "Description": "Gives read only access to a DynamoDB Table",
  "Parameters": {
    "TableName": {
      "Description": "Name of the DynamoDB Table"
    }
  },
  "Definition": {
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "dynamodb:GetItem",
          "dynamodb:Scan",
          "dynamodb:Query",
          "dynamodb:BatchGetItem",
          "dynamodb:DescribeTable"
        ],
        "Resource": [
          {
            "Fn::Sub": [
              "arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tableName}",
              {
                "tableName": {
                  "Ref": "TableName"
                }
              }
            ]
          },
          {
            "Fn::Sub": [
              "arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tableName}/index/*",
              {
                "tableName": {
                  "Ref": "TableName"
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

こういったポリシーテンプレートはいわばIAMのチートシートです。SAM以外のツール、例えばServerless FrameworkやTerraform等々を使ったとしても、このチートシートからあなたのケースにあてはまるポリシーを見つけて、参考、活用して下さい。これはSAMのGitHunリポジトリにあります。

実行環境について まとめ

  • より多くのメモリ = より多くのCPUとI/O(比例する)
    • 安価にもなり得る
  • ワークロードをプロファイルするためにX-Rayを使おう
  • 1.8GB以上のメモリ→2 (CPU) コア 使わない/必要ないかもしれないけど
  • 実行モデルと呼び出し元のニーズについて熟考しよう
    • すべてがAPIである必要はない
  • 非同期処理を考えると、スケーリングの課題のうちの最大のものをいくつか克服できる
  • キュー、トピック、ストリーム、イベントバスを使う際はその様々な側面を理解しよう
  • IAM パーミッションのスコープを最小化する
    • AWS SAMのようなツールを活用する

Lambdaサービスについて

もう一度Lambda関数のライフサイクルについてお話します。Lambda関数がリクエスト受けた際、もしまだリソースが用意されていなかったら、私達はコンピューティングリソースを取ってきて、それをあなたのアカウントに割り当て、あなたのコードを引っ張ってきて、実行環境をブートストラッピングし、pre-handlerコードを実行します。あなたのビジネスロジックが実行される前に、これらのことすべてが必要です。

お客様からLambda関数の事前暖気を試みたという話を聞きます。cronで定期的に関数を叩くことで、事前暖気された実行環境が利用可能になります。しかしながらこのやり方は2つの観点から非効率です。

  • このやり方はAWSが事前暖気した実行環境を保持し続けてくれることを前提にしていますが、その保証はありません。
  • 事前暖気処理は実際に関数を実行していますので課金が発生しますし、同時実行数も消費するので他のリクエストの実行をブロックする可能性があります。

そこでProvisioned Concurrency for Lambda(プロビジョニングされた同時実行)という新機能をリリースしました。この機能によってイベント処理に必要な利用可能な実行環境の数を設定できます。AWSはコンピューティングリソースを用意してそれをあなたのアカウントに割り当て、あなたのコードを引っ張ってきて、ランタイムを起動します。つまり初期化処理を最後まで実行します。関数のハンドラーは実行しません。

これはProvisioned Concurrencyのローンチブログに記載されている図です。Provisioned Concurrencyが無い場合、95%のパーセンタイル以降レイテンシーが大きくなりますが、Provisioned Concurrencyがあればその後も一貫したパフォーマンスを発揮できることがわかります。

コストの面では、5分間隔で定期実行していた分の課金が発生しなくなりますが、Provisioned Concurrencyで用意した実行環境数の課金は発生します。しかしこれは関数実行よりは非常に安価です。

また、この機能はAutoScalingにも対応しています。


まとめ再掲

あなたの関数について

  • 依存関係を最小化する
  • 控えめに、かつ戦略的にpre-handlerロジックを使う
  • アプリのスコープに基づいてシークレットを共有する
    • 単一関数:環境変数
    • 複数関数/共通環境:パラメーターストア
  • 変数、接続、依存関係の再利用が与える影響を考慮する
  • Layer(Lambda Layer)はコードの重複を避けることができ、関数間の標準化の実現に役立つ
  • RDS ProxyはLambdaとRDBの組み合わせシンプルにする
  • 簡潔なロジック
  • オーケストレーションをStep Functionsに任せる
  • Lambda destinationsは非同期ワークフローの単純化、改良に役立つ

実行環境について

  • より多くのメモリ = より多くのCPUとI/O(比例する)
    • 安価にもなり得る
  • ワークロードをプロファイルするためにX-Rayを使おう
  • 1.8GB以上のメモリ→2 (CPU) コア 使わない/必要ないかもしれないけど
  • 実行モデルと呼び出し元のニーズについて熟考しよう
    • すべてがAPIである必要はない
  • 非同期処理を考えると、スケーリングの課題のうちの最大のものをいくつか克服できる
  • キュー、トピック、ストリーム、イベントバスを使う際はその様々な側面を理解しよう
  • IAM パーミッションのスコープを最小化する
    • AWS SAMのようなツールを活用する

Lambdaサービスについて

  • Provisioned Concurrencyにより関数実行の一貫性と、全体的なレイテンシが改善されます

感想

私の知識と英語力が乏しくてキャッチアップが大変でしたが、まとめてサーバーレスアプリのノウハウを学べる良いセッションでした!

セッション動画