Amazon Web Services ブログ
コンテナイメージとしてパッケージ化された Lambda 関数の最適化
AWS Lambda は、re: Invent 2020 でコンテナイメージとしての関数のパッケージ化とデプロイのサポートを開始しました。この記事では、イメージサイズを縮小し、ビルド、デプロイ、更新時間を短縮するコンテナイメージの構築方法を学習します。Lambdaコンテナイメージには、最適化のために考慮すべき固有の特性があります。Lambda 関数のコンテナイメージを最適化するために使用するテクニックは、他の環境で使用するものとは少し異なります。
コンテナイメージを最適化する方法を理解するためには、コンテナイメージのパッケージ方法と、Lambda サービスがどのようにコンテナイメージの取得、キャッシュ、デプロイ、およびリタイアするかを理解することが役に立ちます。
前提条件と想定
この記事は、AWS アカウントの IAM ユーザーまたはロールにアクセス可能で、マシン上のtar ユーティリティが利用できることを前提としています。また、Docker と AWS SAM CLI をインストールして Docker を起動する必要があります。
Lambdaコンテナイメージのパッケージ化
Lambda コンテナイメージは、Open Container Initiative (OCI) イメージフォーマット仕様に従ってパッケージ化されます。仕様書では、プログラムが個々のレイヤを単一のコンテナイメージにビルドしてパッケージ化する方法を定義しています。OCIイメージフォーマットの例を調べるには、ターミナルを開いて次の手順に従います。
- AWS SAMアプリケーションの作成
sam init --name container-images
- 1を選択して AWS クイックスタートテンプレートを選択し、2を選択してコンテナイメージをパッケージ形式として選択し、最後に 9 を選択して amazon-go1.x-base イメージを使用します。
- AWS SAM CLI がアプリケーションを生成したら、次のコマンドを入力して新しいディレクトリに移動し、Lambda コンテナイメージをビルドします。
cd container-images sam build
- AWS SAM は関数をビルドし、helloworldfunction: go1.x-v1 としてパッケージ化します。このコンテナイメージを tar アーカイブにエクスポートし、ファイルシステムを新しいディレクトリに抽出して、イメージ形式を調べます。
docker save helloworldfunction:go1.x-v1 > oci-image.tar mkdir -p image tar xf oci-image.tar -C image
イメージディレクトリには、いくつかのサブディレクトリ、コンテナメタデータ JSON ファイル、マニフェスト JSON ファイル、およびリポジトリ JSON ファイルが含まれています。各サブディレクトリは 1つの Layer を表し、バージョンファイル、独自のメタデータ JSON ファイル、および Layer を構成するファイルの tar アーカイブが含まれています。
manifest.json
ファイルには、コンテナメタデータファイルの名前、リポジトリタグのリスト、および含まれるレイヤのリストを含む 1つの JSON オブジェクトが含まれています。含まれるレイヤのリストは、Dockerfile でのビルド順序に従って順序付けされます。各サブフォルダのメタデータ JSON ファイルには、各レイヤからその親レイヤまたは最終コンテナへのマッピングも含まれます。
Lambda関数には、以下のようなレイヤが必要です。コンテナイメージにファイルが追加されるたびに、別のレイヤが作成されます。これには、Dockerfile およびベースイメージの Dockerfile の FROM、RUN、ADD、および COPY ステートメントが含まれます。レイヤの特定のレイヤID、レイヤサイズ、数量、レイヤの構成は、時間の経過とともに変化する可能性があります。
ID | Size | Description | Your function’s Dockerfile step |
5fc256be… |
641 MB | Amazon Linux | |
c73e7f67… |
320 KB | Third-party licenses | |
de5f5100… |
12 KB | Lambda entrypoint script | |
2bd3c722… |
7.8 MB | AWS Lambda RIE | |
5d9d381b… |
10.0 MB | AWS Lambda runtime | |
cb832ffc… |
12 KB | Bootstrap link | |
1fcc74e8… |
560 KB | Lambda runtime library | FROM public.ecr.aws/lambda/go:1 |
acb8dall… |
9.6 MB | Function code | COPY –from=build-image /go/bin/ /var/task/ |
ランタイムは、各イメージレイヤを親の上に破壊的に上書きすることによって、ファイルシステムイメージを生成します。つまり、1つのレイヤに変更を加えると、すべての子レイヤを再作成する必要があります。次の例では、レイヤ cb832ffc...
を変更すると、レイヤ 1fcc74e8...
と acb8da111...
も「ダーティ」とみなされ、新しい親イメージから再作成する必要があります。これにより、8 つのレイヤを持つ新しいコンテナイメージが作成されます。最初の 5 つのレイヤは元のイメージと同じ、最後の 3 つのレイヤは新しく構築され、それぞれ新しい ID と親が割り当てられます。
コンテナイメージの階層構造は、コンテナイメージを最適化するときに行ういくつかの決定について影響を与えます。
コンテナイメージを最適化するための戦略
コンテナイメージを最適化するには、主に 4 つの戦略があります。まず、可能な時はいつでもAWS が提供するベースイメージをコンテナイメージの出発点として使用します。次に、マルチステージビルドを使用して、不要なレイヤやファイルを最終イメージに追加しないようにします。第三に、Dockerfile 内の操作を、最も安定したものから頻繁に変更するように順序付けます。第 4 に、アプリケーションのすべての関数で 1つ以上の大きなレイヤを使用する場合は、すべての関数を 単一のリポジトリに格納します。
AWS 提供のベースイメージを使用する
コンテナランタイム用の従来のアプリケーションをパッケージングした経験がある場合は、AWS が提供するベースイメージを使用すると直感に反するように見えることがあります。AWS が提供するベースイメージは、通常、他の最小コンテナベースイメージよりも大きくなります。たとえば、Goランタイム public.ecr.aws/lambda/go: 1
の AWS 提供のベースイメージは 670 MB ですが、最小コンテナイメージを構築するための一般的な開始点であるalpine:latest
は 5.58 MB です。ただし、AWS が提供するベースイメージを使用すると、3 つの利点があります。
まず、AWS が提供するベースイメージは、Lambda サービスによってプロアクティブにキャッシュされます。つまり、ベースイメージは近くの別のアップストリームキャッシュにあるか、ワーカーインスタンスキャッシュにすでに存在しています。はるかに大きいにもかかわらず、キャッシュされないかもしれないサードパーティのベースイメージと比較するとデプロイ時間が短くなる可能性があります。Lambda サービスがコンテナイメージをキャッシュする方法の詳細については、re: Invent 2020 のトーク Deep dive into AWS Lambda security: Function isolationを参照してください。
第 2に、AWS が提供するベースイメージは安定しています。ベースイメージはコンテナイメージの最下層にあるため、変更を加えると、他のすべてのレイヤを再構築して再デプロイする必要があります。基本イメージへの変更が少なくなると、再構築と再デプロイが少なくなるため、ビルドコストが削減されます。
最後に、AWS が提供するベースイメージは Amazon Linux および Amazon Linux 2上に構築されます。選択したランタイムによっては、関数に必要なユーティリティやライブラリが既に含まれている場合があります。つまり後でレイヤを追加する必要はなく、これによってビルドステップが増えてコストが増加する追加のレイヤを作成する必要がなくなります。
マルチステージビルドを使用する
マルチステージビルドでは、より大きな事前のイメージでコードをビルドし、必要なアーティファクトのみを最終コンテナイメージにコピーし、事前のビルドステップを破棄することができます。つまり、任意に多数のコマンドを実行し、中間イメージにファイルを追加またはコピーできますが、アーティファクト用のコンテナイメージには ひとつの追加レイヤしか作成しません。これにより、ランタイムイメージからビルド時の依存関係を除外することで、コンテナイメージの最終的なサイズと外部からの攻撃対象領域の両方が削減されます。
AWS SAM CLI は、マルチステージビルドを使用する Dockerfile を生成します。
FROM golang:1.14 as build-image
WORKDIR /go/src
COPY go.mod main.go ./
RUN go build -o ../bin
FROM public.ecr.aws/lambda/go:1
COPY --from=build-image /go/bin/ /var/task/
# Command can be overwritten by providing a different command in the template directly.
CMD ["hello-world"]
この Dockerfile は 2ステージのビルドを定義します。まず、golang:1.14
コンテナイメージをプルし、build-image
という名前を付けます。中間ステージの命名は任意ですが、最終コンテナイメージをパッケージ化するときに、前のステージを参照しやすくなります。golang:1.14
イメージは 810 MB で、Lambda サービスによってキャッシュされない可能性が高く、本番環境のイメージには含める必要の無いビルドツールが多数含まれていることに注意してください。ビルドイメージステージは、関数を構築し、/go/bin
に保存します。
第 2および最終ステージは、public.ecr.aws/lambda/go:1
ベースイメージから始まります。このイメージは 670 MB ですが、AWS が提供するイメージであるため、ワーカーインスタンスにキャッシュされる可能性が高くなります。COPY コマンドは、ビルドイメージステージから /go/bin
の内容をコンテナイメージ内の /var/task
にコピーし、中間ステージを破棄します。
安定版からの頻繁な変更のビルド
イメージ内のレイヤが変更されるたびに、Lambda サービスによって後続するすべてのレイヤを再構築、再パッケージ化、再デプロイし、再キャッシュする必要があります。実際、これは最も頻繁に発生する変更を可能な限りDockerfile内で後に記述する必要があることを意味します。
たとえば、頻繁に更新される機械学習モデルを使用して予測を行う安定した Lambda 関数がある場合、機械学習モデルを追加する前に、関数をコンテナイメージに追加します。一方で安定した Lambda 拡張機能モジュールに依存する、頻繁に変更される関数がある場合は、最初にLambda拡張機能をイメージにコピーします。
頻繁に変更されるコンポーネントを Dockerfile の初期ステージに置くと、そのコンポーネントが変更されるたびに後続のすべてのビルド手順を再実行する必要があります。大規模なライブラリのコンパイルや複雑なシミュレーションの実行など、これらのアクションの ひとつにコストがかかる場合は、これらの繰り返しによってデプロイパイプラインに不要な時間とコストがかかります。
大きなレイヤを伴う関数には 単一のリポジトリを使用する
複数の Lambda 関数を使用してアプリケーションを作成する場合、コンテナイメージを 1つの Amazon ECR リポジトリに格納するか、または関数ごとに 1つずつ、複数のリポジトリに格納できます。アプリケーションが、すべての関数で 1つ以上の大きなレイヤを使用する場合は、すべての関数を 1つのリポジトリに格納します。
ECR リポジトリは、コンテナイメージのプッシュ時に各レイヤを比較し、重複のアップロードと保存を回避します。アプリケーションの各関数が同じ大きなレイヤ(カスタムランタイムや機械学習モデルなど) を使用する場合、そのレイヤは共有リポジトリに 1回だけ保存されます。関数ごとに個別のリポジトリを使用する場合、そのレイヤはリポジトリ間で複製され、各リポジトリに個別にアップロードする必要があります。これには時間とネットワークの帯域幅のコストがかかります。
結論
Lambda 関数をコンテナイメージとしてパッケージ化すると、使い慣れたツールを使用し、より大きなデプロイサイズを利用できます。この記事では、イメージサイズを縮小し、ビルド、デプロイ、更新時間を短縮するコンテナイメージの構築方法を学習しました。また最適化に影響する Lambda コンテナイメージの特性をいくつか学習しました。最後に、コンテナランタイム用の従来のアプリケーションのパッケージングと比較して、Lambda 関数のイメージ最適化について異なる考え方を学習しました。
ソースコード、ブログ、ビデオなど、サーバーレスアプリケーションのビルド方法の詳細については、Serverless Landの Web サイトを参照してください。
日本語翻訳はSA福井 厚が担当しました。原文はこちらです。