CDKでPython3.12ランタイムのLambdaをデプロイしようとして躓いた話

記事タイトルとURLをコピーする

はじめに

アプリケーションサービス部の鎌田(義)です。

最近CDKを使った構築に関わる機会があり、
プログラミング言語で記述できる点が気に入っていて、個人的にもIaCにはCDKを使う機会が増えています。

その中でPython3.12ランタイムのLambdaをデプロイしようとして
躓いたことがあった為、その内容を共有します。

前提

CDKのコンストラクタには、@aws-cdk/aws-lambda-python-alphaを使用していました。
上記機能は、現在も開発中のExperimentalな機能です。
Dockerコンテナ上でPipfile/poetry.lock/requiements.txtなどのファイルをもとに
依存関係を解決した上で、Lambdaをデプロイすることが可能になります。

以下の条件下で使用している際にエラーに遭遇しました。
今回の記事に関係のある部分のみ抜粋して記載しています。

  • Lambdaランタイムには、Python3.12を使用
  • コンストラクタには、@aws-cdk/aws-lambda-python-alphaのPythonFunctionを使用
  • パッケージングには、Pipenvを使用

起こったこと

以下サンプルのようなスタックをデプロイしようとしました。
CDKコードにはTypescriptを使用しています。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { aws_lambda as lambda } from 'aws-cdk-lib';
import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha';
  
export class TestCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const entry = 'src'
    new PythonFunction(this, 'TestFunction', {
      functionName: "testFunction",
      runtime: lambda.Runtime.PYTHON_3_12,
      entry,
      index: 'index.py',
      handler: 'handler',
      bundling: {
        assetExcludes: ['.venv'],
      },
    });
  }
}

cdk synthすると以下のようなエラーが出力されました。

