📛

PhysicalName.GENERATE_IF_NEEDED理解にphysical-name-generator.ts等確認してみる

2024/05/31に公開

前書き

cdkで物理名つけるのが面倒だったら使ってねというPhysicalName.GENERATE_IF_NEEDEDというものが存在するんですが、S3で試しに何も指定しない場合と、これで書いた場合の違いを確認してみました。

早速

※RemovalPolicy.DESTROYやautoDeleteOjbects: trueは省略。

lib/test-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { PhysicalName } from 'aws-cdk-lib/core';

export class CdkReleasesStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    new s3.Bucket(this, 'Bucket1', {
      bucketName: PhysicalName.GENERATE_IF_NEEDED, 
    });

    new s3.Bucket(this, 'Bucket2');
    new s3.CfnBucket(this, 'Bucket3');

    // new s3.CfnBucket(this, 'Bucket4',{
    //   bucketName: PhysicalName.GENERATE_IF_NEEDED, // 🙅‍♀️ Invalid physical name passed to CloudFormation. Use "this.physicalName" instead...
    // });
  }
}

🔻
結果


論理名:Bucket12520700A(ConstructId+8文字の15文字)
物理名:teststack-bucket12520700a-iobo3sn7tqqq(スタック名-論理名-ConstructId+12文字の38文字)


論理名:Bucket25524B414(ConstructId+8文字の15文字)
物理名:teststack-bucket25524b414-l5buxtkez6bq(スタック名-論理名-ConstructId+12文字の38文字)


論理名:Bucket3(ConstructId)
物理名:teststack-bucket3-lspo0pgif1o1(スタック名-論理名-ConstructId+12文字の30文字)

・PhysicalName.GENERATE_IF_NEEDEDはCfnBucket上では(或いはL1コンストラクトにおいては)機能しない。
・S3Bucketにおいては、PhysicalName.GENERATE_IF_NEEDEDを指定した場合も、bucketNameというpropsを指定しなかった場合も結果は同じ。
・どちらにおいてもスタック名-論理名-ConstructId+12文字になりそうでスタック名部分と論理名部分にランダム文字列を足したり、全体の文字数が決まった文字数に満たない場合であってもその分ランダム文字列で埋めるような事はしない。

って感じかな思いましたが、確信がないので少し変えてデプロイしてみます。

スタックやリソースのConstructIdの文字沢山入れてみる

bin/test.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkReleasesStack } from '../lib/cdk_releases-stack';

const app = new cdk.App();
new CdkReleasesStack(app, 'a'.repeat(1000), {
  synthesizer: new cdk.DefaultStackSynthesizer({
    generateBootstrapVersionRule: false,
}),
});
lib/test-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import { PhysicalName } from "aws-cdk-lib/core";

export class CdkReleasesStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    new s3.Bucket(this, "b".repeat(1000), {
      bucketName: PhysicalName.GENERATE_IF_NEEDED,
    });

    new s3.Bucket(this, "c".repeat(1000));
    new s3.CfnBucket(this, "d".repeat(1000));
  }
}

🔻

cdk deploy --all

✨  Synthesis time: 2.15s

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaCABE45DC:  start: Building XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:current_account-current_region
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaCABE45DC:  success: Built XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:current_account-current_region
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaCABE45DC:  start: Publishing XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:current_account-current_region
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaCABE45DC:  success: Published XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:current_account-current_region
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: deploying... [1/1]
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaCABE45DC: creating CloudFormation changeset...

 ✅  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

✨  Deployment time: 39.78s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:012345678901:stack/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaCABE45DC/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

✨  Total time: 41.93s

🔻
結果

スタック名:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaCABE45DC
(120文字のConstructIdと8文字のランダム文字列)

①②③共に
論理名:
(240文字のConstructIdと8文字のランダム文字列)
物理名:
(25文字のスタック名-24文字のConstructId-12文字のランダム文字列の系63文字)

この63文字という上限はあくまでS3の物理名のMaxで間違いないよなという事も念の為確認。

    new s3.CfnBucket(this, "e".repeat(1000),{
      bucketName: "e".repeat(64)
    });

🔻

Resource handler returned message: "Bucket name should be between 3 and 63 characters long"

