コンテナ利用者に捧げる AWS Lambda の新しい開発方式 !

2021-03-02
デベロッパーのためのクラウド活用方法

Author : 下川 賢介

第一回 コンテナ Lambda の”いろは”、AWS CLI でのデプロイに挑戦 !

こんにちは、サーバーレス スペシャリストソリューションアーキテクトの下川 (@_kensh) です。
builder’s flashをご覧になっている皆様は AWS re:Invent 2020 で発表された「AWS Lambda  の新機能 – コンテナイメージのサポート」についてももご存知の方が多いと思いますが、ご存知ですか ?

今回はこのコンテナイメージサポートの基本的な話について触れていきたいと思います。

ご注意

本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

builders.flash メールメンバーへの登録・特典の入手はこちら »

*ハンズオン記事およびソースコードにおける免責事項 »


なぜコンテナイメージサポートが必要だったか ?

AWS Lambda は 2014 年 11 月 13 日にサービス提供を開始 して以来、当初は想像もしなかったほどの多くのワークロードで利用されてるようになってきました。

その中で、開発や運用にかかわる要望が出てきました。たとえばこんな要望。

  • プロセス・ガバナンスに対するよくあるご要望
    • サーバーレスもコンテナも使っているが、CI/CD とかツールが個別なのをどうにかしたい。
    • ローカルテストの方式を統一したい
    • 依存関係の管理方法を統一したい
    • イミュータブルイメージとしての配布や、ステージング環境や本番環境などの環境間を超えたデプロイをしたい
  • 処理における制限へのよくあるご要望
    • 機械学習関連ライブラリが大きくてサイズ上限にぶつかる。

AWS Lambda を利用しているシステムでは、システム内の別のエンドポイントが Amazon ECS や Amazon EKS で実装されていたり、コンテナとしてアプリケーションをデプロイしていることもよくあります。そのため Lambda は Zip デプロイ、コンテナ系サービスは Docker  イメージを利用したデプロイと、CI/CD の体験が異なり、開発者は二種類のデプロイ方法を学習する必要がありました。

またコンテナベースでの開発に慣れている人や組織は、ローカルのテスト方式もコンテナベースで行っていたり、依存関係もイメージ内に梱包することが一般的だと思います。さらに、ビルド環境でビルドしたイメージをテスト環境、ステージング環境、本番環境とイミュータブルイメージとして持っていきたいという思いもあるでしょう。

そこで、そういったお客様の要望に応えるように取り入れられた機能が AWS Lambda のコンテナイメージサポートです。


ZIP 形式との違い

Zip 形式とコンテナ形式の AWS Lambda サービスのクオータの違いですが、アーティファクトサイズ上限が Zip 形式の 250 MB に比べて、コンテナ形式の場合は、10 GB のイメージもデプロイすることが可能になっています。このため大きな機械学習ライブラリのような場合でも、10 GB を超えない範囲であれば EFS などの利用なしに Lambda 関数から依存関係を参照することができるようになりました。

  Zip コンテナ
ストレージ場所 S3 ECR
ストレージサイズ上限 (リージョン単位) 75GB (上限緩和可能) ECR のクォータに準拠
アーティファクトサイズ上限 250 MB (展開後) 10 GB
Layer 対応 あり なし
コード署名 あり なし

※この表は 2021/03/02 時点でのクォータになります。(最新の情報は AWS 公式ドキュメントを参照ください。)

もちろん、コンテナ形式のデプロイ方法が登場しても、これまでどおり Zip 形式のデプロイも可能です。

AWS Lambda はこれまで関数コードを Zip ファイルに圧縮してデプロイする形式のみがサポートされていました。Zip 形式の Lambda 関数のデプロイ最大サイズは 250 MB (展開後のサイズ、展開前は 50 MB が最大) ですので、機械学習のライブラリを梱包してデプロイしようとすると、この最大サイズ制限に引っかかることがよくありました。

もちろん、Amazon EFS に依存ライブラリを展開しても良いのですが、EFS を利用するためには VPC に接続した Lambda 関数として設定 する必要があり、EFS 以外の VPC リソースを利用する予定の無いユーザーにとっては依存ライブラリのためだけに、リージョナルサービス (Amazon S3, Amazon SQS など) にアクセスするためのネットワーク設定を行うのも多少手間となっていました。


ZIP 形式の Lambda 関数作成方法をまずおさらい

コンテナのデプロイ方式を見る前にまず、これまでどうやって Zip 形式の Lambda 関数をデプロイしていたか確認してみましょう。

img_new-lambda-container-development_01

クリックすると拡大します

まず、コマンドラインで必要な情報を環境変数に入れておきましょう。
ここでは、AWS のリージョン情報とアカウント ID を取得して環境変数に入れています。

