maybe daily dev notes

私の開発日誌

Python Lambdaのコールドスタートが遅いときの対処法

AWS Lambdaのコールドスタートはアプリ開発でしばしば悩まされる問題です。この記事では、特にPythonのLambda関数 (コンテナも含む) において、コールドスタートが遅い際の対処方法をいくつか紹介します (注意: 網羅は目指してません)。

第一歩: 計測する

Pythonプログラムのコールドスタートが遅い場合、モジュール群のインポートに時間が掛かっていることが多いと思います。

モジュールのインポートに要する時間は次の方法で計測・可視化できます。

まず、-X importtime オプション付きでPythonプログラムを実行します。Lambda環境そのもので実行する*1のはログの取得が面倒なので、最初はローカル環境で実行して良いでしょう。厳密には異なると思われますが、十分良い近似を出してくれるはずです。

このオプションにより、標準エラー出力に以下のような形式のテキストが出力されます:

import time: self [us] | cumulative | imported package
import time:        86 |         86 |   _io
import time:        16 |         16 |   marshal
import time:       177 |        177 |   posix
...

次のコマンドなどを利用して、上記の出力をファイルに保存しましょう:

python -X importtime main.py 2> prof.txt

importtimeの結果は読みづらいので、別のツールで可視化します。今回はtunaを使います。

github.com

tunaはpipからインストールでき、上記で得られた出力を渡すことで利用できます。

pip install tuna
tuna prof.txt

解析が終わると自動的にウェブブラウザが起動し、結果が可視化されます。Framegraphに似た形式です。

以下はDifyapiで実際に使ってみた例です:

インポートされるモジュールはツリー構造を成します。一番上がrootのモジュールで、下に行くほど親からインポートされる子のモジュールになります。横幅がインポートにかかる時間の長さを示します。

それでは、この結果を元に対処方法を考えていきましょう。

対処方法

基本的には、インポート時間の長いモジュールに対処していくことになります。

大まかには、以下の方法があるでしょう:

  1. モジュールを遅延ロードする
  2. モジュールの初期化処理を変更する
  3. モジュールへの依存をやめる

それぞれ詳細をまとめます:

1. モジュールを遅延ロードする

Pythonのimport文は通常ファイルの頭に書きますが、ローカルスコープに書くこともできます。これにより、import処理の実行がそのスコープに入ったときに遅延されるため、プログラム自体の初期化時間には影響を与えなくなります。

この方法は、初期化後もめったに使われないようなモジュールでは有効です。

例えば以下の vertexai.generative_models は、インポートに1秒以上要している割に、VertexAIを利用するとき以外は不要なモジュールと考えられるため、遅延ロードの効果は大きいでしょう。

一方で頻繁に利用されるモジュールでは、初期化処理の後にすぐにインポート処理が走ることが多いと考えられるため、あまり効果がないことも多いでしょう。ただし、Lambdaでは初期化処理が10秒を超えると初期化が中断・再実行されるという仕様があります (参照)。これを避けるため、遅延ロードにより初期化処理を10秒未満に収めるという方法が有効な場合もあるでしょう。

Pythonにおける遅延ロードの実装パターンはこちらにまとまっていました: Lazy import in Python

上記の記事の要点をまとめます。まず、オリジナルのコードが下記だとします:

import foo

def func():
  foo.bar()

import文を実行時に移動すれば、遅延ロードが実現できます:

def func():
  import foo
  foo.bar()

なお、インポート処理が走るのは最初の一度だけなので、func関数の初回の呼び出し時は遅くなりますが、それ以降はパフォーマンスが下がるということはありません。

importlib を使えば、インポートされたモジュールを変数に格納することもできます。複数のスコープでモジュールを共有したい場合は使えそうです:

from importlib import import_module

def init():
  global foo
  foo = import_module('foo')

# 以下はinitの呼び出し後のみ使える
def funcA():
  foo.barA()

def funcB():
  foo.barB()

ただし、type annotationにおいては直接インポートされたモジュールのみ参照できる (変数は不可) ようで、遅延読み込みされた型を使う方法がなさそうでした。この辺りの議論を見る限り、まだできないような気がしています。Pythonに詳しい方の知見をお待ちしております。

# foo.BarTypeは遅延読み込みできる?
def func() -> foo.BarType:
  foo.bar()

2. モジュールの初期化処理を変更する

モジュールによっては、importされた際に時間のかかる処理を実行するものがあります。極端な例は下記です:

# foo.py
import time
time.sleep(10)

# main.py
import foo # これで10秒待つことに

自作のモジュールであれば、こうした時間のかかる処理を消す・あるいは初期化後に移動することができるか検討すると良いでしょう。

例えば、下図の core.tools.tool_manager のように、子のインポートではなく自分自身で時間が掛かっている場合は、このパターンのはずです。

3. モジュールへの依存をやめる

どうしようもないモジュール (ライブラリ) は、代替手段を考えるのも良いですね。

以降は少し毛色は違いますが、別解として書いておきます。

4. __pycache__ をデプロイパッケージに含める

Pythonはモジュールがインポートされた際に、pyファイルをコンパイルしたバイトコードのキャッシュを __pycache__ フォルダに生成します。これにより、次回実行時には高速な初期化を実現できます。

しかしLambdaでは1つの実行環境では1度しか初期化されないため、__pycache__ の恩恵は受けづらいです。では、事前に生成してパッケージに埋め込んでおけばどうでしょうか?

その方法には落とし穴があります。バイトコードは環境依存のため、ローカル端末で作成した __pycache__ をLambda環境で使えるとは限らないことです。このため、zipデプロイではLambdaパッケージに含めないことが明確に推奨されています*2

We recommend that you don't include __pycache__ folders in your function's deployment package. Python bytecode that's compiled on a build machine with a different architecture or operating system might not be compatible with the Lambda execution environment.

しかし、コンテナLambdaでは話は変わると思われます。コンテナ内で __pycache__ を生成すれば、OSやアーキテクチャの差異は発生しないはずのためです。

実際にDifyで試したところ、コールドスタート時間は60%ほどまで短くなりました (40秒 → 25秒)。効果は抜群です。__pycache__ の恩恵は、ローカルでも .venv を作り直して実行するなどすれば、実感することができるでしょう。

__pycache__ の生成には、compileall を利用できます。以下はDifyにおけるDockerfileの例です:

