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

  1. Lambda を作成
  2. 外部ライブラリーを利用
  3. 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 を利用して初期化しましょう。

/packages/lambda/foo/index.py

def handler(event, context):
    return {
        'statusCode': 200,
        'body': 'This is a foo!'
    }

適当に Lambda からレスポンスを返すように実装します。

/packages/infra/python-lambda-stack.ts

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 が呼び出される時間をログに出力してみます。

/packages/infra/python-lambda-stack.ts

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 を追加します。

/packages/lambda/foo/index.py

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 をなくす必要があります。

/packages/infra/python-lambda-stack.ts

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) でした。

Reference