AWS CDKを使ってLambda Layersを複数環境(開発/検証/本番)にデプロイするプロジェクトを実験的に作りました。cdk等のコマンドをShell Scriptでラップして極力シンプルなコマンドでビルドからデプロイまでを実行できるように試行錯誤しています。ちなみに今回は扱っていませんが、Lambda LayersだけでなくVPCやEC2などもデプロイできます。
リポジトリ
機能
- 複数Lambda Layersのデプロイに対応
- 複数環境(開発/検証/本番)に対応
- etc.
必要なもの
- AWSアカウント
- Docker Compose
- aws-sam-cli
- jq
- yq
- OSは
macOS
もしくはLinux
を想定
とりあえずデプロイしてみる
デモ的なやつです。Lambda Layersを含むリソースをdevelopment環境にデプロイしてみます。なお、AWSアカウント取得済みでNamed Profileも設定済みであることが前提になります。
1. Named Profileの指定
cmd/config.ymlのcli.profile
キーにNamed Profileを指定します。
2. Dockerイメージ、およびプロジェクトのビルド
docker-compose build
docker-compose run --rm npm install
./cmd/code.sh build --env development
--env
オプションは省略可能です。省略された場合は development
をデフォルト値として使います。また、環境名はcmd/config.ymlで変更できます。
3. CDKToolkitスタックの作成
./cmd/cdk.sh bootstrap
bootstrapプロセスについて詳しく知りたい場合は、オフィシャルリポジトリのaws-cdk/design/cdk-bootstrap.mdを参照してください。
4. レイヤーを含むスタックのデプロイ
src/stack/service2.ts
が対象スタック、src/resource/service2
に各リソースの定義ファイルが含まれています。src/function
はLambda関数とレイヤーのソースを含みます。ちなみにsrc/stack/service1.ts
は空のスタックです。VPCなどのサンプル定義を書こうと思ってますが、まだ手を付けてません。
./cmd/cdk.sh deploy --env development --stack 'service2'
こんな感じでデプロイが進みます。
❯ ./cmd/cdk.sh deploy --env development --stack 'service2'
Creating cdk-experiment_cdk_run ... done
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
IAM Statement Changes
┌───┬─────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────┬──────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼─────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────┼──────────────────────────────┼───────────┤
│ + │ ${LambdaRole.Arn} │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │
├───┼─────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────┼──────────────────────────────┼───────────┤
│ + │ * │ Allow │ xray:PutTelemetryRecords │ AWS:${LambdaRole} │ │
│ │ │ │ xray:PutTraceSegments │ │ │
├───┼─────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────┼──────────────────────────────┼───────────┤
│ + │ arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* │ Allow │ logs:CreateLogGroup │ AWS:${LambdaRole} │ │
│ │ │ │ logs:CreateLogStream │ │ │
│ │ │ │ logs:PutLogEvents │ │ │
└───┴─────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────┴──────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)? y
service2-development: deploying...
[0%] start: Publishing b6b4159901c4c58ae071d371127d68a28c9e264a3583c7b77013c805a8917605:current
[33%] success: Published b6b4159901c4c58ae071d371127d68a28c9e264a3583c7b77013c805a8917605:current
[33%] start: Publishing e392921aca9408c62afcbbebef20da8635c2c1fbc99fe88aeb87d991a66a17a3:current
[66%] success: Published e392921aca9408c62afcbbebef20da8635c2c1fbc99fe88aeb87d991a66a17a3:current
[66%] start: Publishing f9caa1f9683d6033e1b15d578f01eec056ec166e9aec850c67456e9691e48fee:current
[100%] success: Published f9caa1f9683d6033e1b15d578f01eec056ec166e9aec850c67456e9691e48fee:current
service2-development: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (8/8)
✅ service2-development
マネージメントコンソールを確認すると1つのLambda関数と2つのレイヤーが作成されているのがわかります(レイヤーは2つとも関数にアタッチされています)。
5. 他の環境へのデプロイする
staging環境へのデプロイ。staging用のレイヤーを用意する必要があるのでビルドから始めます。
./cmd/code.sh build --env staging
./cmd/cdk.sh deploy --env staging --stack 'service2'
production環境へのデプロイ。こちらもビルドからやります。
./cmd/code.sh build --env production
./cmd/cdk.sh deploy --env production --stack 'service2'
これで全ての環境へリソースがデプロイされました。
解説を少し
Lambda Layersを環境別にデプロイする仕組みがちょっと複雑です。まず、ソースコードを準備します。今回の例だとsrc/function/layer
がそれにあたります。開発時はここへ収めたレイヤーへの参照を解決するため、以下のとおりpackage.json
にレイヤーのパスを含めています。これをnpm install
するとnode_modules
にシンボリックリンクが作成されます。
"base-layer-1-cdk-experiment-development": "file:./src/function/layer/base1",
"base-layer-2-cdk-experiment-development": "file:./src/function/layer/base2"
また、Lambda Layersをデプロイする際にはそれ自身のディレクトリ構成が以下のとおりでないといけません。
layer.out
└── development
├── base1
│ └── nodejs
│ ├── node_modules
│ ├── package-lock.json
│ └── package.json
└── base2
└── nodejs
├── node_modules
├── package-lock.json
└── package.json
base1
とbase2
がレイヤーです。nodejs
ディレクトリの下にnode_modules
ディレクトリを含む上記構造でないとLambda関数からレイヤーを参照できません。なので./cmd/code.sh build
の実行時にこの構造になるようにゴニョゴニョします。該当するコードは./cmd/code.sh
のこの部分です。
: 'PREPARE LAMBDA LAYER' &&
{
readonly LAMBDA_LAYER_SRC_DIR="${PROJECT_ROOT}/src/function/layer"
readonly LAMBDA_LAYER_DEST_DIR="layer.out/${ENV}"
printf 'Lambda Layer Source: %s\n' "${LAMBDA_LAYER_SRC_DIR}"
printf 'Lambda Layer Target: %s\n' "${LAMBDA_LAYER_DEST_DIR}"
commands=()
while read -r dir; do
{
install_dir="${LAMBDA_LAYER_DEST_DIR}/${dir}/nodejs"
mkdir -p "${install_dir}"
cp "${LAMBDA_LAYER_SRC_DIR}/${dir}/package.json" "${install_dir}"
# add 'dependencies' member if not exists
if [ "$(jq '.dependencies?' "${install_dir}/package.json")" = 'null' ]; then
{
cat < "${install_dir}/package.json" \
| jq ". |= .+ {\"dependencies\": {}}" \
> "${install_dir}/package-tmp.json" \
&& mv "${install_dir}/package-tmp.json" "${install_dir}/package.json"
}
fi
layer_name=$(jq -r '.name' "${install_dir}/package.json")
# add self as a dependency
cat < "${install_dir}/package.json" \
| jq ".dependencies |= .+ {\"${layer_name}-${ENV}\": \"../../../../src/function/layer/${dir}\"}" \
> "${install_dir}/package-tmp.json" \
&& mv "${install_dir}/package-tmp.json" "${install_dir}/package.json"
printf '%s\n' "$(cat "${install_dir}/package.json")"
# install 'dependencies' without 'devDependencies'
commands+=("npm --prefix ${install_dir} install --production")
}
done < <(find "${LAMBDA_LAYER_SRC_DIR}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)
cmd=$(IFS=',' tmp="${commands[*]}" ; echo "${tmp//,/ && }")
printf 'Commands To Be Executed: %s\n' "${cmd}"
$BASH_CMD "${cmd}"
printf '%s\n' "$(find ${LAMBDA_LAYER_DEST_DIR} -maxdepth 3)"
}
ざっくりとした流れは次のとおりです(すべてのレイヤーに対して同様の手続きを繰り返します)。
- ターゲットディレクトリに
nodejs
ディレクトリを作成して、さらにsrc/function/layer
からpackage.json
をコピー - コピーした
package.json
のdependencies
にレイヤー自身を追加(レイヤーをパッケージとしてインストールするため) -
npm install
でレイヤーをインストール
おわりに
AWSよりもShell Scriptのスキルレベルが上がった気がする