CDKのデプロイ待ち時間を1/6に!高速デプロイツール「cdkd」の実力を測ってみた
CloudFormation で IAM Role + InstanceProfile + EC2 を作ると、数分待たされることがあります。特に InstanceProfile の作成完了まで約2分かかるケースがあり、CDK を使っても裏は CloudFormation なので同じです。
「もっと速くならないのか?」と思っていたところ、CloudFormation を経由せず AWS SDK で直接デプロイするツール「cdkd」の存在を知り、試してみました。
cdkd とは
cdkd(CDK Direct)は、CDK アプリを CloudFormation を経由せず AWS SDK / Cloud Control API で直接デプロイする実験的なツールです。対応リソースであれば、既存の CDK コードを変えずに cdk deploy → cdkd deploy に置き換えるだけで利用できます。
インストールと初期設定
# インストール
npm install -g @go-to-k/cdkd
# 初期設定(アカウントごとに1回。CDK bootstrap とは別)
cdkd bootstrap
# デプロイ(cdk deploy の代わりに実行するだけ)
cdkd deploy
今回の検証では Docker で環境を隔離して実行しました。Dockerfile は付録に掲載しています。
検証: 同一構成で4手法を比較
構成
IAM Role、IAM Policy、InstanceProfile、SecurityGroup、EC2 Instance の5リソースです。
IAM Role → IAM Policy
→ InstanceProfile → EC2 Instance ← SecurityGroup
比較対象
| 手法 | 説明 |
|---|---|
| CloudFormation | aws cloudformation deploy 直接 |
| CDK | cdk deploy(CloudFormation 経由) |
| cdkd | cdkd deploy(SDK 直接) |
| 最適化CLI | bash + AWS CLI 並列化(手書き最適化の参考値) |
実行環境
- リージョン: ap-northeast-1(東京)
- Docker (node:20-slim) 上で実行
- aws-cli 2.34.45 / node v20.20.2 / cdk 2.1121.0 / cdkd 0.58.0
- t3.micro / Amazon Linux 2023 (x86_64)
- 各手法3回実行、中央値を採用
結果
| 手法 | Run 1 | Run 2 | Run 3 | 中央値 | 高速化倍率 |
|---|---|---|---|---|---|
| CloudFormation | 188s | 187s | 188s | 188s | 1.0x |
| CDK | 188s | 188s | 188s | 188s | 1.0x |
| cdkd | 29s | 28s | 28s | 28s | 6.7x |
| 最適化CLI | 30s | 30s | 31s | 30s | 6.3x |
今回の EC2 + IAM InstanceProfile 構成では、cdkd は CloudFormation/CDK より約6.7倍高速でした。
なぜこの差が出たのか
InstanceProfile の安定化確認が主因
CloudFormation の Stack Events を見ると、InstanceProfile の作成に2分11秒かかっていることが分かります。
BenchEc2IamStack | 2/6 | 8:57:55 AM | CREATE_IN_PROGRESS | AWS::IAM::InstanceProfile | Resource creation Initiated
BenchEc2IamStack | 4/6 | 9:00:06 AM | CREATE_COMPLETE | AWS::IAM::InstanceProfile
SDK の CreateInstanceProfile 自体は即座に返りますが、CloudFormation は作成後に内部で安定化確認を行っています(Stack Events に「Eventual consistency check initiated」と記録されます)。この待機時間は IAM の伝播完了を保証するための仕組みと推測されます。公式ドキュメントには対象リソースタイプや所要時間の詳細は公開されていません。
cdkd のアプローチ: 楽観的実行 + リトライ
cdkd では安定化確認の長い待機が発生せず、全リソースが短時間で作成完了しました。
Deploying 5 resource(s) (DAG: 3 levels, max parallel: 10)
[1/5] ✅ BenchSecurityGroup (AWS::EC2::SecurityGroup) created
[2/5] ✅ BenchRole (AWS::IAM::Role) created
[3/5] ✅ BenchPolicy (AWS::IAM::Policy) created
[4/5] ✅ BenchInstanceProfile (AWS::IAM::InstanceProfile) created
[5/5] ✅ BenchInstance (AWS::EC2::Instance) created
Deployment Summary:
Duration: 19.83s
※ Duration 19.83s は cdkd のデプロイ本体の時間。結果表の 28s は synth + デプロイ + スクリプトオーバーヘッドを含むコマンド全体の所要時間です。
IAM 伝播が間に合わない場合は、エラーパターンを検知してバックオフリトライします(src/deployment/retryable-errors.ts にリトライパターンが定義されています)。最適化 CLI のログでは、実際にリトライが発生していることを確認できました。
[00:06] Retry 1: IAM not propagated yet (waiting 2s)...
[00:08] Retry 2: IAM not propagated yet (waiting 4s)...
[00:14] EC2 Instance launched
少なくとも今回の環境では、EC2 起動に必要な IAM 伝播待ちはリトライ2回、計6秒で足りました。
InstanceProfile 以外にも有効と考えられるケース
CloudFormation の Eventual consistency check は InstanceProfile に限らず多くのリソースで発生します。今回の検証の Stack Events でも SecurityGroup と EC2 Instance で確認できました。
BenchSecurityGroup | CREATE_IN_PROGRESS | Eventual consistency check initiated
BenchInstance | CREATE_IN_PROGRESS | Eventual consistency check initiated
cdkd のアプローチ(SDK 即時完了 + エラー時リトライ)は、Eventual consistency check が長いリソース全般に対して時短効果が期待できると考えられます。ただし、リソースタイプごとに安定化条件や SDK の挙動は異なるため、今回と同じ短縮効果が常に得られるとは限りません。
手書き最適化 CLI と同等の速度を宣言的コードで
bash で並列化とリトライを手実装した最適化 CLI が 30秒、CDK の宣言的 TypeScript コードをそのまま cdkd でデプロイした結果が 28秒 でした。手書きで並列化やリトライを実装しなくても、CDK の宣言的なコードを維持したまま同等の所要時間を実現できる点が、cdkd の大きな利点だと分かりました。
注意事項・制限
- 全リソースタイプに対応しているわけではない(90+ SDK Provider + Cloud Control API フォールバック)
- ロールバック(
--no-rollbackで無効化可)、ドリフト検出(cdkd drift、--accept/--revert対応)は v0.35.0 以降で実装済み。ただし CFn 未指定プロパティのドリフト検出など一部不具合修正中(CHANGELOG 参照) - 状態管理は S3 ベース(DynamoDB 不要だが、CloudFormation ほどの運用実績はない)
- 活発に開発が進行中のため、最新の対応状況は公式リポジトリを参照することをおすすめします
まとめ
従来、InstanceProfile の作成待ちを回避するために IAM を別スタックに分離するワークアラウンドがありましたが、スタック分割によってリソース単位での権限最小化が難しくなる副作用がありました。cdkd なら IAM と EC2 を同一スタックに書いたまま高速デプロイできるため、権限最小化と速度を両立できます。
IAM 権限の最小化作業でポリシーを少しずつ絞りながら deploy → テスト → 修正を繰り返す場合や、PR ごとに検証環境を作って壊すワークフロー、構成を試行錯誤するプロトタイピングなど、1回のデプロイが3分から30秒になることで開発体験が大きく変わります。
開発・検証環境での CDK や CloudFormation のデプロイ待ち時間に課題を感じている方は、ぜひ試してみてください。
参考リンク
再現用コード(クリックで展開)
CDK アプリ (bin/app.ts)
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { BenchStack } from '../lib/stack';
const app = new cdk.App();
const vpcId = app.node.tryGetContext('vpcId');
const subnetId = app.node.tryGetContext('subnetId');
const amiId = app.node.tryGetContext('amiId');
if (!vpcId || !subnetId || !amiId) {
throw new Error('Required: -c vpcId=xxx -c subnetId=xxx -c amiId=xxx');
}
new BenchStack(app, 'BenchEc2IamStack', { vpcId, subnetId, amiId });
CDK アプリ (cdk.json)
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"versionReporting": false
}
CDK アプリ (lib/stack.ts)
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
interface BenchStackProps extends cdk.StackProps {
vpcId: string;
subnetId: string;
amiId: string;
}
export class BenchStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: BenchStackProps) {
super(scope, id, props);
const role = new iam.CfnRole(this, 'BenchRole', {
assumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [{ Effect: 'Allow', Principal: { Service: 'ec2.amazonaws.com' }, Action: 'sts:AssumeRole' }],
},
});
new iam.CfnPolicy(this, 'BenchPolicy', {
policyName: 'bench-policy',
roles: [role.ref],
policyDocument: {
Version: '2012-10-17',
Statement: [{ Effect: 'Allow', Action: 's3:GetObject', Resource: '*' }],
},
});
const instanceProfile = new iam.CfnInstanceProfile(this, 'BenchInstanceProfile', {
roles: [role.ref],
});
const sg = new ec2.CfnSecurityGroup(this, 'BenchSecurityGroup', {
groupDescription: 'Benchmark security group',
vpcId: props.vpcId,
});
new ec2.CfnInstance(this, 'BenchInstance', {
instanceType: 't3.micro',
imageId: props.amiId,
subnetId: props.subnetId,
iamInstanceProfile: instanceProfile.ref,
securityGroupIds: [sg.attrGroupId],
tags: [{ key: 'Name', value: 'bench-test' }],
});
}
}
Dockerfile
最適化 CLI スクリプト (04-cli-optimized-deploy.sh)
#!/bin/bash
set -euo pipefail
VPC_ID="${1:?Usage: $0 <vpc-id> <subnet-id> <ami-id>}"
SUBNET_ID="${2:?}"
AMI_ID="${3:?}"
RUN_ID="bench-$(date +%s)"
ROLE_NAME="bench-role-${RUN_ID}"
PROFILE_NAME="bench-profile-${RUN_ID}"
START=$(date +%s)
ts() { local e=$(( $(date +%s) - START )); printf '[%02d:%02d]' $((e/60)) $((e%60)); }
echo "$(ts) === Optimized CLI deploy (parallel + retry) ==="
# Phase 1: Role + SecurityGroup in parallel
echo "$(ts) Creating IAM Role & SecurityGroup (parallel)..."
aws iam create-role --role-name "$ROLE_NAME" \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}' \
--no-cli-pager > /dev/null &
ROLE_PID=$!
SG_ID=$(aws ec2 create-security-group --group-name "bench-sg-${RUN_ID}" \
--description "Benchmark SG" --vpc-id "$VPC_ID" --query 'GroupId' --output text)
echo "$(ts) SecurityGroup created: ${SG_ID}"
wait $ROLE_PID
echo "$(ts) IAM Role created: ${ROLE_NAME}"
# Phase 2: Policy + InstanceProfile in parallel (depend on Role)
echo "$(ts) Creating Policy & InstanceProfile (parallel)..."
aws iam put-role-policy --role-name "$ROLE_NAME" --policy-name "bench-policy" \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}' &
POLICY_PID=$!
aws iam create-instance-profile --instance-profile-name "$PROFILE_NAME" --no-cli-pager > /dev/null
aws iam add-role-to-instance-profile --instance-profile-name "$PROFILE_NAME" --role-name "$ROLE_NAME"
echo "$(ts) InstanceProfile created: ${PROFILE_NAME}"
wait $POLICY_PID
echo "$(ts) Policy attached"
# Phase 3: EC2 Instance (retry for IAM propagation)
echo "$(ts) Launching EC2 Instance (with IAM propagation retry)..."
PROFILE_ARN=$(aws iam get-instance-profile --instance-profile-name "$PROFILE_NAME" \
--query 'InstanceProfile.Arn' --output text)
INSTANCE_ID=""
for i in 1 2 3 4 5 6 7 8 9 10; do
INSTANCE_ID=$(aws ec2 run-instances --instance-type t3.micro --image-id "$AMI_ID" \
--subnet-id "$SUBNET_ID" --iam-instance-profile "Arn=${PROFILE_ARN}" \
--security-group-ids "$SG_ID" \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=bench-test}]" \
--query 'Instances[0].InstanceId' --output text 2>/dev/null) && break
echo "$(ts) Retry ${i}: IAM not propagated yet (waiting $((i*2))s)..."
sleep $((i * 2))
done
[ -z "$INSTANCE_ID" ] && echo "ERROR: Failed to launch instance" && exit 1
echo "$(ts) EC2 Instance launched: ${INSTANCE_ID}"
# Phase 4: Wait for running
echo "$(ts) Waiting for instance running..."
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID"
END=$(date +%s)
echo "$(ts) === Done: $((END - START))s ==="
FROM node:20-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
curl unzip jq bash ca-certificates python3 \
&& rm -rf /var/lib/apt/lists/*
# AWS CLI v2 (aarch64の場合。x86_64なら URL を変更)
RUN curl -sL "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o /tmp/awscli.zip \
&& unzip -q /tmp/awscli.zip -d /tmp \
&& /tmp/aws/install \
&& rm -rf /tmp/aws /tmp/awscli.zip
# CDK + cdkd
RUN npm install -g aws-cdk @go-to-k/cdkd
WORKDIR /benchmark
COPY . .
RUN cd cdk-app && npm install
ENTRYPOINT ["/bin/bash"]
実行方法
# ビルド
docker build -t bench-ec2-iam .
# cdkd bootstrap(初回のみ)
docker run --rm -v ~/.aws:/root/.aws -e AWS_REGION=ap-northeast-1 \
bench-ec2-iam -c "cdkd bootstrap"
# cdkd デプロイ
docker run --rm -v ~/.aws:/root/.aws -e AWS_REGION=ap-northeast-1 \
-w /benchmark/cdk-app \
bench-ec2-iam -c "cdkd deploy -c vpcId=<vpc-id> -c subnetId=<subnet-id> -c amiId=<ami-id>"