なんとなく初めて会った気がしないエラーと再会し安心。
(最初からソースを見にいけば良いのですが)

bucket.ts
if (bucketName.length < 3 || bucketName.length > 63) {
      errors.push('Bucket name must be at least 3 and no more than 63 characters');
    }

physical-name.tsを確認してみる

https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/core/lib/physical-name.ts

physical-name.ts
import { GeneratedWhenNeededMarker } from './private/physical-name-generator';
import { Token } from './token';

/**
 * Includes special markers for automatic generation of physical names.
 */
export class PhysicalName {
  /**
   * Use this to automatically generate a physical name for an AWS resource only
   * if the resource is referenced across environments (account/region).
   * Otherwise, the name will be allocated during deployment by CloudFormation.
   *
   * If you are certain that a resource will be referenced across environments,
   * you may also specify an explicit physical name for it. This option is
   * mostly designed for reusable constructs which may or may not be referenced
   * across environments.
   */
  public static readonly GENERATE_IF_NEEDED = Token.asString(new GeneratedWhenNeededMarker());

  private constructor() { }
}
  • リソースが環境(アカウント/リージョン)をまたいで参照される場合のみ、AWS リソースの物理名を自動生成するために使用する。
  • リソースが環境(アカウント/リージョン)をまたいで参照される場合のみ。
  • そうでない場合は、CloudFormationによってデプロイ時に名前が割り当てられる。
  • リソースが環境間で参照されることが確実な場合、
  • 明示的に物理名を指定することもできます。このオプションは
  • このオプションは主に再利用可能なコンストラクトのために設計されています。
  • 再利用可能なコンストラクトのために設計されています。

physical-name-generator.tsを確認してみる

https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/core/lib/private/physical-name-generator.ts

physical-name-generator.ts
physical-name-generator.ts
import * as crypto from 'crypto';
import { Node } from 'constructs';
import { TokenMap } from './token-map';
import { Names } from '../names';
import { IResolvable, IResolveContext } from '../resolvable';
import { IResource } from '../resource';
import { Stack } from '../stack';
import { Token } from '../token';

export function generatePhysicalName(resource: IResource): string {
  const stack = Stack.of(resource);
  const stackPart = new PrefixNamePart(stack.stackName, 25);
  const idPart = new SuffixNamePart(Names.nodeUniqueId(resource.node), 24);

  const region: string = stack.region;
  if (Token.isUnresolved(region) || !region) {
    throw new Error(`Cannot generate a physical name for ${Node.of(resource).path}, because the region is un-resolved or missing`);
  }

  const account: string = stack.account;
  if (Token.isUnresolved(account) || !account) {
    throw new Error(`Cannot generate a physical name for ${Node.of(resource).path}, because the account is un-resolved or missing`);
  }

  const parts = [stackPart, idPart]
    .map(part => part.generate());

  const hashLength = 12;
  const sha256 = crypto.createHash('sha256')
    .update(stackPart.bareStr)
    .update(idPart.bareStr)
    .update(region)
    .update(account);
  const hash = sha256.digest('hex').slice(0, hashLength);

  const ret = [...parts, hash].join('');

  return ret.toLowerCase();
}

abstract class NamePart {
  public readonly bareStr: string;

  constructor(bareStr: string) {
    this.bareStr = bareStr;
  }

  public abstract generate(): string;
}

class PrefixNamePart extends NamePart {
  constructor(bareStr: string, private readonly prefixLength: number) {
    super(bareStr);
  }

  public generate(): string {
    return this.bareStr.slice(0, this.prefixLength);
  }
}

class SuffixNamePart extends NamePart {
  constructor(str: string, private readonly suffixLength: number) {
    super(str);
  }

  public generate(): string {
    const strLen = this.bareStr.length;
    const startIndex = Math.max(strLen - this.suffixLength, 0);
    return this.bareStr.slice(startIndex, strLen);
  }
}

const GENERATE_IF_NEEDED_SYMBOL = Symbol.for('@aws-cdk/core.<private>.GenerateIfNeeded');

/**
 * This marker token is used by PhysicalName.GENERATE_IF_NEEDED. When that token is passed to the
 * physicalName property of a Resource, it triggers different behavior in the Resource constructor
 * that will allow emission of a generated physical name (when the resource is used across
 * environments) or undefined (when the resource is not shared).
 *
 * This token throws an Error when it is resolved, as a way to prevent inadvertent mis-uses of it.
 */