FROM langgenius/dify-api
RUN python -m compileall -f -j 0 -q ./ || true

compileallは、引数で渡したフォルダに対して再帰的にpyファイルを探してコンパイルします。対象にすべきフォルダは環境により異なる可能性があるので、適宜確認してください。 また、今回は特定のライブラリのコンパイルでエラーが発生することがあったので、|| true でエラーを無視しています。

この方法だと、コード自体は変更不要なのも良い点ですね。

5. Lambda関数を分割する

Lambdalithは一部で流行りの方法ですが、複数の機能を一つのLambda関数にまとめる都合上、インポートするモジュールが増えコールドスタートが長くなりがちです。

私はこれがLambdalithの最も大きな欠点だと考えています。Lambdalithはメリットが多いため積極的に採用すべきだと考えますが、コールドスタートがあまりにも長くなったときは対策が必要です。Lambda関数を分割しましょう。

分割の方針を決めるには、各機能が利用するモジュールを観察し、効果的な境界を見出します。すべての機能で遍く使われるライブラリではなく、一部の機能でのみ利用されるライブラリに注目するのがコツです。

分割の実装自体はそれほど大変ではないことも多いです。例えばFastAPIではルーター機能ごとに定義し、エントリポイントから必要なルーターだけをインポートして使うことができます。これにより、1つのFastAPIアプリケーションを複数に分割することは容易です。

Lambda関数へのルーティングは、Amazon API GatewayやCloudFront (FURLの場合) などを使うと良いでしょう。

6. 何回か起動してみる

Docker Lambdaの場合は、Lambdaサービス側でのイメージキャッシュの持ち方の都合 (参考) で、何度かコールドスタートさせると時間が短くなる可能性があります。

デプロイ後初回のコールドスタートで遅かったからといって、それだけで判断するのは禁物です。複数回のコールドスタートの平均値を見ましょう。また、Productionでのメトリクスも参考にすると良いでしょう。

まとめ

Python Lambdaのコールドスタート時間の解析方法・改善方法を紹介しました。ぜひ試してみてください。 (PythonにもSnapStartがほしくなりますね)

*1:Lambdaで実行する場合は、環境変数 PYTHONPROFILEIMPORTTIME を 1 に設定すると良いです。参考

*2:cdkのPythonFunctionのように、コンテナ内でzipパッケージをバンドルする方法であれば問題はなさそうですが。

登壇Tips: 聴講者に挙手をお願いする上で注意する5つのこと

エンジニア向けのイベントで登壇するとき、会場の人に向けて選択式の質問をし、当てはまる項目に挙手させること (以下会場アンケートと呼びます) があると思います。下図が一例です。

私が最近失敗した質問です😓 改善点はどこでしょうか?

会場アンケートはうまく使えば会場と一体感が生まれ盛り上がる方法ですが、思っていたより手が上がらず落ち込んだという登壇者もいるかもしれません (私です😇)。この記事では、失敗から得られた教訓として、会場アンケートする際に気をつけたいことをまとめます。

なお、私自身下記の内容に確証があるわけでもなく、推察が多分に含まれます。個々人の気持ちに関わる部分であり、一人で考えても仕方ないとも思うので、ぜひご意見ください(←こないやつ)!

1. 選択肢は漏れのないようにする

提示する選択肢は、会場にいる全員が一度は手を挙げられるように用意するのが良さそうです。

自分が聴講者だとして、登壇者に質問をされたのに当てはまる選択肢がなく手を挙げられなかったら、仲間外れにされたような気持ちになるかもしれません。インクルーシブネスの観点で、このような状況は避けるべきでしょう。

また、特に発表の序盤では、聴講者の全員が登壇者と何らかインタラクションすることで、両者とも緊張感がほぐれる効果が期待できそうですね。

2. 1つ目の選択肢は、多くの人が手を挙げそうなものに

ファーストペンギンという言葉がありますが、人が手を挙げてない中で自分だけが手を挙げるのは避けたいものです。

気軽に手を挙げられる雰囲気を作るため、回答の1つ目の選択肢は、会場の3〜4割以上は手が挙がると見込めるものを配置するのが良さそうです。1つ目でたくさん手が挙がれば、それ以降のよりマイナーな選択肢でも、多少手を挙げやすくなるんじゃないでしょうか。

3. ネガティブな印象を与えうる選択肢にはフォローを

その選択肢自体にネガティブなイメージが含まれる場合は、手を挙げる事自体を恥ずかしがる人がいるのはもっともです。

このような場合は、登壇者の配慮が一層必要になると思われます。例えば「意外と多いんじゃないかと思いますが」とか「私も実はこれに当たるんですが」とか前置きするのはどうでしょうか。

あるいは場合によっては、そもそも選択肢として含めない対応も必要かもしれません。「漏れのないように」という話と矛盾しますが、ケースバイケースの判断は必要ですね。

4. そもそも回答することが難しくないか検討する

回答することが難しい質問を投げかけている可能性がないか、事前に検討しましょう。

質問を受けた際、答えをはっきりとは知らない・答えが一つに定まらないなどの理由で「何とも答えられない…」と思うことはしばしばあると思います。会場アンケートにおいてこれをやってしまうと、登壇者と聴講者の間に壁が出現し、大きな距離感を生み出してしまいます。

回避するためには、資料準備の段階で周りの人に質問を素振りしておくのが良さそうです。それらの反応を見て回答が難しそうな場合は、1. 回答前に考える時間を設ける 2. 回答を確認する方法を提示する 3. 質問を変える といった対応が考えられるでしょう。

なお、2. 最初の選択肢は、多くの人が手を挙げそうなものにする を実践していれば、もし最初の選択肢であまりにも手が挙がらない場合 (想定外の事態!) に、その場で軌道修正ができそうですね。

5. 質問の意図を明らかにする

意味のないことをしたがる人は少ないです。ゆえに、聴講者は答える意義の分からない質問に何度も答えてくれるわけではないと思われます。

質問する前には意図を十分に説明し、回答が得られたらそれをその場で有効に活用すると良さそうです。

これは特に複数個の質問するときは重要だと思います。1つ目の質問の回答が有効活用されてなければ、その後の質問は答える意欲を失うのが人の心理だと思います。 私はあまり実践できてなかったように思うので、反省しています。

