いつかまたパタゴニアに

主にソフトウェア開発周りで気づいたことなどをまとめています

独断と偏見で2025年のCDKアップデートを振り返る

はじめに

この記事はAWS Community Builders アドベントカレンダー 23日目の記事です。

qiita.com

あっという間に2025年も終わりが近づいてきました。 この1年間にAWS CDKにもたくさんのアップデートがありましたが、独断と偏見で記憶に残ったものをピックアップしていきたいと思います。

これを読めばCDKのメジャーアップデートをおさらい...はできません。あしからず。

機能追加編

VPC Origins by @Tietew

CloudfrontのVPC Origin設定を行うL2コンストラクトの追加です。

とんでもなくニーズのある機能追加である点はもちろんのこと、コントリビュータ目線としては「コミュニティ(AWS CDKのメンテナ以外)からのRFCを経由しないL2コンストラクト作成」がスルッとマージされたプルリクという点でも印象的でした。これ以前はalphaモジュール以外に対してL2新規作成プルリクを発行しても「RFCを介してね」とメンテナに諭されてcloseされるケースが主でした。

修正自体もかなりの規模感であることに加え、VPC Origin作成時に自動生成されるセキュリティグループをカスタムリソースで経由で参照する方法などもドキュメント整備されており、非常にハイクオリティなPRだと感じます。

github.com

TietewさんもTokyo在住の方らしいのですが、一体どんな方なのか全くわかっていません。個人的にはこちらも大変気になっています。

StepfunctionsでのJSONata対応 by @winteryukky

StepfunctionsでJSONataを使えるようにしたPRです。 直感的なファクトリーメソッドでJSONata, JSONPathを使い分けられるインターフェースがとても素敵です。

// For JSONPath
sfn.Pass.jsonPath(stack, "JSONPathPass", {
    outputPath: sfn.JsonPath.stringAt('$foo'),
});

// For JSONata
sfn.Pass.jsonata(stack, "JSONataPass", {
    outputs: {
        count: "{% $states.input.count + 1 %}"
    },
});

github.com

S3レプリケーション by @badmintoncryer

S3バケットレプリケーション設定が行えるようになりました。2025個人的に一番頑張った機能追加です。

cloudformationのS3レプリケーションパラメータに関しては、同一の設定を異なる3箇所で設定可能になっているなどカオスの極みとなっており、そのあたりの仕様整理が最も苦労したポイントでした。

github.com

詳細は別途まとめているのでこちらをご参照ください。

nixieminton.hatenablog.com

特定のScopeに対するRemovalPolicy一括適用 by @watany-dev

あるConstructStack配下の全リソースに対して、削除時の取り扱い(RemovalPolicy)を一括適用する大変素敵なクラス実装です。 以前は自作Aspectsによるおまじないで頑張って実現する必要がありましたが、その必要が完全になくなり一気に使いやすくなりました。

import { RemovalPolicies, MissingRemovalPolicies, Aspects } from 'aws-cdk-lib';

// scope配下の全リソースにDESTROYポリシーを適用
RemovalPolicies.of(scope).destroy();

// 同じことを実現する自作Aspects
// もう二度と使うことはないでしょう。お世話になりました。
Aspects.of(scope).add({
  visit(node: constructs.IConstruct) {
    if (cdk.CfnResource.isCfnResource(node)) {
      node.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
    }
  },
});

こういったcoreへの重要な機能追加PRにはkaizenさんなどCDKチームの重鎮がレビュワーに現れるのも印象的でした。

github.com

API Gateway(REST API)のストリーミングレスポンス対応 by @go-to-k

LLMを用いたアプリケーションでかなりニーズの高い機能ですが、これがAPI Gatewayでも実現できるようになりました。 機能リリースからPRマージまで1週間足らずの超高速コントリビュートでした。

github.com

NLBのプライベートIP指定 by @badmintoncryer

NLBに固定IPをアサインするための引数を追加しています。業務上かなり使うことが多いので、個人的助かったPR第二位にランクインしています。(第一位はRemovalPolicies.of(scope))

github.com

他にも設置するENIに対して色々設定できるので、詳細は別途記事にまとめています

nixieminton.hatenablog.com

NLBのSG設定 by @badmintoncryer

2023年、ついに全人類が待ち望んでいたNLBのセキュリティグループ対応がリリースされました。もちろんCDKもすぐに設定可能となったのですが、CDKでは破壊的変更を嫌うためデフォルトでは旧来のSG無しNLBが作成されてしまう状況が続いていました。

その点がどうしても気になっていたので、機能フラグ導入により破壊的変更を回避しながらデフォルトでSGありNLBが作成されるようにしました。 このあたりの詳細は別記事にまとめています。

今後も破壊的変更回避のために残っているイケてないCDK仕様はどんどん置き換えていきたいと思っています。

github.com

Private API Gateway リソースポリシー設定 by @badmintoncryer

PrivateなAPI Gatewayを稀に作りたくなるのですが、都度都度リソースポリシー設定をベタ書きしないといけないことに苛立ちを感じていました。 そこで、面倒なポリシー定義を1行で行う便利関数を追加したPRです。

declare const apiGwVpcEndpoint: ec2.IVpcEndpoint;

// 従来の設定方法
new apigateway.RestApi(this, 'PrivateRestApi', {
      endpointTypes: [apigateway.EndpointType.PRIVATE],
      handler: fn,
      // 重厚長大なポリシー定義が必要
      policy: new iam.PolicyDocument({
        statements: [
          new iam.PolicyStatement({
            principals: [new iam.AnyPrincipal],
            actions: ['execute-api:Invoke'],
            resources: ['execute-api:/*'],
            effect: iam.Effect.DENY,
            conditions: {
              StringNotEquals: {
                "aws:SourceVpce": vpcEndpoint.vpcEndpointId
              }
            }
          }),
          new iam.PolicyStatement({
            principals: [new iam.AnyPrincipal],
            actions: ['execute-api:Invoke'],
            resources: ['execute-api:/*'],
            effect: iam.Effect.ALLOW
          })
        ]
      })
    })

// 新しい設定方法
const api = new apigateway.RestApi(this, 'PrivateApi', {
  endpointConfiguration: {
    types: [ apigateway.EndpointType.PRIVATE ],
    vpcEndpoints: [ apiGwVpcEndpoint ]
  }
});
// リソースポリシー設定をgrant関数で実現
api.grantInvokeFromVpcEndpointsOnly([apiGwVpcEndpoint]);

github.com

こちらのPRは後藤さんにレビューいただいたものですが、 その内容を題材にしたLazyに関するブログがまとめられていますので、こちらも合わせてご覧ください。

go-to-k.hatenablog.com

Context Provider

既存リソースをCDKに取り込む方法の1つとしてlookup関数があります。lookup関数はCDK CLI上でcloud control APIなどを実行し、取得した情報をもとにConstruct.fromAttributes()でリソースのインポートを行います。今まではlookup関数の実装には独自Context Provider実装が必要でかなり大変でしたが、以下の機能追加によりCloud Control APIを用いた実装が非常に簡単に行えるようになりました。

github.com

実際の応用例を見てみましょう。

Ex.1 Aurora Clusterのlookup by @mazyu36

IaCと相性が悪いサービスランキング上位常連のRDSをlookupでimportする素敵なPRです。 RDS/AuroraをIaCで構築するかは判断が分かれるところですが、この機能追加によりIaC管理しない方針が取りやすくなったはずです。

github.com

Ex2. Prefixlistのlookup by @Tietew

既存のPrefix Listをlookupで取り込めます。

