CDKでLambdaをイメージとしてデプロイする

# まとめ

まとめを先に書きます。

CDKを用いたIaCプロジェクトから、コンテナイメージとしてLambdaをデプロイする事ができます。実際に試すことができるミニマムなコードを紹介します。

https://github.com/coleyon/cdk-containered-lambda-min-sample

# AWS LambdaにおけるCDKとコンテナの位置づけ

本記事のサンプルが狙うポイントの紹介ですが、既にご存じの方は次セクションへスキップしてください。

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html

https://www.docker.com/ja-jp/resources/what-container/

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-create.html


**コンテナを利用する利点**
カスタマイズ性が高いLambdaを開発できることと、イメージサイズの上限が高いことが挙げられます。Zipパッケージでは250MBが上限であるところ、イメージでは10GB[^1]まで利用できます。大きめのpythonパッケージ[^2]を使う場合には250MBは厳しい制約ですが、10GBであればOSレイヤーのサイズも含むとはいえ十分な自由度が得られます。

[^1]: ただしZipパッケージとは異なり、イメージの場合はOSやミドルウェアレイヤーのサイズを含む上限です。Lambdaのクォータに関する仕様 - https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html
[^2]: たとえばNumPyやSciPyのパッケージサイズは250MBを超えます - https://avilpage.com/2020/02/reduce-python-package-footprint.html

**CDKを利用する利点**
AWSのサービスを用いたワークロードのIaC[^3]が上手にできるツールです。著名なプログラミング言語(TypescriptやPythonなど)でワークロード全体 (アプリケーションやインフラなど) を記述し、コードの実行によりデプロイや更新できます[^4]。つまり、Python言語でLambdaそのものとLambda上で実行されるプログラムの両方を記述し、git上で一様に管理することができます。これにより、一例としてプログラムはきちんと変更管理をしているのに、実際にデプロイされたLambdaの設定変更は管理されていなかった…といった分断が予防されます。

[^3]: IaCに関するAWSのホワイトペーパー https://aws.amazon.com/jp/what-is/iac/
[^4]: デベロッパーガイドの冒頭 https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html

**利点の用例**

次のようなケースをまるっとコードで取り扱えます。