その場で出た結果に対して当意即妙なコメントをするのはなかなか難しいですから、想定される回答分布のいくつかのパターンについて、気の利いたコメントを先に考えておくのも必要かもしれません!

三者の意見

ちなみに、Claudeさんに聞いたらこんな感じでした。

エンジニア向けのイベントで選択式の質問をする際、登壇者が気をつけるべきことは以下のようなことが考えられます。

  1. 質問の内容:
    • 参加者にとって興味深く、関連性のある質問を用意する
    • 質問が明確で簡潔であり、誤解を招かないようにする
  2. 選択肢の設計:
    • 選択肢が適切な数(3~5個程度)になるよう設計する
    • 選択肢が互いに排他的で、重複がないようにする
    • 選択肢の表現が中立的で、バイアスがかからないようにする
  3. 参加者への呼びかけ:
    • 参加者全体に向けて明確に質問を伝える
    • 参加者が挙手しやすい環境を作る(照明、空間の使い方など)
    • 参加者の反応を適切に確認し、次の進行に活かす
  4. 回答の扱い:
    • 参加者の反応を丁寧に確認し、理解を深める
    • 回答の傾向を分析し、それを議論に活かす
    • 参加者の意見を尊重し、議論を建設的に進める
  5. 全体の流れ:
    • 質問を適切なタイミングで行い、イベントの流れを乱さない
    • 質問の目的を明確にし、参加者の理解を促す

いや、先に聞いておけば良かったな!なお > 参加者が挙手しやすい環境を作る(照明、空間の使い方など)

ゲリラ豪雨、雷鳴、うっ頭が…!*1

まとめ

この前の教訓を忘れないために、記事化してみました。割と根拠なしに書いているので、他の方の意見も聞きながら自身の感覚を較正していければと思います。登壇はエンジニアリングとは別の工夫の余地が色々あり楽しいので、引き続き頑張っていきたいです。

尺余りもなちゃん

*1:内輪ネタすみません。前回の発表会場は高層ビルの21階だったのですが、私の発表中に豪雨が降り始め、背景で雷鳴と雷光が威圧的な演出をしてくれるという事故があったのです。

Amazon OpenSearch Serviceのfine-grained access control機能をCDKで管理する

Amazon OpenSearch Serviceでは、ドメインに対するアクセス制御の手段の一つとして、Fine-grained access control (きめ細かいアクセス制御、以下FGAC) が利用できます。 この記事では、FGACの設定をAWS CDKから管理する方法を紹介します。

なぜFGACを使うか

はじめに、FGACを知らない方・使っていない方のために、モチベーションを整理します。すでにご存知の方は読み飛ばしてください。

FGACを使わない場合、OpenSearch Serviceでは以下の方法でアクセス制御することができます。

  • リソースベースのポリシー (ドメインアクセスポリシーとも)
    • ドメインに対して1つだけ付与できるポリシーです
  • アイデンティティベースのポリシ
    • 利用側のIAMロール/ユーザーに付与するポリシーです

用意されているIAMのアクションは下表の通りであり、この程度の粒度で権限を設定できます。IAMポリシーのリソースや条件で対象のインデックスや接続元IPアドレスを絞り込むことも可能です。

FGACを利用しない場合の権限管理の粒度 doc

この方法に加えて、FGACでは次の機能が実現できます (公式ドキュメントの受け売りです):

IAMポリシーによる制御よりも柔軟なため、FGACを使いたいユースケースも少なくないのではないでしょうか。OpenSearch Serviceの運用ベストプラクティスにおいても、FGACはドメインをセキュアにする手段として明示的に推奨されています。

FGACを設定する際の課題

しかしながら、FGACを設定する上で課題があります。それはFGACの設定はCloudFormation(CFn)やCDKで公式にサポートされていないことです。このため、従来は次のような方法で設定していたかと思います:

  1. OpenSearchダッシュボードからGUIで設定する
  2. OpenSearchREST APIで設定する

1は自動化が困難なことや手順書の管理作業が生じることが難点です。2は自動化できる点で悪くないですが、VPC内にドメインがある場合はアクセスのため考えることが増えたり、またCDKで自動命名されたIAM Role名を引き回したりといった手間も考えられます。

これらの課題を解消するために、現在は次の方法でCDKからFGACを管理可能です。

CDKから管理する方法

opensearch-rest-resourcesというライブラリを使えば、CDKからFGACの設定を管理することができるようになります。

例えば、あるIAMロールに権限を与えたい場合は、下記のコードで実現可能です:

import { IVpc } from 'aws-cdk-lib/aws-ec2';
import { IRole } from 'aws-cdk-lib/aws-iam';
import { Domain } from 'aws-cdk-lib/aws-opensearchservice';
import { OpenSearchRole, OpenSearchRoleMapping } from 'opensearch-rest-resources';

declare const vpc: IVpc; // OpenSearch Domainが属するVPC (Domainがnon-VPCな場合は省略可)
declare const backendRole: IRole; // 権限を与えたいIAMロール
declare const domain: Domain; // 対象のOpenSearch Domain

// OpenSearch FGACのロールを作成
const role = new OpenSearchRole(this, 'Role', {
    vpc,
    domain,
    roleName: 'Role1',
    payload: {
        // Roleの設定: 以下の仕様に従う
        // https://opensearch.org/docs/latest/security/access-control/api/#create-role
        clusterPermissions: ['indices:data/write/bulk'],
        indexPermissions: [
            {
                indexPatterns: ['*'],
                allowedActions: ['read', 'write', 'index', 'create_index'],
            },
        ],
    }
});

// RoleMappingで上記のロールとIAMロールを紐づける
const roleMapping = new OpenSearchRoleMapping(this, 'RoleMapping', {
    vpc,
    domain,
    roleName: role.roleName,
    payload: {
        backendRoles: [backendRole.roleArn],
    },
    // RemovalPolicyも通常のCFnリソースと同様に利用できる
    removalPolicy: RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
});

他のCFnリソースと同様、設定を書き換えてデプロイすれば反映されますし、削除すればRemovalPolicyに沿って処理されます。またpayload プロパティには型がついているので、安全に設定できます。結構便利ではないでしょうか?

これまで構築が面倒なのでFGACを敬遠していたという方、ぜひお試しください。

現状の制約・注意事項

本コンストラクトライブラリを使う上での現時点での注意点をまとめます。大抵のユースケースにおいては問題にならないはずです。