$ REGION=$(aws configure get region)
$ ACCOUNTID=$(aws sts get-caller-identity --output text --query Account)

さて、今回デプロイする予定の Lambda 関数ですが、とくに何も処理せずに、message として “hello world” を返すだけの関数実装になっています。これを app.py というファイル名で保存しておきます。

app.py

import json

def handler(event, context):
    return {
        "statusCode": 200,
        "body": json.dumps(
            {
                "message": "hello world",
            }
        ),
    }

app.py のみを Zip 形式に圧縮しておきます。

$ zip package.zip app.py

ローカルの package.zip を指定して、Lambda 関数の作成をします。(AWS CLI がローカル環境にインストールされていない場合はインストールしておいてください。)
ROLE_ARN は AWS Lambda の実行ロール です。実行ロールの ARN を環境変数に入れておきます。

$ aws iam create-role --role-name lambda-ex \
--assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'

$ ROLE_ARN=arn:aws:iam::${ACCOUNTID}:role/lambda-ex

$ aws lambda create-function --function-name func1 \
--runtime python3.8 \
--handler app.handler \
--zip-file fileb://./package.zip \
--role ${ROLE_ARN} 

実行しデプロイが成功すると、関数のメタ情報が JSON 形式で出力されます。

$ aws lambda create-function --function-name func1 \
> --runtime python3.8 \
> --handler app.handler \
> --zip-file fileb://./package.zip \
> --role ${ROLE_ARN} 
{
    "FunctionName": "func1",
    "FunctionArn": "arn:aws:lambda:${REGION}:${ACCOUNTID}:function:func1",
    "Runtime": "python3.8",
    "Role": "arn:aws:iam::${ACCOUNTID}:role/<Role Name>",
    "Handler": "app.handler",
    "CodeSize": 290,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2021-02-08T14:21:05.688+0000",
    "CodeSha256": "SuXK7YhVmzzklYlNET8KKXFzzxpkgkJmzrxWbgPEmn0=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "34ca3994-31a5-4d7d-91a6-0d8152c5f0c1",
    "State": "Active",
    "LastUpdateStatus": "Successful",
    "PackageType": "Zip"
}

AWS CLI で作成したばかりの Lambda 関数を実行してみましょう。

aws lambda invoke --function-name func1 output ; cat output
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

正常終了しているのが確認できましたね。
ローカルにあるファイルは、Lambda 関数本体の app.py と、それを梱包した package.zip になっていると思います。

├── app.py 
└── package.zip

コンテナ形式の Lambda 関数作成方法をためしてみよう!

さて、つぎはコンテナを使ってビルド & デプロイしてみましょう。
(ここからの作業はローカル環境に Docker Desktop がインストールされていることを前提で進めます。インストールされていない場合は、インストールしてから進めてください。)

img_new-lambda-container-development_02

クリックすると拡大します

まず、Dockerfile を作っていきましょう。 

$ touch Dockerfile

ローカルにあるファイルは、Lambda 関数本体の app.py と、それを梱包した Dockerfile になっていると思います。
app.py は Zip 形式で関数を作成したときに利用したものをそのまま利用します。

├── Dockerfile 
└── app.py

Dockerfile の中身を書いていきましょう。

Dockerfile

FROM public.ecr.aws/lambda/python:3.8
COPY app.py   ./
CMD ["app.handler"]  

FROM 命令で public.ecr.aws/lambda/python:3.8 と ECR の AWS 公式の公開イメージを指定していますが、amazon/aws-lambda-python:3.8 のように docker hub のイメージを参照することも可能です。

COPY コマンドでローカルに配置されている Lambda 関数本体である app.py ファイルをイメージにコピーしています。そして CMD で Lambda 関数のハンドラーを渡しています。

この Dockerfile を元にビルドしてみましょう。

$ docker build -t func1 .
Sending build context to Docker daemon  7.168kB
Step 1/3 : FROM public.ecr.aws/lambda/python:3.8
 ---> 96d8701bf5d1
Step 2/3 : COPY app.py   ./
 ---> a3121c365747
Step 3/3 : CMD ["app.handler"]
 ---> Running in 458d223e3f30
Removing intermediate container 458d223e3f30
 ---> 91a66a0bc1ed
Successfully built 91a66a0bc1ed
Successfully tagged func1:latest

なんなく、ビルド成功しましたね。
さて、ここから ECR に今回作成する Lambda 関数のイメージ用のリポジトリを作成します。

$ aws ecr create-repository --repository-name func1 
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:${REGION}:${ACCOUNTID}:repository/func1",
        "registryId": "${ACCOUNTID}",
        "repositoryName": "func1",
        "repositoryUri": "${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1",
        "createdAt": "2021-02-09T00:12:39+09:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}

ECR 上にリポジトリが作成されたら、リポジトリの URI を含めたタグを付与します。

