この記事を書いた背景
ソーイでは、機械学習プロジェクトでの SageMaker AIの構築や、1案件で複数のAWSサービスをまとめて管理する場面が日常になってきました。マネジメントコンソールでぽちぽち作ると再現性が落ち、CloudFormation の YAML は記述量が多く保守がつらい——そこでAWS CDK を採用するケースが増えています。
本記事は、CDKに初めて触れる方向けにステップバイステップで触れるように紹介します。
はじめに
AWS CDK(Cloud Development Kit)は、AWSのインフラを「コード」で定義できるツールです。CloudFormationのYAMLを書く代わりに、Python・TypeScript・Java などのプログラミング言語でインフラを記述できます。
この記事では、CDKを初めて触る人向けに、Lambda関数で発行した署名URL(presigned URL)を使ってS3にファイルをアップロードするAPI を、段階的に組み立てていきます。
作るもの
シンプルですが、実務でも頻出のパターンです。Lambda経由でURLを発行することで、S3バケットを直接公開せずに済むのがポイントです。
この記事のゴール
- CDKの基本サイクル(
init/synth/diff/deploy/destroy)を体感する - スタックを分割する書き方を覚える
- スタック間でリソースを参照する方法を知る
- 動くサンプルを手元に残す
想定読者と前提
- AWSアカウントを持っていて、マネジメントコンソールに触れたことがある
- Pythonの基本文法が読める
- CDKは初めての方
用意するもの
| ツール | バージョン目安 | 用途 |
|---|---|---|
| Node.js | 22 以上 | CDK CLI の実行に必要 |
| Python | 3.10 以上 | CDKコード・Lambdaコードの記述 |
| AWS CLI | v2 | 認証情報の設定と動作確認 |
| AWSアカウント | - | デプロイ先 |
なぜ CDK を選ぶか
CloudFormation の YAML は、複数サービスをまたいだ構成になると一気に記述量が膨らみ、IAMロール定義などのボイラープレートで本筋が見えにくくなります。CDK ならプログラミング言語の力で
- IAMポリシーを
bucket.grant_put(fn)の1行で自動生成できる - ループや条件分岐、関数化で重複を排除できる
- 型補完でタイポや必須プロパティの抜け漏れを防げる
これらの恩恵が、複数サービスをまとめて管理するケースほど大きく効いてきます。
なぜ Lambda Function URL を使うか
Lambda を外から呼ぶ手段としては API Gateway も有力ですが、本記事では Function URL を採用しています。理由は次の通りです。
- 設定がシンプル: 1行で公開でき、CDKのコード量が最小
- 追加コストがほぼゼロ: API Gateway のような呼び出し単位の課金がない
- 入門の主役に最適: 概念を増やしすぎず、本筋(CDK の使い方)に集中できる
本番要件(認証、レート制御、WAF、カスタムドメインなど)が増えてきたら、API Gateway への移行を検討する流れがおすすめです。
第1章 環境構築
1-1. AWS CLI の認証情報を設定する
すでに設定済みなら飛ばしてください。
aws configure
# AWS Access Key ID: ********
# AWS Secret Access Key: ********
# Default region name: ap-northeast-1
# Default output format: json
確認:
aws sts get-caller-identity
自分のアカウントIDとユーザーARNが返ってくればOKです。
1-2. CDK CLI をインストールする
$ npm install -g aws-cdk
$ cdk --version
2.1121.0 (build 7abdd4e)
CDK本体はPython(あるいは他の言語)ですが、CLIはNode.jsで動きます。最初に戸惑うポイントなので押さえておきましょう。
1-3. CDK Bootstrap
CDKを使う前に、デプロイ先のアカウント/リージョンに「ブートストラップ」が必要です。これはCDKがデプロイ時に使うS3バケットやIAMロールを事前に作っておく作業で、アカウント × リージョンごとに一度だけ実行します。
$ cdk bootstrap aws://<アカウントID>/ap-northeast-1
⏳ Bootstrapping environment aws://<アカウントID>/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
CDKToolkit: creating CloudFormation changeset...
✅ Environment aws://<アカウントID>/ap-northeast-1 bootstrapped.
アカウントIDは aws sts get-caller-identity で確認できます。
1-4. プロジェクトを作成する
作業用のディレクトリを作り、cdk init で雛形を生成します。
$ mkdir cdk-upload-api && cd cdk-upload-api
$ cdk init app --language python
Applying project template app for python
...
✅ All done!
生成後、仮想環境を有効化して依存をインストールします。
source .venv/bin/activate
pip install -r requirements.txt
ディレクトリ構成は次のようになります(一部抜粋)。
cdk-upload-api/
├── app.py # CDKのエントリポイント
├── cdk_upload_api/
│ ├── __init__.py
│ └── cdk_upload_api_stack.py # デフォルトのスタック
├── cdk.json
└── requirements.txt
app.py がCDKアプリの入り口、cdk_upload_api_stack.py がスタック定義です。これから章ごとにこれらを書き換えていきます。
第2章 最小構成 - S3バケットを作る
まずはS3バケットだけのスタックを作って、CDKのデプロイサイクルに慣れます。
2-1. スタックを書き換える
cdk_upload_api/cdk_upload_api_stack.py を s3_stack.py にリネームし、中身を次のように書き換えます。
# cdk_upload_api/s3_stack.py
from aws_cdk import (
Stack,
RemovalPolicy,
aws_s3 as s3,
)
from constructs import Construct
class S3Stack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
self.upload_bucket = s3.Bucket(
self,
"UploadBucket",
removal_policy=RemovalPolicy.DESTROY, # cdk destroyで消えるように
auto_delete_objects=True, # 中身ごと削除を許可
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
)
ポイント:
-
self.upload_bucketとして保持しているのは、あとで別スタックから参照するため -
RemovalPolicy.DESTROYとauto_delete_objects=Trueは 学習用 の設定です。本番では既定のRETAINのままにしてください。DESTROYのまま運用すると、スタック削除時にデータが消えて取り返しがつかなくなります - パブリックアクセスは全ブロック。アップロードは署名URL経由で行います
2-2. app.py を更新する
# app.py
import aws_cdk as cdk
from cdk_upload_api.s3_stack import S3Stack
app = cdk.App()
S3Stack(app, "UploadApiS3Stack")
app.synth()
2-3. CDKの基本3コマンド
CDKは「コードを書く → デプロイ前に内容を確認 → 反映する」のサイクルが基本です。
① cdk synth — CloudFormationテンプレートを生成
$ cdk synth
Resources:
UploadBucketD2C1DA78:
Type: AWS::S3::Bucket
Properties:
# ... テンプレートが出力される
CDKコードから生成されたCloudFormationテンプレート(YAML)が標準出力に表示されます。「裏で何が作られるのか」を確認できます。
② cdk diff — 既存環境との差分を確認
$ cdk diff
start: Building UploadApiS3Stack Template
success: Built UploadApiS3Stack Template
start: Publishing UploadApiS3Stack Template (current_account-current_region-85cfb58f)
success: Published UploadApiS3Stack Template (current_account-current_region-85cfb58f)
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --method=template to use a less accurate but faster template-only diff)
Stack UploadApiS3Stack
IAM Statement Changes
┌───┬─────────────────────────────────┬────────┬─────────────────────────────────┬───────────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼─────────────────────────────────┼────────┼─────────────────────────────────┼───────────────────────────────────┼───────────┤
│ + │ ${Custom::S3AutoDeleteObjectsCu │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │
│ │ stomResourceProvider/Role.Arn} │ │ │ │ │
├───┼─────────────────────────────────┼────────┼─────────────────────────────────┼───────────────────────────────────┼───────────┤
│ + │ ${UploadBucket.Arn} │ Allow │ s3:DeleteObject* │ AWS:${Custom::S3AutoDeleteObjects │ │
│ │ ${UploadBucket.Arn}/* │ │ s3:GetBucket* │ CustomResourceProvider/Role.Arn} │ │
│ │ │ │ s3:List* │ │ │
│ │ │ │ s3:PutBucketPolicy │ │ │
└───┴─────────────────────────────────┴────────┴─────────────────────────────────┴───────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼─────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────┤
│ + │ ${Custom::S3AutoDeleteObjectsCustomResourceProvider/Role} │ {"Fn::Sub":"arn:${AWS::Partition}:iam::aws:policy/service-ro │
│ │ │ le/AWSLambdaBasicExecutionRole"} │
└───┴─────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Parameters
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/hnb659fds/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}
Conditions
[+] Condition CDKMetadata/Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":...
Resources
[+] AWS::S3::Bucket UploadBucket UploadBucketD2C1DA78
...
✨ Number of stacks with differences: 1
すでにデプロイ済みのスタックと比較して、何が増減するかを表示してくれます。毎回デプロイ前に確認すると事故が減ります。
③ cdk deploy — 実際にデプロイ
$ $ cdk deploy
✨ Synthesis time: 5.97s
....
"--require-approval" is enabled and stack includes security-sensitive updates: Do you wish to deploy these changes? (y/n) y
✅ UploadApiS3Stack
✨ Deployment time: 38.39s
IAM関連の変更がある場合は確認を求められるので y で進めます。
デプロイが終わったら、AWSマネジメントコンソールのS3画面でバケットが作られていることを確認しましょう。
💡 コラム: CDKとCloudFormationの関係
CDKは「最終的にCloudFormationテンプレートを生成して、それをデプロイするツール」です。cdk synthで見えるYAMLが実体。なので困ったらcdk synthでテンプレートを見ると理解が早まります。
デプロイまで終わるとコンソールからバケット作成を確認できます。
第3章 Lambda関数を追加する
S3バケットだけでは何もできないので、署名URLを発行するLambdaを追加します。ここでは 別スタックに分けて 作り、CDKのスタック分割パターンも学びます。
3-1. Lambdaのソースを用意する
プロジェクトルートに lambda/ ディレクトリを作り、ハンドラを置きます。
mkdir lambda
# lambda/handler.py
import json
import os
import uuid
import boto3
from botocore.config import Config
BUCKET_NAME = os.environ["BUCKET_NAME"]
BUCKET_REGION = os.environ["BUCKET_REGION"]
URL_EXPIRES_IN = 300 # 秒
s3 = boto3.client(
"s3",
region_name=BUCKET_REGION,
endpoint_url=f"https://s3.{BUCKET_REGION}.amazonaws.com",
config=Config(signature_version="s3v4"),
)
def handler(event, context):
# クエリパラメータからファイル名を取得(無ければUUIDを生成)
qs = event.get("queryStringParameters") or {}
filename = qs.get("filename", f"{uuid.uuid4()}.bin")
key = f"uploads/{filename}"
url = s3.generate_presigned_url(
ClientMethod="put_object",
Params={"Bucket": BUCKET_NAME, "Key": key},
ExpiresIn=URL_EXPIRES_IN,
HttpMethod="PUT",
)
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"uploadUrl": url,
"key": key,
"expiresIn": URL_EXPIRES_IN,
}),
}
boto3.client(..., region_name=BUCKET_REGION, endpoint_url=..., config=Config(signature_version="s3v4")) で、バケットのリージョン、S3エンドポイント、署名バージョン4を明示しています。署名URLのホストが s3.amazonaws.com のようなグローバルエンドポイントになると、PUT時にS3から TemporaryRedirect が返ることがあります。
3-2. LambdaStack を作る
cdk_upload_api/lambda_stack.py を新規作成します。
# cdk_upload_api/lambda_stack.py
from aws_cdk import (
Stack,
Duration,
aws_lambda as _lambda,
aws_s3 as s3,
)
from constructs import Construct
class LambdaStack(Stack):
def __init__(
self,
scope: Construct,
construct_id: str,
upload_bucket: s3.IBucket,
**kwargs,
) -> None:
super().__init__(scope, construct_id, **kwargs)
self.presign_fn = _lambda.Function(
self,
"PresignFn",
runtime=_lambda.Runtime.PYTHON_3_14,
handler="handler.handler",
code=_lambda.Code.from_asset("lambda"),
environment={
"BUCKET_NAME": upload_bucket.bucket_name,
"BUCKET_REGION": self.region,
},
timeout=Duration.seconds(10),
)
# S3バケットへのPUT権限を付与(IAMポリシーが自動生成される)
upload_bucket.grant_put(self.presign_fn)
ここがCDKの強みです。upload_bucket.grant_put(self.presign_fn) の1行で、必要なIAMポリシーを自動生成してくれます。CloudFormationで書くなら数十行になる箇所が、CDKでは1行です。
3-3. app.py でスタックを繋ぐ
# app.py
import aws_cdk as cdk
from cdk_upload_api.s3_stack import S3Stack
from cdk_upload_api.lambda_stack import LambdaStack
app = cdk.App()
s3_stack = S3Stack(app, "UploadApiS3Stack")
LambdaStack(
app,
"UploadApiLambdaStack",
upload_bucket=s3_stack.upload_bucket,
)
app.synth()
s3_stack.upload_bucket を LambdaStack のコンストラクタに渡しています。これでCDKが「LambdaStack は S3Stack に依存している」と認識し、デプロイ順序も自動で解決してくれます。
3-4. デプロイ
$ cdk diff
start: Building UploadApiS3Stack Template
success: Built UploadApiS3Stack Template
...
$ cdk deploy --all
✨ Synthesis time: 5.76s
UploadApiS3Stack
UploadApiLambdaStack: start: Building PresignFn/Code
UploadApiLambdaStack: success: Built PresignFn/Code
UploadApiS3Stack: creating CloudFormation changeset...
✅ UploadApiLambdaStack
✨ Deployment time: 59.12s
--all は全スタックをまとめてデプロイするオプションです。
コンソール画面でLambda関数がデプロイされたことを確認できます。

第4章 HTTPエンドポイントを追加する(Lambda Function URL)
Lambdaを外から呼ぶには、API GatewayかLambda Function URLが必要です。Function URLは設定がシンプルで、追加コストもないので入門に最適です。
4-1. LambdaStack に Function URL を追加
lambda_stack.py の末尾に追記します。
# cdk_upload_api/lambda_stack.py(追記)
from aws_cdk import CfnOutput
# ... 上の import に追加
# __init__ の最後に追記
fn_url = self.presign_fn.add_function_url(
auth_type=_lambda.FunctionUrlAuthType.NONE, # 認証なし(学習用)
cors=_lambda.FunctionUrlCorsOptions(
allowed_origins=["*"],
allowed_methods=[_lambda.HttpMethod.GET, _lambda.HttpMethod.POST],
),
)
CfnOutput(self, "FunctionUrl", value=fn_url.url)
auth_type=NONE は 誰でも叩ける状態 です。学習用なので一旦これでよいですが、本番では AWS_IAM にするか、Lambdaコード側でトークン検証するなど対策を入れてください。
CfnOutput を使うと、デプロイ後にコンソール出力で URL が表示されます。
4-2. デプロイして URL を取得
# ログは省略
$ cdk diff
$ cdk deploy --all
成功すると、出力にこんな行が出ます。
UploadApiLambdaStack.FunctionUrl = https://xxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/
このURLを以降の動作確認で使います。
4-3. curl で動かしてみる
① 署名URLをもらう
FN_URL="https://xxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/"
curl "${FN_URL}?filename=hello.txt"
レスポンス例:
{
"uploadUrl": "https://upload-bucket-xxxx.s3.ap-northeast-1.amazonaws.com/uploads/hello.txt?X-Amz-Algorithm=...",
"key": "uploads/hello.txt",
"expiresIn": 300
}
② 署名URLにPUTでファイルをアップロード
echo "hello from cdk" > hello.txt
UPLOAD_URL="(上で返ってきたuploadUrl)"
curl -X PUT --upload-file hello.txt "${UPLOAD_URL}"
成功すると何も表示されずに終了することがあります。TemporaryRedirect が返る場合は、署名URLのS3エンドポイントがバケットのリージョンと合っていない可能性が高いので、Lambdaコードの region_name、endpoint_url、環境変数 BUCKET_REGION を確認してください。
③ S3にファイルが入ったか確認
aws s3 ls s3://(バケット名)/uploads/
hello.txt が見えればゴールです。
第5章 片付けとまとめ
5-1. リソースを削除する
学習用に作ったリソースはきっちり消しましょう。
cdk destroy --all
y で確認して進めます。S3バケットも auto_delete_objects=True を設定しているので、中身ごと消えます。
5-2. ハマりやすいポイント
実際にやってみてつまずきそうなポイントを置いておきます。
-
cdk bootstrapを忘れている: 「Stack uses assets, so the toolkit stack must be deployed」と出たら未実行のサイン -
リージョンの取り違え: AWS CLI のデフォルトリージョンと、コンソールで見ているリージョンがズレている。
cdk.jsonやenv=指定でも明示できる -
S3の
TemporaryRedirect: 署名URLがバケットのリージョン用エンドポイントになっていない時に出ます。S3クライアントにregion_nameとendpoint_urlを指定し、デプロイ後に新しい署名URLを取り直してください -
CORS: ブラウザから叩く場合は
cors設定が必須。curl だけなら不要 -
grant_putかgrant_read_writeか: 今回はアップロードだけなのでgrant_putで十分。最小権限の原則
おわりに
CDKは「IaCを書く」ハードルを大きく下げてくれるツールです。とくに bucket.grant_put(fn) のような意図がそのままコードになり、権限管理から解放してくれます。
今回作ったコードは、そのまま実務の足場としても使えます。ぜひ手を動かして、自分の手に馴染ませてみてください。
完成版のディレクトリ構成
cdk-upload-api/
├── app.py
├── cdk.json
├── requirements.txt
├── cdk_upload_api/
│ ├── __init__.py
│ ├── s3_stack.py
│ └── lambda_stack.py
└── lambda/
└── handler.py
参考リンク
- AWS CDK v2 公式ドキュメント (Python)
- CDK Workshop(公式ハンズオン)
- Amazon S3 署名付きURL - AWS公式ドキュメント
- boto3
generate_presigned_urlリファレンス
お知らせ
技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!
https://qiita.com/organizations/sewii