Gateway Endpoint経由でのS3宛通信を許可したいときに、0.0.0.0/0へのOutbound許可を避けるためにS3のPrefix Listを宛先として設定することが多々あると思います。そんなときに既存のS3Prefix Listをlookupすることで、IConnectable経由でのSecurity Groupの穴あけが行いやすくなってるはずです! (PR発行時にこのユースケースで役立つと思って喜んでたのですが、記事を書く今の今まで忘れていました。今度使ってみます。)

github.com

Ex3. ECR Repositoryのlookup by @badmintoncryer

複数環境で共通のコンテナイメージを使いたいときにECRだけは共通化したかったりするので、そんなときに役立つかな〜と思っています。

github.com

Enhanced L1

昨年から今年にかけて、CDKではL1コンストラクトの機能拡張を図る動きが活発化していました。一時はEnhanced L1として議論も進んでいましたが、大規模な破壊的変更が不可避であり、CDK v3として導入を検討ということで一旦はペンディング状態となった印象でした。

github.com

そんな流れを汲んでいるのかわかりませんが、最近はCDKメンテナからL1自体の機能増強を図るPRがいくつか発行されています。

grants

従来grant系関数はL2コンストラクト上で実装していましたが、L1でXxxGrantsクラスも自動生成されるようになりました。 今後はこのXxxGrantsクラスを用いたgrant関数の実装が推奨されるようです。

  // 今までの書き方
  public grantRead(grantee: iam.IGrantable): iam.Grant {
    return iam.Grant.addToPrincipal({
      grantee,
      actions: readPermissions,
      resourceArns: [this.keyArn],
    });
  }

  // 今後推奨される書き方
  import { ApiKeyGrants } from './apigateway-grants.generated';

  // grantsオブジェクトをL1から取得
  public readonly grants = ApiKeyGrants._fromApiKey(this);

  // grantsオブジェクト経由でgrantを実行
  public grantRead(grantee: iam.IGrantable): iam.Grant {
    return this.grants.read(grantee);
  }

github.com

上記Enhanced L1のRFCにて「IAM grantsなども自動生成できると良さそう」という議論もされており、それを反映させた機能追加だと思っています。

While the group agreed that this is not currently part of CFN, it would be very useful if grants can be auto-generated from a data source.

L1の引数にコンストラクトを取る

今までstringしか渡せませんでしたが、L1やL2コンストラクトをL1の引数に渡せるようになりました。 これによりGo言語版のCDKでL1の引数がanyになってしまいますが、それは許容しているようです。

github.com

弊害?としては、既存L2に対してIXxxRefをimplementsさせるPRが必要になることです。既にL2はIXxxRefをimplementsした構造を持っているためTypeScript的に静的解析エラーは発生しませんが、あるべき形に持っていく修正です。 こういったちまいPRが発行され続けていますが、これ既存のL2全部にやるんでしょうか....

github.com

ABC順に全てのコンストラクトにIXxxRef対応を進めるメンテナ

こちらもRFC中で「L2がL1のInterfaceを継承するようにすることで、コンストラクトを引数に取るケースでL1,L2両方を渡すことができる」という議論があったので、その系譜を継いでいるものかと思っています。

github.com

Enhanced L1関連の話は私見も多く混じっているので、実際はこうだよ〜という点があればぜひ教えてください!

番外編

Happy New Year

CDKのライセンスファイルの年号を新年に切り替える恒例のPRです。2025年は@mazyu36 が発行していました。

github.com

今年の年末年始も誰かがやる必要があります。ここまで読んでいただいたあなた、新年の景気づけに1発PR発行してみませんか?!

20260102補足

今年は@matoom-nomuさんに発行いただけました!from日本3年連続かな?ありがとうございます!!

github.com

最後に

以上、個人的に気になったCDKアップデートまとめでした。

(自分のPRをやたらと引っ張っているのもありますが)今回紹介したPRのうちEnhanced L1系以外は全て日本からのコントリビュートです。 日本のCDKコントリビュートの勢いは大変凄まじく、ここ数年ではCDKメンテナを除いた貢献国ランキングはダントツ1位になっているはずです。

(ちなみに2024年にはmazyuさんが「CDKメンテナを含めた人類の年間commit数第1位」という超記録を打ち立てていました。もはや敵がdependabotとかしかいません)

私個人としては2023年からCDKへのコントリビュートを始め、なんだかんだ2年強コンスタントにPR発行を続けることができました。 2025年はAWS社員を除いた(mazyuさんを合法的に取り除ける)コントリビュート数としては世界1位になり、そのあたりを色々アピールしてたらAPJC community leaders awards 2025に選んで頂けたのがハイライトでした。

世界一かわいいトロフィー

また、別の趣味であるバドミントンでも長年目標としていた年代別全国ベスト16に入賞したり、1歳の息子が我が家主催"世界一かわいいで賞"を満場一致で受賞したりと、大変満足感のある状態で年の瀬@グアムを迎えております。 来年もさらなる飛躍を目標としつつ、自分がやりたいことをマイペースに頑張りたいと思います。

乾季のグアムで大雨ツモ

今年1年関わっていただいた皆様、本当にありがとうございました。来年も引き続きよろしくお願いいたします! メリークリスマス!!

CDKでの機能フラグ導入方法

この記事はCDKアドベントカレンダー2025 23日目の記事です。

qiita.com

概要

CDKで破壊的変更を導入する際には機能フラグの実装が義務付けられています。

あまり機能フラグに着目した日本語の情報が無いと思い記事を作成しました。目的は以下の2つです。

  • コントリビュータ向けに実装方法をまとめる
  • 「そもそも機能フラグって何..??」というCDKユーザに向けて役割などをお伝えする

後者の読者の方は実装手順のセクションは読み飛ばして頂ければと思います。

イントロ

AWS Network Load Balancer(NLB)では2023年に待望のセキュリティグループ(SG)対応が導入されました。

大半のNLBユーザが切望していた機能で、これによりレガシーな「SG無しNLB」を使うメリットは存在しなくなりました。

dev.classmethod.jp

AWS CDKにおいてもリリース直後にSG設定が可能となりましたが、破壊的変更を避けるため、デフォルトでは従来のSGなしNLBが作成される状況でした。 しかし一度SGなしNLBを作成してしまうと、後からSG設定を追加することは不可能です。(NLBが置換されます!)

declare const vpc: ec2.IVpc;

// SGなしNLBが作成される
new elbv2.NetworkLoadBalancer(this, 'Nlb', {
  vpc,
});

// SGありNLBが作成される
new elbv2.NetworkLoadBalancer(this, 'Nlb', {
  vpc,
  securityGroups: [sg]イチイチSG指定するのは面倒
});

// 望ましい姿: デフォルトでSG付きNLBが作成される
new elbv2.NetworkLoadBalancer(this, 'Nlb', {
  vpc,
  // securityGroups: [sg] ← 明示的なSG設定を行わない
});

この状況を改善するため、機能フラグを利用して暗黙的なSG有効化設定をデフォルトの挙動とするPRを発行しました。マージまでに半年ほどかかりましたが、v2.221.1から無事にリリースされています。

ここからはPR作成過程を題材にCDKにおける機能フラグ実装のノウハウをまとめていきます。

github.com

機能フラグとは

CDKにおける機能フラグとは、破壊的変更を段階的に導入するための仕組みです。新規プロジェクトでは新しい動作がデフォルトとなりますが、既存プロジェクトでは従来の動作が維持され、明示的にフラグを有効化することで新機能を利用できます。

詳しい解説は既存の素敵な記事に委ねます。これ以上のことは書けません。

www.ogis-ri.co.jp