$ docker tag func1:latest \
  ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1:latest

リポジトリに push する前に、ECR にログインしておきます。

$ aws ecr get-login-password | docker login --username AWS --password-stdin ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com
Login Succeeded

そして、リポジトリに push しましょう。

$ docker push ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1:latest
The push refers to repository [${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1]
29fe8a4ae381: Pushed 
20b4eff3dd4d: Pushing  68.11MB/92.13MB
11284767d41d: Pushing  87.48MB/199.7MB
d6fa53d6caa6: Pushed 
b09e76f63d5d: Pushed 
0acabcf564c7: Pushed 
f2342b1247df: Pushing  125.9MB/294.9MB

ECR への push が終了し、正常に作成されたことを確認します。

29fe8a4ae381: Pushed 
20b4eff3dd4d: Pushed 
11284767d41d: Pushed 
d6fa53d6caa6: Pushed 
b09e76f63d5d: Pushed 
0acabcf564c7: Pushed 
f2342b1247df: Pushed 
latest: digest: sha256:ef29dcb71c314e61b390a48793c7291c44cd0712e9e3e16a88a5c851a9724223 size: 1788

push が完了すると、ダイジェストが発行されるので、これを DIGEST 環境変数に入れておきます。

ここから、Lambda 関数をコンテナイメージを利用して作成していきますが、AWS Lambda のコンテナサポートで、—package-type を指定できるようになりました。デフォルトは Zip ですので、ここでは Image としておきます。

さらに、 --code でイメージ URI を指定できるようになるのですが、ここで先ほど取得したダイジェストを含めた URI にしておきます。

DIGEST=$(aws ecr list-images --repository-name func1 --out text --query 'imageIds[?imageTag==`latest`].imageDigest')

aws lambda create-function \
     --function-name func1-container  \
     --package-type Image \
     --code ImageUri=${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1@${DIGEST} \
     --role ${ROLE_ARN}

実際に、関数作成のコマンドを実行すると、Lambda 関数が新規に作成され、作成された関数のメタ情報が JSON 形式で返ってきます。

$ aws lambda create-function \
>      --function-name func1-container  \
>      --package-type Image \
>      --code ImageUri=${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/func1@${DIGEST} \
>      --role ${ROLE_ARN}
{
    "FunctionName": "func1-container",
    "FunctionArn": "arn:aws:lambda:${REGION}:${ACCOUNTID}:function:func1-container",
    "Role": "arn:aws:iam::${ACCOUNTID}:role/<Role Name>",
    "CodeSize": 0,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2021-02-08T15:56:11.998+0000",
    "CodeSha256": "ef29dcb71c314e61b390a48793c7291c44cd0712e9e3e16a88a5c851a9724223",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "81fc0fd1-3011-4a67-ba08-65d143343aed",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Image"
}

そしていよいよ実行することができます。作成された関数を実行してみましょう。
実行のコマンド構文は Zip 形式の時と変わりません。

$ aws lambda invoke --function-name func1-container output ; cat output
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

正常終了できました。


まとめ

AWS Lambda の新たにサポートされた Docker コンテナ形式のデプロイを試してみました。この手順であるように既存の Lambda 関数のアプリケーションを再利用してコンテナ化した新規 Lambda 関数を作成することも可能です。今回実施した範囲では、手順についての理解は深められたと思いますが、まだコンテナ化した恩恵は感じられていないかなと思います。

次回以降ではさらに、コンテナサポート Lambda 関数の特徴や利点を追っていきたいと思います。

筆者プロフィール

photo_shimokawa-kensuke

下川 賢介 (@_kensh)
アマゾン ウェブ サービス ジャパン株式会社
シニア サーバーレススペシャリスト ソリューションアーキテクト

Serverless Specialist Solutions Architect として AWS Japan に勤務。
Serverless の大好きな特徴は、ビジネスロジックに集中できるところ。
ビジネスオーナーにとってインフラの管理やサービスの冗長化などは、ビジネスのタイプに関わらず必ず必要になってくる事柄です。
でもどのサービス、どのビジネスにでも必要ということは、逆にビジネスの色はそこには乗って来ないということ。
フルマネージドなサービスを使って関数までそぎ落とされたロジックレベルの管理だけでオリジナルのサービスを構築できるという Serverless の特徴は技術者だけでなく、ビジネスに多大な影響を与えています。
このような Serverless の嬉しい特徴をデベロッパーやビジネスオーナーと一緒に体験し、面白いビジネスの実現を支えるために日々活動しています。

AWS のベストプラクティスを毎月無料でお試しいただけます

さらに最新記事・デベロッパー向けイベントを検索

下記の項目で絞り込む
絞り込みを解除 ≫
フィルタ
フィルタ
1

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する