この記事について
この記事は SUPER STUDIO Advent Calendar 2023 の16日目の記事になります。
前提
最近、業務でLambdaをゴリゴリに扱うプロジェクトに配属されており、ちょっとしたコツとか意識しなくてはいけないポイントが見えてきたので備忘録的に書いていきます。
超基本的な内容ばかりで恥ずかしいですが、意識しないと抜けることもあるので。
15分の限界を超えないようにする
これはもはや意識するまでもないLambdaの最も基本的な制約ですね。
1回のLambdaで処理できるプロセスは15分を超えることができません。15分を超えるとTimeOutします。
Task timed out after 3.00 seconds(TimeOut3秒の場合)
これはそもそもLambda自体が長期間の稼働を前提とする重い処理を想定していないからですね。すぐ終わる軽量な処理を(インフラの面倒を見る必要なく)簡単にたくさん作れるというのがLambdaの基本的な思想でありメリットだと思います。
それでもLambdaで15分を超えたい場合は、以下のような方法が考えられます。
- 処理したい内容を、数分程度で終わる想定の関数いくつかで分担し、複数のLambdaでバトンリレーする
- フローがさほど複雑でない処理であればこれで十分だと思います。状態はS3、DynamoDB、RDSなど好きなもので持たせれば良いと思います。つなぎにSQSを利用してキューイングからLambda実行するのも良い方法だと思います。
- Step Functionsでワークフローを構築する
- フローがある程度複雑であればこちらが良いと思います。Lambda関数、API Gateway、SNS、SQS、ECS、DynamoDB、などなども連携できます。
ただ正直、そこまで重い処理であれば素直にAWS Batchとか使う方が良いかなと思います。
あとは、リトライ処理も検討すべきですね。TimeOut時には「Task timed out after」が出るので、それをフィルターにしたClodwatch Logsをトリガーにしてキックするなどの方法があります。(TimeOut以外でも検討すべきですが)
レスポンスサイズの制限6MBの限界を超えないようにする
これは慣れてない人は15分ほどには意識しづらいかな?と思います。履歴系のデータとか分析系のデータとかじゃないと普通は6MB超えないと思いますが、あんまりないことだけに忘れた頃に起きがちとかはあると思います。(ありました)
LAMBDA_RUNTIME Failed to post handler success response. Http response code: 413.
これの対処法ですが、まずは基本的なこととしてレスポンスのサイズを減らすことを検討すべきですね。よくよく見ると使ってないデータとか引受先で再取得できるデータとか、意識してみると結構スリム化できることもあると思います。
他に退避作としては、状態管理の時と同じですがS3、DynamoDB、RDSなどにレスポンスデータを保存して、引受先で再取得するなどがあげられると思います。
(API Gatewayの場合)レスポンスの待機時間30秒の限界を超えないようにする
LambdaではなくAPI Gatewayの話ですが、LambdaでAPIを構築しようとするとAPI Gatewayを前段に置くのがオーソドックスだと思うので書きました。API Gatewayを経由してリクエストを受けレスポンスを返す前提の構築だと、事実上Lambdaの制限が30秒になるような感じですね。
これの対処法ですが、Lambdaに限った話ではなく普通のサービスでもユーザーを30秒も待たせちゃいけないよねという話だと思うので、普通のWebサービスと同じような対策になると思います。
- 処理の高速化をする
- SQLの最適化とかですね。普通のWebサービスで意識することと同じだと思います。
- 非同期処理を用いる
- レスポンスだけすぐに返して、実際の処理は別のLambdaなりAWS Batchなりに渡すといった方法です。
- 分割処理を用いる
- 軽量な複数のAPIを定義して、呼び出し側で複数呼び出してもらうフローにする感じですね。
- API GatewayをやめてElastic Load Balancingなどにする
- それでも30秒を超えたい場合が頻発しそうな場合は、API Gatewayをやめるべきだと思います。
同時実行数1000Lambdaの限界を超えないようにする
Lambda関数の同時実行数は同一アカウントの同一リージョン内につき、1000に制限されています。これを超えるとスロットリングエラーが発生します。
AWSに上限緩和申請をすることで制限を引き上げることができるらしいですが、こちら側でもできることはないか最初に考えた方が良いかなと思います。
例えばですが、S3イベントが一気に実行されるようなオブジェクト更新をしたりとか、API Gateway経由のサービスでユーザーからのリクエストが集中したりとか、そういったことで1000を超えてしまいそうになることはあるのかなと思います。
対策の一つとして、例えばSQSのキューを利用してリクエストを管理すればmaximumConcurrency
というオプションを設定することでSQS経由で起動するLambdaの同時実行数を制限できます。API Gatewayであればレートリミットを最適な値に調整するなども検討できますね。
また、Lambdaそのものの予約済同時実行数を制限したい場合はreservedConcurrency
オプションで制限できます。
Lambda実行環境で利用できるメモリ上限10GB、vCPU上限6の限界を超えないようにする
以前はメモリ上限3008MBという縛りがあったのですが、2020年に神アプデが入り最大10GBまでメモリに積めるようになりました。
これにより、適宜Lambda毎にメモリ最大量を調整すれば不便を感じるシーンはほとんどなくなったと思います。が、10GBとかでLambdaを起動すると結構料金が嵩んでLambdaの大きなメリットである安さが失われると思うので、これもなるべくメモリ最適化は検討した方が良いと思います。
この対策も基本的にはLambdaに限った話ではなく、より良いメモリ効率の処理を心がけましょうという話になると思います。
- メモリ効率の良いアルゴリズムを実装する
- 使用されなくなったオブジェクトや変数の適切なクリーンアップをする
- 大きなファイルやデータセットを処理する場合それらを一度にメモリに読み込むのではなく、ストリーム処理を利用する
- S3、DynamoDB、RDSなどにデータを保持する
- 複数のLambdaに処理を分割する
などになるかなと思います。
AWS Lambda 関数を使用するためのベストプラクティス
ここからはドキュメントに書いてあることの要約になります。
あくまでベストプラクティスなので全て実践するのではなく、理解できるところ、簡単にできるところ、重要そうなところから実践すると良いと思います。
関数コード
関数の単体テストを実行しやすくするため、Lambda ハンドラーをコアロジックから分離する
exports.myHandler = function(event, context, callback) {
var foo = event.foo;
var bar = event.bar;
var result = MyLambdaFunction (foo, bar);
callback(null, result);
}
function MyLambdaFunction (foo, bar) {
// MyLambdaFunction logic here
}
実行環境の再利用をする
ハンドラー外でAWS SDKクライアントやデータベース接続を初期化し、静的なアセットをローカルにキャッシュします。これにより実行時間が短縮されます。
// AWS SDKの初期化(ハンドラー外)
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.myHandler = function(event, context, callback) {
// ハンドラー内の処理
};
セキュリティに注意する
実行環境を使用してユーザーデータやその他の情報を保存しないようにし、セキュリティリスクを避けます。(実行環境を再利用する際には、機密データを関数のグローバル変数に保存しないようにします)
keep-aliveディレクティブを使用する
永続的な接続を維持するために、ランタイムに関連付けられたkeep-aliveディレクティブを使用します。
const http = require('http');
const agent = new http.Agent({ keepAlive: true });
exports.myHandler = function(event, context, callback) {
// HTTPリクエストを実行する際に、カスタムエージェントを使用
};
環境変数を活用する
オペレーショナルパラメータ(例:S3バケット名)を環境変数として設定します。
exports.myHandler = function(event, context, callback) {
const bucketName = process.env.BUCKET_NAME;
// 環境変数を使用した処理
};
依存関係を適切に管理する
LambdaにアップロードするZIPファイルに、必要な依存関係をすべて含めます。
デプロイパッケージの最適化をする
パッケージを小さく保つために、不要なファイルや依存関係を含めないようにします。Javaや.NET Coreでは、必要なSDKコンポーネントのみを含めます。
依存関係の複雑さを最小化する
なるべくシンプルなフレームワークを使用し、実行環境起動時のロードを高速化します。(LambdaにRails乗っけて起動するとかは向いてないですね)
再帰的な自動呼び出しを避ける
関数が自身を無制限に呼び出さないように注意し、誤ったコードがあれば直ちに修正します(自分が前例に出した「LambdaでLambdaを呼び出す」などは場合によっては再帰になってしまいます。ので、実のところ推奨はされないかもしれません)
非公開APIの使用を避ける
AWS Lambdaの公開APIのみを使用し、内部APIの変更による問題を避けます。
冪等性のあるコードを記述する
同じLambdaが何回呼び出されても良いように冪等性を担保したコードにします。(特にSQSのスタンダードキューなどはたまに同じメッセージを重複送信してしまうらしいので、Lambda側でデータの重複登録などをしないようにする仕組みが必須になります)
Function Configuration
パフォーマンステストとメモリサイズの最適化
関数のパフォーマンステストは最適なメモリサイズを決定するのに不可欠です。メモリサイズが増えると利用できるCPUも増加します。
CloudWatchレポート分析で、Max Memory Usedフィールドを分析して、メモリが不足しているか、オーバープロビジョニングされているかを判断します。
AWS Lambda Power Tuningの利用
AWS Lambda Power Tuningツールを使用して、関数のための適切なメモリ設定を見つけます。(AWS Lambda Power Tuning は、AWS Step Functions を利用したステートマシンで、データ駆動型の方法で Lambda 関数のコストやパフォーマンスを最適化するためのツールです)
AWS Lambda Power TuningツールのGitHubリンク
Advanced Vector Extensions 2 (AVX2)の利用
AVX2対応のライブラリを使用して、機械学習、メディア処理などの負荷の大きなワークロードを効率的に処理します。
ロードテストによるタイムアウト値の最適化
ロードテストを実行して、関数の最適なタイムアウト値を決定します。
最小限のIAMポリシーの使用
必要最小限のアクセス許可を持つIAMポリシーを設定します。
Lambda クォータの理解
ランタイムのリソース制限(ペイロードサイズ、ファイル記述子、/tmpスペース)を理解し、考慮します。
不要なLambda関数の削除
使用しなくなった関数は削除して、デプロイパッケージサイズの制限に対して不必要にカウントされることを防ぎます。
Amazon SQSとの統合時の注意
SQSをイベントソースとして使用する場合、関数の予想呼び出し時間がキューの可視性タイムアウトを超えないようにします。
メトリクスおよびアラームの使用
CloudWatch メトリクスとアラームの活用
Lambda関数内で直接メトリクスを作成または更新するのではなく、CloudWatchを活用して関数のヘルスを追跡します。
例えば、関数の所要時間に基づいてアラームを設定し、パフォーマンスの問題を早期に検出します。
# Lambda関数の平均実行時間が500ミリ秒を超えた場合にアラームを発生させる
aws cloudwatch put-metric-alarm \
--alarm-name "LambdaDurationAlarm" \
--metric-name "Duration" \
--namespace "AWS/Lambda" \
--statistic "Average" \
--period 300 \
--threshold 500 \
--comparison-operator "GreaterThanThreshold" \
--dimensions Name=FunctionName,Value=my-lambda-function \
--evaluation-periods 2 \
--alarm-actions arn:aws:sns:region:account-id:alarm-topic \
--unit "Milliseconds"
ログ記録ライブラリの利用
AWS Lambdaメトリクスとディメンションを活用して、アプリケーションエラー(例: ERR、ERROR、WARNING)を捕捉します。
const { LambdaClient, InvokeCommand } = require("@aws-sdk/client-lambda");
exports.handler = async (event) => {
try {
// Lambda関数のビジネスロジック
} catch (error) {
console.error(`ERROR: ${error}`);
}
};
AWSコスト異常の検出
機械学習を使用してアカウントの異常なアクティビティやコストを検出します。
コスト異常の検出はAWS Cost Explorerのデータを使用し、最大24時間の遅延があります。
使用開始前にCost Explorerにサインアップし、コスト異常の検出を設定する必要があります。
ストリームの使用
バッチサイズのテストと調整
バッチサイズとレコードサイズをテストして、関数がタスクを効率的に完了できるようにイベントソースのポーリング間隔を調整します。
CreateEventSourceMappingのBatchSizeパラメータを使用して、各呼び出しで関数に送信できるレコードの最大数を設定します。
# Kinesisストリームからのデータをバッチサイズ100でLambda関数に送信する設定を作成する
aws lambda create-event-source-mapping \
--function-name "my-lambda-function" \
--event-source-arn "arn:aws:kinesis:region:account-id:stream/my-stream" \
--starting-position "LATEST" \
--batch-size 100
バッチ処理ウィンドウの設定
バッチ処理ウィンドウを設定して、レコードを最大5分間バッファリングし、適切なサイズのバッチが集まるまで待機します。
Kinesis ストリームのシャード数の増加
Kinesis ストリームのシャード数を増やして、ストリーム処理のスループットを向上させます。
関連レコードが同じシャードに割り当てられるように、適切なパーティションキーを選択します。
CloudWatchを使用したストリーム処理のモニタリング
IteratorAgeメトリクスを使用して、Kinesis ストリームが効率的に処理されているかどうかを判断します。
# IteratorAgeが30秒(30,000ミリ秒)を超える場合にアラームを発生させる
aws cloudwatch put-metric-alarm \
--alarm-name "KinesisIteratorAgeAlarm" \
--metric-name "IteratorAgeMilliseconds" \
--namespace "AWS/Kinesis" \
--statistic "Maximum" \
--period 300 \
--threshold 30000 \
--comparison-operator "GreaterThanThreshold" \
--dimensions Name=StreamName,Value=my-stream \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:region:account-id:alarm-topic
セキュリティベストプラクティス
AWS Lambdaのセキュリティベストプラクティスとして、AWS Security Hubを活用することが推奨されています。
AWS Security Hubの使用
AWS Lambdaのセキュリティ状況をモニタリングするためにAWS Security Hubを使用します。
Security Hubは、リソース設定とセキュリティ標準を評価し、様々なコンプライアンスフレームワークへの準拠をサポートします。
セキュリティコントロールの利用
Security Hubのセキュリティコントロールを使用して、Lambdaリソースのセキュリティポスチャを評価します。
コンプライアンスフレームワークへの準拠
Security Hubを活用して、コンプライアンスフレームワークに準拠するためのリソースの評価と改善を行います。
追加情報の取得
Security Hubの詳細な使用方法については、「AWS Security Hub ユーザーガイド」の「AWS Lambda コントロール」セクションを参照してください。