🐈

[2024年5月暫定版]ryeでパッケージ管理しているPython Lambda FunctionをAWS CDKでデプロイする

2024/05/26に公開

boto3など以外のサードパーティーライブラリを含むPythonのLambdaのデプロイはそこそこ面倒である。
cdkであれば、experimental扱いであるが、

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html

というモジュールが存在し、2024年5月現在ではPipenvおよびPoetry:

https://pipenv.pypa.io/en/latest/

https://python-poetry.org/

を使ってパッケージ管理されているLambdaはよしなにデプロイまでやってくれる。

ただ、最近は更に後発のパッケージ管理ツールとしてrye:

https://rye.astral.sh/

が出てきている。現在進行形で活発に開発がなされており、逆に言うと安定しきっているとは言えないかもしれないが、
後発な分機能は洗練されており非常に使い心地が良い。
こちらを使いたい場合、先述のaws-lambda-python-alphaではサポートされていないので、cdkにおけるビルド・デプロイ方法は自分で考える必要があり、やってみたというメモ。

雑な結論

改善点は残っているが、一応作ってみたものを置いておく。

https://github.com/junkor-1011/cdk-rye-lambda-bundle-example/tree/zenn_20240525

cdk部分は↓

https://github.com/junkor-1011/cdk-rye-lambda-bundle-example/blob/zenn_20240525/lib/cdk-app-stack.ts

深い意味は無いが、1つのLambdaにライブラリを全部含めるものと、
ついででサードパーティライブラリをLambda Layerに分離する構成のものと2パターンを作っている。

詳細部分は後述するとして、
要点としてはcdkのlambda.Code.fromDockerBuild:

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Code.html#static-fromwbrdockerwbrbuildpath-options

を使うことにより、Dockerfileを書けば非常に高い自由度でLambdaもしくはLambda Layerの内容をカスタマイズできる、というもの。

Lambdaのある程度の仕様の理解とDockerfileを書く技術があればPythonやryeに限らずあらゆる構成でLambdaを作ることができ、
その一環としてryeを使う場合もカバー出来る、という感じ。

ここで、ryeのlockファイルからLambdaおよびLambda Layerのアセットを作成するにあたり、
ryeのlockファイルがほぼ普通の(いわゆる)requirements.txtの形式になっていることを利用し、Dockerfile内で一部編集を加えた上でpipコマンドで所定のパスにライブラリを配置することでLambda内でのPathに含まれるようにしている。

https://github.com/junkor-1011/cdk-rye-lambda-bundle-example/blob/zenn_20240525/python-lambda/hello-world/Dockerfile

https://github.com/junkor-1011/cdk-rye-lambda-bundle-example/blob/zenn_20240525/python-lambda/hello-world-with-layer/dependencies-layer.Dockerfile

解説など

1. Layerなどを使わない単一のLambdaでやる場合

シンプルな方。

注意点としては

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html

などにあるように、2024年5月現在では各関数およびLayerに関してデプロイパケージサイズ:

  • 圧縮済みで50MB以下
  • 解凍後で250MB以下

の制約条件に収まる必要がある。
もしパッケージサイズが上限を超えそうであればLayerなどに分割していくなど工夫する必要がありそう。

とりあえずcdkで記載したLambdaの記載部分は↓のような感じ:

lib/cdk-app-stack.ts(抜粋)
import * as cdk from 'aws-cdk-lib';
import { aws_lambda as lambda } from 'aws-cdk-lib';
import type { Construct } from 'constructs';

// 他、適当にimportなど

export class CdkAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new lambda.Function(this, 'python-lambda', {
      functionName: 'hello-world-function', // 名前などは適当に変える
      runtime: lambda.Runtime.PYTHON_3_12, // 実際使うバージョンに合わせて適当に変える
      handler: 'index.handler', // Lambdaのファイル名とハンドラーに使っている関数名に合わせて変える(ここではindex.pyのhandlerを使用)
      code: lambda.Code.fromDockerBuild( // ★lambda.Code.fromDockerBuildにより、Lambdaにデプロイする内容をDockerfileで記述する
        path.join(__dirname, '../python-lambda/hello-world'), // Dockerfileが置いてあるパスを指定
        { file: 'Dockerfile' },
      ),
      architecture: lambda.Architecture.X86_64,
      // (その他オプションを適宜追加)
    });

    // (中略)

  }
}

上記のように、Lambdaの内容をDockerfileで作成するようにしている。
Dockerfileの置き場所は↓

https://github.com/junkor-1011/cdk-rye-lambda-bundle-example/tree/zenn_20240525/python-lambda/hello-world

ryeによりプロジェクト作成・管理がされており、大体以下のようなファイル構成になっている:

tree -a --gitignore
.
├── Dockerfile
├── .dockerignore
├── .gitignore
├── index.py # Lambdaのエントリーポイント
├── lib # 自作のライブラリやモジュールなど
├── pyproject.toml
├── .python-version
├── requirements-dev.lock # dev-dependenciesのみに入っているパッケージはLambdaに入れないようにする
└── requirements.lock # ★これを使ってDockerfile内でライブラリをインストール・配置する

