[セッションレポート]Lambdaのコンテナイメージ管理の裏側に詳しくなれるセッション SVS404 Deep dive into AWS Lambda security: Function isolation #reinvent

10G近いコンテナイメージのLambdaが何故あんなに早くコールドスタートできるのか?...謎が解けました
2020.12.25

CX事業本部@大阪の岩田です。このブログはre:inventのセッション「SVS404 Deep dive into AWS Lambda security: Function isolation」のレポートです。Lambdaというサービスの裏側が垣間見える興味深い内容でre:invent2018、re:invent2019の「A serverless journey: AWS Lambda under the hood」に興味がある人にはかなり刺さるセッションだと思います。是非読んでみて下さい。

セッション概要

以下公式サイトからの引用です。

This session dives into the security model behind AWS Lambda functions, looking at how you can isolate workloads, build multiple layers of protection, and leverage fine-grained authorization. You learn about the implementation, the open-source Firecracker technology that provides one of the most important layers, and what this means for how you build on Lambda. You also see how AWS Lambda securely runs your functions packaged and deployed as container images. Finally, you learn about SaaS, customization, and safe patterns for running your own customers’ code in your Lambda functions.

ということで、Lambdaの基盤がどのようにFunction間の環境分離を実現しているかLambdaのセキュリティモデルについて学べるセッションとなっています。また、Lambdaがどのようにコンテナイメージをパッケージ化してセキュアに実行しているのか?コンテナイメージサポートの背後にあるアーキテクチャについても学べるセッションとなっています。

スピーカーはre:invent2018、re:invent2019の「A serverless journey: AWS Lambda under the hood」でLambdaの裏側についてガッツリ語ってくれたMarc Brookerさんです。これは否が応にも期待が広がります!

セッション動画及び資料は以下のリンクから確認できます。※要レジストレーション

Deep dive into AWS Lambda security: Function isolation

以下セッションレポートと筆者による所感です。セッションの内容はある程度要約しているので、興味のある方は是非先程のリンクからセッション動画をご確認下さい。

Lambda Functionの環境分離について

まずは、Lambda Functionの分離についてです。

スライドの右側にある小さな黄色い点はメモリを128M割り当てた最小単位のLambda Functionです。対して左側の黄色の円は巨大なサーバー(この例ではm5.metalのEC2インスタンス)です。 この規模のサーバーは最新のデータセンターにおいては典型的なサイズです。スライドから分かる通りハードウェアリソースの観点から考えると数100~数1,000のLambda Functionを1台の物理サーバーで稼働させることが可能です。そしてサーバーレスアーキテクチャのスケーラビリティとパフォーマンスは多くの異なるワークロードを1つのハードウェアに同居させることによって実現されています。ワークロードを同居させるということは、優れたスケーラビリティとパフォーマンスを提供するだけではなくセキュリティを担保することが重要になります。

(筆者の感想など)

複数の異なるワークロードを1つのハードウェアに同居させることでスケーラビリティとパフォーマンスを提供するという考え方はre:invent2018のA Serverless Journey: AWS Lambda Under the HoodでLambdaの内部コンポーネントであるPlacement Serviceについて解説する際に詳しく説明されています。このあたりの説明は過去セッションのおさらいといったところでしょうか。

Lambdaの内部アーキテクチャについて

ここからLambdaの内部アーキテクチャの話に入ります。

この図は1台のハードウェア上で2つのLambda Functionが実行される際の基本的なスタック構成です。実際には1台のハードウェア上で数100~数1,000のLambda Functionが実行されています。スタックの最下層はAWSアカウントを跨いで共有されるリソースで、EC2ベアメタルインスタンス上でAmazonLinuxを実行しています。このホストOS/カーネルはLambda Functionを互いに環境分離するための仮想化機能の一部を提供しています。

図の上部はAWSアカウントごとに専有されるリソースです。各Lambda Functionはそれぞれ専用のリソース、つまりFirecracker(※Firecrackerの詳細については後述)のプロセスを持ちます。MicroVMというコンポーネントはLambda Functionの実行に特化した仮想マシンで、MicroVMの中では

  • Lambdaの実行に特化したゲストカーネル
  • JVMやCLRといった各言語のランタイム
  • Lambda Function及びExtensions

が動作します。現在プレビュー中のLambda Extensionsですが、Lambda Functionやランタイムと同じセキュリティモデルを利用しており、他のLambda Function実行環境からは完全に隔離されています。

(筆者の感想など)

このあたりも過去のre:inventのセッション等で既に言及されている内容ですね。目新しいところとしてはLambda Extensionsの説明でしょうか。

Firecrackerについて