Traceback (most recent call last):
  File "/usr/app/venv/bin/pipenv", line 5, in <module>
    from pipenv import cli
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/__init__.py", line 57, in <module>
    from .cli import cli
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/cli/__init__.py", line 1, in <module>
    from .command import cli  # noqa
    ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/cli/command.py", line 7, in <module>
    from pipenv.cli.options import (
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/cli/options.py", line 3, in <module>
    from pipenv.project import Project
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/project.py", line 21, in <module>
    from pipenv.core import system_which
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/core.py", line 30, in <module>
    from pipenv.utils.resolver import venv_resolve_deps
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/utils/resolver.py", line 14, in <module>
    from pipenv.vendor.requirementslib import Pipfile, Requirement
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/requirementslib/__init__.py", line 7, in <module>
    from .models.lockfile import Lockfile
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/requirementslib/models/lockfile.py", line 14, in <module>
    from ..utils import is_editable, is_vcs, merge_items
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/requirementslib/utils.py", line 11, in <module>
    import pip_shims.shims
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/pip_shims/__init__.py", line 26, in <module>
    from . import shims
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/pip_shims/shims.py", line 12, in <module>
    from .models import (
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/pip_shims/models.py", line 775, in <module>
    Command.add_mixin(SessionCommandMixin)
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/pip_shims/models.py", line 689, in add_mixin
    mixin = mixin.shim()
            ^^^^^^^^^^^^
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/pip_shims/models.py", line 737, in shim
    result = self.traverse(top_path)
             ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/pip_shims/models.py", line 729, in traverse
    result = shim.shim()
             ^^^^^^^^^^^
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/pip_shims/models.py", line 575, in shim
    imported = self._import()
               ^^^^^^^^^^^^^^
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/pip_shims/models.py", line 600, in _import
    result = self._import_module(self.calculated_module_path)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/vendor/pip_shims/models.py", line 352, in _import_module
    imported = importlib.import_module(module)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/lang/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/patched/notpip/_internal/cli/req_command.py", line 15, in <module>
    from pipenv.patched.notpip._internal.cache import WheelCache
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/patched/notpip/_internal/cache.py", line 13, in <module>
    from pipenv.patched.notpip._internal.exceptions import InvalidWheelFilename
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/patched/notpip/_internal/exceptions.py", line 7, in <module>
    from pipenv.patched.notpip._vendor.pkg_resources import Distribution
  File "/usr/app/venv/lib/python3.12/site-packages/pipenv/patched/notpip/_vendor/pkg_resources/__init__.py", line 2164, in <module>
    register_finder(pkgutil.ImpImporter, find_on_path)
                    ^^^^^^^^^^^^^^^^^^^
AttributeError: module 'pkgutil' has no attribute 'ImpImporter'. Did you mean: 'zipimporter'?

pkgutilには、ImpImporterという属性がないと出力されています。
pkgutilパッケージのページを見るとPython3.12のページからはImpImporterが削除されていました。

原因

エラーログにも出力されていたのですが、パッケージのバンドルには以下のコマンドが実行されているようです。
上記で記載したエラーログと合わせて見ると、どうやらpipenvの中でpkgutil.ImpImporterを使用していそうでした。

--> Command: docker run --rm -u "1000:1000" -v "<リポジトリ>/src:/asset-input:delegated" -v "<リポジトリ>/cdk.out/asset.322e9618894cee11d5bc6725b0f29ca137612e076f6c77f43e7b9cbb2014d332:/asset-output:delegated" -w "/asset-input" cdk-2cd508e07a6318940da85c0effe19a529949446120216189336894a12f3a56f1 bash -c "rsync -rLv --exclude='.venv' /asset-input/ /asset-output && cd /asset-output && PIPENV_VENV_IN_PROJECT=1 pipenv requirements > requirements.txt && rm -rf .venv && python -m pip install -r requirements.txt -t /asset-output"

自身の開発環境では以下を使用しており、pipenvも正常に使用できています。

  • Python 3.12.2
  • pipenv==2023.12.1

では、なぜ??と思いましたが、
バンドルに使用するDockerコンテナの中では別バージョンのpipenvが使用されていると思い調べてみました。

実行ログを見るとDockerイメージには、sam/build-python3.12が使われていますが、
イメージに使用されているDockerfileを見ても、
pipenvがインストールされている様子もなくイメージをダウンロードしてpip listで確認してみても、pipenvは存在していませんでした。

次に@aws-cdk/aws-lambda-python-alpha を調べてみると、Dockerfileがありました。
Dockerfileの中で以下記述を見つけました。

# pipenv 2022.4.8 is the last version with Python 3.6 support
    pip install pipenv==2022.4.8 poetry==$POETRY_VERSION && \

pipenv==2022.4.8が使用されているようです。

試しに、自身の環境を以下に変更しました。

  • Python 3.12.2
  • pipenv==2022.4.8

pipenv requirementsを実行してみると、同様のエラーが発生することを確認できました。

回避策

現時点(2024/5/15)では、前提に記載した条件下ではエラーとなってしまうようですが
回避可能ないくつかの案を記載します。

  1. poetryを使用する
  2. requirements.txtを使用する
  3. カスタムDockerイメージを使用する
  4. @aws-cdk/aws-lambda-python-alphaを使用せず、自身でパッケージングする

1. poetryを使用する

私はこちらの案を採用しましたが、
特段記載することがない為、本記事では案2, 3についてのみサンプルを記載します。

2. requirements.txtを使用する

スタックを以下に書き換えました。
bundlingの記述を削除し、entry配下にrequirements.txtを配置します。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { aws_lambda as lambda } from 'aws-cdk-lib';
import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha';
  
export class TestStackOk2 extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const entry = 'src'
    new PythonFunction(this, 'TestFunction', {
      functionName: "testFunction",
      runtime: lambda.Runtime.PYTHON_3_12,
      entry,
      index: 'index.py',
      handler: 'handler',
    });
  }
}

開発環境ではPipfileを使用しつつ、パッケージングにはrequirements.txtを使うパターン

entry配下にPipfileも置いている場合は、以下のようにbundling時のコマンドを上書きする必要があります。
cdk synthする前にpipenv requirements.txt > requirements.txtでrequirements.txtを生成しておきます。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { aws_lambda as lambda } from 'aws-cdk-lib';
import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha';
  