Lambdaの内容はナンセンスな適当なものだが、とりあえずpydanticで適当なスキーマをデシリアライズしたものをレスポンスで返すようにしている。

Dockerfileの内容は以下:

Dockerfile
FROM docker.io/library/python:3.12-slim

# ソースコードなどを/asset以下に配置する(不要なものは.dockerignoreで含まれないようにしておく)
COPY ./ /asset/

WORKDIR /asset

# ref: https://github.com/astral-sh/rye/discussions/239
RUN sed '/^-e/d' /asset/requirements.lock > /asset/requirements.txt && \
    pip install -r /asset/requirements.txt --no-cache-dir -t /asset && \
    rm /asset/requirements* && \
    rm -rf __pycache__

lambda.Code.fromDockerBuildにおいて、
デフォルトでは作成したイメージの/asset以下にあるファイル・ディレクトリが抽出される。参考:

By default, the asset is expected to be located at /asset in the image.

というわけで、冒頭のCOPY ./ /asset/でソースコード一式を/asset以下に配置している。
このとき余計なものが入らないように.dockerignoreなどを書いておく。
内容はケースバイケースな部分もあるが、今回は↓のような感じ:

.dockerignore
*.pyc
*.pyo
.venv

Dockerfile

# EDIT below

README.md
pyproject.toml
requirements-dev.lock

次に、これでは肝心のサードパーティライブラリが/assetに含まれていないので追加していく。

ここで、ryeによって作成されるlockfileであるrequirements.lockは例えば以下のような内容:

requirements.lock
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
#   pre: false
#   features: []
#   all-features: false
#   with-sources: false

-e file:.
annotated-types==0.6.0
    # via pydantic
pydantic==2.6.4
    # via hello-world
pydantic-core==2.16.3
    # via pydantic
typing-extensions==4.10.0
    # via pydantic
    # via pydantic-core

ここで、-e file:.部分が不要だが、そこを除けばRequirements File Format:

https://pip.pypa.io/en/stable/reference/requirements-file-format/

になっている。そのため、requirements.lockファイルを少しだけ手を加えて(ここではsedコマンドによるワンライナー)requirements file formatに直し、pipコマンドを使ってインストールを行っている。

ライブラリはパスが通っているところに置く必要があるが、

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-package.html#python-package-create-dependencies

によると、

Lambda でコードを実行するには、ハンドラーコードとすべての関数の依存関係を含む.py ファイルを.zip ファイルのルートにインストールする必要があります。
(...)
.zip ファイルは、次のように関数のハンドラーコードとすべての依存関係フォルダがルートにインストールされた、フラットなディレクトリ構造である必要があります。

などと記載があり、要はプロジェクトのルートの階層に置けばいいので、pipコマンドのオプションを調整して/asset直下にインストールするようにしている。
(ここで、pipコマンドを使える必要があるため、LambdaのPythonランタイムと同じバージョンのPythonが使えるベースイメージ: python:3.12-slimを使っている)

動作確認は、例えばsam:

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/using-sam-cli-local.html

を使って、

cdk synth

# -tで指定しているテンプレート名および最後の引数の関数名は適宜変更する
sam local invoke -t cdk.out/CdkAppStack.template.json python-lambda

などとすると実行できる。

cdk.out以下のディレクトリを漁るとAWSにデプロイされる関数のアセット内容なども入っているので確認してみると良いかもしれない。

2. Layerにパッケージを分割する方法

  1. までで書くことは大体書いているが、一応補足しておく。

cdkコード部分の抜粋は以下:

lib/cdk-app-stack.ts(抜粋)
import * as cdk from 'aws-cdk-lib';
import { aws_lambda as lambda } from 'aws-cdk-lib';
import type { Construct } from 'constructs';

// その他importなど

export class CdkAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // (中略)

    // Lambda Layer(pythonのサードパーティライブラリを入れる)
    const dependenciesLayer = new lambda.LayerVersion(
      this,
      'LambdaLayer', // 名前などは適当に変える
      {
        layerVersionName: 'python-dependencies-layer-example', // 名前などは適当に変える
        code: lambda.Code.fromDockerBuild( // 関数同様、Layerもlambda.Code.fromDockerBuildで作成できる
          path.join(__dirname, '../python-lambda/hello-world-with-layer'),
          {
            file: 'dependencies-layer.Dockerfile',
          },
        ),
        compatibleRuntimes: [lambda.Runtime.PYTHON_3_12],
        compatibleArchitectures: [lambda.Architecture.X86_64],
      },
    );

    // ↑で作ったLayerを組み合わせる
    new lambda.Function(this, 'python-lambda-with-layer', {
      functionName: 'hello-world-function-with-layer',
      runtime: lambda.Runtime.PYTHON_3_12,
      handler: 'index.handler',
      code: lambda.Code.fromAsset( // Lambdaの内容として、特定のディレクトリの内容を指定できる
        path.join(__dirname, '../python-lambda/hello-world-with-layer'),
        { // ↓Lambdaに含めないものを指定して、余計なものがアップロードされないようにする
          ignoreMode: cdk.IgnoreMode.GIT,
          exclude: [
            '*.Dockerfile',
            '*.md',
            'requirements*',
            'pyproject.toml',
            '.venv',
            '.gitignore',
          ],
        },
      ),
      layers: [dependenciesLayer], // 作成したLambda Layerを指定している
      architecture: lambda.Architecture.X86_64,
      // その他オプションを適宜追加
    });
  }
}