先程のスライドに出てきたFirecrackerとは何でしょうか?Firecrackerは、コンテナとFunctionベースのセキュアなマルチテナントサービスを作成・管理することを目的としたオープンソースのVMM(Virtual Machine Monitor)です。Lambdaのサービスチームが各Lambda Functionのコードをセキュアに分離するための技術を検討した際、既存の技術は全て要件にマッチしなかったため、自分たちで新たな技術を構築することにしました。これがFirecrackerです。

つまりFirecrackerはLambda Functionに仮想化を提供するコンポーネントであり、IO等の処理を実行するために以下のような形でコンポーネントのスタック内に収まります。

図の点線部分がMicroVMで、これが仮想化されたハードウェアによって提供される強力なセキュリティ境界です。この中には2つのレイヤーがあり、ゲストカーネルとユーザースペースがあります。ユーザースペースはUntrusted codeと呼ばれており、Lambda FunctionやExtensions、ライブラリなどを含みます。Lambda Functionの所有者からは信頼できるコードでもAWS側からは信頼できるコードではありません。同じようにAWSユーザーは別のAWSユーザーのコードを信頼すべきではありません。そのため、Lambdaの基盤に置かれたコードは全て信頼されておらず、特権も持たないものとして扱います。Lambda Functionは各MicroVM内のゲストカーネル上で実行され通常のLinuxソフトウェアのようにカーネルが提供するほとんどのAPIにアクセス可能です。

そしてMicroVMから外部への2つのインターフェースがあります。1つ目はKVMです。

ホストカーネル上のKVMによってMicroVM上で発生したページフォルトやレジスタの状態の保存/復元といった低レイヤの処理が実行されます。

より複雑な処理は各MicroVM専用のFirecrackerプロセスによって処理されます。

重要なことは、ネットワークおよびディスクのIO が含まれていることです。つまりMicroVM内で実行されているプロセスやコードが、ネットワーク経由でパケットを送受信したり、ディスクを読み書きしたりする際は常にFirecrackerのプロセスを経由しているということです。これはゲストカーネルが提供しているシステムコールがVirtIOドライバー経由でFirecrackerのプロセスとコミュニケーションすることで実現されています。FirecrackerはVirtIOドライバー経由で受け取ったパケットやIO要求をホストカーネルに渡すことで、実際のハードウェアへのアクセスが行われます。このようにFirecerackerはMicroVMに対してハードウェアエミュレータのように振る舞います。

(筆者の感想など)

このあたりも過去のre:inventのセッション等で既に言及されている内容ですね。参考までにVirtIOドライバとは仮想マシンから利用できる準仮想化デバイスドライバです。準仮想化なので完全仮想化と比べてパフォーマンス面でのメリットが大きくなります。

Inner sandboxとJailer

ここからはLambdaの実行基盤に組み込まれた追加のセキュリティレイヤに関する話です。セキュリティに関しては多層防御という考え方が重要で、複数のセキュリティレイヤを持つことはシステムにとって良いアイデアです。Lambdaの実行基盤における最初のセキュリティレイヤはMicroVM内にあるサンドボックス環境(Inner sandbox)です。

ランタイムやLambda Function、Lambda ExtensionsのコードがInner sandbox内で実行されます。このサンドボックスは以下の2つの役割を持ちます。

  • CloudWatch等で利用するメトリクス情報をフックするための場所を提供する
  • 通常Lambda Functionからはアクセス不要なゲストカーネルの機能へのアクセスを遮断し、セキュリティレベルを向上させる

もう1つのサンドボックスはJailerです。先程FirecrackerがネットワークパケットやディスクIOをホストカーネルに渡すという話をしました。Firecrackerのプロセスが実行している仕事はIO周りの処理が中心で、実際にはあまり多くの仕事をしていません、この特性を利用してFirecrackerのプロセスをJailerと呼ばれる非常に制限の厳しいサンドボックス環境に配置しています。このサンドボックス環境はFirecrackerプロセスがホストOS上でできることを大幅に制限しています。万が一Lambda Functionのコードが侵害され、攻撃者がゲストカーネルと仮想化レイヤーを通過し、Firecrackerのプロセス内で悪意のあるコードを実行したとしても、Firecrackerのプロセスは非常に制限の厳しいサンドボックス内で動作しています。

FirecrackerはOSSなので、GitHubで公開されているソースコードを確認することで、Jailerが許可するシステムコールの一覧を確認することができますす。

(筆者の感想など)

JailerはFirecrackerをビルドすると生成されるバイナリの1つでcgroupやchrootといったシステムコールを利用して、制限された環境下でFirecrackerのプロセスを起動します。以前ラズパイ上にFirecracker環境を構築した際にJailerを試そうとしたのですが、うまく動作せず(当時はFirecrackerのARMサポートがプレビュー段階だったからかもしれません)Jailerに関する理解が浅い状態になっていました。Inner sandboxとJailerがそれぞれ別のものであるということを改めて整理することができ理解を深めることができました。個人的にはInner sandboxとfirecracker-containerdの関連性が気になっています。Fargateの基盤はfirecracker-containerdを利用しているそうですが、Lambdaの基盤はどうなんでしょうか?