OpenSearch Domain L2コンストラクトの利用が必要

APIを単純化するために、対象のドメインL2コンストラクトの利用を必須としています。新規にドメインを構築する方は皆大抵L2を使うだろうと思い、このようにしていますが、問題がある場合はぜひ教えて下さい。

Masterユーザーの認証方法

MasterユーザーはBasic認証 (not IAM)で、ユーザー名とパスワードがSecrets Managerに保存されている必要があります。Domain L2コンストラクトでは、以下のように設定すれば良いです。

const domain = new Domain(this, 'Domain', {
  // 他のプロパティは省略
  fineGrainedAccessControl: {
    masterUserName: 'master-user',  // これでOK
  },
});

これも巷のよくあるユースケースがどんな感じか不明なので、単純化のため一旦こうしています。例えばAuroraだと似た方法 (固定のユーザー名・パスワードの利用) は一般的だと思いますが、OpenSearchだとどうなのか、IAM認証もほしい方はIssueをください。

ドメイン側の設定は利用側の責任

FGACを使うためにはいくつかドメイン側に設定の必要があります。こちらのドキュメントが詳しいです。例えば下記が挙げられます:

  • トラフィックについてHTTPSを利用
  • Encryption of data at restの有効化
  • node-to-node encryptionの有効化
  • Domain access policyの設定

これらの設定は利用側の責任としています。このコンストラクト側では今のところバリデーションなどもしていません (これもどれくらい必要な機能なのか判断つかなかったため。)

ドメインのCDK実装例は、exampleをご覧ください。

対応リソース

本ライブラリでは、FGACに必要なリソースであるRole / RoleMapping / Userをすべて管理することができます。

将来的には、ライブラリ名が示すように、OpenSearchのそれ以外のRESTリソース (インデックスやML Connectorなど) も管理できるようにすることを目指しています。作成/更新(PUT)・削除(DELETE)のRESTエンドポイントが利用可能なリソースであれば、追加の対応は容易なはずです。もし何か必要なリソースがあれば、GitHub Issueにリクエストをください。

あるいは、基盤となる OpenSearchCustomResourceクラス も公開しているので、自分でコンストラクトを作ることもできます。その場合の実装は、他のクラスの実装を参考にしてください (基本的にはエンドポイントを指定して、リクエストのbodyをつくるだけです)。

まとめ

FGACの設定をCDKから管理する方法を紹介しました。ベストプラクティスに沿ったOpenSearch運用を実現するため、ぜひ使ってみてください。

今月のもなちゃん

上半期の換羽と抱卵が終わり、羽が艷やかになりました。

CDK Tips: CDKのインテグレーションテストに救われた話

AWS CDK Tipsシリーズです。

松尾さんの記事に触発されて、今回はCDKにおけるインテグレーションテストの体験談を書きます。なお、本記事では以降integ testという言葉は、CDKのinteg-testsinteg-runnerモジュ―ルで実現される自動テストのことを指すこととします。

Integ testは自動テストの一種で、与えられたCDKのコードをAWS環境へ実際にデプロイするテストを実行します。詳細は以下のブログをご覧ください。

aws.amazon.com

この記事では、integ testを導入して助かった話をします。松尾さんはCDKアプリのお話だったので、私はCDKコンストラクトライブラリでの側面から見ていきます。

背景

きっかけは自作のコンストラクトライブラリ、deploy-time-build に新機能を追加しようとした時のことです。このライブラリは、CDKデプロイ時にフロントエンドアプリをビルドすることで環境変数の注入を楽にするためのものでした (参考: AWS CDKでWebフロントエンドをデプロイする3つの方法)

このライブラリの新機能として、ECSでタスク起動を高速化するSOCIインデックスをビルドする機能を追加することにしました。ライブラリを分離するかは迷いましたが、新しい機能も「デプロイ中にビルドする」という点で元々のコンセプトに合ってますし、部分的に実装を共有できるのは便利なので、同じライブラリにまとめることにしたのです。

今回は、その機能の開発作業中に起きた話です。

リファクタする

2つの機能は大まかな処理の流れが同じです。CloudFormationカスタムリソースのLambda関数から、CodeBuildプロジェクトのビルドを開始します。

このLambda関数のコードは両機能でほぼ同一になるため、共通のコードにすることにしました。

コードが一緒なので、Lambda関数自体も一つにしたくなりますね。今回はSingletonFunctionを使っていたので、uuidlambdaPurpose を両機能で同じにすれば、自ずと関数の実体も一つになります。

元々は以下のコンストラクトです。

  const handler = new SingletonFunction(this, 'CustomResourceHandler', {
    runtime: new Runtime('nodejs18.x', RuntimeFamily.NODEJS),
    code: Code.fromAsset(join(__dirname, '../lambda/trigger-codebuild/dist')),
    handler: 'index.handler',
    uuid: '25648b21-2c40-4f09-aa65-b6bbb0c44659',
    lambdaPurpose: 'NodejsBuildCustomResourceHandler',
    timeout: Duration.minutes(5),
  });

共有するなら lambdaPurpose も汎用的な名前でないと気持ち悪いので、以下のようにリネームしましょう!

  const handler = new SingletonFunction(this, 'CustomResourceHandler', {
    runtime: new Runtime('nodejs18.x', RuntimeFamily.NODEJS),
    code: Code.fromAsset(join(__dirname, '../lambda/trigger-codebuild/dist')),
    handler: 'index.handler',
    uuid: '25648b21-2c40-4f09-aa65-b6bbb0c44659',
-    lambdaPurpose: 'NodejsBuildCustomResourceHandler',
+    lambdaPurpose: 'DeployTimeBuildCustomResourceHandler',
    timeout: Duration.minutes(5),
  });

これで追加のLambda関数を増やさずに済みます。単純で良いですね!

Ship it!

ちょっと待った!

pushする前にinteg testを走らせるんでした。yarn build すれば実行されるようにProjenを設定してあります。今回はスナップショットに変更があるのは明らか*1なので、 --update-on-failed フラグ付きで実行します。

$  yarn integ-runner --update-on-failed

