Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CDK引数設計道場100本ノック

 CDK引数設計道場100本ノック

Avatar for kazuho cryer-shinozuka

kazuho cryer-shinozuka

July 10, 2025
Tweet

More Decks by kazuho cryer-shinozuka

Other Decks in Programming

Transcript

  1. 自作 ニキシー管温湿度気圧計 (シュタゲのダイバージェンスメータです) クライヤー篠塚 一帆 @nixieminton @badmintoncryer @ New Zealand

    (Mount Cook) AWS CDK Top Contributor & Community Reviewer 144 contributions 19 / 1605 contributors AWS Community Builder (Dev Tools) AWS SAPro / IPA SC, ES, NW バドミントンと電子工作が好きです! キャリア20年! 2024年は年代別(30歳以上) 関東ベスト4でした🥉
  2. おさらい https://aws.amazon.com/jp/blogs/aws/boost-your-infrastructure-with-cdk/ CDKアプリケーションの構成 - App - Stack - Construct Constructの構成

    - L1 (Low level) - Cloudformationリソースと1対1対応 - 自動生成 - Cloudformation更新に自動追従 - L2 (High level) - L1を抽象化 (独自の引数を定義) - 型、関数、引数チェック、etc.. を提供 - L3 (Pattern) - 複数のL1, L2を更に抽象化 4
  3. export interface CfnQueueProps { readonly messageRetentionPeriod?: number; readonly fifoQueue?: boolean;

    readonly fifoThroughputLimit?: string; } export class CfnQueue extends cdk.CfnResource { public constructor(scope: Construct, id: string, props: CfnQueueProps = {}) { super(scope, id); this.messageRetentionPeriod = props.messageRetentionPeriod; this.fifoQueue = props.fifoQueue; this.fifoThroughputLimit = props.fifoThroughputLimit; } } Cfnの各propertyがCfnQueuePropsに列挙されている (ただし、型情報は心もとない) L1の基本構成 (SQS Queue) aws-cdk-lib/aws-sqs/lib/sqs.generated.ts (一部改変) AWS::SQS::Queue の Cloudformation document boolean (true / false) ‘perQueue’ or ‘perMessageGroupId’の文字列 5 60-1,209,600の整数 使い方 new sqs.CfnQueue(this, 'Resource', { messageRetentionPeriod: 60, fifoQueue: true, fifoThroughputLimit: ‘perQueue’, });
  4. export interface QueueProps { readonly retentionPeriod?: Duration; readonly fifo?: boolean;

    readonly fifoThroughputLimit?: FifoThroughputLimit; } export class Queue extends QueueBase { constructor(scope: Construct, id: string, props: QueueProps = {}) { const queue = new CfnQueue(this, 'Resource', { messageRetentionPeriod: props.retentionPeriod.toSeconds(), fifoQueue: props.fifo, fifoThroughputLimit: props.fifoThroughputLimit }); } } L2の基本構成 (SQS Queue) aws-cdk-lib/aws-sqs/lib/queue.ts (一部改変) 6 L2用のQueuePropsを定義 (L2独自の型) QueuePropsを受けとり 独自型をL1引数に合わせて変換しながら L1(new CfnQueue())を呼び出す
  5. L2引数とL1引数の関係 export interface QueueProps { readonly retentionPeriod?: Duration; readonly fifo?:

    boolean; readonly fifoThroughputLimit?: FifoThroughputLimit; } L2 (sqs.QueueProps) export interface CfnQueueProps { readonly retentionPeriod?: number; readonly fifoQueue?: boolean; readonly fifoThroughputLimit?: string; } L1 (sqs.CfnQueueProps) L2独自の引数型定義を行うことで使いやすさが向上 QueueProps CfnQueueProps L1 (CfnQueue) L2 (Queue) fifoThroughputLimit (FifoThroughputLimit) fifoQueue (boolean) fifoThroughputLimit (string) fifoQueue (boolean) retentionPeriod (number) 7 retentionPeriod (Duration) そのまま 渡す enum を渡す Duration .toSeconds() を実行
  6. boolean 11 export interface CfnQueueProps { readonly fifoQueue?: boolean; }

    L2 export interface QueueProps { readonly fifoQueue?: boolean; } boolean boolean L1実装 (SQS Queue) const queue = new CfnQueue(this, 'Resource', { fifoQueue: props.fifoQueue, }); L2実装 そのまま渡せばOK L1 new CfnQueue(this, 'Queue', { fifoQueue: true, // FIFOキューを作成 }); L1呼び出し new Queue(this, 'Queue', { fifoQueue: true, // FIFOキューを作成 }); L2呼び出し
  7. L2 L1 データサイズ 12 export interface CfnVolumeProps { readonly size?:

    number; // ボリュームサイズの指定 } export interface VolumeProps { readonly size?: Size; } new CfnVolume(this, 'Resource', { size: props.size?.toGibibytes() }); データサイズを表すnumber Size L1実装 (EBS Volume) L2実装 Size.toXxxbytes()関数で任意の単位への 変換を行ったうえでL1に渡す const volume = new Volume(this, 'Volume', { size: Size.mebibytes(1024), }); L2呼び出し Byte, kiB, MiB, GiB単位などの ファクトリーメソッドで作成可能 const volume = new CfnVolume(this, 'Volume', { size: 1, // 1GiBのボリュームを作成 }); L1呼び出し GiB単位の整数を記述するという制約あり number型では制約として不十分
  8. L2 L1 期間 13 export interface CfnClusterProps { // バックアップ保持期間の指定

    readonly backupRetention?: number; } export interface ClusterProps { readonly backupRetention?: Duration; } new CfnVolume(this, 'Resource', { backupRetention: props.backupRetention?.toDays() }); 期間を表すnumber Duration L1実装 (Neptune Cluster) L2実装 Duration.toXxxs()関数で任意の単位への 変換を行ったうえでL1に渡す const volume = new Cluster(this, 'Cluster', { backupRetention: Duration.hours(48), }); L2呼び出し ミリ秒, 秒, 分, 時間, 日単位での ファクトリーメソッドで作成可能 const volume = new CfnCluster(this, 'Cluster', { backupRetention: 1, // 1日 }); L1呼び出し 日数単位の整数を記述するという制約あり number型では制約として不十分
  9. L2 L1 パターンが決まった文字列 14 export interface CfnCertificateProps { readonly validationMethod?:

    string; } export enum ValidationMethod { EMAIL = 'EMAIL', DNS = 'DNS', HTTP = 'HTTP', } export interface CertificationValidationProps { readonly method?: ValidationMethod; } new CfnCertificate(this, 'Resource', { validationMethod: props.method, }); 2 〜 数十種類の文字列 enum L1実装 (ACM Certificate) L2実装 そのまま渡せばOK ACM の証明書検証方法は ‘EMAIL’, ‘DNS’, ‘HTTP’ の3種類の文字列が許容 許容される文字列だけの enumを定義! new Certificate(this, 'Certificate', { method: ValidationMethod.EMAIL, }); L2呼び出し enumで型安全に設定 new CfnCertificate(this, 'Certificate', { method: ’EMAIL’, }); L1呼び出し typoや存在しない検証方法の指定が頻発! string型では制約として不十分
  10. L2 L1 パターンが決まった文字列 2 15 export interface CfnDomainProps { readonly

    instanceType?: string; } export class InstanceType { public static readonly T2_XLARGE = InstanceType.of('ml.t2.xlarge'); public static of(version: string) { return new InstanceType(version); } private constructor(public readonly instanceType: string) { } } new CfnDomain(this, 'Resource', { elasticSearchVersion: props.instanceType.instanceType, }); 2 〜 数十種類の文字列 (頻繁に増加する) enum-like class L1実装 (OpenSearch Domain) L2実装 .of()で任意の値を持った インスタンスを作成可能 new Domain(this, 'Domain', { version: InstanceType.T2_XLARGE,// 推奨 version: InstanceType.of(‘ml.t2.xlarge’), // これでもOK }); L2呼び出し 未実装のインスタンスタイプであっても InstanceType.of()で対応可能 OpenSearchのインスタンスタイプは随時追加される string型では制約として不十分 enumだと更新に追いつけない
  11. L2 L2がもつattributeの文字列 16 export interface VolumeProps { readonly encryptionKey?: kms.IKey;

    } new CfnVolume(this, 'Resource', { kmsKeyId: props.encryptionKey?.kmsKeyArn, }); L2コンストラクトのInterface L2実装 kms.Key L2コンストラクトの Interfaceであるkms.IKeyを受け取る declare const kmsKey: kms.IKey; new Domain(this, 'Domain', { kmsKey, }); L2呼び出し IKeyなので、KmsKey.fromAttributes()などで importしたKey Constructも渡すことができる IConstructのもつattributeに アクセスして L1へ渡す L1 export interface CfnVolumeProps { // ボリュームを暗号化するCMK情報 readonly kmsKeyId?: string; } ARNなどの文字列 L1実装 (EC2 EBS Volume) L1呼び出し new Domain(this, 'Domain', { kmsKeyId: ‘arn:aws:kms:ap-northeast-1:12345 6789012:key/abcd1234-a1b2-3c4d-5e6f-7890abcdef12’, }); ARNのtypoが頻発! string型では制約として不十分
  12. L2 L1 アカウント, リージョン 17 export interface FleetProps { readonly

    peerVpc?: ec2.IVpc; } new CfnFleet(this, 'Resource', { // Interfaceのもつattributeにアクセス peerVpcId: props.peerVpc?.vpcId, peerVpcAwsAccountId: props.peerVpc?.env.account, peerVpcRegionId: props.peerVpc?.env.region, }); アカウント, リージョンを表す文字列 L2コンストラクトのInterface L2実装 ec2.Vpc L2コンストラクトの Interfaceであるec2.IVpcを受け取る declare const vpc: ec2.IVpc; new Fleet(this, 'Fleet', { peerVpc: vpc, }); L2呼び出し Interface(IResource)がもつ ResourceEnvironment(env)情報にアクセス export interface CfnFleetProps { // peeringするVPCのID readonly peerVpcId?: string; // peeringするVPCを持つアカウント ID readonly peerVpcAwsAccountId?: string; } L1実装 (GameLift Fleet) new CfnFleet(this, 'Fleet', { peerVpcId: 'vpc-0abc123de456fgh78', peerVpcAwsAccountId: '123456789012', }); L1呼び出し クロスアカウント連携設定時に (i) リソースの識別情報 (ii) (i)を持つアカウント情報 を同時に設定することがある VPC IDやアカウント番号の typoが頻発! string型では制約として不十分
  13. L2 L1 失効日時 18 export interface CfnApiKeyProps { readonly expires?:

    number; } export interface ApiKeyConfig { readonly expires?: Expiration; } new CfnFleet(this, 'Resource', { expires: props.expires.toEpoch(), }); (失効日時を表す) unixtime Expiration L1実装 (AppSync API Key) L2実装 Expirationを受け取る new ApiKey(this, 'ApiKey', { expires: Expiration.after(Duration.days(90)), }); L2呼び出し Expiration.toEpoch()でunixtimeに変換 new CfnApiKey(this, 'ApiKey', { expires: 1751328000, }); L1呼び出し 現時点から90日後のunixtimeを設定 unixtimeの計算ミスが頻発! いつ失効するのかも分かりづらい
  14. L2 L1 シークレット 19 export interface CfnClusterProps { readonly masterUserPassword?:

    string; } export interface ApiKeyConfig { readonly masterUserPassword?: SecretValue } new CfnCluster(this, 'Resource', { masterUserPassword: props.masterUserPassword?.unsafeUnwrap() }) (パスワードなどの)文字列 SecretValue L1実装 (Redshift Cluster Master Password) L2実装 SecretValueを受け取る new Cluster(this, 'Cluster', { masterUserPassword: SecretValue.secretsManager(‘ARN’), // SecretsManager SecretValue.ssmSecure(‘ParameterName’), // ParameterStore }); L2呼び出し SecretValue.unsafeUnwrap()で文字列に変 換 パスワードは必ずSecresManager, ParameterStore (SecureString) 経由でSecretValueに渡す!!!!! Cloudformationの動的参照で解決されるので、パスワードが Cfnテンプレートに現れない 平文の記述は好ましくない new CfnCluster(this, 'Cluster', { masterUserPassword: ’unsafe-plain-password’, }); L1呼び出し
  15. L2 L1 タイムゾーン 20 export interface ScheduledActionProperty { readonly timezone?:

    string; } export interface ScalingSchedule { readonly timeZone?: TimeZone; } new CfnScalableTarget(this, 'Resource', { timezone: props.timeZone?.timezoneName, }); タイムゾーンを表す文字列 TimeZone L1実装 (Application Auto Scaling Action) L2実装 TimeZoneを受け取る new ScalableTarget(this, 'Target', { timezone: TimeZone.ASIA_TOKYO, }); L2呼び出し TimeZone.timezoneNameでタイムゾーンを取得 new CfnScalableTarget(this, 'Target', { timezone: ‘Asia/Tokyo’, }); L1呼び出し staticメソッドでタイムゾーンを指定 TimeZoneのtypoが頻発
  16. L2 L1 サブネット情報 21 export interface CfnServerlessClusterProperty { readonly subnetIds?:

    string[]; } export interface ServerlessClusterProps { readonly vpcSubnets?: SubnetSelection; readonly vpc?: IVpc; } new CfnServerlessCluster(this, 'Resource', { subnetIds: props.vpc.selectSubnets(props.vpcSubnets).subnetIds }); サブネットID (文字列) IVpc & SubnetSelection L1実装 (MSK Serverless Cluster) L2実装 new ServerlessCluster(this, 'Cluster', { vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS }, vpc, }); L2呼び出し IVpcとSubnetSelection を受け取る new CfnServerlessCluster(this, 'Resource', { subnetIds: [ ‘subnet-0123456789abcdef0’, ’subnet-abcdef0123456789a’, ], }); L1呼び出し IVpc.selectSubnets(SubnetSelection)で 条件を満たす subnet情報を取得 タイプ, AZ, 名前, onePerAz, etc… 多様な条件でのサブネットフィルタが可能 サブネット IDの調査が面倒 IDのtypoも頻発
  17. L2 L1 ここまで紹介されていない文字列 22 export interface CfnClusterSubnetGroupProps { readonly description:

    string; } 文字列 文字列 L1実装 (Redshift Cluster Subnet Group) L2実装 L2呼び出し new CfnClusterSubnetGroup(this, 'SubnetGroup', { description: ‘my subnet group’, }); L1呼び出し export interface ClusterSubnetGroupProps { readonly description: string; } new ClusterSubnetGroup(this, 'SubnetGroup', { description: ‘my subnet group’, }); 型の恩恵にあずかるべく、 可能な限り string型の引数は回避する
  18. stringが好ましいケース 23 渡したい情報をL1コンストラクトがattributeとして持つ場合があるが、 L1を引数とするより文字列を渡す方が好ましい(気がする)(要出典) interface GlueStartCrawlerRunOptions { readonly crawler: glue.CfnCrawler;

    } const crawler = new glue.CfnCrawler(this, 'Crawler'); const crawlerTask =  new tasks.GlueStartCrawlerRun(this, 'GlueCrawlerTask', { crawler: crawler, }); こういった実装も考えられる L1コンストラクトを受け取る CfnCrawlerをそのまま渡せる interface GlueStartCrawlerRunOptions { readonly crawlerName: string; } L2実装 (Stepfunctions glue-start-crawler-job) L2呼び出し const crawler = new glue.CfnCrawler(this, 'Crawler'); const crawlerTask =  new tasks.GlueStartCrawlerRun(this, 'GlueCrawlerTask', { crawlerName: crawler.ref, }); CfnCrawlerのattributeからクローラ名を取得 CrawlerのL2コンストラクトが存在しないため、 ICrawler(Interface)を引数に取ることができない
  19. L2 L1 複数引数をまとめたオブジェクト 26 export interface DatabaseClusterProperty { readonly serverlessV2ScalingConfiguration?:

    { readonly minCapacity?: number, readonly maxCapacity?: number, }; } Interface Interface or 個別引数定義 L1実装 (RDS Database Cluster) L2実装 L2呼び出し new CfnDBCluster(this, 'Cluster', { serverlessV2ScalingConfiguration: { minCapacity: 0, maxCapacity: 2, }, }); L1呼び出し export interface DatabaseClusterProps { readonly serverlessV2ScalingConfiguration?: { readonly minCapacity?: number, readonly maxCapacity?: number, }; } new Cluster(this, 'Cluster', { serverlessV2ScalingConfiguration: { serverlessV2MinCapacity: 0, serverlessV2MaxCapacity: 2, }, }); L1と同じInterface
  20. L2 L1 複数引数をまとめたオブジェクト 27 export interface DatabaseClusterProperty { readonly serverlessV2ScalingConfiguration?:

    { readonly minCapacity?: number, readonly maxCapacity?: number, }; } Interface Interface or 個別引数定義 L1実装 (RDS Database Cluster) L2実装 L2呼び出し new CfnDBCluster(this, 'Cluster', { serverlessV2ScalingConfiguration: { minCapacity: 0, maxCapacity: 2, }, }); L1呼び出し export interface DatabaseClusterProps { readonly serverlessV2MinCapacity?: number; readonly serverlessV2MaxCapacity?: number; } new Cluster(this, 'Cluster', { serverlessV2MinCapacity: 0, serverlessV2MaxCapacity: 2, }); ネストが解消されている
  21. L2 L1 密接に関連した複数の引数 29 export interface ReplicationConfiguration { readonly fileSystemId:

    string, readonly region: string, readonly availabilityZoneName: string, } 独立した個別引数 L1踏襲 or クラス定義 L1実装 (EFS FileSystem Replication Configuration) L1呼び出し レプリケーション タイプ fileSystemId region availability ZoneName リージョナル undefined リージョン名 undefined ワンゾーン undefined リージョン名 AZ名 既存FileSystem 既存のファイル システムID undefined undefined 各引数の関係性 // リージョナル const regionalConfig = { region: 'ap-northeast-1' }; // ワンゾーン const oneZoneConfig = { region: 'ap-northeast-1', availabilityZoneName: 'ap-northeast-1a' }; // 既存FileSystem const existingConfig = { fileSystemId: 'file-system-id' }; ユーザが作成する必要のある引数オブジェクト 使いやすいものであるとは言えなさそう ...
  22. L2 L1 密接に関連した複数の引数 30 独立した個別引数 L1踏襲 or クラス定義 export class

    ReplicationConfiguration { public static existingFileSystem( destinationFileSystem: IFileSystem, ): ReplicationConfiguration { return new ReplicationConfiguration({ destinationFileSystem }); } public static regionalFileSystem( region?: string, ): ReplicationConfiguration { return new ReplicationConfiguration({ region }); } public static oneZoneFileSystem( region: string, availabilityZone: string, ): ReplicationConfiguration { return new ReplicationConfiguration({ region, availabilityZone }); } public readonly destinationFileSystem?: IFileSystem; public readonly region?: string; public readonly availabilityZone?: string; private constructor(options: ReplicationConfigurationProps) { this.destinationFileSystem = options.destinationFileSystem; this.region = options.region; this.availabilityZone = options.availabilityZone; } } // Regional const regionalCofig = // 自動的に複製元ファイルシステムと同一リージョンに設定 efs.ReplicationConfiguration.regionalFileSystem(); // One Zone const oneZoneCofig = efs.ReplicationConfiguration.oneZoneFileSystem(    'us-east-1', // リージョン名    'us-east-1a', // AZ名  ); // 既存ファイルシステム declare const destinationFileSystem: efs.FileSystem; const userManagedConfig = efs.ReplicationConfiguration.existingFileSystem( destinationFileSystem, ); 直感的なファクトリーメソッドで設定可能
  23. まとめ 32 どんな引数設計を行えばよいのか? - string型は可能な限り避けよう! - 意図しない文字列を渡してしまい、デプロイエラーに繋がりやすくなります - ✕ ‘arn:aws:iam::123456789012:role/IamRoleName

    ‘ - ◯ iam.IRoleを渡し、L2内でIRole.roleArnを呼び出す - number型もDuration, Sizeあたりを使えるケースは多いです 結局は状況次第 - 使い勝手とカスタマイズ性は表裏一体 - ベストな定義よりもベターな定義を目指そう - L1より僅かでも使いやすくなっていれば十分だと思います カスタムコンストラクト作成や CDKコントリビュートに役立てて頂けると嬉しいです