Lambdaのコンテナイメージサポートについて

ここから話題が変わってLambdaのコンテナイメージサポートに関する話になります。

内部アーキテクチャの変更

LambdaのパッケージフォーマットとしてコンテナイメージをサポートするためにLambadの内部アーキテクチャに変更を加えました。

「Sparse filesystem」と呼ばれるMicroVM専用の新しいコンポーネントが追加されています。これはFirecrackerの隣にあるコンポーネントでディスクへの読み取りと書き込みを処理するための仮想化レイヤーを追加提供します。

従来のZIPパッケージではLambda Functionのサイズ上限は250MBでした。それに対してコンテナイメージでは最大10GBのサイズをサポートしています。これは約40倍のサイズです。LambdaのパッケージフォーマットがZIPの場合コールドスタート時にコードをWorker上にダウンロードするのですが、コンテナイメージのサイズがZIPの40倍だからといって、40倍もの時間を待ちたくありません。Lambdaのサービスチームはコンテナイメージを利用する場合もZIP形式と同じようにスケールさせ、顧客にサーバーレスのメリットを感じて欲しいと考える一方でパフォーマンスについて妥協するつもりはありませんでした。

(筆者の感想など)

これまでの内容はre:invent2019までで言及されていた内容でしたが、新たにSparse filesystemという概念が登場しました。ゆくゆくはFargateの基盤にもSparse filesystemが展開されていくのでしょうか?こんな妄想をしてみるのも楽しいですね。

IOの最適化

コンテナイメージ形式のLambdaでもハイパフォーマンスを実現するべくコンテナの中に何が入っているかを分析しました。 するとコンテナは共通のベースレイヤーから派生していることが多いと気付きました。例えばLambda用に提供しているベースレイヤーだったり、AWSが提供しているコンテナベースレイヤーだったり、OSSで広く使われているベースレイヤーだったり...です。つまり多くのコンテナイメージ間でデータの中身は共通部分が非常に多いのです。

コンテナイメージのもう1つの特性はIOがまばらであるということです。ファイルシステム上の全ファイルにアクセスしたり、ディスク上の全ブロックにアクセスするようなLambda Functionは非常に稀です。通常ランタイムの起動時にはいくつかのファイルを読み込み、ライブラリとデータセットをロードしますが、ディスク上の大部分にはアクセスしないのです。コンテナのワークロードを分析すると、ディスク上のブロックの数%しか読み書きしていないことは珍しくありませんでした。この特性はロード時間を最適化するための絶好のチャンスです。

コンテナイメージ形式のLambdaではコンテナイメージを小さなチャンクに分割し、それらのチャンクを必要に応じてオンデマンドでロードすることでLambda Functionの起動を高速化します。このアーキテクチャを採用すると、たとえコンテナイメージにファイルやライブラリを追加導入しても、それらのファイルにアクセスしない限りはLambda Function起動のオーバーヘッドは増えません。

(筆者の感想など)

先日コンテナイメージからデプロイしたLambdaのコールドスタートについて検証したところ、10GB近いイメージのコールドスタートが概ね1秒未満に収まるという結果に終わりました。どうやってこの速度を実現していうのか不思議でしたが、イメージのチャンクをオンデマンドでロードすることで実現しているんですね。謎が解けました。確かに検証で利用したコンテナイメージは巨大なファイルを含んでいるものの、ファイルにはアクセスしていないため、このアーキテクチャの恩恵を受けられそうです。Lambdaの公式ドキュメントによると、コンテナイメージ形式のLambdaデプロイ時の挙動として

When you deploy code as a container image to a Lambda function, the image undergoes an optimization process for running on Lambda. This process can take a few seconds, during which the function is in pending state. When the optimization process completes, the function enters the active state.

という解説があります。このoptimization processに中でコンテナイメージのチャンクを分割していると考えて良さそうです。

コンテナイメージに対するセキュリティの担保

コンテナイメージのIOに関するセキュリティ特性について見ていきます。

FirecrackerのプロセスはMicroVMからのIOリクエストを処理しています。そしてFirecrackerのプロセスはローカルのSparse filesystemエージェントに対して読み書きが可能です。Sparse filesystemエージェントの内部にはいくつかの重要な概念があります。

  • まず1つ目はMicroVM専用のローカルキャッシュ(Dedicated local cache)です。このキャッシュにより高速なIOが可能になります。
  • もう1つはWrite overlayです。 LambdaのコードからFirecracker経由で流入する全ての書き込み要求はこのSparse filesystemエージェントで処理されます。後ほど説明しますがSparse fileSystemエージェントはKMS と統合されています。
  • Dedicated local cacheからの読み取りがキャッシュミスを引いた場合は、さらに物理マシン(Lambda Worker)上の共有ローカルキャッシュ(Shared local cache)を利用します。物理マシンは高速なローカルストレージを持ち、このローカルストレージは複数のSparse filesystemエージェントによって共有されています。
  • さらに3つ目のキャッシュとしてAZの共有ローカルキャッシュ(Shared AZ-local cache)があります。このローカルキャッシュはさらに大容量のキャッシュを持ちます。また、EC2ネットワークが高速なため、このローカルキャッシュからのロードも非常に高速に行われます。