Resources
[-] AWS::IAM::Role NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleCB01FBE6 destroy
[-] AWS::IAM::Policy NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleDefaultPolicyCF8879D3 destroy
[-] AWS::Lambda::Function NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c446591C4101F8 destroy
[+] AWS::IAM::Role DeployTimeBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRole0880C187 
[+] AWS::IAM::Policy DeployTimeBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleDefaultPolicy80A0FC9E 
[+] AWS::Lambda::Function DeployTimeBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659FC29CE6F 
[~] Custom::CDKNodejsBuild ExampleBuild61F1D79B 
 └─ [~] ServiceToken
     └─ [~] .Fn::GetAtt:
         └─ @@ -1,4 +1,4 @@
            [ ] [
            [-]   "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c446591C4101F8",
            [+]   "DeployTimeBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659FC29CE6F",
            [ ]   "Arn"
            [ ] ]

Snapshot Results: 
Tests:    1 failed, 2 total
Running integration tests for failed tests...

# 中略

NodejsBuildIntegTest | 4/10 | 11:13:17 AM | UPDATE_FAILED        | AWS::CloudFormation::Stack  | NodejsBuildIntegTest The following resource(s) failed to update: [ExampleBuild61F1D79B]. 

Failed resources:
NodejsBuildIntegTest | 11:13:14 AM | UPDATE_FAILED        | Custom::CDKNodejsBuild      | ExampleBuild/Resource/Default (ExampleBuild61F1D79B) Modifying service token is not allowed.

 ❌  NodejsBuildIntegTest failed: Error: The stack named NodejsBuildIntegTest failed to deploy: UPDATE_FAILED (The following resource(s) failed to update: [ExampleBuild61F1D79B]. )

なんと!思いがけずデプロイが失敗しました。気づかずにpushしていれば、ライブラリをアップデートしたユーザーのすべてのデプロイコケてしまうところでしたね。

さて、integ testでなぜこんなエラーが起きたのでしょうか?

前提知識

Integ testを実行するinteg-runnerは、以下の順でCloudFormationに対して実際にAPIを発行して動作を検証します:

  1. 変更前の古いスナップショットを使ってCloudFormationスタックをデプロイ
  2. 変更後のテンプレートを同じスタック名でデプロイして、スタックを更新
  3. スタックを削除
  4. 1-3でエラーがなければローカルのスナップショットを更新し、成功を返す

1→2で更新の挙動を検証していることが今回のポイントでした。これにより、既存ユーザーがライブラリをアップグレードした時のシナリオを模擬して、予期せぬリグレッションを可能な限り検知することができます。

失敗の理由

さて、先のinteg testで発生したエラーも UPDATE_FAILED とあり、2のスタック更新時に起きたエラーです。エラーの本文は以下です:

Modifying service token is not allowed.

Service tokenとはなんでしょうか?これはCloudFormationのカスタムリソースに必要なプロパティです:

ServiceToken The service token that was given to the template developer by the service provider to access the service, such as an Amazon SNS topic ARN or Lambda function ARN. The service token must be from the same Region in which you are creating the stack.

Updates aren't supported.

要はカスタムリソースのハンドラとして呼び出すLambda関数のARNですね。ドキュメントにこの値の「更新はサポートされていない」と明記されていました。発生したエラーも、この制約によるものだと分かりました。

つまり、カスタムリソースは一度作成した後は、裏のLambda関数のARNを変更できないことになります (コード自体の変更は可)。これはなかなか未然に知ることは難しい仕様ですし、「古いスタックをデプロイして新しいスタックで更新する」という特殊なシナリオを検証しないかぎりは、気付けない問題ですね。

integ-testを導入しておいて良かった!

まとめ

ということで、integ-test導入しておいて助かった話でした。ちなみにエラー自体の解消は、既存のLambdaはそのままに、新たに別のLambdaを作る方針で解消しました。

integ testはaws-cdk-lib本体でフル活用されていることもあり、コンストラクトライブラリ開発には特に相性が良い手段だと思います。

来月開催のCDK Conference Japan 2024では、このようなライブラリ開発者向けの話をたくさんできればと思いますので、ぜひご視聴ください!

補足

というきれいな話だけで終わらせても良いですが、上記integ testに対する所感も少し書いておきます。

CDKのinteg testに対してよく耳にする評価は、実行時間が長すぎるために開発イテレーションに組み込むことが難しいという指摘です。これは全くその通りです。テスト実行の度にリソースの作成・削除が必要になるため、例えばAuroraやOpenSearchなど "重たい" リソースが含まれるシステムの場合は、相応に時間がかかります。

Integ testはこの特性を理解したうえで利用するのが良いと思います。私自身がライブラリ開発で利用する際は、integ testとは別に開発用の環境をデプロイし、開発中の高速なイテレーションはそちらの環境で回しています (例えばカスタムリソースハンドラーのデバッグなど)。integ testの実行は、十分に実装に自信が持てた段階で、仕上げとして実行するイメージです。

Integ testが成功すればその時点でリリースできますし、もし途中で失敗した場合は、(やれやれと思いながら)再度開発環境でのイテレーションに戻ります。ここで失敗するのはたいてい考慮漏れがあったときなので、良いことではあるのですが。ちなみに、--parallel-regions オプションでテストをデプロイするリージョンを変更できるので、前回のテスト終了を待たずに次のテストを開始する、という小技も使えます (テストのclean upに時間がかかる場合)。

例えばopensearch-rest-resourcesはinteg testの実行に1回あたり1時間強かかる地獄のような構成なのですが、上記の方法でなんとかやれています。integ testがなければ見落としていたであろう破壊的な変更を未然に防ぐことができたことも実際にあったので、まだメリットのほうが大きいと判断しています。なお、テストシナリオの実装はLambda関数の中身に寄せています。IntegTest.assertions を駆使して書くこともできそうですが、そのコード自体のデバッグイテレーションが大変なので、そちらはできるだけ単純にしています。

// Lambdaを実行し正常終了することだけを確認
const assertion = integ.assertions.invokeFunction({
  functionName: stack.testHandler.functionName,
});
assertion.expect(ExpectedResult.objectLike({ StatusCode: 200 }));

このLambda関数自体のデバッグはinteg testを使わずにできるため、integ testの実行時間に振り回されることがなくなるのは嬉しい点です。(しかしinteg testの機能を活用できてない点で理想的でもないので、Issueで改善要望を出しても良さそうですね。)

一方CDKアプリ (ライブラリでなく、運用されているシステムそのもののことです) の場合は、integ testとは異なるアプローチも可能と考えます。