export class GeneratedWhenNeededMarker implements IResolvable {
  public readonly creationStack: string[] = [];

  constructor() {
    Object.defineProperty(this, GENERATE_IF_NEEDED_SYMBOL, { value: true });
  }

  public resolve(_ctx: IResolveContext): never {
    throw new Error('Invalid physical name passed to CloudFormation. Use "this.physicalName" instead');
  }

  public toString(): string {
    return 'PhysicalName.GENERATE_IF_NEEDED';
  }
}

/**
 * Checks whether a stringified token resolves to a `GeneratedWhenNeededMarker`.
 */
export function isGeneratedWhenNeededMarker(val: string): boolean {
  const token = TokenMap.instance().lookupString(val);
  return !!token && GENERATE_IF_NEEDED_SYMBOL in token;
}

ChatGPT先生による説明


Bucket.tsファイル内でthis.physicalName指定を確認。

bucket.ts
    const resource = new CfnBucket(this, 'Resource', {
      bucketName: this.physicalName,

ついでにlogical-id.tsを確認してみる

https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/core/lib/private/logical-id.ts

logical-id.ts
logical-id.ts
/**
 * Class that keeps track of the logical IDs that are assigned to resources
 *
 * Supports renaming the generated IDs.
 */
export class LogicalIDs {
  /**
   * The rename table (old to new)
   */
  private readonly renames: {[old: string]: string} = {};

  /**
   * All assigned names (new to old, may be identical)
   *
   * This is used to ensure that:
   *
   * - No 2 resources end up with the same final logical ID, unless they were the same to begin with.
   * - All renames have been used at the end of renaming.
   */
  private readonly reverse: {[id: string]: string} = {};

  /**
   * Rename a logical ID from an old ID to a new ID
   */
  public addRename(oldId: string, newId: string) {
    if (oldId in this.renames) {
      throw new Error(`A rename has already been registered for '${oldId}'`);
    }
    this.renames[oldId] = newId;
  }

  /**
   * Return the renamed version of an ID or the original ID.
   */
  public applyRename(oldId: string) {
    let newId = oldId;
    if (oldId in this.renames) {
      newId = this.renames[oldId];
    }

    // If this newId has already been used, it must have been with the same oldId
    if (newId in this.reverse && this.reverse[newId] !== oldId) {
      // eslint-disable-next-line max-len
      throw new Error(`Two objects have been assigned the same Logical ID: '${this.reverse[newId]}' and '${oldId}' are now both named '${newId}'.`);
    }
    this.reverse[newId] = oldId;

    validateLogicalId(newId);
    return newId;
  }

  /**
   * Throw an error if not all renames have been used
   *
   * This is to assure that users didn't make typos when registering renames.
   */
  public assertAllRenamesApplied() {
    const keys = new Set<string>();
    Object.keys(this.renames).forEach(keys.add.bind(keys));

    Object.keys(this.reverse).map(newId => {
      keys.delete(this.reverse[newId]);
    });

    if (keys.size !== 0) {
      const unusedRenames = Array.from(keys.values());
      throw new Error(`The following Logical IDs were attempted to be renamed, but not found: ${unusedRenames.join(', ')}`);
    }
  }
}

const VALID_LOGICALID_REGEX = /^[A-Za-z0-9]{1,255}$/;

/**
 * Validate logical ID is valid for CloudFormation
 */
function validateLogicalId(logicalId: string) {
  if (!VALID_LOGICALID_REGEX.test(logicalId)) {
    throw new Error(`Logical ID must adhere to the regular expression: ${VALID_LOGICALID_REGEX.toString()}, got '${logicalId}'`);
  }

ChatGPT先生による説明



以上でした

若干眠気で朦朧としたまま書いてたのでなんらか間違いあったらすみません。

ただ明日から堂々と↓のような呼び方をするぞーというモチベーションにだけなりました。

前部分: stackPart
中部分: idPart
後部分: hashLength

有り難うございました。

おまけ

良いなと思いました。
https://qiita.com/hntk/items/5e941f658b64962a84d1

Discussion