これらのコンポーネントのセキュリティについて考えてみます。

Shared local cache、Shared AZ-local cacheにキャッシュされたチャンクは全て暗号化されており、平文のチャンクは存在しません。さらに、Shared local cacheには他のLambda Workerから書き込まれることはありません。つまりこれら2つのキャッシュは暗号化されたチャンクの読み取り専用ストアでしかありません。

それに対して各MicroVMが専有するDedicated local cacheはコンテナイメージ内の平文にアクセス可能です。なぜ平文にアクセスする必要があるかというと、Lambda Functionからコンテナ内のファイルを読みとる際に、暗号化されたファイルではなく平文のファイルそのものを取得したいからです。

これらの暗号化/複合処理はSparseFileSystemとKMSの統合によって実現されており、共有コンポーネントに暗号化されて保存されているデータはKMSによって復号されます。

(筆者の感想など)

コンテナイメージのキャッシュ先として3種類のキャッシュが存在することが分かりました。Shared AZ-local cacheは高速とはいえNW経由でのアクセスになるため他のローカルキャッシュに比べて少し遅くなりそうですね。先日のコールドスタート検証ブログで最大値で2秒ぐらいかかっていたのはShared AZ-local cacheからコンテナイメージを取得していたためでしょうか?このあたりを検証してみるのも面白そうですね。暗号化/複合のアーキテクチャに関しても説明を聞くと「なるほど!」という感想です。

重複排除

先程各コンテナには共通のベースレイヤーがあるという話をしましたが、この特徴はどのように活用したのでしょうか?この特徴は重複排除のプロセスで活用されています。つまり、各コンテナイメージ固有の特別なデータだけを分離し、他のAWSアカウントや同一AWSアカウント内の他のコンテナイメージと共通のデータはまとめて保存することでストレージの容量を最適化しているのです。

セキュアな重複排除を実現するためには収束暗号化(Convergent Encryption)と呼ばれる手法が利用されています。この方式ではデータのチャンクからキーハッシュ等の技術を用いて計算されたデータから暗号化キーを抽出します。その後抽出された暗号化キーを用いてチャンクが暗号化されます。抽出された暗号化キーはさらにKMSが管理するキーによって暗号化されマニュフェストに保存されます。

マニュフェストというのはチャンクのリストで

  • チャンクがディスク上のどこに配置されているか?
  • チャンクの暗号化に利用したキーがどれか?

という情報を管理しています。

このように全てのデータのチャンクが固有の暗号化キーを持ち、さらにこのキーをユーザー固有の暗号化キーで暗号化したマニュフェストがLambdaの基盤で管理されています。マニュフェストには全てのデータチャンクの鍵が含まれています。この方式では、どのコンポーネントも平文のデータにアクセスすることなくセキュアな重複排除が実現できます。平文のコンテナデータにアクセスできるのはローカルエージェントだけであり、ローカルエージェントはKMSキーを利用してデータにアクセスしています。

(筆者の感想など)

重複排除自体はなじみのある技術だったのですが、収束暗号化は初めて知る技術で勉強になりました。収束暗号化の概要について理解するのに以下のリンクが役立ちました。ありがとうございます。

その他の参考資料

セッションの最後にその他参考資料として以下の3つが紹介されていました。

  • Security Overview of AWS Lambda
    • このセッションでも紹介されていたLamda実行環境のスタック構成、EC2モデルとFirecrackerモデルの違いなどを学べます。
  • re:invent2019のセッション Speculation & leakage: Timing side channels & multi-tenant computing (SEC355)

感想&まとめ

個人的にLamdbaというサービスを実現している裏側のアーキテクチャにとても興味があり、これまでもre:invent2018、re:invent2019のセッションをブログにまとめたり、Firecrackerの論文を読んでみたりと、色々とLambdaの内部アーキテクチャについて情報収集してきました。今回のre:inventでコンテナイメージのサポートが追加された際に、裏側のアーキテクチャは一体どうなっているんだろう?と疑問に思ったのですが、こんなに早く答えを知れるとは思っていませんでした。このセッションを聞いて正解でした。もし同じようにLambdaの裏側に興味をお持ちの方がいれば以下のブログも読んでみて下さいm(_ _)m