CDKアプリでは、大抵の場合dev, int, stagingといった、検証用の環境を用意しているのではないでしょうか。であれば、本来integ testで検証される 1. 新テンプレートでのスタック更新 や、2. E2Eテストシナリオの実行 はinteg-runnerを使わずとも、そちらの環境でカバーできます。共用の環境にデプロイするまで検証できないことにはなりますが、例えばdev環境へのデプロイは未検証でも許容できるといった場合であれば、十分実用可能な方策だと思います。(もちろん、スナップショットテストなど安価なテストはより早い段階で成功している前提です。)

あるいは個々の開発者が専有のAWS環境を持つという方針も考えられます。これなら個々人の開発中自由にデプロイ・検証できて便利ですが、ワークロードによってはコストが見合わなくなるかもしれません。その意味では、必要なときだけデプロイするinteg testがコスト的には優位でしょう。開発体験と勘案して選択するのが良いと思います。

上記まとめると、integ testに対する個人的な考えは以下のとおりです:

  • CDKライブラリ開発では導入する方が良いことが多いと思われる。ただし開発体験を犠牲にしないよう柔軟に取り入れる。
  • CDKアプリでは他にも同様の目的を実現できる手段はあるので、それらと比べながら総合的に方針を判断する 。

補足というには少々長くなりましたが、以上です。

*1:integ testはスナップショットテストも兼ねるので、リファクタによりCFnテンプレートが変化しないことも容易に検証可能です。今回は変化があることが事前に分かっているので、その手順を飛ばしています。

AWS CDKでSOCIインデックスをデプロイする

TL;DR;

以下のコードでCDKからSOCIインデックスをビルドし、ECRにプッシュすることができます。

# 依存関係のインストール
npm install deploy-time-build
import { SociIndexBuild } from 'deploy-time-build;

const asset = new DockerImageAsset(this, 'Image', { directory: 'example-image' });
// DockerImageAssetに対応するSOCIインデックスをビルド・プッシュ
SociIndexBuild.fromDockerImageAsset(this, 'Index', asset);

はじめに

先日AWS FargateでSeekable OCI (SOCI)が利用できるようになりました。これにより、ECSタスクの起動時間を高速化することができます。

こちらのポストによれば、特にサイズの大きなイメージで効果を発揮するようです。 (141MBで25%、1GBで75%の高速化)

この機能を利用するためには、対象のコンテナイメージに対応するSOCIインデックスをビルドし、同じECRリポジトリにプッシュする必要があります。 このために主に次の方法が用意されており、それぞれ以下の特徴があります *1

  1. soci-snapshotter CLIの利用
    • ✅ シンプルなインターフェースのCLIで、他のパイプラインに組み込みやすい
    • Linuxのみ対応、containerd が必要など、実行環境を選ぶ
  2. cfn-ecr-aws-soci-index-builder ソリューションの利用
    • ✅ CloudFormation一発で構築可能
    • ❌ 非同期にインデックスがプッシュされるため、タスク実行時にはまだインデックスが存在しない状況が起こりえる
    • ❌ Lambda上でインデックスをビルドするため、扱えるコンテナイメージサイズに上限がある

ということで、どちらも微妙に面倒なんですよね。巷のベンチマーク結果だけ見て、うちは(試すのも大変だし)良いかと見送った人もいるんじゃないでしょうか。

さて、AWS CDKではコンテナイメージをCDKデプロイ時にビルドする慣習があります。 コンテナイメージと同様に、CDKからSOCIインデックスをデプロイできると便利そうですよね。 この記事では、そのための機能を作った話や使い方をまとめます。

作ったもの

deploy-time-build というコンストラクトライブラリを開発しました。これにより、CDKのデプロイ中にSOCIインデックスをビルド・プッシュできるようになります。

github.com

使い方

イメージのタグとECRリポジトリを指定することで、イメージに対応するインデックスをビルドし、同リポジトリにプッシュします。

以下は someRepository という名前のECRリポジトリの中の someTag タグがついたイメージに対して、インデックスを付加する例です:

import { Repository } from 'aws-cdk-lib/aws-ecr';
import { SociIndexBuild } from 'deploy-time-build;

new SociIndexBuild(this, 'Index', {
    imageTag: 'someTag', 
    repository: Repository.fromRepositoryName(this, "Repo", "someRepository") 
});

実用的には、CDKの DockerImageAsset に対してインデックスを付加したい場合が多いと思います。これも簡単に書けて、以下は example-image ディレクトリのDockerfileをビルドしつつ、そのイメージのインデックスを付加する例です:

import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';

const asset = new DockerImageAsset(this, 'Image', { directory: 'example-image' });
SociIndexBuild.fromDockerImageAsset(this, 'Index', asset);

CDKのECSモジュールからコンテナイメージを参照する際は、AssetImage クラスを使うことも多いと思います。その場合は、先に定義した DockerImageAsset から AssetImage 作成すると良いでしょう:

import { AssetImage } from 'aws-cdk-lib/aws-ecs';
const assetImage = AssetImage.fromDockerImageAsset(asset);

SOCIインデックスがプッシュされてない状態では、ECSサービスをデプロイしたくない場合もあると思います。その場合は、ECSサービスのリソースに対する依存関係を設定すれば良いです:

import { FargateService } from 'aws-cdk-lib/aws-ecs';
const service = new FargateService(/* 略 */);
const index = SociIndexBuild.fromDockerImageAsset(/* 略 */);

// indexをデプロイした後にECSサービスをデプロイする
service.node.defaultChild!.node.addDependency(index);

Python CDKユーザーの人も同様に使えます:

pip install deploy-time-build

from deploy_time_build import SociIndexBuild

asset = DockerImageAsset(self, "Image", directory="example-image")
SociIndexBuild(self, "Index", image_tag=asset.asset_hash, repository=asset.repository)
SociIndexBuild.from_docker_image_asset(self, "Index2", asset)

上記が基本的な使い方となります。仕組みが気になる方は、次をご覧ください。

仕組み

CDKデプロイ時にCodeBuildプロジェクトのジョブを開始し、そのジョブ内でインデックスをビルド・プッシュしています。