* [cx-Oracle](https://pypi.org/project/cx-Oracle/) を入れて[^5]Oracle DBにアクセスするLambdaを開発するぞ。**[アクセス先DB](https://github.com/aws-samples/aws-cdk-examples/blob/master/python/rds/oracle/oracle.py#L25)も込みで！**
* [openCV](https://pypi.org/project/opencv-python/) に独自の学習モデルを与えて[^6]物体認識させるLambdaを開発するぞ。**認識結果の[出力先ストレージ](https://github.com/aws-samples/aws-cdk-examples/blob/master/python/lambda-s3-trigger/s3trigger/s3trigger_stack.py#L19)も込みで！**

[^5]: 環境要件に[Oracle Instant Clientのインストールが要求される](https://cx-oracle.readthedocs.io/en/latest/user_guide/installation.html#overview)ので、イメージデプロイが向いている
[^6]: [extraモジュール](https://docs.opencv.org/4.x/)や[独自の分類器](https://qiita.com/takanorimutoh/items/5bd88f3d17239a147581)もインストールして使いたい場合なども、イメージデプロイが向いている

# 開発環境

当記事の作成環境です。

* Ubuntu Linux 22.x
  * Python 3.11
  * Docker Server 20.x - https://docs.docker.com/engine/install/ubuntu/
  * node v18.x - https://github.com/nvm-sh/nvm
  * Poetry 1.7.x - https://python-poetry.org/

# cdkコマンドをインストール

```bash
$ npm install -g aws-cdk
$ cdk --version
2.114.1 (build 02bbb1d)
$
```

AWS Cliのプロファイル作成などの前提は下記デベロッパーガイドをご覧ください。

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/work-with.html

# プロジェクト作成

CDKのサンプルアプリを新規作成します。ミニマムな例を示したいので、サンプルコードを改変する流れで紹介していきます。

```bash
$ cdk init sample-app --language=python
...
✅ All done!
$
```

初期状態では、以下のようファイル一式が作成されます。

```bash
$ ls
README.md  cdk.json              requirements-dev.txt  sample
app.py     requirements.txt      tests                 source.bat
$ 
```

# Poetryを使う

当記事はPoetryを利用した状況での説明内容になっていますので、requirementsをPoetry経由でインストールします。

```bash
$ poetry init
$ poetry add $( cat requirements.txt )
$ poetry add --dev $( cat requirements-dev.txt )
```

今回、requirementsは使いませんので削除しておきます。

```bash
$ rm requirements.txt
$ rm requirements-dev.txt
```


# Python仮想環境に入る

pythonパッケージをインストールし、仮想環境に入ります。aws-cdk-libがインストールされている事も確認しておきます。

```bash
$ poetry install
$ poetry shell
$ poetry show aws-cdk-lib
 name         : aws-cdk-lib                                        
 version      : 2.110.0    
...
$
```


# Dockerfileを書く

Lambda のベースコンテナイメージが Amazon ECR リポジトリで公開されていますので利用します。ほかにも多数のバージョンや開発言語向けのイメージがありますので、ご興味がある方は次のドキュメントをご覧ください。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-image.html#python-image-base


```dockerfile:Dockerfile
FROM public.ecr.aws/lambda/python:3.11
# Install poetry
RUN curl -sSL https://install.python-poetry.org \
    | POETRY_HOME=/opt/poetry python \
    && cd /usr/local/bin \
    && ln -s /opt/poetry/bin/poetry \
    && poetry config virtualenvs.create false
COPY src/ ${LAMBDA_TASK_ROOT}/
COPY pyproject.toml poetry.lock ${LAMBDA_TASK_ROOT}/
# Install python packages without dev pkgs
RUN poetry install --no-root --no-dev
# CMD [ "sample.handler" ] # ★1
```

★1の `CMD` はコンテナイメージが起動対象とするLambdaのモジュールと関数名を指定する行ですが、次セクションでCDKのStack定義から指定しますので、Dockerfile内では無指定でOKです。

# Lambdaのコード

処理は本記事の主題ではないのでこんにちは世界。

```python:src/sample.py
def handler(event, context):
    return {"message": "Hello World!"}
```

# Stack定義を書く

一つのLambda関数リソースだけを定義したシンプルなCDKスタック定義です。

```python:sample_stack.py
class SampleStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)
        # コンテナイメージを定義
        image_asset = aws_lambda.DockerImageCode.from_image_asset(
            cmd=["sample.handler"],
            directory=".",
        )
        # コンテナイメージ方式のLambda関数を定義し、利用イメージを指定
        aws_lambda.DockerImageFunction(
            scope=self, id="SampleFunction", code=image_asset
        )
```

`aws_lambda.DockerImageCode.from_image_asset` メソッドでは、どのLambdaで使うコンテナイメージのAsset一式を指定します。`directory`　パラメータに与える値は、[docker buildコマンドのPATH](https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/commandline/build/#usage)を指定することに対応します。`cmd` パラメータに与える値は、Dockerfileにおける `CMD` の指定に対応しており、先のDockerfile内でCOPYした `src/` 配下の `sample.py`  `handler` 関数が実行対象プログラムであることを示します。[docker run コマンドの COMMAND オプション](https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/commandline/run/#usage) を指定することに対応します。

https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_lambda/DockerImageCode.html#aws_cdk.aws_lambda.DockerImageCode.from_image_asset

`aws_lambda.DockerImageFunction` メソッドでは、AWS上にプロビジョニングするLambdaリソースを定義しています。先のコンテナイメージを利用させる指定をしていますが、LambdaのTimeoutやメモリサイズなどLambdaリソースへのセッティングができます。

https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_lambda/DockerImageFunction.html#dockerimagefunction

いずれのメソッドも、指定可能なパラメータ数が豊富ですね。

これは私感ですが、CDKの仕様からは、AWSサービスを広くモデリングして、AWSでできること全体をCDKでもIaCできるようにしようという意思を感じます。実際、CDKはそのカバー範囲の広さがAPIドキュメントからよく分かりますし、更新頻度やコントリビューションの活発さもあって頼もしいツールです。

# Bootstraping Stackの作成

予め、bootstrap コマンドを一度だけ実行しておきます。

```bash
$ cdk bootstrap
 ✅  Environment aws://ACCOUNT_NO/REGION bootstrapped.
$
```

CDKを介してインフラやアプリケーションをAWS環境にデプロイするには、ブートストラッピングスタックと呼ばれる、リソースプロビジョニングを行ってくれる仕組みが介在します。デプロイ先リージョンのCloudformationスタックとして一度作成しておく操作が bootstrap コマンドです。

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping.html

# デプロイ

いよいよ、Lambda関数をCDK経由でデプロイします。

```bash
$ cdk deploy
...
✨  Synthesis time: 4.1s

SampleStack:  start: Building 
...
Step 1/4 : FROM public.ecr.aws/lambda/python:3.11
 ---> be5c629c78a6
...
1:21:11 AM | CREATE_IN_PROGRESS   | AWS::CloudFormation::Stack | SampleStack
...
✨  Total time: 70.32s

$
```

# デプロイ結果の確認

### Cloudformation Stacks

Cloudformation Stack `Sample Stack` としてデプロイされました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/6b42c426-8df7-525a-ec0d-e1cc2f2a7c79.png)

### ECR

ECR リソースが、 CDKToolkit スタックのリソースとして作られ、デプロイ対象のLambdaイメージの管理に利用されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/9906d28e-e13a-d30b-8964-dedd971f27ca.png)

LambdaのコンテナイメージはローカルPC上のDocker Serverでビルドされ、ECRにpushされてからLambda関数としてデプロイされているわけですが、CDKのAPIが一連のプロビジョニングからデプロイまでのプロセスを、Stack定義の記述だけで済ませられるようラップしてくれている点がGoodです。

### Lambda

イメージデプロイされたLambdaが存在する事も確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/c3606af5-10a1-a50e-7aa3-45813eda9f9e.png)

テストも通りますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/98af6a56-70b2-b8b4-a643-5a37fc3465d1.png)

以後、DockerfileやStack定義を育てる過程で、前述の利点を享受し続けることになります。


# 余談＆紹介

本記事は、私が所属しているAWSサービスプロバイダーの社内Slackで、アドベントカレンダーやろうぜ！と募集があり楽しげでしたので参加する形で執筆しました。カレンダーにはAWS関連の記事が主に投稿されていますので、ご興味がありましたらぜひ覗いてみてください。

https://qiita.com/advent-calendar/2023/serverworks

余談まで読んでくださりありがとうございます。良い年末をお過ごしください。