AWS CDK の Construct ID はどのように命名するべきか?
こんにちは。リテールアプリ共創部のきんじょーです。
AWS CDK でインフラを定義する際、皆さんは Construct ID をどのようなルールで命名していますか?
Construct ID を適切に命名することで、自動生成されるリソース名や CloudFormation の 論理ID の可読性を高く保ち、保守しやすいインフラを構築できます。
以下のブログでは CDK と CloudFormation でデプロイされるリソースの命名規則について全体像が解説されています。
この記事では CDK による論理 ID 生成処理をさらに深掘りし、それを踏まえた上でどのように Construct ID を命名すべきか実装例を交えてご紹介します。
先に結論
以下を意識することで、自動生成されるリソース名と CloudFormation の 論理ID の可読性を高められます。
- Construct ID は scope の中で一意にする
- Construct ID は
PascalCase
で記述する - Construct ID に
Construct
やStack
とつけない - 親 コンストラクト で表している情報を繰り返さない
- 技術詳細を指定しすぎない
Default
とResource
を適切に活用し Construct ID を短縮するDefault
を使うべき場合- コンストラクトツリー のリファクタリングを行う場合
- 再利用を考慮しない カスタムコンストラクト を定義し、将来的にリファクタリングの可能性がある場合
Resource
を使う場合- 再利用可能な カスタムコンストラクト を作成する場合
なぜ Construct ID の命名にこだわる必要があるか
Construct ID の重要性を説明する前に、事前知識として Construct ID と 論理 ID について説明します。
Construct ID とは
Construct ID とは、CDK で コンストラクト を インスタンス化 する際に、第 2 引数に渡す文字列のことです。
CDK では コンストラクト を利用して Stack 内のリソースを構造化できます。指定する Construct ID は コンストラクト のスコープ内で一意である必要があります。
new sqs.Queue(this, "MyQueue"); // MyQueue が Construct ID
論理 ID とは
論理 IDは CloudFormation テンプレートの Resources セクションでリソースを一意に示す ID です。論理 ID の変更はリソースの置換を意味します。
Resources:
MyEC2Instance: # 論理ID
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0ff8a91507f77f867
Construct ID と論理 ID の関係性
Construct ID は CDK から CloudFormation テンプレートを合成する際、CloudFormation の論理 ID を決めるために使用されます。
リソースに明示的に名前を指定しない場合、CloudFormation は論理 ID をもとにリソース名を生成します。図にすると以下の関係性です。
CloudFormation の論理 ID は 255 文字まで指定可能です。
しかし自動生成されるリソース名は論理 ID を一定の桁数で切り捨てるため、長すぎる論理 ID はリソース名を正しく表せないことがあります。
また論理 ID が冗長な場合、CloudFormation の TreeView や FlatView で Stack 内のリソース識別がしづらくなる問題もあります。
Construct ID を適切に命名することで、自動生成されるリソース名や CloudFormation の TreeView や FlatView の可読性を高めることに繋がります。
論理 ID からリソースの命名が決まる CloudFormation 側の仕組みは、前述のブログで詳細に解説されているので併せてそちらもご覧ください。
CDK の論理 ID 生成処理
CDK の論理 ID を生成する実装は以下にあります。
特筆すべき点をかいつまんで説明します。
上位 コンストラクト の ID を連結する
命名対象の コンストラクトツリー を走査し、top-level コンストラクト(Stack 直下の コンストラクト)までの Construct ID を取得します。
protected allocateLogicalId(cfnElement: CfnElement): string {
const scopes = cfnElement.node.scopes;
const stackIndex = scopes.indexOf(cfnElement.stack);
const pathComponents = scopes.slice(stackIndex + 1).map(x => x.node.id);
return makeUniqueId(pathComponents);
}
たとえば以下のような Construct ID を振られた Tree があった場合、
生成される CloudFormation テンプレートで確認できるMetadata/aws:cdk:path
と、ID 生成に使用されるパスツリーは以下になります。
Construct ID | Metadata/aws:cdk:path | ID 生成に使用されるパスツリー |
---|---|---|
Api | ServerStack/Api | Api |
ApiGateway | ServerStack/Api/ApiGateway | ApiApiGateway |
Lambda | ServerStack/Api/Lambda | ApiLambda |
Construct ID から除外される文字列
論理 ID 生成時に Construct ID に含まれる英数字以外の文字列は除去されます。
function removeNonAlphanumeric(s: string) {
return s.replace(/[^A-Za-z0-9]/g, "");
}
Construct ID をkebab-case
で記述している場合、-
は除外されてしまい、区切り文字が無くなってしまう点に注意が必要です。
上記の理由から、意図せぬ ID 重複を防いだり、論理 ID とリソース名の可読性を上げるため Construct ID にはPascalCase
が推奨されています。
論理 ID の Suffix にハッシュ値がつく仕組み
CloudFormation の 論理 ID を一意にするため、CDK では Construct ID のパスツリーを元に md5 のハッシュ値を生成し、先頭 8 文字を 論理 ID の suffix に付与します。
// ハッシュ値の生成処理
function pathHash(path: string[]): string {
const md5 = md5hash(path.join(PATH_SEP));
return md5.slice(0, HASH_LEN).toUpperCase();
}
// 人間が可読できるIDとハッシュ値の付与
const hash = pathHash(components);
const human = removeDupes(components)
.filter((x) => x !== HIDDEN_FROM_HUMAN_ID)
.map(removeNonAlphanumeric)
.join("")
.slice(0, MAX_HUMAN_LEN);
return human + hash;
これには例外があり、命名対象が top-level コンストラクト である場合は suffix がつきません。
これは、既存の CloudFormation テンプレートから CDK スタックに移行する際に、top-level コンストラクト として定義することで論理 ID の変更なしでの移行を可能にする目的です。
冗長な情報を 論理 ID から除去する仕組み
コンストラクト のパスツリーの最後で同じ文字列が繰り返されている場合、その文字列は除去されます。
具体的には以下のような実装をした場合です。
// SQSのカスタムコンストラクトを定義
export class QueueConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
new sqs.CfnQueue(this, "MyQueue");
}
}
// SQSのカスタムコンストラクトを利用
export class SampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new QueueConstruct(this, "MyQueue");
}
}
コンストラクトツリー は以下のようになり、パスツリーはSampleStack/MyQueue/MyQueue
となります。
通常であれば、論理 ID はMyQueueMyQueue
+hash
となるはずですが、MyQueue
の重複が除去され、MyQueue
+hash
となります。
実際には以下のテンプレートが出力されました。
MyQueue4F9177CF:
Type: AWS::SQS::Queue
Metadata:
aws:cdk:path: SampleStack/MyQueue/MyQueue
Default
, Resource
)
特殊な Construct ID (Construct ID にはDefault
とResource
という特殊な ID を指定可能です。
どちらも論理 ID 生成時にパスツリーから除外されるという特徴を持っており、これらを適切に使用することで論理 ID が長くなることを防げます。
Construct ID のパスツリーは、論理 ID 生成時に 3 つの処理で使用されます。
- top-level コンストラクト かどうかの判定
- 論理 ID の人間が可読できるパートの生成
- suffix のハッシュ計算
Default
とResource
の違いは、Default
は上記 1、2、3 すべてから除外され、Resource
は 2 からのみ除外される点です。
以下のような コンストラクト があったとします。
// 3つのSQSキューを定義するコンストラクト
export class QueuesConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
new sqs.CfnQueue(this, "MyQueue");
new sqs.CfnQueue(this, "Default");
new sqs.CfnQueue(this, "Resource");
}
}
// 3つのSQSキューを定義するConstructを利用
export class SampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new QueuesConstruct(this, "QueuesConstruct");
}
}
この場合の コンストラクトツリー は以下のようになります。
それぞれの判定時のパスツリーの値と、最終的に生成される論理 ID を以下にまとめました。
Construct ID | Metadata/aws:cdk:path | 1. top-level コンストラクト 判定時 | 2. 可読部の生成時 | 3. ハッシュ計算時 | 論理 ID |
---|---|---|---|---|---|
MyQueue | SampleStack/QueuesConstruct/MyQueue | QueuesConstruct/MyQueue | QueuesConstructMyQueue | QueuesConstructMyQueue -> 56B5211F | QueuesConstructMyQueue56B5211F |
Default | SampleStack/QueuesConstruct/Default | QueuesConstruct | QueuesConstruct | top-level Construct 扱いで hash は付与されない | QueuesConstruct |
Resource | SampleStack/QueuesConstruct/Resource | QueuesConstruct/Resource | QueuesConstruct | QueuesConstruct/Resource -> D22CAD06 | QueuesConstructD22CAD06 |
Default
を利用した場合、top-level コンストラクト 扱いになり hash は付与されませんでした。
これはコンストラクトツリーの階層が 2 階層なため top-level コンストラクト と判定されていますが、さらに深い階層でDefault
を利用した場合、Default
を除いたパスツリーで hash が計算されます。
通常コンストラクトの階層が違うとパスツリーが異なるため、hash 値が異なって論理 ID が衝突することはありません。Default
を利用した場合パスツリーの計算時に無視されるので、コンストラクトのスコープ外だと思っているリソースと論理 ID が衝突する可能性があります。
下の例だと、末端の葉のリソースはParent/Default/MyQueue
とParent/MyQueue
異なるパスツリーになりますが、hash 計算時にどちらもParent/MyQueue
で計算されるため論理 ID が衝突します。
後述しますが、これを利用してコンストラクトツリーのリファクタリングが可能です。
Construct ID 命名時の注意点
ここからは、実際に Construct ID を命名する際に気をつけるべき点を見ていきます。
PascalCase
で記述する
Construct ID はConstruct ID はPascalCase
で記述することで、リソース名の可読性を高め、意図せぬ 論理 ID 衝突を防ぐことができます。
⭕️ Good
new sqs.CfnQueue(this, "MyQueue");
❌ Bad
// 上位のConstruct IDと結合されて区切りがわかりづらくなる
new sqs.CfnQueue(this, "myQueue");
// 以下は論理IDが衝突する
new sqs.CfnQueue(this, "my-queue");
new sqs.CfnQueue(this, "myqueue");
Construct
やStack
とつけない
Construct ID にConstruct ID は Construct のスコープ内での一意性を担保するだけではなく、論理 ID と自動生成されるリソース名に使用されます。
Construct ID にConstruct
を含めてしまうと、CDK の世界で止めるべき事情が CloudFormation テンプレート や AWS の世界に漏れてしまいます。
Construct のクラス名と同じ名前を Construct ID につけたくなった場合、一歩踏みとどまって、論理 ID やリソース名に使われた時にどう見えるかを考えて命名しましょう。
Stack
のConstruct IDについても同様です。
⭕️ Good
new ApiConstruct(this, "Api");
new Feature1Stack(this, "Feature1")
❌ Bad
new ApiConstruct(this, "ApiConstruct");
new Feature1Stack(this, "Feature1Stack")
技術詳細を指定しすぎない
Construct ID に技術詳細を指定してしまうと、コンストラクトの中で変更があったときにConstruct IDも変更したくなります。
本番稼働後であれば論理 ID 変更によるリソース再作成を防ぐため、Construct IDを変更できない負債となってしまう可能性があります。
適切に抽象化しておくことで論理 ID の変更を防げます。
⭕️ Good
new ApiConstruct('Api')
❌ Bad
new ApiGatewaySqsLambdaConstruct('ApiGatewaySqsLambda')
親 コンストラクト で表している情報を繰り返さない
論理 ID は コンストラクト のパスツリーを結合して生成されます。
親 コンストラクト で表している情報を繰り返さないようにしましょう。
⭕️ Good
// ApiGatewayとRoleを定義するカスタムコンストラクト
class ApiConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
const role = new iam.Role(this, "ExecutionRole");
// 省略
}
}
// 親コンストラクト
class ParentConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
new ApiConstruct(this, "Api");
}
}
IAM Role の論理 ID 生成用パスツリーはApi
+ExecutionRole
でApiExecutionRole
となります。
❌ Bad
// ApiGatewayとRoleを定義するカスタムコンストラクト
class ApiConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
const role = new iam.Role(this, "ApiExecutionRole");
// 省略
}
}
// 親コンストラクト
class ParentConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
new ApiConstruct(this, "Api");
}
}
IAM Role の論理 ID 生成用パスツリーはApi
+ApiExecutionRole
でApiApiExecutionRole
となり、ApiApi
が冗長です。
Resource
を使うべき場合
Resource
は、再利用するカスタムコンストラクトを作成する際に、論理 ID を短くするテクニックとして使用できます。
たとえば、共通する設定を事前に定義しておく以下のようなコンストラクト です。
export class LambdaConstruct extends Construct {
readonly function: nodejs.NodejsFunction;
constructor(scope: Construct, id: string, props: LambdaConstructProps) {
super(scope, id);
this.function = new nodejs.NodejsFunction(this, "Resource", {
functionName,
handler: "handler",
tracing: lambda.Tracing.ACTIVE,
runtime: lambda.Runtime.NODEJS_20_X,
timeout: Duration.seconds(5),
memorySize: 1769,
architecture: lambda.Architecture.ARM_64,
...props,
});
}
}
このようなケースでは LambdaConstruct
を利用する側で、LambdaConstruct
に対して関数の役割を示す Construct ID を指定することになります。
export class ParentConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
new LambdaConstruct(this, "CreateTodo");
new LambdaConstruct(this, "GetTodo");
new LambdaConstruct(this, "DeleteTodo");
}
}
new ParentConstruct(this, "Parent");
カスタムコンストラクト 側でResource
を使用しているため、それぞれのパスツリーは以下になります。
Parent/CreateTodo/Resource
Parent/GetTodo/Resource
Parent/DeleteTodo/Resource
前述の通り、Resource
は論理 ID の可読部を生成する際に除去されるため、論理 ID を短くできます。
ParentCreateTodoXXXXXXXX
ParentGetTodoXXXXXXXX
ParentDeleteTodoXXXXXXXX
この理由から、多くの L2 コンストラクトの中でメインとなるリソースのConstruct ID はResource
が採用されています。
コンストラクトライブラリの開発者でなくても、Resource
を適切に使用することでリソース名の可読性を高めることができるため、覚えておいて損はないでしょう。
Default
を使う場合
Default
もResource
と同様に論理 ID を短くするテクニックとして使えますが、前述した論理 ID 重複の可能性を意識しつつリファクタリングの用途での使用できます。
Default
を利用したコンストラクトツリーの整理方法は以下のブログで紹介されています。
再利用を想定するカスタムコンストラクトは、そのコンストラクトからリソース移動などのリファクタリングを考慮する可能性は低く、論理 ID 重複が発生しづらいResource
で定義しておくことをオススメします。
機能単位やリソース単位など再利用を想定せずにコンストラクトを構造化する場合、将来的にリソース移動のリファクタリングが必要になる可能性があるため、Default
で定義しておくのはいかがでしょうか。
たとえば、ApiConstruct
の中にApiGateway
と複数のLambda
を定義する際、ApiGateway
にDefault
を指定しておくことで将来的に APIGateway をコンストラクトの外に引き上げることができます。
Construct ID は論理 ID と生成されるリソース名を考えて命名しましょう
CDK によって自動生成されるリソース名は、論理 ID が利用されつつも一定で打ち切られてランダムな文字列が付与されるため、一定の可読性が損なわれるのは仕方ないと諦めている節がありました。
こちらの記事により CDK で定義したリソースの命名の仕組みが理解できたので、それを踏まえた上で Construct ID はどう指定すべきなのか、自戒も込めてこの記事にまとめました。
Construct ID の命名方法が腹落ちしたので、今後は適切に指定して運用しやすいインフラを構築していきたいです。
最後に
この記事が誰かの役に立つと幸いです。
以上。リテールアプリ共創部のきんじょーでした。
参考
大変参考にさせていただきました。