Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

AWS Lambda Functionsについて知っておきたかった3つの注意(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

AWS Lambda Functionsについて知っておきたかった3つの注意(翻訳)

ここ1年半の間、かなり多くのサーバーレスアプリケーションを構築してきましたが、そろそろAWS Lambdaエンジンについて学んだ重要なことをいくつか皆さんにご紹介するときがやってまいりました。

1. 実行コンテキストが共有されることがある

私やチームメンバーが驚かされたのは、同一のLambda関数のまったく無関係な2つの実行(execution)がグローバル実行コンテキストを共有してしまう場合がありうるということでした。以下のスニペット例をご覧ください。

/* my-lambda-function.js */

const Logger = require('...');
const logger = new Logger('...');

exports.handler = () => {
    // ...
}

皆さんも、オブジェクトのグローバルインスタンスを1つ作成して、それを引き回したり他のリスティングで使うためにエクスポートしたりする場面(ロガーインスタンスの再利用など)で同じようなことをしているのではないでしょうか。

このスニペットには改変可能なステートはないので、これをグローバルインスタンスとして扱うことはアンチパターンとはみなされません。しかし問題はここからです。これはLambdaエンジンのコンテキストではアンチパターンなのです。

知らないうちに他の誰かがLoggerクラスを更新して、改変可能なステートを持てるようにすると、人生で最悪級のバグ「暗黙のデータ改変」がコードで発生します。データがどこで改変されたのかも見えず、修正に何時間も費やすはめになります。

しかもこのバグはすぐには顕在化しません。各Lambda関数は直感的には独立したアプリケーションとして扱うものですし、各コードの実行もまったく新しいグローバルコンテキストでまったくの白紙状態から開始されると直感的に期待するものだからです。直感では、データの改変など思いもよりません

なお、Lambdaの次回の関数呼び出しリクエストは、実際には既存の実行中エンジンイメージ上の同じNode.jsプロセス上で、単にLambda関数ハンドラーメソッドに新しいリクエストペイロードを与える形で呼び出されます。直前の実行でグローバルコンテキストが改変されると、その影響は以後の実行に広がります。

ここで知っておいていただきたいのは、この「リクエストバンドル」現象は、特定の時間スレッショルド以内でたて続けに2つのリクエストが行われたときにしか発生しないことです。私の経験では、このスレッショルドは2〜3秒以内というところです。

2. ネットワーク接続のタイムアウト

もうひとつのわかりにくい現象は、Lambda関数で発生する謎のタイムアウトです。こんな振る舞いが発生するなど考えもしないので、それだけでこのデバッグもかなりつらい作業になります。

Lambda関数を書く場合、自分の関数が現在の作業を完了したというシグナルをLambdaエンジンに送るのにコールバックやPromiseを用い、その呼び出しリクエストに対するレスポンスを受け取れるようになります。

直感的には「自分のハンドラーに返されたPromiseはresolveまたはrejectされるかのどちらか」あるいは「ハンドラーcallback()関数を呼ぶと自分の関数は実行を停止し、実行結果を受け取る」ことを期待するものです。しかし、事態はそれほど単純ではありません。

自分のNode.jsイベントループ内のcallback()呼び出しやPromise解決のタイミングで何かが起きると、Lambdaエンジンはそのイベントループが終了するまでシグナル上で動かなくなります。イベントループ内でスクリプトのフローを完了した後にデータベース接続を開く場合が典型的です。これらを終了しないで放置すると、関数はタイムアウトします

この振る舞いを無効にするには、自分のLambda関数ハンドラに渡すcontextオブジェクトでcallbackWaitsForEmptyEventLoopフラグfalseを設定します。

/* my-lambda-function.js */
exports.handler = (event, context, callback) => {
    context.callbackWaitsForEmptyEventLoop = false;
}

3. Lambdaレイヤーの制限

もうひとつ知っておきたかったことは、Lambdaレイヤーの制限です。Lambda関数1つにつき最大で5つのレイヤーをアタッチできますが、Lamdba関数1つあたりのバンドルサイズの最大値は250MBです。そしてこのサイズには、関数のコードや依存関係のほかに、アタッチされた全レイヤーも含まれます。

私はうっかり、レイヤー数や関数のサイズに上限はないものと思い込んでいました。その結果この問題をもろに踏んでしまい、サーバーレスアプリケーションの設計をいくつもしくじってやり直すはめになりました。

これらの制限を頭に叩き込み、今後アプリケーションのアーキテクチャを設計するときはこれらの制限を必ず考慮に含めましょう。どうか私の轍を踏みませんように!


本記事がお役に立ちましたら、ぜひシェアしてください。お読みいただいた皆さんに感謝いたします!

おたより発掘

関連記事

AWSマネジメントコンソールからEC2インスタンスの再起動を行う方法(非エンジニア向け)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。