既存実装

まずは修正前の実装を振り返ります。

export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoadBalancer {
  ...
  this.connections = new ec2.Connections({
    securityGroups: props.securityGroups,
  });
  ...
}

一見分かりづらいコードですが、、、

NetworkLoadBalancerクラスはINetworkLoadBalancer経由でIConnectableを継承しており、connections経由でセキュリティグループへの穴あけを実行できます。以下のようなallowTo/From()にお世話になっている方は多いのではないでしょうか?これを実現しているのがIConnectableインターフェースです。

declare const instance: ec2.Instance;

const nlb = new elbv2.NetworkLoadBalancer(this, 'Nlb');
nlb.connections.allowTo(instance, ec2.Port.tcp(8080));

IConnectableを継承したクラスでは、this.connectionsにセキュリティグループを引数としたec2.Connectionsインスタンスを格納する必要があります。これが先ほどのCDK実装の正体です。

  this.connections = new ec2.Connections({
    securityGroups: props.securityGroups,
  });

ここで、ec2.ConnectionssecurityGroupsundefinedを渡すと、セキュリティグループは新規作成されず未設定状態となり、セキュリティグループがアタッチされないレガシーNLBが作成されます。 つまり、既存実装では明示的にprops.securityGroupsにセキュリティグループを渡さない限り、セキュリティグループ設定がなされたNLBが作成できない状況でした。

安直な修正方針

デフォルトでセキュリティグループを作成するようにするにはどうすればよいでしょうか? 最も単純なアプローチは、props.securityGroupsundefinedならばL2コンストラクト内でセキュリティグループを作ってしまう方法です。

export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoadBalancer {
  ...
  this.connections = new ec2.Connections({
    // props.securityGroupsがundefinedなら新規にSGを作成
    securityGroups: props.securityGroups ?? new ec2.SecurityGroup({...}),
  });
  ...
}

しかしこの実装を実際にリリースした場合、大変大きな問題が発生します。

例えば、あなたが既にSGなしのレガシーNLBをCDKで作成していたとしましょう。この状態でaws-cdk-libをアップグレードしてデプロイすると、全てのNLBはデフォルトでSG設定されたNLBに変更されるため、既存レガシーNLBが全て再作成されてしまいます。NLB再作成はアクセスURLのFQDN及びIP変更を伴いますので、稼働済みシステムが軒並み停止しうる阿鼻叫喚の嵐になることは想像に難くありません。

この問題を回避するため、機能フラグを実装することで、

  • (i)既存ユーザには変更が起こらない
  • (ii)新規ユーザにはセキュリティグループ設定されたNLBを提供する

という一石二鳥を図ってみましょう。

実装手順

1. 機能フラグの定義

まずはpackages/aws-cdk-lib/cx-api/lib/features.tsにフラグを追加します。このファイルには機能フラグの一覧が定数として定義されています。

export const NETWORK_LOAD_BALANCER_WITH_SECURITY_GROUP_BY_DEFAULT = '@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault';

機能フラグの名前は、そのフラグによりどんな変化が起こるかをつらつらとlowerCamelCaseで書き連ねればOKです。 @aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptionsなどなど長さは気にせず定義してしまいましょう。

合わせてfeatures.tsFLAGSにその機能フラグの定義を追記します。最下部に追記しないとrosettaでエラーとなるはずです (未検証)