CodeBuild上でSOCIインデックスをビルドするためにスタンドアロンなツールがほしかったので、soci-wrapperというCLIを作成・利用しています。これは上で紹介した cfn-ecr-aws-soci-index-builder のコードを流用しているため、生成物も同一になります。 このCLIの背景についてはこちらのIssueも参考になると思います: Ability to run soci create command in CodeBuild #760

理想的にはCDK CLIに同機能が組み込まれていると良いでしょう。(コンテナイメージ自体のビルドはローカルで行うため。)しかしながら、現状soci-snapshotterがLinux上でしか動かないことを考えると、Linux/Windows/Macで同様に動作する必要があるCDK CLIでは難しそうに思います。とりあえずIssueだけは立てています: core: push SOCI index when publishing docker image assets #26413

まとめ

AWS CDKから簡単にSOCIインデックスをデプロイするためのCDKコンストラクトライブラリを紹介しました。

昨日のJAWSコンテナ支部でSOCIインデックスが話題に出ていたので、半年前書きかけた記事を完成させられました。ありがとうございます!

CDK Tips: 自作コンストラクトをPython向けに公開する

AWS CDK Tipsシリーズです。

AWS CDKのコンストラクトライブラリPython向けに公開する手順のメモです。 最近 upsert-slr というライブラリをPyPIにも公開したので、その時のコードを例に紹介します。

モチベーション

最近のサーベイによれば、CDKのPythonユーザーは23%ほどいるようです。

TS/JS + Pythonで97%を超えるため、npmとPyPIで利用できるようにすれば、ほとんどのユーザーをカバーできることになります。

PyPIへのリリース設定は以下の通りそれほど大変ではないため、やっておいて損はないかもしれません。

手順

1. .projenrc.ts の編集

ライブラリの .projenrc.tspublishToPypi プロパティを、以下を参考に指定します。

const project = new awscdk.AwsCdkConstructLibrary({
  // 中略
  publishToPypi: {
    distName: 'upsert-slr',
    module: 'upsert_slr',
  },
});

distName はPyPIにおけるパッケージ名になります。module はインポートするときの名前です。上記の例だと、以下のようになります。

# インストールコマンド
pip install upsert-slr

# Pythonコードからのインポート文
import * from upsert_slr 

命名に関する慣例や制約はこちらが参考になります。PEP 8 – Style Guide for Python Code | peps.python.org

変更したら、projenを実行して変更を反映します。

yarn projen

2. PyPIAPIトークンを取得

まずはPyPIのユーザー登録をしてください。PyPI · The Python Package Index

登録後 Account settings を開くと、APIトークンを作成するパネルに飛べると思います。

作成するとトークン文字列 (pypi-xxxx) が得られるので、コピーしておきます。 もしコピーし忘れたなどでトークン文字列を失ったときは、削除して作り直しましょう。

3. GitHub actionsの環境変数を設定

CDKコンストラクトのProjenでは、Twineというツールを用いてPyPIにアクセスするようです。

このツールを使うため、GitHubリポジトリのSettings -> Security -> Secrets and variablesから以下の変数を作成します。こちらも参考になります。Using secrets in GitHub Actions - GitHub Docs

  • TWINE_USERNAME: __token__
  • TWINE_PASSWORD: 2で生成したAPIトークンの文字列

これにて設定は完了です!

動作確認

動作確認をするため、適当な変更をしてmainブランチにpushしましょう。自動的にPyPIへのリリースプロセスが走るはずです。

作成されたパッケージは、PyPIのアカウント画面などから確認できます。

この時点で、APIトークンの権限範囲として該当のパッケージにしぼり込めるようになります。 セキュリティを更に強化したい場合は、範囲を制限した上でトークンを作り直し、TWINE_PASSWORD 変数を再設定すると良いでしょう。

10分程度待つと、Construct HubもPythonパッケージを認識してくれます。

それでは!


今月のもなちゃんです。今年2回目の産卵を始めました。

AWS Lambda特化のJavaScriptランタイム「LLRT」を紹介

最近にわかに話題沸騰中のJavaScriptランタイム LLRT を紹介する記事です。

github.com

LLRTとは

LLRT (Low Latency Runtime) は、軽量なJavaScriptランタイムです。サーバーサイド向けのJavaScriptランタイムはNode.js、Deno、Bunなどが有名ですが、それらにまた一つ加わった形になります。主にLambdaでの利用が念頭に置かれているようです。その他必要な情報は README.md にまとまっています。以下は抜粋です。

AWSのソリューションアーキテクト Richard Davison さんにより開発されています。リポジトリAWSGitHub organization (awslabs) で公開されているため、実験的ではありますが、AWS公式のプロジェクトと言って良いでしょう。

ここ5日間ほどでとんでもない勢いでGitHubスター数が伸びており、注目度は高いと言えます。このため、今はほぼ一人で開発されているようですが、より多くの投資を受ける可能性も低くはないかもしれません。

awslabsリポジトリ上位6件のGitHubスター履歴。LLRTの注目度が窺える。

実装の詳細

より踏み込んだ部分を見ていきましょう。

内部のJavaScriptエンジンは、QuickJSというC言語で実装されたエンジンが利用されています。これはFabrice Bellardさんが開発する個人プロジェクトで、ES2020/ES2023準拠 (LLRTは現状ES2020のバージョンを利用) の軽量なエンジンのようです。

このQuickJSをRustから呼び出す rquickjs というライブラリを利用し、LLRTがNode.js特有のAPIをRustで独自実装することで、Node.jsとの互換性を高めています。

もちろん完全な互換性があるわけではないため、Node.jsのプログラムがそのままLLRTで動作しない場合も多いです。互換性を示した表がREADMEにあるため、見てみると良いでしょう。今のところは、最低限AWS SDK for JS v3が動作するところまでの互換性を実現しているように見えます。