参照されているLambdaのプロジェクトは↓:

https://github.com/junkor-1011/cdk-rye-lambda-bundle-example/tree/zenn_20240525/python-lambda/hello-world-with-layer

関数本体はlambda.Code.fromAsset:

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Code.html#static-fromwbrassetpath-options

を使ってローカルのディレクトリの内容を元に作成しており、AssetOptions:

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3_assets.AssetOptions.html

によって不要なファイルが入らないようにしている。

PythonのライブラリはLayerの方のlambda.Code.fromDockerBuildによって作成している。

dependencies-layer.Dockerfile
FROM docker.io/library/python:3.12-slim

COPY requirements.lock /workspace/

WORKDIR /workspace

# ref:
RUN sed '/^-e/d' /workspace/requirements.lock > /workspace/requirements.txt && \
    mkdir /workspace/tmp && \
    pip install -r /workspace/requirements.txt --no-cache-dir --prefix=/workspace/tmp && \
    rm -rf /workspace/tmp/bin && \
    mkdir -p /asset/python && \
    mv /workspace/tmp/lib -t /asset/python

sedを使ったワンライナーによってrequirements.lockを編集しているのは先ほどと同様である。

ここで、

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-layers.html#python-layer-paths

参照すると、

関数にレイヤーを追加すると、Lambda はレイヤーのコンテンツをその実行環境の /opt ディレクトリに読み込みます。Lambda ランタイムごとに、PATH 変数には /opt ディレクトリ内の特定のフォルダパスがあらかじめ含まれます。PATH 変数がレイヤーコンテンツを取得できるようにするには、レイヤーの .zip ファイルの依存関係が次のフォルダーパスにある必要があります。

  • python
  • python/lib/python3.x/site-packages
    • (xはpython3のマイナーバージョン)

とのことなので、Layerを使ってライブラリを含める場合はPython3.12の場合、Dockerfileで

  • /asset/python
  • /asset/python/lib/python3.12/site-packages

のいずれかにライブラリが配置するように出来れば良いことがわかる。
後者はPythonのバージョンを変更するときに一緒に変えないといけなくなりメンテナンス性が微妙なので、今回は前者を採用している。

今回はpip install/workspace/tmpに一旦インストールしておいて、不要そうなファイルやディレクトリを削除した上で/asset/pythonに置き直している。

こちらも動作確認ではsam localなどを使って

cdk synth

# -tで指定しているテンプレート名および最後の引数の関数名は適宜変更する
sam local invoke -t cdk.out/CdkAppStack.template.json python-lambda-with-layer

などとするとローカルでlambdaが動作することを確認できる。

(内容に特に意味はないので割愛していたが、こちらのLambdaではサードパーティライブラリとしてhttpリクエストを投げるためにrequestsを入れており、 http://checkip.amazonaws.com にアクセスしてIPアドレスを返す)

なお、ここでは雑にパッケージを単一のLayerに全て突っ込んでいるため、
全章で書いたような関数・レイヤー毎のサイズ上限のクォータの問題を直接解決は出来ていないが、参考にはなると思うので一応記載した。

改善点など

boto3をどうするか問題

boto3を使う場合、こちらはPythonのLambdaではデフォルトで含まれているため、
特定のバージョンを指定して使いたい場合を除けば自身でアップロードする必要はない。(容量の無駄になる)

だが、boto3の依存ライブラリまで含めて考えるとrequirements.lockからうまく選んで除去するのは結構難しい。
boto3はdev-dependenciesに入れて管理するという方法も考えられるが、ソースコードでimportして使うライブラリをdev-dependenciesに入れないといけないのはちょっと気持ち悪い気もする。。。

ランタイムのバージョン管理

現状、PythonのLambdaのランタイムバージョンはcdkのコードで指定している部分に加えて、アセットを作るためのDockerfileやryeのプロジェクト中の.python-versionファイルなどに分散して記載されている。

バージョンを変更する場合、これらを全て整合が取れるように手で変えないといけない。

(ただ、この辺の辛さは@aws-cdk/aws-lambda-python-alphaを使ってもほとんど変わらない問題なような気もする)

その他参考

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-package.html

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-layers.html

https://github.com/astral-sh/rye/discussions/239

GitHubで編集を提案

Discussion