export class TestStackOk2 extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const entry = 'src'
    new PythonFunction(this, 'TestFunction', {
      functionName: "testFunction",
      runtime: lambda.Runtime.PYTHON_3_12,
      entry,
      index: 'index.py',
      handler: 'handler',
      bundling: {
        // Pipfileがentryと同じ階層に存在する場合、コンテナ内で自動的にPipfileからrequierements.txtが生成される為commandを上書きする必要がある
        // 既存コマンド: bash -c "rsync -rLv /asset-input/ /asset-output && cd /asset-output && PIPENV_VENV_IN_PROJECT=1 pipenv requirements > requirements.txt && rm -rf .venv && python -m pip install -r requirements.txt -t /asset-output"
        command: [
          'bash', '-c', 'rsync -rLv --exclude=".venv" --exclude="Pipfile*" /asset-input/ /asset-output && cd /asset-output && rm -rf .venv && python -m pip install -r requirements.txt -t /asset-output',
        ],
      },
    });
  }
}

3. カスタムDockerイメージを使用する

Dockerfile内で、pipenv==2022.4.8が指定されている点が原因の為
Dockerfileを別途用意しカスタムしたDockerイメージを使用してみます。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { aws_lambda as lambda } from 'aws-cdk-lib';
import { DockerImage } from 'aws-cdk-lib';
import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha';
  
export class TestStackOk3 extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const entry = 'src'
    const image = DockerImage.fromBuild(entry)
    new PythonFunction(this, 'TestFunction', {
      functionName: "testFunction",
      runtime: lambda.Runtime.PYTHON_3_12,
      entry,
      index: 'index.py',
      handler: 'handler',
      bundling: {
        image,
        assetExcludes: ['.venv', 'Dockerfile'],
      },
    });
  }
}

entryと同じ階層に配置したDockerfileを使用するように書き換えました。 Dockerfileは以下を用意しましたが、もとのDockerfileをほぼコピペで
1行目のFROMとpipenv==2023.12.1の部分のみ書き換えました。

# The correct AWS SAM build image based on the runtime of the function will be
# passed as build arg. The default allows to do `docker build .` when testing.
FROM public.ecr.aws/sam/build-python3.12

ARG PIP_INDEX_URL
ARG PIP_EXTRA_INDEX_URL
ARG HTTPS_PROXY
ARG POETRY_VERSION=1.5.1

# Add virtualenv path
ENV PATH="/usr/app/venv/bin:$PATH"

# set the pip cache location
ENV PIP_CACHE_DIR=/tmp/pip-cache

# set the poetry cache
ENV POETRY_CACHE_DIR=/tmp/poetry-cache

RUN \
# create a new virtualenv for python to use
# so that it isn't using root
    python -m venv /usr/app/venv && \
# Create a new location for the pip cache
    mkdir /tmp/pip-cache && \
# Ensure all users can write to pip cache
    chmod -R 777 /tmp/pip-cache && \
# Upgrade pip (required by cryptography v3.4 and above, which is a dependency of poetry)
    pip install --upgrade pip && \
# Create a new location for the poetry cache
    mkdir /tmp/poetry-cache && \
# Ensure all users can write to poetry cache
    chmod -R 777 /tmp/poetry-cache && \
# pipenv 2022.4.8 is the last version with Python 3.6 support
    pip install pipenv==2023.12.1 poetry==$POETRY_VERSION && \
# Ensure no temporary files remain in the caches
    rm -rf /tmp/pip-cache/* /tmp/poetry-cache/*

CMD [ "python" ]

最後に

最後までご覧頂きありがとうございます。
繰り返しにはなりますが@aws-cdk/aws-lambda-python-alphaはExperimentalな機能です。
今回の件については、Issueで報告している為その内解消されるかもしれません。

前提にも記載したように特定の条件下のみで発生する為、
遭遇するケースも少ないかもしれませんがどなたかの参考になれば幸いです。

鎌田 義章 (執筆記事一覧)

2023年4月入社 AS部DS3課