export const FLAGS: Record<string, FlagInfo> = {

  ...(既存機能フラグの定義),

  //////////////////////////////////////////////////////////////////////
  [NETWORK_LOAD_BALANCER_WITH_SECURITY_GROUP_BY_DEFAULT]: {
    // ApiDefault ,BugFix, VisibleContext, Temporaryから選択。ほぼほぼApiDefault, BugFixのいずれかのはず。
    type: FlagType.ApiDefault,
    // 要旨
    summary: 'When enabled, Network Load Balancer will be created with a security group by default.',
    // 詳細説明
    detailsMd: `
      When this feature flag is enabled, Network Load Balancer will be created with a security group by default.
    `,
    // いつから導入されたか。V2NEXTでOK。リリース時にsedで置換されます。
    introducedIn: { v2: 'V2NEXT' },
    // 推奨値
    // 新規プロジェクト構築(cdk init)時にはこの値がcdk.jsonに設定される
    recommendedValue: true,
    // cdk.jsonに値が未設定時のデフォルト値
    // 既存ユーザがaws-cdk-libアップグレードしたときはこの値が用いられるため、既存の挙動を再現する値を設定する
    // 未設定の場合falseとなる。
    unconfiguredBehavesLike: { v2: false },
    // 従来の挙動を実現する方法
    compatibilityWithOldBehaviorMd: 'Disable the feature flag to create Network Load Balancer without a security group by default.',
  },

機能フラグはtrueが新しい挙動、falseが従来の挙動をするようにすることが推奨されています。 この方針に則る場合はrecommendedValue: trueのみを設定し、unconfiguredBehavesLikeは未定義で問題ありません。

github.com

(なぞの///////によるセクション区切りに一抹のクセを感じます)

2. 機能フラグのドキュメント追加

ここは正しい記述方法が未だに分からないところです。

まず、ドキュメントの記述先としてはREADME.mdまたはFEATURE_FLAGS.mdの2選択肢があり、公式ドキュメントでは前者が紹介されていますが、マージ実績では後者が大半となっています。時には両方に記載しているPRもあります。

更にFEATURE_FLAGS.mdは記述先のファイル候補が2つあり、どちらに書けば良いのかわかりません。

後述しますが、packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.mdのみの修正でのマージ実績が複数ありますので、きっとそれで十分なのだと捉えています。

README.mdを修正する場合

packages/aws-cdk-lib/cx-api/README.mdに追記します。

* `@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault`

When this feature flag is enabled, Network Load Balancer will be created with a security group by default.

_cdk.json_

{
  "context": {
    "@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault": true
  }
}

FEATURE_FLAGS.mdを修正する場合

packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.mdまたはpackages/@aws-cdk/cx-api/FEATURE_FLAGS.mdに追記します。 個人的には前者のみに記載しています。

まずは一覧テーブルに概要を追加します。

| Flag | Summary | Since | Type |
| ----- | ----- | ----- | ----- |
| [@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault](#aws-cdkaws-elasticloadbalancingv2networkloadbalancerwithsecuritygroupbydefault) | When enabled, Network Load Balancer will be created with a security group by default. | V2NEXT | new default |

次にcdk.jsonでの記載例一覧に推奨値を追記します。

{
  "context": {
    ... (既存フラグ一覧),
    "@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault": true,
}

最後に機能フラグのサマリを追記します。

### @aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault

*When enabled, Network Load Balancer will be created with a security group by default.*

Flag type: New default behavior

When this feature flag is enabled, Network Load Balancer will be created with a security group by default.


| Since | Unset behaves like | Recommended value |
| ----- | ----- | ----- |
| (not in v1) |  |  |
| V2NEXT | `false` | `true` |

**Compatibility with old behavior:** Disable the feature flag to create Network Load Balancer without a security group by default.

Sinceにはどのバージョンから導入されたかを記載しますが、PR時点ではV2NEXTと記載すればOKです。リリース時に該当バージョンに置換されます。

3. コンストラクト内での実装

ここが本丸です。実際のL2コンストラクト内で機能フラグをチェックし、値に応じて動作を分岐させます。 trueなら新しい挙動、falseなら従来通りの挙動を行うようにさせましょう。

import { FeatureFlags } from '../../core';

constructor(scope: Construct, id: string, props: NetworkLoadBalancerProps) {
  // このCDK Appに設定された機能フラグを取得
  const enableDefaultSg = FeatureFlags.of(this).isEnabled(
    cxapi.NETWORK_LOAD_BALANCER_WITH_SECURITY_GROUP_BY_DEFAULT
  );

  // フラグが有効ならデフォルトでセキュリティグループを作成 (新しい挙動)
  if (enableDefaultSg) {
    this.connections = new ec2.Connections({
      securityGroups: props.securityGroups ?? new ec2.SecurityGroup({...}),
    });
  // フラグが無効なら従来通りの挙動
  } else {
    this.connections = new ec2.Connections({
      securityGroups: props.securityGroups,
    });
  }
}

実装自体は非常にシンプルですね。

4. 単体テストの作成

機能フラグのtrue/false時それぞれにおけるcloudformationテンプレートの内容をテストします。

一般的なケースでは機能フラグ無効化時(既存の挙動)での単体テスト実装済みのはずなので、機能フラグを有効化した状態での単体テストを追加します。

      test('creates NLB with auto-generated security group', () => {
        app = new cdk.App({
          postCliContext: { // ここでAppに機能フラグを注入する
            '@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault': true,
          },
        
         // GIVEN
        // 機能フラグを有効化したApp配下のStackとして定義
        const stack = new cdk.Stack(app);
        const vpc = new ec2.Vpc(stack, 'Stack');

        // WHEN
        new elbv2.NetworkLoadBalancer(stack, 'LB', {
          vpc,
          internetFacing: true,
        });

        // THEN
        const template = Template.fromStack(stack);
      template.hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', {
          Scheme: 'internet-facing',
          SecurityGroups: [
            {
              'Fn::GetAtt': [
                Match.stringLikeRegexp('LBSecurityGroup.*'),
                'GroupId',
              ],
            },
          ],
          Type: 'network',
        });

        template.hasResourceProperties('AWS::EC2::SecurityGroup', {
          GroupDescription: Match.stringLikeRegexp('Automatically created Security Group for ELB.*'),
          VpcId: { Ref: Match.stringLikeRegexp('Stack.*') },
          SecurityGroupEgress: [{
            CidrIp: '255.255.255.255/32',
            Description: 'Disallow all traffic',
            FromPort: 252,
            IpProtocol: 'icmp',
            ToPort: 86,
          }],
        });
      });

機能フラグを有効化することで、無事にデフォルトでNLBにセキュリティグループが設定されていることが確認できました。

6. 統合テスト(integ test)の作成

実際のデプロイ動作を確認するため、integ testも作成します。 単体テスト同様、大抵のケースでは機能フラグが無効状態(すなわち従来の挙動)でのinteg testは存在しますので、機能フラグを有効化した状態でのinteg testを追加します。

const app = new App({
  // CDK App作成時にcontextとして機能フラグを設定する
  postCliContext: {
    '@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault': true, // 機能フラグを有効化
  },
});
const stack = new Stack(app, 'NetworkLoadBalancerSecurityGroupFlagStack');

new NetworkLoadBalancer(stack, 'NLB', {
  vpc: vpc,
  internetFacing: true,
});

new IntegTest(app, 'NetworkLoadBalancerSecurityGroupFlag', {
  testCases: [stack],
});

実際のPRではセキュリティグループ設定の検証用assertionを実装しています。 integ testでのassertionは修正内容に応じて必要性が変わってくると思いますので、適宜素敵なinteg testを実装してみてください。

github.com

まとめ

以上でCDKに機能フラグを導入することができました。 機能フラグは後方互換性を維持するため非常に重要な役割を果たしていることがご理解いただけたかと思いますが、一般的なCDKユーザにとってはあまり意識する機会が多くない存在だと思っています。(それはCDKの設計として素晴らしいことだと思います)

ただ、そんなマイナーな存在を実装するコントリビューションガイドなんてものはなかなかあるはずがありません。 ということで、今回は気合を入れて記事にしてみました。

「あ〜〜このCDKの振る舞いイケてなさすぎる。。俺色に染めてやりてぇ。。。」と感じてらっしゃる方がいらっしゃいましたら、ぜひぜひこちらを参考にPRを発行してみてください。

補足

機能フラグの立ち位置

こちらの記事でも紹介されている通り、本来機能フラグは最終奥義的な立ち位置ですので、安易な追加は避けるべきものです。

www.ogis-ri.co.jp

ただし機能フラグ無しでは実現できない変更が数多くあることは事実であり、今後も一定のペースで増加していくことは避けられないと感じています。 私個人の思いとしては、CDKでは後方互換性を確保するあまりデフォルトで非推奨な振る舞いをするケースが結構あるので、そういったもの関してはバシバシと機能フラグ導入で直してしまったほうが良いと考えています。

目下はcloudfront functionsのランタイム設定やCloudFrontのオリジンとしてのLambdaの関数URLのIPアドレスタイプについて、時代の変化に合わせたデフォルト値となるような修正を提案しています。きっと1年以内にはマージされる...といいな......

github.com

github.com

alphaモジュールにおける機能フラグ

GA前のCDK alphaモジュールでは破壊的変更が許容されているため、機能フラグの実装は必要ありません。 ただしPR本文に破壊的変更である旨を明記する必要があります。

BREAKING CHANGE: Corrected LogRetention IDs for DatabaseCluster. Previously, regardless of the log type, the string ‘objectObject’ was always included, but after the correction, the log type is now included.

github.com

GA前にゴリゴリに振る舞いを変えるPRを発行して、より磨かれたモジュールを作り上げていきましょう。

新米CDKコントリビュータに贈る コントリビュートネタ探索ノウハウ集

はじめに

AWS CDKはOSSとして公開されているため誰でも手軽にコントリビュートを行うことが可能です。

この記事では「とりあえずコントリビュートしてみたい!」「でも取り組みやすいissueってどう探せば良いんだろう...??」という方向けに、コントリビュートネタを量産できるノウハウをレベル別に紹介していきたいと思います。

コントリビュートの流れ

プルリクエスト発行までの流れはこちらの記事を参照ください。

nixieminton.hatenablog.com

他にも様々な先駆者達の知見が記事としてまとめられているはずです。

コントリビュートネタ探索編

ここから本題の取り組むIssue探しについてです。

いきなり掌返しですが、今回はIssue化されていないコントリビュートネタをどんどん探していきます。

レベル1 (〜数行修正レベル)

Interface VPC endpointの追加

特定のAWSサービスへVPC内からPrivate Link経由で通信を行うInterface Endpointですが、CDKではpublic staticなファクトリーメソッドが大量に用意されており、直接エンドポイントを記述することなく設置することができます。

  vpc.addInterfaceEndpoint("EcrVpce", {
    service: ec2.InterfaceVpcEndpointAwsService.ECR
  })

CDK上での実装箇所はこちらです

github.com

public static readonly ECR = new InterfaceVpcEndpointAwsService('ecr.api');
public static readonly ECR_DOCKER = new InterfaceVpcEndpointAwsService('ecr.dkr');
public static readonly ECS = new InterfaceVpcEndpointAwsService('ecs');

...

Interface Endpointは週2-3個ペースで日々追加されており、それに対応したファクトリーメソッドを追加するPRを発行することができます。修正内容はわずか1行であり、とっても取り組みやすいものとなっています。

github.com

CDKで未対応なEndpointについては、こちらのリポジトリで一覧を確認することができます。ぜひご活用ください!

github.com

RDSなどのエンジンバージョン追加

RDSなどのサービスではエンジンのバージョンがpublic staticなファクトリーメソッドとして定義されており、直接正確なバージョンを記述せずとも利用することが可能です。

    const dbCluster = new rds.DatabaseCluster(scope, 'Database', {
      engine: rds.DatabaseClusterEngine.auroraMysql({
        version: rds.AuroraMysqlEngineVersion.VER_2_11_1
      }),

CDK上での実装は以下のとおりです。

github.com

  /** Version "8.0.mysql_aurora.3.08.2". */
  public static readonly VER_3_08_2 = AuroraMysqlEngineVersion.builtIn_8_0('3.08.2');
  /** Version "8.0.mysql_aurora.3.09.0". */
  public static readonly VER_3_09_0 = AuroraMysqlEngineVersion.builtIn_8_0('3.09.0');

エンジンバージョンも日々追加されているため、それに合わせてPR発行が可能です。 こちらも1行実装 + コメント数行といった規模感です。

上述したInterface Endpointよりも競争率は高めで、割とすぐに誰かしらがPRを出すイメージです。

github.com

github.com

他にも様々なenum追加を行うことができます。

- Neptune, RedShiftなどのエンジンバージョン

github.com

  • SyntheticsなどのRuntime

  - なぜかSyntheticsだけInteg Test要求されがちなのでちょっと大変かも

github.com

  • Bedrockでのモデル

github.com

  • etc..

 レベル2 (〜数十行修正レベル)

 Typed Errorへの置き換え

CDKコンストラクタ内では引数に対するバリデーションが行われます。従来はJSのErrorクラスを使ってthrowされていましたが、最近CDKチームから型付きのTyped Errorがリリースされ、既存実装の置き換えが進行しています。

// 既存の書き方

throw new Error(`Asset ${this.sourcePath} is expected to be either a directory or a regular file`);




// 新しい書き方

import { ValidationError } from '../../core';

throw new ValidationError(`Asset ${this.sourcePath} is expected to be either a directory or a regular file`, this);

まだ置き換えが完了していないサービスは.eslintrc.jsに一覧が記載されています。従って、ここに書かれているサービスに対してエラーの置き換えを行うPRを発行することができます。

github.com

const noThrowDefaultErrorNotYetSupported = [
  'aws-ecs-patterns',
  'aws-ecs',
  'aws-elasticsearch',
  'aws-events-targets',
  'aws-globalaccelerator',
  'aws-globalaccelerator-endpoints',
  'aws-iam',
  'aws-lambda-destinations',
  'aws-lambda-event-sources',
  'aws-lambda-nodejs',
  'aws-scheduler-targets',
  'aws-scheduler',
  'aws-secretsmanager',
  'aws-servicecatalog',
  'aws-sns-subscriptions',
  'aws-stepfunctions',
  'aws-stepfunctions-tasks',
  'core',
  'custom-resources',
  'region-info',
];

Typed ErrorにはValidationErrorとUnscopedValidationErrorクラスの2種類があり、construct内部では前者を、外部では後者を使うようにします。

import { ValidationError, UnscopedValidationError } from '../../../core/lib/errors';

// construct外部では`UnscopedValidationError`を用いる
public static factoryMethod() {
  if (errorCondition) {
   throw new UnscopedValidationError(`This is UnscopedValidationError.`);
  }
} 

// construct内部では`ValidationError`を用いる
constructor(scope: Construct, id: string, props: ConstructProps) {
  if (errorCondition) {
        throw new ValidationError('This is ValidationError.', this);
   }
}

実際のPR例はこちら💁

github.com

github.com

こちらはメンテナの方が大喜びするPRなので、出すや否やapproveされてマージされるはずです。スピード感を求める方はぜひ!

 レベル3 (数十~100行程度)

 L2コントストラクトへの引数追加

L1はcloudformationから自動生成されますが、L2には自動更新を行う仕組みはありません。従って、cloudformationに追加された新機能をL2からも使えるようにするには、PRを通してコードを追加する必要があります。これに実際に取り組んでみましょう。

ここからようやくCDKへのコントリビュート感が出てくるのでいくつか壁に感じる点もあるはずです。そんな状況を打破してくれる、CDKへの引数追加を行う一連の流れを体験できるAWS CDK コントリビュートワークショップが存在します!!

環境構築からPR発行まで模擬リポジトリに対して実際に行えますので、実PR発行前にぜひお試しください。

jaws-ug-cdk.github.io

ワークショップ自体はAWS DevTools HEROの@goto.k, AWSJの@winteryukkyと私(@badmintoncryer)で作ったものなので、不明点などあれば誰かしらにご連絡ください。

 L2未対応引数の探し方

コントリビュートの流れが掴めたら次は取り組むL2未対応引数を探します。

基本的にはCloudformation定義とL2コードを睨めっこしながら探していきますが、簡単な探し方をご紹介します。

L2未対応引数一覧サイト

こちらのページから各L2コンストラクタごとの未対応引数一覧を確認することができます。

d1upnzw71mlot9.cloudfront.net

未対応引数の例

この場合はaws-amplify-alphaパッケージのCfnBranchに渡すbackend, computeRoleArn, enableSkewProtection, frameworkが、L2のBranchコンストラクトで未対応であることを意味します。

実際にBranchのL2コンストラクトのコードを見てみると、上記の引数達はL1コンストラクトに渡されていなさそうですね。

    const branch = new CfnBranch(this, 'Resource', {
      appId: props.app.appId,
      basicAuthConfig: props.basicAuth && props.basicAuth.bind(this, `${branchName}BasicAuth`),
      branchName,
      buildSpec: props.buildSpec && props.buildSpec.toBuildSpec(),
      description: props.description,
      enableAutoBuild: props.autoBuild ?? true,
      enablePullRequestPreview: props.pullRequestPreview ?? true,
      environmentVariables: Lazy.any({ produce: () => renderEnvironmentVariables(this.environmentVariables) }, { omitEmptyArray: true }),
      pullRequestEnvironmentName: props.pullRequestEnvironmentName,
      stage: props.stage,
      enablePerformanceMode: props.performanceMode,
    });

L2実装の方法によっては誤検知(false-positive)している引数もありますので、取り組む前にL2コードのチェックは忘れずにお願いします。

誤検知解消は私がサボっているだけなので、どなたかからのPRもお待ちしています🙏

github.com

L1への新機能追加を見張る

AWS CDKのリポジトリには週次でcloudformationの更新を元にしたL1コンストラクトの更新PRが自動で発行されています。

github.com

PRの本文には差分一覧が掲載されており、新規の引数やAttributeの更新情報が乗っています。これらは確実にL2コンストラクトで対応していません。

CodeBuildへの引数追加

こちらの例ではCodeBuildProject中のEnvironmentという引数に対して、新しくDockerServerという引数が追加されています。DockerServer型はComputeTypeSecurityGroupIdsを持っており、ComputeTypeが必須の引数のようです。

L1の機能追加を見張っていると、AWSでは公式発表されていない新機能が早めにリリースされることが多々あります。数ヶ月前にDocumentDBにAuroraのServerlessV2と同じ引数が生えていたことに気づき、いつ公式リリースがあるのかな〜と待っていたりします。

PR例

L2への引数追加のPR例を紹介します。

はじめはbooleanや整数が取り組みやすくておすすめです。enum定義や別コンストラクト、独自クラスという順番で難易度が上がっていきます。

  • boolean

github.com

  • 数値

github.com

  • 数値(Size)

github.com

  • 数値(Duration)

github.com

github.com

  • 別コンストラクト (kms.IKey)

github.com

  • 独自クラス

github.com

最後に

紹介した通り、CDKへの機能追加ネタは日々追加されており、到底今のコントリビュータの規模感では賄いきれないものになっています。

これを見て「意外と簡単そうだな!CDKコントリビュートやってみよう!」と1人でも思っていただけたら大変嬉しいです。

Community Review待ちのPRは私の方でレビュー可能ですので、PR作成したらぜひXやGitHubでご連絡いただければと思います。不明な点があった際にも遠慮なくです!

世界最速?!Aurora DSQLをAWS CDKで構築してみる

はじめに

昨年のRe:InventでAurora DSQLが公開されました。現在はPreview段階ですが、将来が気になるサービスの一つです。

ふとCDKのプルリクを眺めていたらDSQLのL1リソース(Cloudformation)がしれっとリリースされていたので、試しに作ってみました。

github.com

CDKでの作り方

DSQLのL1コンストラクトはv2.189.0からリリースされています。

  "dependencies": {
    "aws-cdk-lib": "2.189.0",
    "constructs": "^10.4.2"
  }

aws-cdk-lib/aws-dsqlモジュールからCfnClusterをimportして作ってみましょう。 現状存在するリソースはAWS::DSQL::Clusterのみで、与えられるオプションも削除保護のdeletionProtectionEnabledtagsのみです。

ClusterのattributeとしてはID, ARN, ステータス, 作成時刻が取得可能です。Endpointが取得できたら嬉しかったですね。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dsql from 'aws-cdk-lib/aws-dsql';

export class TempStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const cluster = new dsql.CfnCluster(this, 'Cluster', {
      // 削除保護を有効化
      deletionProtectionEnabled: true,
    });

    // クラスターID
    new cdk.CfnOutput(this, 'ClusterIdentifier', {
      value: cluster.attrIdentifier,
      description: 'The identifier of the cluster',
      exportName: 'ClusterIdentifier',
    });
    // クラスターARN
    new cdk.CfnOutput(this, 'ClusterArn', {
      value: cluster.attrResourceArn,
      description: 'The ARN of the cluster',
      exportName: 'ClusterArn',
    });
    // クラスターステータス
    new cdk.CfnOutput(this, 'ClusterStatus', {
      value: cluster.attrStatus,
      description: 'The status of the cluster',
      exportName: 'ClusterStatus',
    });
    // クラスター作成時刻
    new cdk.CfnOutput(this, 'ClusterCreationTime', {
      value: cluster.attrCreationTime,
      description: 'The creation time of the cluster',
      exportName: 'ClusterCreationTime',
    });
}

デプロイします。

❯ npx cdk deploy                    

✨  Synthesis time: 5.98s

TempStack: start: Building TempStack Template
TempStack: success: Built TempStack Template
TempStack: start: Publishing TempStack Template (current_account-current_region)
TempStack: success: Published TempStack Template (current_account-current_region)
TempStack: deploying... [1/1]
TempStack: creating CloudFormation changeset...

 ✅  TempStack

✨  Deployment time: 177.51s

Outputs:
TempStack.ClusterArn = arn:aws:dsql:us-east-1:123456789012:cluster/uaabuamveximj3hogehoge
TempStack.ClusterCreationTime = 2025-04-21T14:22:15.114Z
TempStack.ClusterIdentifier = uaabuamveximj3hogehoge
TempStack.ClusterStatus = ACTIVE
Stack ARN:
arn:aws:cloudformation:us-east-1:1234567899012:stack/TempStack/feb0e990-1ebb-11f0-b7e5-12e26c33ca05

✨  Total time: 183.49s

コンソール上でもしっかり作成できているようです。

マネコンでのクラスター情報

おわりに

ということでサイレントリリースされていたDSQLのCDK対応に関するポストでした。 もちろんCloudformationでも同様に作成可能なはずです。

外部キー制約が無いなどなかなか尖った仕様のRDBサービスですが、 VPCレスのwebアプリ構築の可能性を色々と広げてくれるのではと期待しています。

直に公式から正式アナウンスがあるかと思います。楽しみに待ちましょう〜!

AWS CDKでNetwork Load Balancerに固定IPを設定したい!(2025年度版)

はじめに

AWS CDKでNetwork Load Balancer (NLB)のサブネットマッピングを可能にするPRを発行しマージされました。CDKのv2.189.0から利用可能です。

github.com

主には固定IP指定時に役立つと思っています。

この記事では、実装した機能の概要と使い方について解説します。

概要

Network Load Balancerで特定のサブネットを指定する際、従来はvpcSubnetsプロパティを使って指定していました。

今回の追加機能では、より詳細な設定が可能なsubnetMappingsプロパティを実装しており、以下のことが可能になっています。

使い方

基本的な使い方として、NLBに紐づくENIをどのサブネットに設置するか明示的に指定が可能です。

declare const vpc: ec2.Vpc;
declare const subnet: ec2.ISubnet;

const lb = new elbv2.NetworkLoadBalancer(this, 'LB', {
  vpc,
  // NLBに紐づくENIを`subnet`に設置
  subnetMappings: [{
      subnet,
  }],
});

その他の設定も合わせて行えます。

declare const vpc: ec2.IVpc;
declare const subnet: ec2.ISubnet;
declare const cfnEip: ec2.CfnEIP;

// インターネット向けNLBにElastic IPv4アドレスを割り当てる
new elbv2.NetworkLoadBalancer(this, 'InternetFacingLb', {
  vpc,
  internetFacing: true,
  subnetMappings: [
    {
      subnet,
      // Elastic IPアドレスのallocation ID
      allocationId: cfnEip.attrAllocationId,
    },
  ],
});

// 内部NLBにプライベートIPv4アドレスを割り当てる
new elbv2.NetworkLoadBalancer(this, 'InternalLb', {
  vpc,
  internetFacing: false,
  subnetMappings: [
    {
      subnet,
      // サブネットのCIDR範囲内のプライベートIPv4アドレス
      privateIpv4Address: '10.0.12.29',
    },
  ],
});

// デュアルスタックNLBにIPv6アドレスとソースNAT用プレフィックスを設定
new elbv2.NetworkLoadBalancer(this, 'DualstackLb', {
  vpc: dualstackVpc,
  // デュアルスタックNLBの設定
  ipAddressType: elbv2.IpAddressType.DUAL_STACK,
  enablePrefixForIpv6SourceNat: true,
  subnetMappings: [
    {
      subnet: dualstackSubnet,
      // IPv6アドレス
      ipv6Address: '2001:db8:1234:1a00::10',
      // ソースNAT用IPv6プレフィックス
      sourceNatIpv6Prefix: elbv2.SourceNatIpv6Prefix.autoAssigned(),
    },
  ],
});

バリデーション

NLBの設定には様々な制約があります。例えば:

  • インターネット向けロードバランサーにはプライベートIPv4アドレスを指定できない
  • 内部ロードバランサーにはElastic IPを割り当てられない
  • enablePrefixForIpv6SourceNatが有効でない場合、ソースNATのIPv6プレフィックスを指定できない
    • そもそもenablePrefixForIpv6SourceNatがなんなのかよくわかっていないのは内緒です。

これらの制約に対してはCDKコード中でバリデーションが実行されるため、synth時にエラーを投げてくれます。

まとめ

ということでNLBのsubnetMappings設定のご紹介でした。

5年ほど前からの古のp1 issueに対応するものなので、地味にニーズがあるのではと思っています。

github.com

個人的にはNLBにプライベートIPを指定したいケースが多々あり、その時にエスケープハッチ要らずで便利になるなあと自画自賛しています。

皆様もぜひご活用ください〜〜

CDKのInteg Testでcloudformation管理外のリソースをイジイジする

はじめに

既存のECR RepositoryをlookupしてCDK Appに取り込むPRが先日マージされました!

github.com

そこでのInteg Testでかなり詰まったので、メモを残しておきます。

結論

Integ Testの実行前後にcommand hookとして任意のCLIを叩けます。

今回は(i)ECR Repositoryを作成, (ii)Repositoryをlookupするinteg test実行, (iii)Repositoryの削除という手順を踏むよう、以下のように実装しました。

import { IntegTest } from '@aws-cdk/integ-tests-alpha';

declare const lookupStack: cdk.Stack;

new IntegTest(app, 'EcrRepoLookupTest', {
  testCases: [lookupStack],
  // CLI実行
  hooks: {
    // デプロイ前にECR Repositoryを作成
    preDeploy: [`aws ecr create-repository --repository-name ${repositoryName}`],
    // デプロイ後にECR Repositoryを削除
    postDeploy: [`aws ecr delete-repository --repository-name ${repositoryName} --force`],
  },
});

テスト全体像

実際のコードはこちら

github.com

ECR Repositoryのlookup関数を実装したので、実際にlookupするStackをInteg Testでデプロイするようにしました。

const app = new App();
const repositoryName = 'my-repo';

const lookupStack = new Stack(app, 'EcrRepoLookupStack', {
  // Stack内でlookupを実行する場合、明示的にaccount, regionの設定が必要
  env: {
    account: process.env.CDK_INTEG_ACCOUNT ?? process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_INTEG_REGION ?? process.env.CDK_DEFAULT_REGION,
  },
});
// 自作lookup関数
const lookupRepo = ecr.Repository.fromLookup(lookupStack, 'LookupRepo', {
  repositoryName,
});

lookupしたRepositoryは、Repository.fromRepositoryArn()などでimportしただけなので、実際にはリソースを持ちません。

したがって、スタックをデプロイするためには適当なリソースを実際に作成する必要があります。

今回はRepositoryへのアクセス権限を持つlambdaを作成してみました。

declare const lookupRepo: ecr.IRepository;

const testFunction = new lambda.Function(lookupStack, 'Lambda', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  code: lambda.Code.fromInline('exports.handler = async function(event, context) { return "Hello, World!"; }'),
});
// Repositoryの情報を使ったIAMポリシーを作成
lookupRepo.grantRead(testFunction);

最後に、このStackをIntegTestクラスに渡します。この時、lookupするRepositoryの作成及び削除をcommand hookで行います。

declare const lookupStack: cdk.Stack;

new IntegTest(app, 'EcrRepoLookupTest', {
  // lookupするスタックをテストは有効化
  enableLookups: true,
  // `enableLookups`有効化時にはこちらを`false`にする
  stackUpdateWorkflow: false,
  testCases: [lookupStack],
  // IntegTestの実行前後で任意のCLIを実行!
  hooks: {
    preDeploy: [`aws ecr create-repository --repository-name ${repositoryName}`],
    postDeploy: [`aws ecr delete-repository --repository-name ${repositoryName} --force`],
  },
});

これで、Repository作成 -> Integ Testでlookup -> Repository削除が自動で実行できます。やったね。

おまけ

snapshotに含まれるlookup結果

IntegTestが成功するとSnapshot(cloudformation template)が作成されます。しかし、このときtemplateに含まれるlookup結果は実際のRepositoryの情報ではなく、コンストラクト内で定義されたダミー情報です。

"LambdaServiceRoleDefaultPolicyDAE46E21": {
   "Type": "AWS::IAM::Policy",
   "Properties": {
    "PolicyDocument": {
     "Statement": [
      {
       "Action": [
        "ecr:DescribeImages",
        "ecr:DescribeRepositories"
       ],
       "Effect": "Allow",
       "Resource": {
        "Fn::Join": [
         "",
         [
          "arn:",
          {
           "Ref": "AWS::Partition"
          },
          ":ecr:us-east-1:123456789012:repository/DUMMY_ARN" // DUMMY_ARN ?!
         ]
        ]
       }
      }
     ],
     "Version": "2012-10-17"
    },

このダミー値はlookup関数内部で定義されており、context情報が存在しない場合の暫定値として活用されます。

github.com

実際にデプロイされるtemplate(2回目のsynthで作成)ではlookup結果が反映されていますが、snapshotには初回synth時のtemplateが格納されるようです。デプロイ結果を眺めると正しくlookupできているのに、なぜかsnapshotではlookupできていないように見え、最初はとても混乱しました。。

このあたりはk.gotoさんのこちらのブログが大変参考になります。

go-to-k.hatenablog.com

リソースゼロのスタックをIntegTest実行

今回はlookupしたRepositoryに加えてlambdaもStack上で定義しましたが、これをしない場合リソースがないStackを定義したことになります。

実際に以下のテストを作って実行しましたが、なぜかStackのデプロイが行われずかなり混乱しました。CfnOutputだけあってもだめなようです。

const app = new App();
const repositoryName = 'my-repo';

const lookupStack = new Stack(app, 'EcrRepoLookupStack', {
  env: {
    account: process.env.CDK_INTEG_ACCOUNT ?? process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_INTEG_REGION ?? process.env.CDK_DEFAULT_REGION,
  },
});
// 実際にRepositoryが作られるわけではないので、リソース数は0
const lookupRepo = ecr.Repository.fromLookup(lookupStack, 'LookupRepo', {
  repositoryName,
});

// CfnOutputを作ってみたが、リソース0だとIntegTestでデプロイされなかった
new CfnOutput(lookupStack, 'RepositoryUri', {
  value: lookupRepo.repositoryUri,
});

new IntegTest(app, 'EcrRepoLookupTest', {
  enableLookups: true,
  stackUpdateWorkflow: false,
  testCases: [lookupStack],
  hooks: {
    preDeploy: [`aws ecr create-repository --repository-name ${repositoryName}`],
    postDeploy: [`aws ecr delete-repository --repository-name ${repositoryName} --force`],
  },
});

同様にlookup関数のPR発行される方はお気をつけてください。

CDKでのCI

CDKではPR発行時にInteg TestのCIが実行されますが、実際にデプロイ及びcommand hook実行が行われるわけではありません。 したがってInteg Test中のlookup関数についても、CI実行時には何もない環境に向けてlookupを実行し、ダミー値を設定したtemplateが生成されます。 結果的に、上記の通りダミー値が記載されたSnapshotと一致するので、問題なくCI通過しているようです。

最後に

ということで細かすぎて伝わらないInteg Test tipsでした。そのほかのユースケースって何があるんでしょう...

活躍の幅は広そう(?)なので、Integ Test巧者の皆様ぜひご活用ください。

S3のレプリケーションをCDKで設定する (2025年度版)

はじめに

8ヶ月ほど前にS3のレプリケーション設定を有効化するPRを発行し、先日ようやくマージされました。CDKのv2.177.0から利用可能になっています。

巷のブログなどではエスケープハッチを用いた実装例がたくさんでてくるので、改めて使い方や実装解説などをしたいと思います。

github.com

追記

作成した修正PRがマージされましたので、v2.182.0以降は以下の注意の内容を気にする必要はなくなりました。

ただしv2.182.0以前から作成したスタックの場合、バグ解消にはAppの@aws-cdk/aws-s3:setUniqueReplicationRoleNameという機能フラグを明示的にtrueにする必要があります。

const app = new App({
  context: {
    '@aws-cdk/aws-s3:setUniqueReplicationRoleName': true,
  },
});

注意

v2.178.0現在、私の実装がポンコツなせいで、同一スタックに複数のレプリケート元バケットを作成するとデプロイエラーとなる問題が生じています。

github.com

バグ修正のPRは作成済みですが、マージまでは多少時間がかかると思われます。

github.com

使い方

s3.Bucketコンストラクトに新たに追加されたreplicationConfigurationを設定します。 このとき、複製元/複製先バケット両方でバージョニング設定(versioned)を有効化する必要があることに注意してください。

基本の使い方

// 複製先バケットの定義
declare const destinationBucket: s3.IBucket;

// 複製元バケット
const sourceBucket = new s3.Bucket(this, 'SourceBucket', {
  // バージョニングを有効化
  versioned: true,
  replicationRules: [
    {
      // レプリケーション先バケットの定義
      destination: destinationBucket,
    },
  ],
});

たったのこれだけでレプリケーションを有効化できます。簡単ですね。

オプション設定

レプリケーションには多くのオプション設定が存在しています。

複数のレプリケート先設定

レプリケート先を配列として複数設定することができます。 このとき、各ルールに対して異なるpriorityの設定が必須となるので注意してください。

// レプリケーション先バケットの定義
declare const destinationBucket1: s3.IBucket;
declare const destinationBucket2: s3.IBucket;

const sourceBucket = new s3.Bucket(this, 'SourceBucket', {
  versioned: true,
  replicationRules: [
    {
      destination: destinationBucket1,
      priority: 1,
    },
    {
      destination: destinationBucket2,
      priority: 2,
    },
  ],
});

ReplicationTimeControl (RTC)

レプリケーションにかかる時間のSLA設定を行える機能です。 v2.177.0では15分間のみ設定可能で、99.99%のオブジェクトについて15分以内のレプリケーション完了を保証してくれます。 また、15分以上かかったかどうかを記録するメトリクス設定も合わせて行えます。

replicationRules: [
  {
    destination: destinationBucket,
    replicationTimeControl: s3.ReplicationTimeValue.FIFTEEN_MINUTES,
    metrics: s3.ReplicationTimeValue.FIFTEEN_MINUTES,
  },
]

実装Tips

将来的に15分以外の時間設定が可能になる可能性を踏まえて、ReplicationTimeValueというEnum-likeクラスを定義しています。 Enum-likeクラスについてはこちらに詳しく記載しています。

export class ReplicationTimeValue {
  /**
   * Fifteen minutes.
   */
  public static readonly FIFTEEN_MINUTES = new ReplicationTimeValue(15);

  /**
   * @param minutes the time in minutes
   */
  private constructor(public readonly minutes: number) {};
}

ストレージクラス

レプリケート先バケットにおけるオブジェクトのストレージクラスを設定できます。 デフォルトではレプリケート元のストレージクラスが設定されます。

replicationRules: [
  {
    destination: destinationBucket,
    storageClass: s3.StorageClass.INFREQUENT_ACCESS,
  },
]

KMSによる暗号化

デフォルトではKMS(Key Management Service)により暗号化されたオブジェクトはレプリケートされません。 レプリケートするためには、以下のようにsseKmsEncryptedObjectsの有効化と暗号化に用いるkmsKeysを指定します。

declare const kmsKey: kms.IKey;

replicationRules: [
  {
    destination: destinationBucket,
    kmsKeys: [kmsKey],
    sseKmsEncryptedObjects: true,
  },
]

実装Tips

KMS関連の設定を行う場合、レプリケーションを行うIAMロールに対して様々な権限を付与する必要があります。 このあたりは与えられたkmsKeyに対してひたすらgrantを叩いて実現しています。

    kmsKeys.forEach(kmsKey => {
      kmsKey.grantEncrypt(replicationRole);
    });
    // If KMS key encryption is enabled on the source bucket, configure the decrypt permissions.
    this.encryptionKey?.grantDecrypt(replicationRole);

削除マーカーのレプリケート

バージョニングを有効化しているため、オブジェクトへの削除は削除マーカを用いて実行されます。 デフォルトでは削除マーカはレプリケートされませんが、明示的にレプリケートするよう指示したい場合はdeleteMarkerReplicationを有効化します。

replicationRules: [
  {
    destination: destinationBucket,
    deleteMarkerReplication: true,
  },
]

削除マーカのレプリケートはRTCを遵守しない、タグフィルターと両立できない、ライフサイクルルールで付与されたマーカはレプリケートできない等々様々な制約があるため、利用する際にはご注意ください。

フィルター

オブジェクトのタグ設定, prefixをもとにレプリケートのフィルターを設定することができます。 両方指定した場合、AND条件で適用されることに注意してください。

replicationRules: [
  {
    destination: destinationBucket,
      filter: {
        // prefix フィルター
        prefix: 'prefix',
        // タグフィルター
        tags: [
          {
            key: 'tagKey',
            value: 'tagValue',
          },
        ],
      }
  },
]

実装Tips

S3のレプリケーション設定はV1とV2設定が存在します。 cloudformationもこれに依存しており、結果としてフィルタ周りはかなり冗長な設定項目が存在しています。

      ReplicationConfiguration:
        Role: !GetAtt ReplicationRole.Arn
        Rules:
          - Id: ReplicationRule1
            Status: Enabled
            Priority: 1
            Prefix: 'Prefixフィルタ設定その1'
            Filter:
              Prefix: 'Prefixフィルタ設定その2'
              TagFilter: 
                  - Key: タグフィルタ設定
                    Value: その1
              And:
                Prefix: 'Prefixフィルタ設定その3'
                TagFilter: 
                    - Key: タグフィルタ設定
                      Value: その2
}

詳細は割愛しますが、Filter.And.PrefixまたはFilter.Prefixを設定すると暗黙的にV2になったり、各種Prefix/Tagフィルタを同時に複数設定するとエラーになったり、なかなか複雑怪奇な様相を呈しています。 CDK実装ではこれらの問題を回避するため、常にV2設定を活用しつつ、TagフィルタとPrefixフィルタを直感的に設定できるようにしています。

        // Whether to configure filter settings by And property.
        const isAndFilter = rule.filter?.tags && rule.filter.tags.length > 0;
        // To avoid deploy error when there are multiple replication rules with undefined prefix,
        // CDK set the prefix to an empty string if it is undefined.
        const prefix = rule.filter?.prefix ?? '';
        const filter = isAndFilter ? {
          and: {
            prefix,
            tagFilters: rule.filter?.tags,
          },
        } : {
          prefix,
        };

クロスアカウント

異なるアカウントのS3バケットへのレプリケーションも設定することができます。 この際、レプリケート先バケットのリソースポリシーを色々設定する必要があるため、明示的にaddReplicationPolicy()を呼び出す必要があります。

// 別アカウントのレプリケート先バケット
declare const destinationBucket: s3.IBucket;

const sourceBucket = new s3.Bucket(this, 'SourceBucket', {
  versioned: true,
  replicationRules: [
    {
      destination: destinationBucket,
      priority: 1,
      // レプリケートしたオブジェクトの所属先をレプリケート先バケットアカウントに移行するかどうか
      // デフォルトではソースバケットアカウントが所属先となる
      accessControlTransition: true,
    },
  ],
});

// レプリケート先バケットのリソースポリシーを設定
if (sourceBucket.replicationRoleArn) {
  destinationBucket.addReplicationPolicy(sourceBucket.replicationRoleArn, true, '111111111111');
  }

まとめ

S3のオブジェクトレプリケーションは2019年5月にissueとして投稿されていたため、約6年越しに解消したことになります。 出番は多い機能だと思いますので、ぜひ皆様ご活用いただけると嬉しいです!