LambdaでLambda Layerを作成し、CDKにおけるレイヤービルドのダルさを終わらせる
はじめに
AWS CDKにおけるLambda layerの作成、どのようにしていますか?
CDKにおける、Lambda layerの管理手法は複数ありますが、どれも決め手に欠けるイメージです。
本記事では、CDK内で完結し、自動化されたLayer管理を実現する手法として、Lambda関数自身でLayerを生成し、CustomResourceを用いて紐付けるアプローチを紹介します。
今回実装したコードのリポジトリは以下です。
CDKでのLambda Layerの一般的な作成方法(メリット/デメリット)
一般的には以下の方法が考えられると思います。
-
既存LayerをARNで指定して利用
- AWS公式Layerや既存LayerをARNで参照して追加する方法。
-
requirements.txtからCDK BundlingでLayerを生成
- CDKのbundling機能を使い、requirements.txtをもとに依存ライブラリをインストールしてLayerを生成する方法。
-
@aws-cdk/aws-lambda-python-alpha を使用してLayerを生成
- bundling自体は②と同様だが、Python Lambda専用のコンストラクトを使うことで依存管理が簡潔になる。
-
手動で用意したライブラリをLayerとして追加
- 自作ユーティリティなどをzip化またはディレクトリとして用意し、そのままLayerに組み込む方法。
-
Dockerで事前ビルドしたLayerをzip化して追加
- コンテナで依存ライブラリをビルドしてzip化し、Lambdaに追加する方法。プラットフォーム差異の影響を避けやすい。
上記手法は以下の記事を参考にしています。それぞれの詳細は、記事を参照してください。
https://dev.classmethod.jp/articles/2025-08-05-cdk-python-lambda-layer/
手法ごとのメリット・デメリット整理
| 項番 | 手法 | 主な課題・デメリット |
|---|---|---|
| ① | ARN参照 | バージョン管理が他者依存/変更検知しづらい |
| ② | bundling(素のCDK) | 設定がやや煩雑/Docker環境が必要 |
| ③ | Python-alpha | Alpha モジュールのため将来互換の懸念/Docker依存 |
| ④ | 手動アップロード | 自動化しづらい/属人化しやすい |
| ⑤ | Docker 事前ビルド | CI前提になりがち/Docker環境が必要 |
特に、②番・③番の方法はCDKをデプロイする環境にDockerが必要となります。ローカル環境ではまだしも、CI/CDパイプラインに組み込む際には手間が増えるのが課題です。
⑤番のDocker事前ビルドも同様に、ローカルでDockerが使えない場合は利用困難です。
従来手法の限界と運用上の問題点
他の手法を採用すると、CDKの外でLayerを準備する必要が出てきます。しかし、これはモダンなIaC運用としては望ましくありません。
さらに、Lambda Layerのベストプラクティスとして、Lambdaと同じランタイム環境でLayerをビルドすることが挙げられます。これを実現するには専用のDockerイメージ上でビルドするのが理想ですが、ローカル環境では手間がかかります。
Lambda自身でLayerを作る
そこで本記事では、Lambda関数自身でLayerを生成し、CustomResourceを用いてCDKから管理する方法を提案します。
- LambdaがLayerを生成するため、CDK外でLayerを準備する必要がない
- ローカル環境でもCI/CD環境でもDockerを意識せずに完結
- 依存関係やバージョン管理もCDK内で自動化可能
を目指します。
次章からCDKのサンプルコードを紹介します。
Lambda関数によるセルフビルドの流れ
主な処理の流れは以下です。
- LayerBuilder Lambda関数をデプロイ
- CustomResourceでLayerBuilder実行
- LayerBuilder内でpip install & Layer作成
- 作成したLayerを使うLambda関数をデプロイ
CustomResourceを使用することで、CDK内で完結する処理が可能であり、実行時間もそこまで掛からないものになっています。
LayerBuilderの実装
LayerBuilder Lambda関数の実装例を示します。この関数は、指定されたパッケージをインストールし、Lambda Layerとして登録します。
import json
import subprocess
import boto3
import os
import tempfile
import shutil
import hashlib
import logging
from typing import Dict, Any
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# AWS クライアント
lambda_client = boto3.client("lambda")
def create_or_update_layer(requirements: str, layer_name: str) -> Dict[str, str]:
"""Lambda Layerを作成または更新"""
# requirements ハッシュを計算(Description に埋め込む用)
req_hash = hashlib.sha256(requirements.encode("utf-8")).hexdigest()[:16]
logger.info(f"Requirements hash: {req_hash}")
# 既存 Layer を確認して同じハッシュのものがあれば再利用
try:
existing_versions = lambda_client.list_layer_versions(
LayerName=layer_name,
MaxItems=50, # 最新50個まで確認
).get("LayerVersions", [])
for v in existing_versions:
if req_hash in v.get("Description", ""):
logger.info(f"Reusing existing layer: {v['LayerVersionArn']}")
return {"LayerVersionArn": v["LayerVersionArn"]}
except lambda_client.exceptions.ResourceNotFoundException:
logger.info(f"Layer {layer_name} does not exist yet, will create new one")
# 一時ディレクトリを作成
tmp_dir = None
try:
tmp_dir = tempfile.mkdtemp(prefix="layer-builder-")
logger.info(f"Created temp directory: {tmp_dir}")
# requirements.txt を /tmp に書き出し
req_path = os.path.join(tmp_dir, "requirements.txt")
with open(req_path, "w") as f:
f.write(requirements)
logger.info(f"Written requirements to: {req_path}")
# python ディレクトリに pip install
python_dir = os.path.join(tmp_dir, "python")
os.makedirs(python_dir, exist_ok=True)
logger.info("Installing packages...")
result = subprocess.run(
[
"pip",
"install",
"-r",
req_path,
"-t",
python_dir,
"--no-cache-dir",
"-q",
],
capture_output=True,
text=True,
check=True,
)
if result.stderr:
logger.warning(f"pip install warnings: {result.stderr}")
logger.info("Packages installed successfully")
# zip 作成
zip_base_path = os.path.join(tmp_dir, "layer")
zip_path = f"{zip_base_path}.zip"
shutil.make_archive(zip_base_path, "zip", tmp_dir, "python")
logger.info(f"Created zip file: {zip_path}")
# ファイルサイズを確認(Lambdaの制限は50MB)
zip_size = os.path.getsize(zip_path)
logger.info(f"Zip file size: {zip_size / 1024 / 1024:.2f} MB")
if zip_size > 50 * 1024 * 1024:
raise ValueError(
f"Layer zip file is too large: {zip_size / 1024 / 1024:.2f} MB (max 50 MB)"
)
# Lambda Layer を作成
with open(zip_path, "rb") as f:
zip_bytes = f.read()
response = lambda_client.publish_layer_version(
LayerName=layer_name,
Content={"ZipFile": zip_bytes},
CompatibleRuntimes=["python3.13"],
Description=f"Requirements hash: {req_hash}",
)
logger.info(f"Created new layer: {response['LayerVersionArn']}")
# 古いバージョンを削除(最新5個を残す)
cleanup_old_versions(layer_name, keep_versions=5)
return {"LayerVersionArn": response["LayerVersionArn"]}
finally:
# 一時ディレクトリをクリーンアップ
if tmp_dir and os.path.exists(tmp_dir):
try:
shutil.rmtree(tmp_dir)
logger.info(f"Cleaned up temp directory: {tmp_dir}")
except Exception as e:
logger.warning(f"Failed to cleanup temp directory: {str(e)}")
def cleanup_old_versions(layer_name: str, keep_versions: int = 5) -> None:
"""古いLayerバージョンを削除"""
try:
versions = lambda_client.list_layer_versions(
LayerName=layer_name, MaxItems=50
).get("LayerVersions", [])
if len(versions) > keep_versions:
for v in versions[keep_versions:]:
try:
lambda_client.delete_layer_version(
LayerName=layer_name, VersionNumber=v["Version"]
)
logger.info(f"Deleted old layer version: {v['Version']}")
except Exception as e:
logger.warning(
f"Failed to delete layer version {v['Version']}: {str(e)}"
)
except Exception as e:
logger.warning(f"Failed to cleanup old versions: {str(e)}")
def delete_layer(layer_name: str) -> None:
"""Layerを削除(全バージョン)"""
try:
versions = lambda_client.list_layer_versions(
LayerName=layer_name, MaxItems=50
).get("LayerVersions", [])
for v in versions:
try:
lambda_client.delete_layer_version(
LayerName=layer_name, VersionNumber=v["Version"]
)
logger.info(f"Deleted layer version: {v['Version']}")
except Exception as e:
logger.warning(
f"Failed to delete layer version {v['Version']}: {str(e)}"
)
except lambda_client.exceptions.ResourceNotFoundException:
logger.info(f"Layer {layer_name} does not exist, nothing to delete")
except Exception as e:
logger.warning(f"Failed to delete layer: {str(e)}")
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""Provider用 Layer Builder Lambda関数"""
logger.info(f"Received event: {json.dumps(event)}")
request_type = event.get("RequestType", "")
physical_resource_id = event.get("PhysicalResourceId", "")
# プロパティを取得
properties = event.get("ResourceProperties", {})
requirements = properties.get("Requirements", "")
layer_name = properties.get("LayerName", "DynamicRequirementsLayer")
if request_type in ["Create", "Update"]:
if not requirements:
raise ValueError("Requirements property is required")
logger.info(f"Processing {request_type} request for layer: {layer_name}")
result = create_or_update_layer(requirements, layer_name)
layer_version_arn = result["LayerVersionArn"]
# Provider用のレスポンス形式
return {
"Data": {"LayerVersionArn": layer_version_arn},
"PhysicalResourceId": layer_version_arn, # LayerVersionArnを識別子として使用
}
elif request_type == "Delete":
logger.info(f"Processing Delete request for layer: {layer_name}")
delete_layer(layer_name)
# Provider用のレスポンス形式(Deleteの場合)
return {"Data": {}, "PhysicalResourceId": physical_resource_id or "deleted"}
else:
# 通常のLambda呼び出し(テスト用)
logger.info("Processing direct invocation (test mode)")
requirements = event.get("requirements", "boto3==1.26.137")
layer_name = event.get("layer_name", "TestLayer")
result = create_or_update_layer(requirements, layer_name)
layer_version_arn = result["LayerVersionArn"]
# テスト用のレスポンス
return {
"Data": {"LayerVersionArn": layer_version_arn},
"PhysicalResourceId": layer_version_arn,
}
解説
CustomResourceとしてLambdaを呼ぶ
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
request_type = event.get("RequestType", "")
if request_type in ["Create", "Update"]:
...
elif request_type == "Delete":
...
else:
...
CustomResourceから呼び出す用の処理です。
LambdaはRequestTypeを受け取り、Create / Update / Deleteで処理を分岐しています。
- Create/Update → Layer生成・更新
- Delete → Layer削除
- それ以外(テスト呼び出し) → 直接Layer生成
となります。
ハッシュ値によるrequirements.txtの変更検知
def create_or_update_layer(requirements: str, layer_name: str) -> Dict[str, str]:
req_hash = hashlib.sha256(requirements.encode("utf-8")).hexdigest()[:16]
# 既存 Layer を確認して同じハッシュのものがあれば再利用
try:
existing_versions = lambda_client.list_layer_versions(
LayerName=layer_name,
MaxItems=50, # 最新50個まで確認
).get("LayerVersions", [])
for v in existing_versions:
if req_hash in v.get("Description", ""):
logger.info(f"Reusing existing layer: {v['LayerVersionArn']}")
return {"LayerVersionArn": v["LayerVersionArn"]}
response = lambda_client.publish_layer_version(
LayerName=layer_name,
Content={"ZipFile": zip_bytes},
CompatibleRuntimes=["python3.12", "python3.11", "python3.10"],
Description=f"Requirements hash: {req_hash}",
)
デプロイの度に pip install が走るのを防ぐため、requirements.txt の内容からハッシュ値を計算し、Layer の Description に埋め込んでいます。
CustomResource は requirements.txt に変更があれば常に LayerBuilder Lambda を実行しますが、このハッシュ値チェックにより、過去に作成した全く同じ内容の Layer Version があれば、それを再利用します。
これにより、デプロイ時間の短縮と、同一内容の Layer Version が際限なく増えることを防ぎます。
Layerビルドの流れ
tmp_dir = tempfile.mkdtemp(prefix="layer-builder-")
req_path = os.path.join(tmp_dir, "requirements.txt")
with open(req_path, "w") as f:
f.write(requirements)
python_dir = os.path.join(tmp_dir, "python")
os.makedirs(python_dir, exist_ok=True)
subprocess.run(
["pip", "install", "-r", req_path, "-t", python_dir, "--no-cache-dir", "-q"],
capture_output=True,
text=True,
check=True,
)
zip_path = shutil.make_archive(os.path.join(tmp_dir, "layer"), "zip", tmp_dir, "python")
zip_size = os.path.getsize(zip_path)
if zip_size > 50 * 1024 * 1024:
raise ValueError(
f"Layer zip file is too large: {zip_size / 1024 / 1024:.2f} MB (max 50 MB)"
)
- 依存ライブラリの管理
- requirements.txtを元にpip installでLayer用ディレクトリにインストールします。
- 一時ディレクトリの活用とクリーンアップ
- tempfile.mkdtempで作業用ディレクトリを作成し、処理後にshutil.rmtreeで削除します。
- zip化とサイズチェック
- Lambda Layerの制限(50MB)を超えないか確認します。
CDKでのCustomResource定義
LayerBuilder Lambda関数自体もCDKで定義します。
import * as cdk from 'aws-cdk-lib';
import { aws_iam as iam, aws_lambda as lmd, aws_logs as logs } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class LayerBuilder extends Construct {
public readonly function: lmd.Function;
constructor(scope: Construct, id: string) {
super(scope, id);
const logGroup = new logs.LogGroup(this, 'LogGroup', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
retention: logs.RetentionDays.THREE_MONTHS,
});
// Lambda関数を作成
this.function = new lmd.Function(this, 'Function', {
runtime: lmd.Runtime.PYTHON_3_13,
handler: 'lambda_function.lambda_handler',
code: lmd.Code.fromAsset('assets/lambda/layer-builder'),
memorySize: 1024,
description: 'LayerBuilderFunction',
logGroup: logGroup,
timeout: cdk.Duration.minutes(15),
});
// Lambda Layer管理に必要な権限を付与
this.function.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['lambda:PublishLayerVersion', 'lambda:ListLayerVersions', 'lambda:DeleteLayerVersion'],
resources: [
cdk.Stack.of(this).formatArn({
service: 'lambda',
resource: 'layer:*',
}),
],
}),
);
}
}
解説
LayerBuilderの権限
// Lambda Layer管理に必要な権限を付与
this.function.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'lambda:PublishLayerVersion',
'lambda:ListLayerVersions',
'lambda:DeleteLayerVersion'
],
resources: [
cdk.Stack.of(this).formatArn({
service: 'lambda',
resource: 'layer:*'
})
]
})
);
実行に必要な権限は、上記の通りです。
layerの名前に関しては、動的に決定されるものなので、* としています。
CustomResourceでLayerBuilderを実行
import * as cdk from 'aws-cdk-lib';
import { custom_resources as cr, aws_lambda as lmd, aws_logs as logs } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as fs from 'fs';
export interface IRequestsLambdaProps {
/**
* LayerBuilderインスタンス
*/
readonly layerBuilder: lmd.IFunction;
}
export class RequestsLambda extends Construct {
constructor(scope: Construct, id: string, props: IRequestsLambdaProps) {
super(scope, id);
const logGroup = new logs.LogGroup(this, 'LogGroup', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
retention: logs.RetentionDays.THREE_MONTHS,
});
// requirements.txtを読み込む
const requirementsPath = 'assets/lambda/requests-lambda/requirements.txt';
const requirements = fs.readFileSync(requirementsPath, 'utf-8');
// 共通のLayerBuilderを使用してCustomResourceを作成
const provider = new cr.Provider(this, 'RequestsLambdaLayerProvider', {
onEventHandler: props.layerBuilder,
});
const layerResource = new cdk.CustomResource(this, 'RequestsLambdaLayer', {
serviceToken: provider.serviceToken,
properties: {
Requirements: requirements,
LayerName: 'RequestsLambdaLayer',
},
});
// CustomResourceから返されるLayerVersionArnを取得
const layerVersionArn = layerResource.getAttString('LayerVersionArn');
// LayerVersionオブジェクトを作成
const layer = lmd.LayerVersion.fromLayerVersionArn(this, 'ImportedRequestsLambdaLayer', layerVersionArn);
// Lambda関数を作成(Layerを適用)
const lambdaFunction = new lmd.Function(this, 'Function', {
runtime: lmd.Runtime.PYTHON_3_13,
handler: 'lambda_function.lambda_handler',
code: lmd.Code.fromAsset('assets/lambda/requests-lambda'),
memorySize: 1024,
description: 'RequestsLambda',
logGroup: logGroup,
timeout: cdk.Duration.minutes(15),
layers: [layer],
});
// CustomResourceのLayer作成が完了してからLambda関数をデプロイするように明示的な依存関係を追加
lambdaFunction.node.addDependency(layerResource);
}
}
解説
requirements.txt の渡し方
const requirementsPath = 'assets/lambda/requests-lambda/requirements.txt';
const requirements = fs.readFileSync(requirementsPath, 'utf-8');
const provider = new cr.Provider(this, 'LayerProvider', {
onEventHandler: props.layerBuilder.function,
});
const layerResource = new cdk.CustomResource(this, 'RequestsLambdaLayer', {
serviceToken: provider.serviceToken,
properties: {
Requirements: requirements,
LayerName: 'RequestsLambdaLayer',
},
});
CustomResourceのproviderとして、layerBuilderを渡します。
これにより、デプロイ時にLambdaが実行され、レイヤーが作成されます。
また、CustomResourceのpropertiesとして、requirements.txtを渡します。
そうすることで、requirements.txtに変更が合った際にCustomResourceのトリガーとすることができます。
requirements.txtには以下を定義しています。
requests==2.32.5
複数のLambdaでlayerBuilderを共有する
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { LayerBuilder } from '../construct/layer-builder';
import { MyLambdaFunction } from '../construct/my-lambda-function';
export class LayerBuilderStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 共通のLayerBuilderを作成(複数のLambda関数で共有)
const layerBuilder = new LayerBuilder(this, 'LayerBuilder');
// Lambda関数を追加(共通のLayerBuilderを使用)
new RequestsLambda(this, 'RequestsLambda', {
layerBuilder: layerBuilder.function,
});
// 今後、他のLambda関数を追加する場合も同じlayerBuilderを使用
// 例:
// new AnotherFunction(this, 'AnotherFunction', {
// layerBuilder: layerBuilder.function,
// });
}
}
layerBuilderをコンストラクトとして定義することで、Lambdaを共通で使いまわすことが可能になります。
他のLambdaが追加された場合でも、問題なくビルドできます。
ただし、Lambdaの同時実行数に関しては注意してください。
デプロイ
デプロイした場合、3つのLambdaが作成されます。
- LayerBuilder Lambda: レイヤーをビルドするためのLambdaです。
- Layerを付与するLambda: (RequestsLambda など)作成されたレイヤーを使用するLambdaです。
- CustomResource: LayerBuilder Lambdaを呼び出し、レイヤーのARNを取得するためのリソースです。
まとめ
LambdaでLambda Layerを作成することで、非常に簡単に管理することができました。
制約の非常に多い環境で作業されている方にとっても、悩ましい問題が一つ解消されるのではないかなと思います!
ぜひ皆さんもやってみてください!
Discussion