2026年最先端の
L1コンストラクト活用術
CDK Conference Japan 2026
クライヤー篠塚 一帆
クライヤー篠塚 一帆
𝕏 @nixieminton GitHub @badmintoncryer
クライヤー篠塚 一帆
  • AWS CDK Top Contributor & Community Reviewer
    • 194 merged PRs / 141 reviewed PRs
    • 17 / 1899 contributors(新旧AWS CDKチームを除くと世界2位🥈)
  • APJC community leaders awards 2025
  • AWS community builders (Dev Tools)
  • IPA資格 ネスペ, セキスペ, エンスペ
  • バドミントンはキャリア20年!2025年は年代別(30歳以上)全国ベスト16でした🎉
おさらい
App/Stack/Constructの構成図
CDKアプリケーションの構成: App / Stack / Construct
今日の話は、この Construct の中身
L3 (Pattern) 手作業(PR)
複数のL1, L2を更に抽象化
L2 (High level) 手作業(PR)
L1を抽象化。型、関数、引数チェック、etc.. を提供
L1 (Low level) 自動生成
Cloudformationリソースと1対1対応・CFN更新に自動追従
L2 / L3 はPRを介した手作業 → 機能追加に数週間〜数ヶ月かかる
L1 は自動生成 → 新機能・新プロパティが即日反映されるのが大きな強み
注意点
ここからの内容はクライヤーの独自解釈を多く含みます。
AWS CDKの公式見解では無いことをご承知おきください
L2への機能追加は盛んに行われているが、
人手を介するという点が実装の大きなボトルネック
▼
自動生成されるL1自体を強化できれば、
わざわざPRを経由した機能追加を行わずとも
ユーザは便利にCDKコンストラクトを扱えるのでは?
Enhanced L1
2024/11にRFCが作成される
RFC 655: Enhanced L1s #657
自動生成されるL1自体の機能拡張を行うことで、
L2に頼らずにユーザビリティの向上を図る
❶ バリデーション
❷ L1インターフェース導入
❸ Grantsクラスの自動生成
❹ stringよりも型安全な引数
https://github.com/aws/aws-cdk-rfcs/pull/657
提案1 バリデーション
L1コンストラクトにおいても引数に対してバリデーションを実行する
L2
{}new lambda.Function(this, 'Fn', { ephemeralStorageSize: Size.mebibytes(100), └ 512-10240 のみ設定可能 });
synth時にCDK側でバリデーションエラー😆
⏱ 数秒でフィードバック
L1
{}new lambda.CfnFunction(this, 'Fn', { ephemeralStorage: { size: 100 }, └ 512-10240 のみ設定可能 });
synth正常終了 (数秒)
▼ 十数分…
deploy時にcloudformation側でデプロイエラー😢
⏱ 十数分待たされてエラー
L1も synth時に弾ければ数秒でエラーに気づける。
デプロイ失敗の十数分待ちが消え、開発サイクルが圧倒的に速くなる
提案2 L1インターフェース導入
継承 extends 実現 implements
既存実装(2025年以前)
«L2 interface»IBucket
+bucketName: string
+bucketArn: string
+grantRead(grantee): Grant
+addEventNotification(): void
implements
«L2 Construct»Bucket
+grantRead(grantee): Grant
+grantWrite(grantee): Grant
«L1 Construct»CfnBucket
+bucketName: string
+accessControl: string
+versioningConfiguration: object
CfnBucket(L1)とBucket(L2)は無関係のクラス
RFC 655 提案
«L1 interface»IBucketRef(新設)
+bucketName: string
+bucketArn: string
+bucketDomainName: string
extends
implements
«L2 interface»IBucket
+grantRead(grantee): Grant
+addEventNotification(): void
implements
«L2 Construct»Bucket
+grantRead(grantee): Grant
«L1 Construct 強化»CfnBucket
+bucketName: string
+accessControl: string
+validate()
CfnBucket(L1)とBucket(L2)は同一Interfaceの具象クラス
L1,L2コンストラクトが継承する新しいInterfaceをL1として自動生成する
提案3 Grantsクラスの自動生成
RFCへの@mrgrain(CDKメンテナ)のコメント — re:invent 2024にて3人のAWS Heroesと会話したメモ
mrgrainのRFCコメント
L1へのバリデーション導入めっちゃええやん!
同様にIAM grants周りもCFNから自動生成できたら
なお良いんじゃない?!
grants周りの例
sns.Topic.grantPublish() agentcore.Runtime.grantInvoke()
提案4 Stringよりも型安全な引数
Cloudformation定義
{}ValidationMethod Type: String Allowed values: EMAIL | DNS | HTTP
証明書検証方法は 'EMAIL', 'DNS', 'HTTP' の3文字列のみ許容
従来のL1実装(引数はString型)
{}new CfnCertificate(this, 'Certificate', { method: 'EMAIL', ← String型 });
'Email' 'mail' なども型的にはOK → デプロイ時にエラー😢
解決策: Stringより型安全な引数型を定義(例: Allowed valuesから Enum型を自動生成)
{}// CFNのAllowed valuesから自動生成 enum ValidationMethod { EMAIL, DNS, HTTP }
{}new CfnCertificate(this, 'Certificate', { method: ValidationMethod.EMAIL, ← 型安全! });
型安全な引数型なら 静的解析(型チェック)の時点でバリデーションできる 💬 後で出てきます
提案4のハードルが高すぎてRFCはペンディング
シンプルな解決策
{}validationMethod: string | ValidationMethod;
CDKでは引数のunion型が許容されないのでボツ
提案された実装案
{}interface CfnL1Props { validationMethod: string; // Enum型引数を並列追加 validationMethod_v2: ValidationMethod; }
「xxxProps_v2 として並列にEnum型を追加」
全世界から集まる数多の反対意見
「requiredな引数の場合、既存とv2引数を両方optionalにするので型安全性が落ちない?」 友岡さん (@tmokmss) — Community reviewer
「AspectsでL1引数のバリデーションを独自実装してる場合に書き直しが必要...」 後藤さん (@go-to-k) — Community reviewer / AWS Hero
型安全な引数の導入はハードルが高く、CDKv3へのアップグレードが不可避 → ペンディング
そして2026/02/12
提案の全てが実装される前に、RFC自体がクローズされました
RFC 655: Enhanced L1s #657
⊘ Closed mrgrain closed this on Feb 12, 2026
提案もRFCと共に
闇に葬られたかのように見えたが....??
2026/07時点での実装状況
✅バリデーション
✅Grantsクラスの自動生成
✅L1インターフェース導入
✅stringよりも型安全な引数
4つとも実装済みです。
順に紹介していきます
① Grantsクラスの導入
L2
{}declare const topic: sns.Topic; declare const role: iam.IRole; // roleにSNS Topicへのpublish権限が付与される topic.grantPublish(role); // 意味は同じだが新しい書き方 topic.grants.publish(role);
非常に便利なgrantXxx関数だが、
L2でしか使えないことが長年の欠点だった
L1
{}import { TopicGrants } from 'aws-cdk-lib/aws-sns'; declare const cfnTopic: sns.CfnTopic; declare const role: iam.IRole; // Grantsクラスのインスタンスを生成 const topicGrants = TopicGrants.fromTopic(cfnTopic); // L1のCfnTopicでもgrantが実行できる!! topicGrants.publish(role);
L1コンストラクトであっても、
Grantsクラス(TopicGrants)を生成することでgrant関数が実行可能に!!!!!
https://github.com/aws/aws-cdk/pull/35782
① Grantsクラスの導入 (L2の内部実装)
{}// roleにpublish権限が付与される topic.grantPublish(role); // 意味は同じだが新しい書き方 topic.grants.publish(role);
L2でのgrant関数の使い方 (再掲)
{}// Grantsクラスのインスタンスを生成 const topicGrants = TopicGrants.fromTopic(cfnTopic); topicGrants.publish(role);
L1でのgrant関数の使い方 (再掲) ← ユーザが生成
L2内部でのgrant実装
{}export class Topic extends TopicBase { // grantsメンバにTopicGrantsを代入(コンストラクトが暗黙的に生成) public readonly grants: TopicGrants = TopicGrants.fromTopic(this); public grantPublish(grantee: iam.IGrantable) { return this.grants.publish(grantee); } }
両側とも TopicGrants.fromTopic() を呼ぶ = 同じ関数。
L1でユーザが行う生成を、L2はコンストラクト内で暗黙的に行っているだけで、grantの中身は完全に同一
① Grantsクラスの導入 (grantXxxはどうなる?)
L2でのgrant関数の使い方 (再掲)
{}// 従来どおり呼べる topic.grantPublish(role);
grantPublish() の内部実装
{}public grantPublish(grantee: iam.IGrantable) { // 内部的には grants.publish() を呼ぶだけ return this.grants.publish(grantee); }
既存の grantXxx() は破壊的変更回避のため存続。内部は grants.xxx() を呼ぶだけにリファクタ済み。
実質非推奨だが、混乱回避のため明示的な @deprecated は付けていない
① Grantsクラスの導入 (こぼれ話1)
grants.jsonというファイルが各サービス毎に設置されています
{}// aws-sns/grants.json { "resources": { "Topic": { "hasResourcePolicy": true, "grants": { "publish": { "actions": ["sns:Publish"] } } } } }
必要なIAM actionを定義
「grants.publish()では sns:Publish を追加する」
  • 当初目標としていた「CloudFormationからの自動生成」には至っていない
  • grantに必要なIAM action定義を、
    CloudFormationから引っ張ってこれないことが理由
  • まだ一部のサービスでしか作成されていないため、
    コントリビュートチャンスなはず?!
① Grantsクラスの導入 (こぼれ話2)
@ren-yamanashi さんのAWS CDK向けeslintプラグインでも、
grants.xxx()を推奨するルールが追加されています!ステキ!
eslint-plugin-awscdk
{}topic.grantPublish(role);
{}✖ Use 'topic.grants.publish()' instead of 'topic.grantPublish()'. awscdk/prefer-grants-property
ルール: prefer-grants-property(実際のエラーメッセージ)
https://github.com/ren-yamanashi/eslint-plugin-awscdk
① Grantsクラスの導入 (こぼれ話3)
用意された publish() 等だけでなく、任意のIAMアクションも付与できる
{}const topicGrants = TopicGrants.fromTopic(cfnTopic); // 用意された grant topicGrants.publish(role); // 任意のアクションを直接指定 topicGrants.actions(role, [ 'sns:Publish', 'sns:GetTopicAttributes', ]);
actions(grantee, actions[])
publish() / subscribe() も内部はこれを呼ぶだけ
  • これで任意のIAMアクションをL1に付与できる
  • 用意された grant で足りないときの逃げ道にもなる
② L1インターフェース(Reference Interface)導入
継承 extends 実現 implements
従来の実装
«L2 interface»IBucket
+grantRead(grantee): Grant
+addEventNotification(): void
implements
«L2 Construct»Bucket
+grantRead(grantee): Grant
«L1 Construct»CfnBucket
+bucketName: string
+accessControl: string
2つは無関係のクラスだった
導入後
«Reference Interface(自動生成)»IBucketRef
+bucketRef: BucketReference
extends
implements
«L2 interface»IBucket
+grantRead(grantee): Grant
implements
«L2 Construct»Bucket
+grantRead(grantee): Grant
«L1 Construct»CfnBucket
+bucketName: string
+accessControl: string
同一の基底Interfaceを持つように
L1,L2ともにReference Interfaceをimplementsするように
https://github.com/aws/aws-cdk/pull/35032
なにが嬉しいのか?
Reference Interface導入で嬉しいこと
その1
(i) 引数にL1&L2を受け取れる
{}const someFunction = (bucket: s3.IBucketRef) => { // 処理 } declare const l1Bucket: s3.CfnBucket; declare const l2Bucket: s3.Bucket; declare const importedBucket: s3.IBucket; someFunction(l1Bucket); someFunction(l2Bucket); someFunction(importedBucket);
⟵
多様なS3バケットのコンストラクト達
- CfnBucket: L1
- Bucket: L2
- IBucket: ImportしたL2など
⟵
全てsomeFunction()に渡すことができる
引数をReference Interface型にすると
L1,L2コンストラクト全てを受け取ることができる
引数はL2 InterfaceよりもReference Interfaceが推奨に
コンストラクトを引数で受け取るときにはL2 Interface型が推奨されていたが、
可能な限りReference Interface型に置換するPRが既にマージ済み
ISubnet→ISubnetRefへの置換PR
https://github.com/aws/aws-cdk/pull/35032
引数はL2 InterfaceよりもReference Interfaceが推奨に
DESIGN_GUIDELINESにおいても、引数でのL2 Interfaceの使用は “Most likely for legacy code” と表現😢
DESIGN_GUIDELINESの表
https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md
Reference Interface導入で嬉しいこと
その2
(ii) L1の引数にコンストラクト&文字列を渡せる
従来のL1
{}new sns.CfnSubscription(this, 'Subscription', { topicArn: 'arn:aws:sns:...:topic-name', └─ 文字列しか受け取れず、型安全性が低い })
新しいL1
{}new sns.CfnSubscription(this, 'StringSub', { topicArn: 'arn:aws:sns:...:topic-name', └─ 従来通り文字列はOK }) new sns.CfnSubscription(this, 'L1Sub', { topicArn: l1Topic, └─ L1コンストラクト(CfnTopic)が渡せる!! }) new sns.CfnSubscription(this, 'L2Sub', { topicArn: l2Topic, └─ L2コンストラクト(Topic)も渡せる!! })
StringとReference Interfaceが引数型として許容されている
どうやってString or Reference Interfaceの
受け取りを実現している?
{}export interface CfnSubscriptionProps { readonly topicArn: snsRefs.ITopicRef | string; }
Cfnから自動生成されるtopicArnの引数型が
ITopicRefとstringのunion型に!!!
……引数のunion型は許容されないはずでは..??
引数におけるunion型
{}string | number
→
jsii
各言語版のCDKコードを
生成するツール
→
{}Union[str, Union[int, float]]
{}Object
{}interface{} // 実質any
Javaやgolangではunionを表現できず、緩い制約の型に劣化してしまう
L1における Reference Interface | string もこの問題に直面するが、
利用頻度の少ないgo版CDKなどでのデメリットを上回るメリットがあると判断され、例外的に採用に至っている
(ii) L1の引数にコンストラクト&文字列を渡せる (再掲)
{}new sns.CfnSubscription(this, 'L1Subscription', { topicArn: l1Topic, └ L1コンストラクトが渡せる!! }) new sns.CfnSubscription(this, 'L2Subscription', { topicArn: l2Topic, └ L2コンストラクトも渡せる!! })
Reference Interfaceを引数型として活用することで
「L1にstringよりも型安全な引数を渡せるようになった」
💬 見覚えありますね
Enhanced L1での提案
RFCでの目的
「L1にstringより型安全な引数を渡せるようにする」
Enum型を自動生成する
RFCでの提案
→
ボツ
Reference Interfaceを引数の型にする
実現方法
→
採用
当初の目的を「Reference Interface」活用という形で
一部実現したと解釈しています
Reference Interface導入で嬉しいこと
その3
(iii) XxxGrantsを簡単に作れる
L1でのgrant関数の使い方 (再掲)
{}const topicGrants = TopicGrants.fromTopic(cfnTopic); topicGrants.publish(role);
└ fromTopic() の引数 cfnTopic は L1
L2内部でのgrant実装 (再掲)
{}public readonly grants: TopicGrants = TopicGrants.fromTopic(this);
└ fromTopic() の引数 this は L2
TopicGrantsクラスの実装
{}export class TopicGrants { public static fromTopic(resource: sns.ITopicRef): TopicGrants { ... } }
fromTopic()の引数はReference Interface!! だからL1とL2を受け取れる
L1とL2どちらも受け取れるありがたみが伝わったでしょうか?
非常に応用の幅が広いステキなアップデートだと感じています
引数の型を「上流のInterface」にするほど、広く受け取れる
IEnvironmentAware «core»  { env: account / region }
env を持つ全コンストラクトを受け取れる(最も広い)
IXxxRef «Reference Interface・自動生成»
L1(CfnXxx) と L2(Xxx) の両方を受け取れる
IXxx «L2 interface»
L2(Xxx) のみ / 使える機能は最も豊富
外側(上流)ほど「受け取れる範囲」が広く、内側(下流)ほど「使える機能」が豊富
CfnXxx(L1)は IXxxRef を、Xxx(L2)は IXxx(⊃ IXxxRef) を実装 → どちらも上流の型で受け取れる
同じ構図の関数は他にも — リソースポリシー
L2にしか無かったこの操作が
{}declare const bucket: s3.Bucket; bucket.addToResourcePolicy(statement);
L1でもL2でも、同じ of() で実行可能に!!
{}declare const cfnBucket: s3.CfnBucket; // L1 declare const bucket: s3.Bucket; // L2 declare const statement: iam.PolicyStatement; ResourceWithPolicies.of(cfnBucket) ← L1 ?.addToResourcePolicy(statement); ResourceWithPolicies.of(bucket) ← L2 ?.addToResourcePolicy(statement);
of()の実装
{}export class ResourceWithPolicies { public static of(resource: IEnvironmentAware): IResourceWithPolicyV2 | undefined { ... } }
引数は IEnvironmentAware(Reference Interfaceより上流)
→ あらゆるL1,L2コンストラクトを引数に受け取れる
of(x) が
内部で分岐
L2 (Bucket) を渡す
→
自身の addToResourcePolicy() を実行
L1 (CfnResource) を渡す
→
BucketPolicy 等を自動生成(automaticL1Traits)
https://github.com/aws/aws-cdk/pull/36849
リソースポリシー対応の中身
標準対応サービス
S3KMSLogs SQSDynamoDBSNS
  • 機能フラグ @aws-cdk/core:automaticL1Traits(2.239.0で導入、未設定でも既定で有効)
    • 後方互換性のため既存プロジェクトではoffが通例だが、これは珍しく既存にも影響する
  • grants.jsonの "hasResourcePolicy": true が対応の目印
grant → リソースポリシーと、
L1とL2を同じように扱える関数は着々と増えています
最後に: バリデーションはどうなった?
CDKではなく、CloudFormationの機能として実装された!
L1に埋め込むのではなく、CloudFormationがデプロイ前に検証する
✗ Enhanced L1 RFCの当初構想
生成されるL1コンストラクト1つ1つにバリデーションを埋め込む
✓ 実装された方針
CloudFormationがchangeset作成時にプロパティを検証
CFN層なので L1〜L3すべてが恩恵
実際の書き方: 追加コードなし。cdk deploy するだけ
{}new lambda.CfnFunction(this, 'Fn', { /* ... */ }); // → cdk deploy すると、CloudFormation が changeset 作成時に検証してエラーを返す
提案1の「デプロイ前に弾く」は、CloudFormation側でもう動いている
p7 の例、今はこうなります
L1 の synth は今も正常終了するが、deploy の直前で弾かれる
{}new lambda.CfnFunction(this, 'Fn', { ephemeralStorage: { size: 100 }, });
▼ cdk deploy → 十数秒
{}Early validation failed for change set cdk-deploy-change-set: TempCdkValidateStack/L1Function (AWS::Lambda::Function L1Function) #/EphemeralStorage/Size: 100 is not greater or equal to 512 (at /Resources/L1Function/Properties/EphemeralStorage/Size) Source Location: new TempCdkValidateStack (lib/temp-cdk-validate-stack.ts:10:5)
  • p7で十数分待たされてエラーだったのが、デプロイ前に10秒ほどで返ってくる。
  • Source Location が自分の書いた行を指す。このあたりはCDK CLIが頑張っています。
  • 対象は CFNスキーマに書いてある制約(型 / enum / 必須 / 範囲 / パターン)
  • これとは別に、CDK側でもValidationの仕組みが実装されています。きっと誰かが紹介してくれるはず..??
提案1の「synth時にバリデーションエラー😆」は、synthではなくデプロイ直前という形で叶いました
まとめ
  • Enhanced L1
    • L1自体の機能拡充が図られる新しい動き
    • RFC自体はクローズされたが、提案内容はすべて実装済み
      • Grantsクラスの自動生成
      • L1インターフェース導入
      • stringよりも型安全な引数
      • バリデーション
    • リソースポリシー追加など、抽象的なInterfaceを活かした機能は今も拡充中
    • バリデーションは CloudFormationのデプロイ前検証 という形で実現
L1コンストラクトにも使いこなし方が要求されてきました。
AIが全然学習していないので、みんなでベスプラを模索していきましょう!
おまけ: PRICOACH のご紹介
PRICOACH コーチ選択画面
プロからマンツーマンレッスン動画がもらえる
  • 知り合いのプロゴルファー・プロバドミントンプレイヤーが直接コーチ
  • あなたの悩みに合わせた個別レッスン動画が届きます
  • ちなみに私もバドミントンコーチとして参加しています🏸
興味のある方はぜひアクセスしてみてください!
※ このアプリも AWS CDK 製です(L1はあまり使いこなせていません)