概要
-
sam local ***
(invokeやstart-apiなど)を実行する前に、sam build -t cdk.out/***.template.json
を実行することでローカルでDockerイメージをビルド出来る - TypeScriptやJavaScriptのイメージを作成したい場合、pnpmのワークスペース機能を有効+shared-workspace-lockfile設定を
false
にする、という選択肢がある
0. 動機など
まず、極力ローカル環境でLambdaなどの開発を進めたい状況とする。
その際、AWS CDKとAWS SAMを組み合わせることでLambdaなどをローカル実行する機能があり、それを使うと開発が便利になる。
ZipタイプのLambdaの場合、公式デベロッパーガイドのAWS SAMとAWS CDKの開始方法や、DevelopersIOのSAM を使って CDK + Typescript で実装した Lambda をローカル環境で実行するなどを参考にすると良い。
が、「ImageタイプのLambdaはどうするのか?」 については意外とハマったので備忘録にメモする。
気づいてしまえば、結論としては概要に書いた通りでそれ以上のことは特に無いが、一応まとめておくことにする。
1. 前提条件
1-1. 前提知識
cdkおよびsam、およびnodeによるプロジェクト作成・管理のごく基本の部分は既知としている。
1-2. 検証した環境やツールのバージョンなど
- WSL2@Windows11
- Ubuntu 22.04LTS
- nodejs v18.12.1(ホストOS)
- pnpm@7.17.0
- sam-cli@1.65.0
- aws-cdk@2.51.1
- Docker version 20.10.21, build baeda1f
2. 検証
2-1. samによるimageタイプのLambdaのサンプルの生成
本質的ではない部分だが、ImageタイプのLambdaサンプルを生成するのには多分最適?なのでsamで作っておく。
詳細は長いので折りたたみ表示にするとして、大まかには
- Hello World Example
- ランタイム: Nodejs 18.x
- Imageタイプ
を選択している。
(sam initのオプション選択詳細)
sam init
You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.
Which template source would you like to use?
1 - AWS Quick Start Templates
2 - Custom Template Location
Choice: 1
Choose an AWS Quick Start application template
1 - Hello World Example
2 - Multi-step workflow
3 - Serverless API
4 - Scheduled task
5 - Standalone function
6 - Data processing
7 - Infrastructure event management
8 - Serverless Connector Hello World Example
9 - Multi-step workflow with Connectors
10 - Lambda EFS example
11 - Machine Learning
Template: 1
Use the most popular runtime and package type? (Python and zip) [y/N]: N
Which runtime would you like to use?
1 - aot.dotnet7 (provided.al2)
2 - dotnet6
3 - dotnet5.0
4 - dotnetcore3.1
5 - go1.x
6 - graalvm.java11 (provided.al2)
7 - graalvm.java17 (provided.al2)
8 - java11
9 - java8.al2
10 - java8
11 - nodejs18.x
12 - nodejs16.x
13 - nodejs14.x
14 - nodejs12.x
15 - python3.9
16 - python3.8
17 - python3.7
18 - ruby2.7
19 - rust (provided.al2)
Runtime: 11
What package type would you like to use?
1 - Zip
2 - Image
Package type: 2
Based on your selections, the only dependency manager available is npm.
We will proceed copying the template using npm.
Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]: N
Project name [sam-app]:
Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)
-----------------------
Generating application:
-----------------------
Name: sam-app
Base Image: amazon/nodejs18.x-base
Architectures: x86_64
Dependency Manager: npm
Output Directory: .
Next steps can be found in the README file at ./sam-app/README.md
Commands you can use next
=========================
[*] Create pipeline: cd sam-app && sam pipeline init --bootstrap
[*] Validate SAM template: cd sam-app && sam validate
[*] Test Function in the Cloud: cd sam-app && sam sync --stack-name {stack-name} --watch
生成されたプロジェクトは以下のような感じ:
cd sam-app
tree .
.
├── README.md
├── events
│ └── event.json
├── hello-world
│ ├── Dockerfile
│ ├── app.mjs
│ ├── package.json
│ └── tests
│ └── unit
│ └── test-handler.mjs
└── template.yaml
4 directories, 7 files
hello-world
ディレクトリ以下が目当ての部分(今回はcdkでインフラ部分を記述する)となる。
参考に、Lambdaの本体であるapp.mjs
は
export const lambdaHandler = async (event, context) => {
try {
return {
'statusCode': 200,
'body': JSON.stringify({
message: 'hello world',
})
}
} catch (err) {
console.log(err);
return err;
}
return response
};
のようになっており、API Gateway経由で呼ぶと{"message": "hello world"}
を返す。
また、初期状態ではpackage.json
しかなく、lockファイルが無いので適宜追加する。
個人的にはpnpmを使っている(+後述の通り、workspace機能との連携が良い)ので、
必要に応じてDockerfileを以下のような感じで書き換えておく
FROM public.ecr.aws/lambda/nodejs:18
COPY app.mjs package*.json pnpm-lock.yaml ./
# RUN npm install
RUN corepack enable pnpm
RUN pnpm install --prod --frozen-lockfile
CMD ["app.lambdaHandler"]
後で、hello-world
ディレクトリ以下をcdkプロジェクト内に移植する。
2-2. CDKプロジェクトの作成
2-2-1. ワークスペースの設定
今回はcdk-app
という名前で作成する。
まずはcdk-app
という名前のディレクトリを作成し、そのディレクトリに移動してcdk init
を実行する。
mkdir cdk-app
cd cdk-app
# 執筆時点でのcdk最新版は2.51.1
npx aws-cdk@2.51.1 init --language typescript
# 今回はpnpmでパッケージ管理をしたいので、package-lock.jsonは削除してpnpmで再インストールしておく
rm package-lock.json
pnpm install
この時点でプロジェクト内に作成されたファイル・ディレクトリは以下のようになっている。
tree -a -I ".git|node_modules"
.
├── .gitignore
├── .npmignore
├── README.md
├── bin
│ └── cdk-app.ts
├── cdk.json
├── jest.config.js
├── lib
│ └── cdk-app-stack.ts
├── package.json
├── pnpm-lock.yaml
├── test
│ └── cdk-app.test.ts
└── tsconfig.json
3 directories, 11 files
ここで、先ほどsamの方で作ったImageタイプのLambdaのコードをcdkプロジェクト内で管理したいが、
設定を変えずにそのまま持ってこようとするとパッケージ管理がうまく動作しないため、workspace機能を使ったモノレポ構成にする。
まずはcdkプロジェクトのルートディレクトリ直下にpnpm-workspace.yaml
を作成する。
内容については公式を参考に例えば:
packages:
# all packages in direct subdirs of packages/
- 'packages/*'
のようにしておく。
この場合、例えばpackages
ディレクトリを作成してその下にnodeプロジェクトを置いていくとwork
次に、このままの設定だとロックファイル: pnpm-lock.yaml
はcdkプロジェクトのルート直下のみに生成されることになる。
LambdaのイメージをDockerfileで作成する際に難しくなるため、Lambdaのファイルで使うパッケージ分のロックファイルはcdkプロジェクト本体とは別に、Lambda側で出来ていると容易になる。
pnpmだと、.npmrc
を作成して設定をデフォルトから変えることでワークスペース毎にロックファイルを生成することが可能になる。
すなわち、
shared-workspace-lockfile=false
の内容で.npmrc
を作成して、cdkプロジェクトのルートディレクトリに置いておけば良い。
あとは先ほどsam cliで作ったプロジェクトのhello-world
ディレクトリをpackages
以下に置き、更にlockファイルを作っておく。
# workspace用ディレクトリ作成
mkdir packages
# sam-cliで作ったImageタイプのLambdaのプロジェクトを持ってくる
cp -r /path/to/sam-app/hello-world/ packages/
# 追加したworkspace分のパッケージのインストール+lockファイル作成
pnpm install
ここまでで、cdkプロジェクトにおけるディレクトリ・ファイルは以下のような形になる。
tree -a -I ".git|node_modules"
.
├── .gitignore
├── .npmignore
├── .npmrc ★ 追加
├── README.md
├── bin
│ └── cdk-app.ts
├── cdk.json
├── jest.config.js
├── lib
│ └── cdk-app-stack.ts
├── package.json
├── packages ★ 追加
│ └── hello-world ★ 2-1.で作成したものから移植
│ ├── .npmignore
│ ├── Dockerfile
│ ├── app.mjs
│ ├── package.json
│ ├── pnpm-lock.yaml ★ 追加(自動生成)
│ └── tests
│ └── unit
│ └── test-handler.mjs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml ★ 追加
├── test
│ └── cdk-app.test.ts
└── tsconfig.json
7 directories, 19 files
2-2-2. スタックの作成と実行
次に、追加したImageタイプLambda部分に相当するcdkのコードを記述していく。
今回の例ではAPI Gatewayから呼び出されて使われる想定のLambdaなので、例えば以下のようにしておく。
import {
Stack,
StackProps,
aws_apigateway as apigateway,
aws_lambda as lambda,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class CdkAppStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const imageTypeLambda = new lambda.DockerImageFunction(this, 'DockerImageFunction', {
// ↓cdkプロジェクトのルートから見て、対象のDockerfileが置いてあるパスを指定
code: lambda.DockerImageCode.fromImageAsset('packages/hello-world'),
});
const api = new apigateway.RestApi(this, 'example api');
const hello = api.root.addResource('hello');
hello.addMethod('GET', new apigateway.LambdaIntegration(imageTypeLambda))
}
}
元々のsamによって作ったサンプルプロジェクトに倣って、
/hello
へのGETリクエストでLambdaが動作するようにしている。
(詳細は公式リファレンスのaws_apigateway.RestApiおよびaws_apigateway.LambdaIntegration、aws_lambda.DockerImageFunction部分なども参考のこと。)
ここで、
pnpm cdk synth
などとすることで、lib/cdk-app-stack.ts
の内容を元にCloudFormationテンプレートを生成出来る。
この場合はcdk.out/CdkAppStack.template.json
にテンプレートファイルが作成されている。
この状態で、公式ガイドのステップ4: Lambda関数をテストする に倣って、
sam local start-api -t ./cdk.out/CdkAppStack.template.json
を実行するとローカルでapiの立ち上げとLambdaの実行が出来る。。。はずなのだが、Imageタイプの場合は後述の準備をしておかないと以下のような感じでエラーが出る。
# localhost:3000 でapi gatewayの模擬環境に相当するローカルサーバーが立ち上がる
sam local start-api -t cdk.out/CdkAppStack.template.json
# Lambdaを呼び出すため、lib/cdk-app-stack.tsで設定したパス /helloをGETで叩く
curl -X GET localhost:3000/hello
(エラー内容詳細)
2022-11-21 23:54:23 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
Invoking Container created from dockerimagefunction28b773e6
Image was not found.
Removing rapid images for repo dockerimagefunction28b773e6
Building image......
Failed to build Docker Image
NoneType: None
Exception on /hello [GET]
Traceback (most recent call last):
File "flask/app.py", line 2073, in wsgi_app
File "flask/app.py", line 1518, in full_dispatch_request
File "flask/app.py", line 1516, in full_dispatch_request
File "flask/app.py", line 1502, in dispatch_request
File "samcli/local/apigw/local_apigw_service.py", line 361, in _request_handler
File "samcli/commands/local/lib/local_lambda.py", line 144, in invoke
File "samcli/lib/telemetry/metric.py", line 277, in wrapped_func
File "samcli/local/lambdafn/runtime.py", line 177, in invoke
File "samcli/local/lambdafn/runtime.py", line 88, in create
File "samcli/local/docker/lambda_container.py", line 94, in __init__
File "samcli/local/docker/lambda_container.py", line 236, in _get_image
File "samcli/local/docker/lambda_image.py", line 165, in build
File "samcli/local/docker/lambda_image.py", line 279, in _build_image
samcli.commands.local.cli_common.user_exceptions.ImageBuildException: Error building docker image: pull access denied for dockerimagefunction28b773e6, repository does not exist or may require 'docker login': denied: requested access to the resource is denied
2022-11-21 23:56:24 127.0.0.1 - - [21/Nov/2022 23:56:24] "GET /hello HTTP/1.1" 502 -
ローカルでImageタイプのLambdaをエミュレートしようとしているため、当然Lambdaのコンテナイメージが必要であり、デフォルトでは実際のAWS環境から取得してこようとする様子。
ただ、今回の例では一切実際のクラウド環境は使っておらず、イメージのPullも出来ないため当然失敗する。
対応策として、sam buildコマンドでローカルでイメージをビルドしておけば問題は解決する。(クラウド上にイメージを置いてPullする、などの必要はなくローカルで完結出来る)
この際、sam local start-api
やsam local invoke
などを実行するときと同様、buildを行うときもcdkで作ったテンプレートを指定することに注意。(デフォルトではカレントディレクトリのtemplate.yml
を見に行こうとする)
sam build -t cdk.out/CdkAppStack.template.json
これによってDockerのビルドプロセスが走り、最終的に
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided
のような表示が出てきたら、無事にイメージのビルドが完了したことが確認出来る。
この後に再度sam local
コマンドでローカル実行を試すと今度はLambdaを問題無く実行出来る。
# localhost:3000 でapi gatewayの模擬環境に相当するローカルサーバーが立ち上がる
sam local start-api -t cdk.out/CdkAppStack.template.json
# Lambdaを呼び出すため、lib/cdk-app-stack.tsで設定したパス /helloをGETで叩く
curl -X GET localhost:3000/hello
# {"message":"hello world"}
# ↑今度は無事にLambdaの返したレスポンスを受け取れる
補足、感想など
- 個人的には基本TypeScriptで統一して書きたいところがあったので、cdkプロジェクトも実行するLambdaもnodejsでプロジェクト作成しており、その関係でワークスペース機能(モノレポ)を使っている。が、例えばcdkプロジェクトはTypeScriptでLambda(Image)のランタイムはPythonなどnode以外ということであればモノレポ周りの考慮は必須では無くなるので、その辺に関しては楽かもしれない
- あまりちゃんと調べていないが、少し眺めた程度だと
npm
やyarn v1
ではlockファイルをワークスペース毎に作る設定は見つからなかった?ので、pnpmを使わない場合だとやりづらい部分が出てくるかもしれない(調べ足りないだけの可能性もあるのでそこはご容赦)。
- あまりちゃんと調べていないが、少し眺めた程度だと