AWS CDK で Lambda (Python) の依存性を楽に管理しようぜ
Introduction
今コミットしている案件で約 20個以上の Lambda (Python) が手動運用されていて、しかもステージング環境もなく本番環境だけだったので、下の利点をもとに AWS CDK で IaC 化した方が良さそうっていう提案をしました。
- メインテナンスのコストが減る
- 協業がやりやすい
- デプロイが自動化される
- テストが書ける
ちょうど Python 向けの Lambda モジュール (aws-lambda-python)が Lambda Layer までサポートしていたので、試しに触ってみた経験を共有します。
必須条件
- AWS CDK
- v1.69.0 or later
関連プルリクエスト
https://github.com/aws/aws-cdk/pull/9582 https://github.com/aws/aws-cdk/pull/10959 https://github.com/aws/aws-cdk/pull/10022 https://github.com/aws/aws-cdk/pull/9763 https://github.com/aws/aws-cdk/pull/9355 https://github.com/aws/aws-cdk/pull/9182
Goal
- Lambda を作成
- 外部ライブラリーを利用
- Lambda Layer を活用
登場人物
- Lambda
- Python
Getting Started
- バージョン
- AWS CDK: 1.69.0
- Python: 3.8.6
- Docker
aws-lambda-python
モジュールが依存性管理に利用
1. Lambda を作成
$ mkdir packages/lambda/foo && cd packages/lambda/foo $ pipenv --python 3.8
まず、Lambda の置き場を作って pipenv
を利用して初期化しましょう。
def handler(event, context): return { 'statusCode': 200, 'body': 'This is a foo!' }
適当に Lambda からレスポンスを返すように実装します。
import * as path from "path"; import * as cdk from '@aws-cdk/core'; import * as lambdapython from '@aws-cdk/aws-lambda-python'; export class PythonLambdaStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const fnFoo = new lambdapython.PythonFunction(this, 'fn-foo', { functionName: 'foo', runtime: lambda.Runtime.PYTHON_3_8, entry: path.resolve(__dirname, '../lambda/foo'), index: 'index.py', handler: 'handler' }) cdk.Tags.of(fnFoo).add("runtime", "python") } }
lambdapython.PythonFunction
は既存の lambda.Function
を継承しているので、使い方は似ているんですが、Lambda ファイルを指定するやり方が若干異なります。
Attribute | Role |
---|---|
entry | Lambda 関数が入っているディレクトリ経路 |
index | Lambda 関数のファイル名 |
handler | Lambda 関数のハンドラー名 |
$ cdk deploy --all --require-approval never --profile kim
CDK をデプロイして何がアップロードされたのか AWS Console 上で確認しましょう。
$ aws lambda invoke --function-name foo response.json && cat response.json { "StatusCode": 200, "ExecutedVersion": "$LATEST" } {"statusCode": 200, "body": "This is a foo!"} $ aws lambda invoke \ --function-name foo out \ --log-type Tail \ --query 'LogResult' \ --output text | base64 -d START RequestId: 10c2a9a8-f1b8-4ca9-b6e5-6f9e381d064b Version: $LATEST END RequestId: 10c2a9a8-f1b8-4ca9-b6e5-6f9e381d064b REPORT RequestId: 10c2a9a8-f1b8-4ca9-b6e5-6f9e381d064b Duration: 0.77 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 51 MB
レスポンスとログも問題なさそうですね。
2. 外部ライブラリーを利用
$ mkdir packages/lambda/bar && cd packages/lambda/bar $ pipenv --python 3.8 $ pipenv install chronyk
二番目の Lambda も同様に Lambda 置き場の作成と pipenv
で初期化します。今回はライブラリーも入れたいので、適当に Chronyk を追加します。
from chronyk import Chronyk def handler(event, context): print(Chronyk("now").ctime()) return { 'statusCode': 200, 'body': 'This is a bar!' }
chronyk
をインポートして Lambda が呼び出される時間をログに出力してみます。
import * as path from "path"; import * as cdk from '@aws-cdk/core'; import * as lambdapython from '@aws-cdk/aws-lambda-python'; export class PythonLambdaStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); ... const fnBar = new lambdapython.PythonFunction(this, 'fn-bar', { functionName: 'bar', runtime: lambda.Runtime.PYTHON_3_8, entry: path.resolve(__dirname, '../lambda/bar'), index: 'index.py', handler: 'handler' }) cdk.Tags.of(fnBar).add("runtime", "python") } }
二番目の Lambda 定義も先ほどの stack に追加します。
$ cdk deploy --all --require-approval never --profile kim
Lambda をデプロイして AWS Console を確認します。pipenv
に追加した chronyk
ライブラリーが勝手にアップロードされていることが分かります。
$ aws lambda invoke --function-name bar response.json && cat response.json { "StatusCode": 200, "ExecutedVersion": "$LATEST" } {"statusCode": 200, "body": "This is a bar!"} $ aws lambda invoke \ --function-name bar out \ --log-type Tail \ --query 'LogResult' \ --output text | base64 -d START RequestId: f4e9b19f-4eb7-4c87-86b6-9ef22d486257 Version: $LATEST Mon Nov 23 08:15:14 2020 END RequestId: f4e9b19f-4eb7-4c87-86b6-9ef22d486257 REPORT RequestId: f4e9b19f-4eb7-4c87-86b6-9ef22d486257 Duration: 0.93 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 51 MB
レスポンスも問題ないし、ログにも chronyk
を利用した出力 (Mon Nov 23 08:15:14 2020
)が存在しているので問題なさそうですね。
3. Lambda Layer を活用
chronyk
ライブラリーを両方の Lambda で使いたくなった場合、Lambda Layer を使って一箇所で管理した方が良さそうです。
$ mkdir packages/lambda/layer && cd packages/lambda/layer $ pipenv --python 3.8 $ pipenv install chronyk
まず、Lambda Layer の置き場と pipenv
初期化及び chronyk
を追加します。
from chronyk import Chronyk def handler(event, context): print(Chronyk("now").ctime()) return { 'statusCode': 200, 'body': 'This is a foo!' }
一番目の Lambda に二番目の Lambda と同様な処理を入れましょう。
$ cd packages/lambda/bar && pipenv uninstall chronyk
そして、二番目の Lambda の依存性から chronyk
をなくす必要があります。
import * as path from "path"; import * as cdk from '@aws-cdk/core'; import * as lambdapython from '@aws-cdk/aws-lambda-python'; export class PythonLambdaStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const layer = new lambdapython.PythonLayerVersion(this, 'python-lambda-layer', { layerVersionName: 'python-lambda-layer', entry: path.resolve(__dirname, '../lambda/python/layer'), compatibleRuntimes: [ lambda.Runtime.PYTHON_3_8 ] }) const fnFoo = new lambdapython.PythonFunction(this, 'fn-foo', { ..., layers: [ layer ] }) cdk.Tags.of(fnFoo).add("runtime", "python") const fnBar = new lambdapython.PythonFunction(this, 'fn-bar', { ..., layers: [ layer ] }) cdk.Tags.of(fnBar).add("runtime", "python") } }
その後、stack に Lambda Layer を定義し、それぞれの Lambda の layers
に追加します。
$ cdk deploy --all --require-approval never --profile kim
Lambda をデプロイして AWS Console を確認します。一番目と二番目 Lambda 両方 chronyk
ライブラリーがアップロードされていないことが分かります。chronyk
に関するコードは Lambda Layer にアップロードされているので想定している挙動です。
$ aws lambda list-layers --compatible-runtime python3.8 { "Layers": [ { "LayerName": "python-lambda-layer", "LayerArn": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:layer:python-lambda-layer", "LatestMatchingVersion": { "LayerVersionArn": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:layer:python-lambda-layer:4", "Version": 4, "CreatedDate": "2020-11-23T08:30:18.374+0000", "CompatibleRuntimes": [ "python3.8" ] } } ] }
Lambda Layer も問題なくアップロードされたっぽいですね。引き続き挙動確認を行いましょう。
// 一番目の Lambda $ aws lambda invoke --function-name foo response.json && cat response.json { "StatusCode": 200, "ExecutedVersion": "$LATEST" } {"statusCode": 200, "body": "This is a foo!"} $ aws lambda invoke \ --function-name foo out \ --log-type Tail \ --query 'LogResult' \ --output text | base64 -d START RequestId: 4268b266-230e-4e7c-bf38-e034e45f0b06 Version: $LATEST Mon Nov 23 08:33:46 2020 END RequestId: 4268b266-230e-4e7c-bf38-e034e45f0b06 REPORT RequestId: 4268b266-230e-4e7c-bf38-e034e45f0b06 Duration: 1.04 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 51 MB // 二番目の Lambda $ aws lambda invoke --function-name bar response.json && cat response.json { "StatusCode": 200, "ExecutedVersion": "$LATEST" } {"statusCode": 200, "body": "This is a bar!"} $ aws lambda invoke \ --function-name bar out \ --log-type Tail \ --query 'LogResult' \ --output text | base64 -d START RequestId: 434c66ff-64fc-4b9d-b40f-781a502c90e6 Version: $LATEST Mon Nov 23 08:35:56 2020 END RequestId: 434c66ff-64fc-4b9d-b40f-781a502c90e6 REPORT RequestId: 434c66ff-64fc-4b9d-b40f-781a502c90e6 Duration: 1.20 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 51 MB
レスポンスとログも両方問題ないですね。嬉しい!
Summary
これまで Lambda の依存性管理とか Lambda Layer の扱いにシェルスクリプトなどを加味しないといけないちょっと面倒な箇所があったんですが、AWS CDK X Python で Lambda を実装する場合は結構楽になった気がします。aws-lambda-python
に該当するプルリクエストのコメントを見た感じだと、一つのプルリクエストに 2ヶ月ほど投資してくださった方があったので、その方に感謝しています。
そして、AWS CDK X TypeScript の場合は aws-lambda-nodejs
っていうモジュールがあるんですが、Lambda Layer の扱いをきれいにサポートするメカニズムがないので、ちょっと工夫してみたいと思っています。
この記事が誰かのお役に立てれば幸いです。
以上、CX事業本部 MADチーム、キム (@sano3071) でした。