今後細かな実装は変わりうるので、上記は今のところのスナップショットとして理解してください。現に今も、QuickJSではなくHermesというエンジンを利用すべきではという議論が進んでいるようです (Issue#110)。 また、Node.js互換というよりは、WinterCG互換を目指すという目標もあるようです (Issue#112)。

LLRTの特徴

名前の通り、特徴は軽量で低レイテンシーなことです。 Node.jsなど他のJavaScriptランタイムよりLambdaのコールドスタートが最大で10倍短くなることが謳われています

実際のデータはこちらのサイトが参考になるでしょう: Lambda Cold Starts analysis

C++, Rustに匹敵するコールドスタートの短さです。Goよりも短いですね。

簡単に比率を表にまとめてみました (zip、2024-02-13のデータ)

Avg Cold start duration Avg Memory Avg duration
LLRT 100 % (35.9 ms) 100 % (23.6 MB) 100 % (1.29 ms)
Rust (al2023) 52 % 56 % 115 %
Go (al2023) 137 % 60 % 121 %
Node.js v20 428 % 268 % 704 %
Bun (al2) 944 % 270 % 21308 %

LLRTの強みが見て取れると思います。普段Node.jsばかりLambdaで使っている身としては、コールドスタートが2桁msで終わるのは驚異的です。

なお、Bunのdurationが異常に長いですが、これはコールドスタート時のみの実行時間なので、不利な比較ではあるのかもしれません。

なぜ速いか

LLRTはいかにしてこの性能を実現できたのでしょうか。また、代償として何が失われているでしょうか。 Rationaleの章を読んでみましょう。

Node.js, Bun, Denoとの大きな違いは、LLRTはランタイムにJITコンパイラの機能を持たないことです。これにより、次の2つのメリットを得られました:

  1. 複雑なJITコンパイルの仕組みを排除することで、システムは単純になり、ランタイムのサイズも小さくなります
  2. JITコンパイルのオーバーヘッドがなくなり、CPU・メモリリソースの消費を低減できます

定性的には理解できる話と思います。実際にこれでどの程度数値が改善したのかは、上で見たとおりです。

しかしながら、これによるデメリットもあります。Limitationsの章で議論されていますが、

JITコンパイル自体による性能改善が効くタスク、例えば同じ処理を何度も実行する大規模なデータ処理や数値計算などでは、性能低下が見込まれる

とのことです。記事の後半で議論しますが、何でも置き換えれば良いという話ではなく、使い所を考える必要があります。

その他の高速化のポイントとして、JavaScriptのライブラリ (主にAWS SDK関連; uuid, fast-xml-parserなど) をRustによるネイティブ実装に置き換えるということもされているようです。

LLRTの使い方

LLRTはLambdaのカスタムランタイムとして利用できます。

LambdaでLLRTを利用するには、LambdaのランタイムとしてOS-only runtime (AL2 or AL2023)を指定し、コードのバンドルにLLRTのバイナリ (bootstrapファイル) を含めることが必要です。バイナリはGitHubのリリースからダウンロードできます。

コードは例えば以下のesbuildコマンドでバンドルすると良いようです。 一部のライブラリ (主にAWS SDK関連) はLLRTランタイムに同梱されているため、バンドルに含める必要がありません。

esbuild index.js --platform=node --target=es2020 --format=esm --bundle --minify --external:@aws-sdk --external:@smithy --external:uuid

上記の手間を省くため、LLRTのLambda関数をデプロイするCDKコンストラクトを公開しました。簡単に使えるはずなので、お試しください。

GitHub - tmokmss/cdk-lambda-llrt: Deploy LLRT Lambda functions

import { LlrtFunction } from 'cdk-lambda-llrt';

const handler = new LlrtFunction(this, 'Handler', {
    entry: 'lambda/index.ts',
});

ちなみに、ローカル環境でも同様にLLRTでJavaScriptを実行できます。

# release https://github.com/awslabs/llrt/releases から対応するバイナリをダウンロードする
wget https://github.com/awslabs/llrt/releases/download/v0.1.7-beta/llrt-darwin-arm64.zip
unzip llrt-darwin-arm64.zip

./llrt -h # help 表示

./llrt -e "console.log('OK')" # ワンライナーを実行

echo "console.log('OK')" > index.js
./llrt index.js #  ファイルから実行

LLRTの使い所

最後に、LLRTの使い所を考察します。

注意: 以降は個人的な感想を多分に含みますし、特に結論もでていません。

まず考慮のポイントは挙げると、以下でしょうか:

  • 制約
    1. (少なくとも現状は)Node.jsとの互換性は限定されたもので、多くのNode.js向けライブラリは動作しない
  • 強み
    1. コールドスタート時間の短さ
    2. 実行速度の速さ
    3. 1, 2によるユーザー体験の向上
    4. 2によるコスト削減効果
  • 弱み
    1. JITコンパイルがないことによる性能低下の可能性
    2. 互換性を検証するコスト

強み4を活かすには、それなりの規模で実行されている関数が良さそうです。さもなくば、弱み2のコストを上回るほどのメリットは享受できないためです。

また、強み3を考えると、エンドユーザーのリクエストに関わるLambda (API Gatewayのプロキシ先など)が良いでしょう。エンドユーザーと関わりのない部分においては、コールドスタート時間が数百ms程度縮んだところでメリットは大きくないためです。

ただし強み2を考えるなら、サーバーレスのイベント連携で生じがちな、イベントを受け渡し・加工・AWS API呼び出しだけのLambdaにも向いているかもしれません。このような基盤部分で実行時間を削減できると、チリツモで大きな効果が得られる場合もあるためです。その場合は、弱み1を意識して、何度も呼び出されたら結局JIT付きランタイムのほうが速くなる可能性も検証したいところです。

制約1は重要で、LLRTを使うにはLambda特化で薄く実装されたコードが向いてそうです。現時点の対応状況を見る限りでは、いっそAWS SDKのみを利用するコードに用途を絞るべきかもしれません。

そうなると、もし昨今流行りのLambdalithのような実装をしている場合は、部分的に切り出す作業が必要そうですね。 どうせ処理を切り出すのであれば、その部分だけ速い言語 (Rustなど) で書き換えるという選択も視野に入るため、

  1. LLRTに載せ替え: JavaScriptはそのまま使える (pro) が、互換性を検証する必要がある(con)
  2. 速い言語で書き換え: 新しい言語を使う必要がある (con) が、その言語においてはスタンダードな方法に乗っかれる (pro)

という2つの道のPros/consを検討することになるかもしれません。

上記のような点を考慮しながら、既存システムのパフォーマンスを観測し、LLRTという新しい道具を意識して使い所を探せば、見えてくることもあるのではないでしょうか。今後報告されてくるであろう利用事例に注視したいです。

まとめ

LLRTという新しいJavaScriptランタイムを紹介しました。AWS Lambdaユーザーにとっては面白い道具になりえるため、要チェックです。