From 3dc9e541def0ed3e07183257eccb3ec1e6071bcc Mon Sep 17 00:00:00 2001 From: sm-utkarsh Date: Tue, 28 Jul 2026 01:06:26 +0530 Subject: [PATCH 1/3] feat: add hosted zone stack --- .gitignore | 1 + .projenrc.js | 1 + API.md | 1149 +++++++++++++++++++++++++++++++++ README.md | 29 +- src/constructs/hosted-zone.ts | 82 +++ src/constructs/network.ts | 10 +- src/index.ts | 3 +- 7 files changed, 1270 insertions(+), 5 deletions(-) create mode 100644 src/constructs/hosted-zone.ts diff --git a/.gitignore b/.gitignore index 3ee15fd..e2d7198 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,5 @@ junit.xml .jsii tsconfig.json !/API.md +.DS_Store !/.projenrc.js diff --git a/.projenrc.js b/.projenrc.js index 7f0ab6a..62c80e7 100644 --- a/.projenrc.js +++ b/.projenrc.js @@ -41,4 +41,5 @@ const project = new AwsCdkConstructLibrary({ // devDeps: [], /* Build dependencies for this module. */ // packageName: undefined, /* The "name" in package.json. */ }); +project.gitignore.addPatterns('.DS_Store'); project.synth(); diff --git a/API.md b/API.md index f0db2be..46d3b0d 100644 --- a/API.md +++ b/API.md @@ -2,6 +2,978 @@ ## Constructs +### HostedZoneStack + +#### Initializers + +```typescript +import { HostedZoneStack } from '@smallcase/cdk-vpc-module' + +new HostedZoneStack(scope: Construct, id: string, props: HostedZoneStackProps) +``` + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| scope | constructs.Construct | *No description.* | +| id | string | *No description.* | +| props | HostedZoneStackProps | *No description.* | + +--- + +##### `scope`Required + +- *Type:* constructs.Construct + +--- + +##### `id`Required + +- *Type:* string + +--- + +##### `props`Required + +- *Type:* HostedZoneStackProps + +--- + +#### Methods + +| **Name** | **Description** | +| --- | --- | +| toString | Returns a string representation of this construct. | +| addDependency | Add a dependency between this stack and another stack. | +| addMetadata | Adds an arbitrary key-value pair, with information you want to record about the stack. | +| addStackTag | Configure a stack tag. | +| addTransform | Add a Transform to this stack. A Transform is a macro that AWS CloudFormation uses to process your template. | +| exportStringListValue | Create a CloudFormation Export for a string list value. | +| exportValue | Create a CloudFormation Export for a string value. | +| formatArn | Creates an ARN from components. | +| getLogicalId | Allocates a stack-unique CloudFormation-compatible logical identity for a specific resource. | +| regionalFact | Look up a fact value for the given fact for the region of this stack. | +| removeStackTag | Remove a stack tag. | +| renameLogicalId | Rename a generated logical identities. | +| reportMissingContextKey | Indicate that a context key was expected. | +| resolve | Resolve a tokenized value in the context of the current stack. | +| splitArn | Splits the provided ARN into its components. | +| toJsonString | Convert an object, potentially containing tokens, to a JSON string. | +| toYamlString | Convert an object, potentially containing tokens, to a YAML string. | +| setParameter | Assign a value to one of the nested stack parameters. | + +--- + +##### `toString` + +```typescript +public toString(): string +``` + +Returns a string representation of this construct. + +##### `addDependency` + +```typescript +public addDependency(target: Stack, reason?: string): void +``` + +Add a dependency between this stack and another stack. + +This can be used to define dependencies between any two stacks within an +app, and also supports nested stacks. + +###### `target`Required + +- *Type:* aws-cdk-lib.Stack + +--- + +###### `reason`Optional + +- *Type:* string + +--- + +##### `addMetadata` + +```typescript +public addMetadata(key: string, value: any): void +``` + +Adds an arbitrary key-value pair, with information you want to record about the stack. + +These get translated to the Metadata section of the generated template. + +> [https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html) + +###### `key`Required + +- *Type:* string + +--- + +###### `value`Required + +- *Type:* any + +--- + +##### `addStackTag` + +```typescript +public addStackTag(tagName: string, tagValue: string): void +``` + +Configure a stack tag. + +At deploy time, CloudFormation will automatically apply all stack tags to all resources in the stack. + +###### `tagName`Required + +- *Type:* string + +--- + +###### `tagValue`Required + +- *Type:* string + +--- + +##### `addTransform` + +```typescript +public addTransform(transform: string): void +``` + +Add a Transform to this stack. A Transform is a macro that AWS CloudFormation uses to process your template. + +Duplicate values are removed when stack is synthesized. + +> [https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-section-structure.html](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-section-structure.html) + +*Example* + +```typescript +declare const stack: Stack; + +stack.addTransform('AWS::Serverless-2016-10-31') +``` + + +###### `transform`Required + +- *Type:* string + +The transform to add. + +--- + +##### `exportStringListValue` + +```typescript +public exportStringListValue(exportedValue: any, options?: ExportValueOptions): string[] +``` + +Create a CloudFormation Export for a string list value. + +Returns a string list representing the corresponding `Fn.importValue()` +expression for this Export. The export expression is automatically wrapped with an +`Fn::Join` and the import value with an `Fn::Split`, since CloudFormation can only +export strings. You can control the name for the export by passing the `name` option. + +If you don't supply a value for `name`, the value you're exporting must be +a Resource attribute (for example: `bucket.bucketName`) and it will be +given the same name as the automatic cross-stack reference that would be created +if you used the attribute in another Stack. + +One of the uses for this method is to *remove* the relationship between +two Stacks established by automatic cross-stack references. It will +temporarily ensure that the CloudFormation Export still exists while you +remove the reference from the consuming stack. After that, you can remove +the resource and the manual export. + +See `exportValue` for an example of this process. + +###### `exportedValue`Required + +- *Type:* any + +--- + +###### `options`Optional + +- *Type:* aws-cdk-lib.ExportValueOptions + +--- + +##### `exportValue` + +```typescript +public exportValue(exportedValue: any, options?: ExportValueOptions): string +``` + +Create a CloudFormation Export for a string value. + +Returns a string representing the corresponding `Fn.importValue()` +expression for this Export. You can control the name for the export by +passing the `name` option. + +If you don't supply a value for `name`, the value you're exporting must be +a Resource attribute (for example: `bucket.bucketName`) and it will be +given the same name as the automatic cross-stack reference that would be created +if you used the attribute in another Stack. + +One of the uses for this method is to *remove* the relationship between +two Stacks established by automatic cross-stack references. It will +temporarily ensure that the CloudFormation Export still exists while you +remove the reference from the consuming stack. After that, you can remove +the resource and the manual export. + +Here is how the process works. Let's say there are two stacks, +`producerStack` and `consumerStack`, and `producerStack` has a bucket +called `bucket`, which is referenced by `consumerStack` (perhaps because +an AWS Lambda Function writes into it, or something like that). + +It is not safe to remove `producerStack.bucket` because as the bucket is being +deleted, `consumerStack` might still be using it. + +Instead, the process takes two deployments: + +**Deployment 1: break the relationship**: + +- Make sure `consumerStack` no longer references `bucket.bucketName` (maybe the consumer + stack now uses its own bucket, or it writes to an AWS DynamoDB table, or maybe you just + remove the Lambda Function altogether). +- In the `ProducerStack` class, call `this.exportValue(this.bucket.bucketName)`. This + will make sure the CloudFormation Export continues to exist while the relationship + between the two stacks is being broken. +- Deploy (this will effectively only change the `consumerStack`, but it's safe to deploy both). + +**Deployment 2: remove the bucket resource**: + +- You are now free to remove the `bucket` resource from `producerStack`. +- Don't forget to remove the `exportValue()` call as well. +- Deploy again (this time only the `producerStack` will be changed -- the bucket will be deleted). + +###### `exportedValue`Required + +- *Type:* any + +--- + +###### `options`Optional + +- *Type:* aws-cdk-lib.ExportValueOptions + +--- + +##### `formatArn` + +```typescript +public formatArn(components: ArnComponents): string +``` + +Creates an ARN from components. + +If `partition`, `region` or `account` are not specified, the stack's +partition, region and account will be used. + +If any component is the empty string, an empty string will be inserted +into the generated ARN at the location that component corresponds to. + +The ARN will be formatted as follows: + + arn:{partition}:{service}:{region}:{account}:{resource}{sep}{resource-name} + +The required ARN pieces that are omitted will be taken from the stack that +the 'scope' is attached to. If all ARN pieces are supplied, the supplied scope +can be 'undefined'. + +###### `components`Required + +- *Type:* aws-cdk-lib.ArnComponents + +--- + +##### `getLogicalId` + +```typescript +public getLogicalId(element: CfnElement): string +``` + +Allocates a stack-unique CloudFormation-compatible logical identity for a specific resource. + +This method is called when a `CfnElement` is created and used to render the +initial logical identity of resources. Logical ID renames are applied at +this stage. + +This method uses the protected method `allocateLogicalId` to render the +logical ID for an element. To modify the naming scheme, extend the `Stack` +class and override this method. + +###### `element`Required + +- *Type:* aws-cdk-lib.CfnElement + +The CloudFormation element for which a logical identity is needed. + +--- + +##### `regionalFact` + +```typescript +public regionalFact(factName: string, defaultValue?: string): string +``` + +Look up a fact value for the given fact for the region of this stack. + +Will return a definite value only if the region of the current stack is resolved. +If not, a lookup map will be added to the stack and the lookup will be done at +CDK deployment time. + +What regions will be included in the lookup map is controlled by the +`@aws-cdk/core:target-partitions` context value: it must be set to a list +of partitions, and only regions from the given partitions will be included. +If no such context key is set, all regions will be included. + +This function is intended to be used by construct library authors. Application +builders can rely on the abstractions offered by construct libraries and do +not have to worry about regional facts. + +If `defaultValue` is not given, it is an error if the fact is unknown for +the given region. + +###### `factName`Required + +- *Type:* string + +--- + +###### `defaultValue`Optional + +- *Type:* string + +--- + +##### `removeStackTag` + +```typescript +public removeStackTag(tagName: string): void +``` + +Remove a stack tag. + +At deploy time, CloudFormation will automatically apply all stack tags to all resources in the stack. + +###### `tagName`Required + +- *Type:* string + +--- + +##### `renameLogicalId` + +```typescript +public renameLogicalId(oldId: string, newId: string): void +``` + +Rename a generated logical identities. + +To modify the naming scheme strategy, extend the `Stack` class and +override the `allocateLogicalId` method. + +###### `oldId`Required + +- *Type:* string + +--- + +###### `newId`Required + +- *Type:* string + +--- + +##### `reportMissingContextKey` + +```typescript +public reportMissingContextKey(report: MissingContext): void +``` + +Indicate that a context key was expected. + +Contains instructions which will be emitted into the cloud assembly on how +the key should be supplied. + +###### `report`Required + +- *Type:* aws-cdk-lib.cloud_assembly_schema.MissingContext + +The set of parameters needed to obtain the context. + +--- + +##### `resolve` + +```typescript +public resolve(obj: any): any +``` + +Resolve a tokenized value in the context of the current stack. + +###### `obj`Required + +- *Type:* any + +--- + +##### `splitArn` + +```typescript +public splitArn(arn: string, arnFormat: ArnFormat): ArnComponents +``` + +Splits the provided ARN into its components. + +Works both if 'arn' is a string like 'arn:aws:s3:::bucket', +and a Token representing a dynamic CloudFormation expression +(in which case the returned components will also be dynamic CloudFormation expressions, +encoded as Tokens). + +###### `arn`Required + +- *Type:* string + +the ARN to split into its components. + +--- + +###### `arnFormat`Required + +- *Type:* aws-cdk-lib.ArnFormat + +the expected format of 'arn' - depends on what format the service 'arn' represents uses. + +--- + +##### `toJsonString` + +```typescript +public toJsonString(obj: any, space?: number): string +``` + +Convert an object, potentially containing tokens, to a JSON string. + +###### `obj`Required + +- *Type:* any + +--- + +###### `space`Optional + +- *Type:* number + +--- + +##### `toYamlString` + +```typescript +public toYamlString(obj: any): string +``` + +Convert an object, potentially containing tokens, to a YAML string. + +###### `obj`Required + +- *Type:* any + +--- + +##### `setParameter` + +```typescript +public setParameter(name: string, value: string): void +``` + +Assign a value to one of the nested stack parameters. + +###### `name`Required + +- *Type:* string + +The parameter name (ID). + +--- + +###### `value`Required + +- *Type:* string + +The value to assign. + +--- + +#### Static Functions + +| **Name** | **Description** | +| --- | --- | +| isConstruct | Checks if `x` is a construct. | +| isStack | Return whether the given object is a Stack. | +| of | Looks up the first stack scope in which `construct` is defined. | +| isNestedStack | Checks if `x` is an object of type `NestedStack`. | + +--- + +##### ~~`isConstruct`~~ + +```typescript +import { HostedZoneStack } from '@smallcase/cdk-vpc-module' + +HostedZoneStack.isConstruct(x: any) +``` + +Checks if `x` is a construct. + +###### `x`Required + +- *Type:* any + +Any object. + +--- + +##### `isStack` + +```typescript +import { HostedZoneStack } from '@smallcase/cdk-vpc-module' + +HostedZoneStack.isStack(x: any) +``` + +Return whether the given object is a Stack. + +We do attribute detection since we can't reliably use 'instanceof'. + +###### `x`Required + +- *Type:* any + +--- + +##### `of` + +```typescript +import { HostedZoneStack } from '@smallcase/cdk-vpc-module' + +HostedZoneStack.of(construct: IConstruct) +``` + +Looks up the first stack scope in which `construct` is defined. + +Fails if there is no stack up the tree. + +###### `construct`Required + +- *Type:* constructs.IConstruct + +The construct to start the search from. + +--- + +##### `isNestedStack` + +```typescript +import { HostedZoneStack } from '@smallcase/cdk-vpc-module' + +HostedZoneStack.isNestedStack(x: any) +``` + +Checks if `x` is an object of type `NestedStack`. + +###### `x`Required + +- *Type:* any + +--- + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| node | constructs.Node | The tree node. | +| account | string | The AWS account into which this stack will be deployed. | +| artifactId | string | The ID of the cloud assembly artifact for this stack. | +| availabilityZones | string[] | Returns the list of AZs that are available in the AWS environment (account/region) associated with this stack. | +| bundlingRequired | boolean | Indicates whether the stack requires bundling or not. | +| dependencies | aws-cdk-lib.Stack[] | Return the stacks this stack depends on. | +| environment | string | The environment coordinates in which this stack is deployed. | +| nested | boolean | Indicates if this is a nested stack, in which case `parentStack` will include a reference to it's parent. | +| notificationArns | string[] | Returns the list of notification Amazon Resource Names (ARNs) for the current stack. | +| partition | string | The partition in which this stack is defined. | +| region | string | The AWS region into which this stack will be deployed (e.g. `us-west-2`). | +| stackId | string | An attribute that represents the ID of the stack. | +| stackName | string | An attribute that represents the name of the nested stack. | +| synthesizer | aws-cdk-lib.IStackSynthesizer | Synthesis method for this stack. | +| tags | aws-cdk-lib.TagManager | Tags to be applied to the stack. | +| templateFile | string | The name of the CloudFormation template file emitted to the output directory during synthesis. | +| templateOptions | aws-cdk-lib.ITemplateOptions | Options for CloudFormation template (like version, transform, description). | +| urlSuffix | string | The Amazon domain suffix for the region in which this stack is defined. | +| nestedStackParent | aws-cdk-lib.Stack | If this is a nested stack, returns it's parent stack. | +| nestedStackResource | aws-cdk-lib.CfnResource | If this is a nested stack, this represents its `AWS::CloudFormation::Stack` resource. | +| terminationProtection | boolean | Whether termination protection is enabled for this stack. | +| certificateOutputs | {[ key: string ]: aws-cdk-lib.aws_certificatemanager.Certificate} | *No description.* | +| hostedZoneOutputs | {[ key: string ]: aws-cdk-lib.aws_route53.IHostedZone} | *No description.* | + +--- + +##### `node`Required + +```typescript +public readonly node: Node; +``` + +- *Type:* constructs.Node + +The tree node. + +--- + +##### `account`Required + +```typescript +public readonly account: string; +``` + +- *Type:* string + +The AWS account into which this stack will be deployed. + +This value is resolved according to the following rules: + +1. The value provided to `env.account` when the stack is defined. This can + either be a concrete account (e.g. `585695031111`) or the + `Aws.ACCOUNT_ID` token. +3. `Aws.ACCOUNT_ID`, which represents the CloudFormation intrinsic reference + `{ "Ref": "AWS::AccountId" }` encoded as a string token. + +Preferably, you should use the return value as an opaque string and not +attempt to parse it to implement your logic. If you do, you must first +check that it is a concrete value an not an unresolved token. If this +value is an unresolved token (`Token.isUnresolved(stack.account)` returns +`true`), this implies that the user wishes that this stack will synthesize +into an **account-agnostic template**. In this case, your code should either +fail (throw an error, emit a synth error using `Annotations.of(construct).addError()`) or +implement some other account-agnostic behavior. + +--- + +##### `artifactId`Required + +```typescript +public readonly artifactId: string; +``` + +- *Type:* string + +The ID of the cloud assembly artifact for this stack. + +--- + +##### `availabilityZones`Required + +```typescript +public readonly availabilityZones: string[]; +``` + +- *Type:* string[] + +Returns the list of AZs that are available in the AWS environment (account/region) associated with this stack. + +If the stack is environment-agnostic (either account and/or region are +tokens), this property will return an array with 2 tokens that will resolve +at deploy-time to the first two availability zones returned from CloudFormation's +`Fn::GetAZs` intrinsic function. + +If they are not available in the context, returns a set of dummy values and +reports them as missing, and let the CLI resolve them by calling EC2 +`DescribeAvailabilityZones` on the target environment. + +To specify a different strategy for selecting availability zones override this method. + +--- + +##### `bundlingRequired`Required + +```typescript +public readonly bundlingRequired: boolean; +``` + +- *Type:* boolean + +Indicates whether the stack requires bundling or not. + +--- + +##### `dependencies`Required + +```typescript +public readonly dependencies: Stack[]; +``` + +- *Type:* aws-cdk-lib.Stack[] + +Return the stacks this stack depends on. + +--- + +##### `environment`Required + +```typescript +public readonly environment: string; +``` + +- *Type:* string + +The environment coordinates in which this stack is deployed. + +In the form +`aws://account/region`. Use `stack.account` and `stack.region` to obtain +the specific values, no need to parse. + +You can use this value to determine if two stacks are targeting the same +environment. + +If either `stack.account` or `stack.region` are not concrete values (e.g. +`Aws.ACCOUNT_ID` or `Aws.REGION`) the special strings `unknown-account` and/or +`unknown-region` will be used respectively to indicate this stack is +region/account-agnostic. + +--- + +##### `nested`Required + +```typescript +public readonly nested: boolean; +``` + +- *Type:* boolean + +Indicates if this is a nested stack, in which case `parentStack` will include a reference to it's parent. + +--- + +##### `notificationArns`Required + +```typescript +public readonly notificationArns: string[]; +``` + +- *Type:* string[] + +Returns the list of notification Amazon Resource Names (ARNs) for the current stack. + +--- + +##### `partition`Required + +```typescript +public readonly partition: string; +``` + +- *Type:* string + +The partition in which this stack is defined. + +--- + +##### `region`Required + +```typescript +public readonly region: string; +``` + +- *Type:* string + +The AWS region into which this stack will be deployed (e.g. `us-west-2`). + +This value is resolved according to the following rules: + +1. The value provided to `env.region` when the stack is defined. This can + either be a concrete region (e.g. `us-west-2`) or the `Aws.REGION` + token. +3. `Aws.REGION`, which is represents the CloudFormation intrinsic reference + `{ "Ref": "AWS::Region" }` encoded as a string token. + +Preferably, you should use the return value as an opaque string and not +attempt to parse it to implement your logic. If you do, you must first +check that it is a concrete value an not an unresolved token. If this +value is an unresolved token (`Token.isUnresolved(stack.region)` returns +`true`), this implies that the user wishes that this stack will synthesize +into a **region-agnostic template**. In this case, your code should either +fail (throw an error, emit a synth error using `Annotations.of(construct).addError()`) or +implement some other region-agnostic behavior. + +--- + +##### `stackId`Required + +```typescript +public readonly stackId: string; +``` + +- *Type:* string + +An attribute that represents the ID of the stack. + +This is a context aware attribute: +- If this is referenced from the parent stack, it will return `{ "Ref": "LogicalIdOfNestedStackResource" }`. +- If this is referenced from the context of the nested stack, it will return `{ "Ref": "AWS::StackId" }` + +Example value: `arn:aws:cloudformation:us-east-2:123456789012:stack/mystack-mynestedstack-sggfrhxhum7w/f449b250-b969-11e0-a185-5081d0136786` + +--- + +##### `stackName`Required + +```typescript +public readonly stackName: string; +``` + +- *Type:* string + +An attribute that represents the name of the nested stack. + +This is a context aware attribute: +- If this is referenced from the parent stack, it will return a token that parses the name from the stack ID. +- If this is referenced from the context of the nested stack, it will return `{ "Ref": "AWS::StackName" }` + +Example value: `mystack-mynestedstack-sggfrhxhum7w` + +--- + +##### `synthesizer`Required + +```typescript +public readonly synthesizer: IStackSynthesizer; +``` + +- *Type:* aws-cdk-lib.IStackSynthesizer + +Synthesis method for this stack. + +--- + +##### `tags`Required + +```typescript +public readonly tags: TagManager; +``` + +- *Type:* aws-cdk-lib.TagManager + +Tags to be applied to the stack. + +--- + +##### `templateFile`Required + +```typescript +public readonly templateFile: string; +``` + +- *Type:* string + +The name of the CloudFormation template file emitted to the output directory during synthesis. + +Example value: `MyStack.template.json` + +--- + +##### `templateOptions`Required + +```typescript +public readonly templateOptions: ITemplateOptions; +``` + +- *Type:* aws-cdk-lib.ITemplateOptions + +Options for CloudFormation template (like version, transform, description). + +--- + +##### `urlSuffix`Required + +```typescript +public readonly urlSuffix: string; +``` + +- *Type:* string + +The Amazon domain suffix for the region in which this stack is defined. + +--- + +##### `nestedStackParent`Optional + +```typescript +public readonly nestedStackParent: Stack; +``` + +- *Type:* aws-cdk-lib.Stack + +If this is a nested stack, returns it's parent stack. + +--- + +##### `nestedStackResource`Optional + +```typescript +public readonly nestedStackResource: CfnResource; +``` + +- *Type:* aws-cdk-lib.CfnResource + +If this is a nested stack, this represents its `AWS::CloudFormation::Stack` resource. + +`undefined` for top-level (non-nested) stacks. + +--- + +##### `terminationProtection`Required + +```typescript +public readonly terminationProtection: boolean; +``` + +- *Type:* boolean + +Whether termination protection is enabled for this stack. + +--- + +##### `certificateOutputs`Required + +```typescript +public readonly certificateOutputs: {[ key: string ]: Certificate}; +``` + +- *Type:* {[ key: string ]: aws-cdk-lib.aws_certificatemanager.Certificate} + +--- + +##### `hostedZoneOutputs`Required + +```typescript +public readonly hostedZoneOutputs: {[ key: string ]: IHostedZone}; +``` + +- *Type:* {[ key: string ]: aws-cdk-lib.aws_route53.IHostedZone} + +--- + + ### Network #### Initializers @@ -1280,6 +2252,183 @@ public readonly routerId: string; --- +### HostedZoneConfig + +#### Initializer + +```typescript +import { HostedZoneConfig } from '@smallcase/cdk-vpc-module' + +const hostedZoneConfig: HostedZoneConfig = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| publicZone | boolean | *No description.* | +| zoneName | string | *No description.* | +| createAcmCertificate | boolean | *No description.* | + +--- + +##### `publicZone`Required + +```typescript +public readonly publicZone: boolean; +``` + +- *Type:* boolean + +--- + +##### `zoneName`Required + +```typescript +public readonly zoneName: string; +``` + +- *Type:* string + +--- + +##### `createAcmCertificate`Optional + +```typescript +public readonly createAcmCertificate: boolean; +``` + +- *Type:* boolean + +--- + +### HostedZoneStackProps + +#### Initializer + +```typescript +import { HostedZoneStackProps } from '@smallcase/cdk-vpc-module' + +const hostedZoneStackProps: HostedZoneStackProps = { ... } +``` + +#### Properties + +| **Name** | **Type** | **Description** | +| --- | --- | --- | +| description | string | A description of the stack. | +| notificationArns | string[] | The Simple Notification Service (SNS) topics to publish stack related events. | +| parameters | {[ key: string ]: string} | The set value pairs that represent the parameters passed to CloudFormation when this nested stack is created. | +| removalPolicy | aws-cdk-lib.RemovalPolicy | Policy to apply when the nested stack is removed. | +| timeout | aws-cdk-lib.Duration | The length of time that CloudFormation waits for the nested stack to reach the CREATE_COMPLETE state. | +| hostedZones | HostedZoneConfig[] | *No description.* | +| vpc | aws-cdk-lib.aws_ec2.IVpc | *No description.* | + +--- + +##### `description`Optional + +```typescript +public readonly description: string; +``` + +- *Type:* string +- *Default:* No description. + +A description of the stack. + +--- + +##### `notificationArns`Optional + +```typescript +public readonly notificationArns: string[]; +``` + +- *Type:* string[] +- *Default:* notifications are not sent for this stack. + +The Simple Notification Service (SNS) topics to publish stack related events. + +--- + +##### `parameters`Optional + +```typescript +public readonly parameters: {[ key: string ]: string}; +``` + +- *Type:* {[ key: string ]: string} +- *Default:* no user-defined parameters are passed to the nested stack + +The set value pairs that represent the parameters passed to CloudFormation when this nested stack is created. + +Each parameter has a name corresponding +to a parameter defined in the embedded template and a value representing +the value that you want to set for the parameter. + +The nested stack construct will automatically synthesize parameters in order +to bind references from the parent stack(s) into the nested stack. + +--- + +##### `removalPolicy`Optional + +```typescript +public readonly removalPolicy: RemovalPolicy; +``` + +- *Type:* aws-cdk-lib.RemovalPolicy +- *Default:* RemovalPolicy.DESTROY + +Policy to apply when the nested stack is removed. + +The default is `Destroy`, because all Removal Policies of resources inside the +Nested Stack should already have been set correctly. You normally should +not need to set this value. + +--- + +##### `timeout`Optional + +```typescript +public readonly timeout: Duration; +``` + +- *Type:* aws-cdk-lib.Duration +- *Default:* no timeout + +The length of time that CloudFormation waits for the nested stack to reach the CREATE_COMPLETE state. + +When CloudFormation detects that the nested stack has reached the +CREATE_COMPLETE state, it marks the nested stack resource as +CREATE_COMPLETE in the parent stack and resumes creating the parent stack. +If the timeout period expires before the nested stack reaches +CREATE_COMPLETE, CloudFormation marks the nested stack as failed and rolls +back both the nested stack and parent stack. + +--- + +##### `hostedZones`Required + +```typescript +public readonly hostedZones: HostedZoneConfig[]; +``` + +- *Type:* HostedZoneConfig[] + +--- + +##### `vpc`Optional + +```typescript +public readonly vpc: IVpc; +``` + +- *Type:* aws-cdk-lib.aws_ec2.IVpc + +--- + ### LoadBalancerConfig #### Initializer diff --git a/README.md b/README.md index f11d824..d38c1f9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ cdk-vpc-module construct library is an open-source extension of the AWS Cloud De - :white_check_mark: VPC Peering with route table entry - :white_check_mark: Configurable NACL as per subnet group - :white_check_mark: NATGateway as per availabilityZones +- :white_check_mark: Public/private Route 53 hosted zones with optional public ACM certificate Using cdk a vpc can be deployed using the following sample code snippet: @@ -195,6 +196,32 @@ app.synth(); ``` Please refer [here](/API.md) to check how to use individual resource constructs. +## Route 53 hosted zones + +Hosted zones are modeled separately from the `Network` construct. For private +hosted zones, pass the VPC from the network stack and the zone is associated +with that VPC automatically. ACM certificate creation is supported only for +public hosted zones and creates a certificate for both `` and `*.`. + +```typescript +import { HostedZoneStack } from '@smallcase/cdk-vpc-module/lib/constructs/hosted-zone'; + +const hostedZones = new HostedZoneStack(this, 'HostedZones', { + vpc: network.vpc, + hostedZones: [ + { + zoneName: 'example.smallcase.com', + publicZone: true, + createAcmCertificate: true, + }, + { + zoneName: 'internal.example.smallcase.com', + publicZone: false, + }, + ], +}); +``` + ## :clapper: Quick Start The quick start shows you how to create an **AWS-VPC** using this module. @@ -296,4 +323,4 @@ Here’s a breakdown of the configuration options available: -- :white_check_mark: Configurable route table entry naming for subnet routes via `routeTableStringFormat` \ No newline at end of file +- :white_check_mark: Configurable route table entry naming for subnet routes via `routeTableStringFormat` diff --git a/src/constructs/hosted-zone.ts b/src/constructs/hosted-zone.ts new file mode 100644 index 0000000..811afc4 --- /dev/null +++ b/src/constructs/hosted-zone.ts @@ -0,0 +1,82 @@ +import { + aws_certificatemanager as acm, + aws_ec2 as ec2, + aws_route53 as route53, + CfnOutput, + Fn, + NestedStack, + NestedStackProps, +} from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +export interface HostedZoneConfig { + readonly zoneName: string; + readonly publicZone: boolean; + readonly createAcmCertificate?: boolean; +} + +export interface HostedZoneStackProps extends NestedStackProps { + readonly hostedZones: HostedZoneConfig[]; + readonly vpc?: ec2.IVpc; +} + +export class HostedZoneStack extends NestedStack { + public readonly hostedZoneOutputs: { [key: string]: route53.IHostedZone } = {}; + public readonly certificateOutputs: { [key: string]: acm.Certificate } = {}; + + constructor(scope: Construct, id: string, props: HostedZoneStackProps) { + super(scope, id, props); + + props.hostedZones.forEach((hostedZoneConfig) => { + this.addHostedZone(hostedZoneConfig, props.vpc); + }); + } + + private addHostedZone(hostedZoneConfig: HostedZoneConfig, vpc?: ec2.IVpc) { + const idPrefix = hostedZoneConfig.zoneName.replace(/[^A-Za-z0-9]/g, ''); + if (!hostedZoneConfig.publicZone && !vpc) { + throw new Error( + `Private hosted zone requires a VPC association: ${hostedZoneConfig.zoneName}`, + ); + } + if (!hostedZoneConfig.publicZone && hostedZoneConfig.createAcmCertificate) { + throw new Error( + `ACM certificate generation is supported only for public hosted zones: ${hostedZoneConfig.zoneName}`, + ); + } + + const hostedZone = hostedZoneConfig.publicZone + ? new route53.PublicHostedZone(this, `${idPrefix}PublicHostedZone`, { + zoneName: hostedZoneConfig.zoneName, + }) + : new route53.PrivateHostedZone(this, `${idPrefix}PrivateHostedZone`, { + zoneName: hostedZoneConfig.zoneName, + vpc: vpc!, + }); + + this.hostedZoneOutputs[hostedZoneConfig.zoneName] = hostedZone; + new CfnOutput(this, `${idPrefix}HostedZoneId`, { + value: hostedZone.hostedZoneId, + description: `Hosted zone ID for ${hostedZoneConfig.zoneName}`, + }); + if (hostedZoneConfig.publicZone) { + new CfnOutput(this, `${idPrefix}NameServers`, { + value: Fn.join(',', hostedZone.hostedZoneNameServers ?? []), + description: `Name servers for ${hostedZoneConfig.zoneName}`, + }); + } + + if (hostedZoneConfig.createAcmCertificate) { + const certificate = new acm.Certificate(this, `${idPrefix}Certificate`, { + domainName: hostedZoneConfig.zoneName, + subjectAlternativeNames: [`*.${hostedZoneConfig.zoneName}`], + validation: acm.CertificateValidation.fromDns(hostedZone), + }); + this.certificateOutputs[hostedZoneConfig.zoneName] = certificate; + new CfnOutput(this, `${idPrefix}CertificateArn`, { + value: certificate.certificateArn, + description: `ACM certificate ARN for ${hostedZoneConfig.zoneName}`, + }); + } + } +} diff --git a/src/constructs/network.ts b/src/constructs/network.ts index bbb57e4..93c3b07 100644 --- a/src/constructs/network.ts +++ b/src/constructs/network.ts @@ -1,4 +1,10 @@ -import { aws_ec2 as ec2, CfnOutput, Tags, aws_iam as iam, Stack } from 'aws-cdk-lib'; +import { + aws_ec2 as ec2, + CfnOutput, + Tags, + aws_iam as iam, + Stack, +} from 'aws-cdk-lib'; import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; import { Construct } from 'constructs'; import { SubnetStack } from './subnet-stack'; @@ -500,5 +506,3 @@ export class Network extends Construct { }); } } - - diff --git a/src/index.ts b/src/index.ts index 98987c3..15aac08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ +export * from './constructs/hosted-zone'; export * from './constructs/network'; -export * from './constructs/vpc-endpoint-service'; \ No newline at end of file +export * from './constructs/vpc-endpoint-service'; From 3eb2f4c0d23a6550c5e0fa1c391a6499e0178674 Mon Sep 17 00:00:00 2001 From: sm-utkarsh Date: Tue, 28 Jul 2026 01:32:47 +0530 Subject: [PATCH 2/3] feat: add hosted zone support to Network construct --- API.md | 22 ++++++++++++++++++++++ src/constructs/network.ts | 9 +++++++++ 2 files changed, 31 insertions(+) diff --git a/API.md b/API.md index 46d3b0d..67b8ba4 100644 --- a/API.md +++ b/API.md @@ -1092,6 +1092,7 @@ Any object. | natProvider | aws-cdk-lib.aws_ec2.NatProvider | *No description.* | | securityGroupOutputs | {[ key: string ]: aws-cdk-lib.aws_ec2.SecurityGroup} | *No description.* | | vpc | aws-cdk-lib.aws_ec2.Vpc | *No description.* | +| hostedZoneStack | HostedZoneStack | *No description.* | | natSubnets | aws-cdk-lib.aws_ec2.PublicSubnet[] | *No description.* | | pbSubnets | aws-cdk-lib.aws_ec2.PublicSubnet[] | *No description.* | | pvSubnets | aws-cdk-lib.aws_ec2.PrivateSubnet[] | *No description.* | @@ -1151,6 +1152,16 @@ public readonly vpc: Vpc; --- +##### `hostedZoneStack`Optional + +```typescript +public readonly hostedZoneStack: HostedZoneStack; +``` + +- *Type:* HostedZoneStack + +--- + ##### `natSubnets`Required ```typescript @@ -3203,6 +3214,7 @@ const vPCProps: VPCProps = { ... } | --- | --- | --- | | subnets | ISubnetsProps[] | *No description.* | | vpc | aws-cdk-lib.aws_ec2.VpcProps | *No description.* | +| hostedZones | HostedZoneConfig[] | *No description.* | | natEipAllocationIds | string[] | *No description.* | | peeringConfigs | {[ key: string ]: PeeringConfig} | *No description.* | | useNestedStacks | boolean | *No description.* | @@ -3231,6 +3243,16 @@ public readonly vpc: VpcProps; --- +##### `hostedZones`Optional + +```typescript +public readonly hostedZones: HostedZoneConfig[]; +``` + +- *Type:* HostedZoneConfig[] + +--- + ##### `natEipAllocationIds`Optional ```typescript diff --git a/src/constructs/network.ts b/src/constructs/network.ts index 93c3b07..48fde69 100644 --- a/src/constructs/network.ts +++ b/src/constructs/network.ts @@ -7,6 +7,7 @@ import { } from 'aws-cdk-lib'; import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; import { Construct } from 'constructs'; +import { HostedZoneConfig, HostedZoneStack } from './hosted-zone'; import { SubnetStack } from './subnet-stack'; import { VpcEndpointServiceNestedStack, VpcEndpontServiceConfig } from './vpc-endpoint-service'; import { ObjToStrMap } from '../utils/common'; @@ -77,6 +78,7 @@ export interface VPCProps { readonly vpcEndpoints?: VpcEndpointConfig[]; // List of VPC endpoints to configure readonly natEipAllocationIds?: string[]; readonly subnets: ISubnetsProps[]; + readonly hostedZones?: HostedZoneConfig[]; readonly vpcEndpointServices?: VpcEndpontServiceConfig[]; // List of VPC endpoint Service to configure readonly useNestedStacks?: boolean; } @@ -124,6 +126,7 @@ export class Network extends Construct { public readonly vpc!: ec2.Vpc; public readonly securityGroupOutputs: { [key: string]: ec2.SecurityGroup } = {}; // Store Security Group outputs public readonly endpointOutputs: { [key: string]: ec2.InterfaceVpcEndpoint | ec2.GatewayVpcEndpoint } = {}; // Store Endpoint outputs + public readonly hostedZoneStack?: HostedZoneStack; private peeringConnectionIds: PeeringConnectionInternalType = {}; public readonly natProvider!: ec2.NatProvider; constructor(scope: Construct, id: string, props: VPCProps) { @@ -204,6 +207,12 @@ export class Network extends Construct { }); } new CfnOutput(this, 'VpcId', { value: this.vpc.vpcId }); + if (props.hostedZones) { + this.hostedZoneStack = new HostedZoneStack(this, 'HostedZones', { + hostedZones: props.hostedZones, + vpc: this.vpc, + }); + } // Add VPC endpoints if specified in the props if (props?.vpcEndpoints) { for (const endpointConfig of props.vpcEndpoints) { From a5db9d6df8919c1bf3593ec118d828515edff59f Mon Sep 17 00:00:00 2001 From: sm-utkarsh Date: Tue, 28 Jul 2026 02:30:38 +0530 Subject: [PATCH 3/3] feat: enhance HostedZoneStack and Network interfaces with certificate validation and tagging support --- API.md | 44 +++++++++++++++++++++++ src/constructs/hosted-zone.ts | 68 ++++++++++++++++++++++++++++++----- src/constructs/network.ts | 2 ++ 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/API.md b/API.md index 67b8ba4..7a3e927 100644 --- a/API.md +++ b/API.md @@ -2279,7 +2279,9 @@ const hostedZoneConfig: HostedZoneConfig = { ... } | --- | --- | --- | | publicZone | boolean | *No description.* | | zoneName | string | *No description.* | +| certificateValidationZoneName | string | *No description.* | | createAcmCertificate | boolean | *No description.* | +| tags | {[ key: string ]: string} | *No description.* | --- @@ -2303,6 +2305,16 @@ public readonly zoneName: string; --- +##### `certificateValidationZoneName`Optional + +```typescript +public readonly certificateValidationZoneName: string; +``` + +- *Type:* string + +--- + ##### `createAcmCertificate`Optional ```typescript @@ -2313,6 +2325,16 @@ public readonly createAcmCertificate: boolean; --- +##### `tags`Optional + +```typescript +public readonly tags: {[ key: string ]: string}; +``` + +- *Type:* {[ key: string ]: string} + +--- + ### HostedZoneStackProps #### Initializer @@ -2333,6 +2355,7 @@ const hostedZoneStackProps: HostedZoneStackProps = { ... } | removalPolicy | aws-cdk-lib.RemovalPolicy | Policy to apply when the nested stack is removed. | | timeout | aws-cdk-lib.Duration | The length of time that CloudFormation waits for the nested stack to reach the CREATE_COMPLETE state. | | hostedZones | HostedZoneConfig[] | *No description.* | +| hostedZoneTags | {[ key: string ]: string} | *No description.* | | vpc | aws-cdk-lib.aws_ec2.IVpc | *No description.* | --- @@ -2430,6 +2453,16 @@ public readonly hostedZones: HostedZoneConfig[]; --- +##### `hostedZoneTags`Optional + +```typescript +public readonly hostedZoneTags: {[ key: string ]: string}; +``` + +- *Type:* {[ key: string ]: string} + +--- + ##### `vpc`Optional ```typescript @@ -3215,6 +3248,7 @@ const vPCProps: VPCProps = { ... } | subnets | ISubnetsProps[] | *No description.* | | vpc | aws-cdk-lib.aws_ec2.VpcProps | *No description.* | | hostedZones | HostedZoneConfig[] | *No description.* | +| hostedZoneTags | {[ key: string ]: string} | *No description.* | | natEipAllocationIds | string[] | *No description.* | | peeringConfigs | {[ key: string ]: PeeringConfig} | *No description.* | | useNestedStacks | boolean | *No description.* | @@ -3253,6 +3287,16 @@ public readonly hostedZones: HostedZoneConfig[]; --- +##### `hostedZoneTags`Optional + +```typescript +public readonly hostedZoneTags: {[ key: string ]: string}; +``` + +- *Type:* {[ key: string ]: string} + +--- + ##### `natEipAllocationIds`Optional ```typescript diff --git a/src/constructs/hosted-zone.ts b/src/constructs/hosted-zone.ts index 811afc4..d280af9 100644 --- a/src/constructs/hosted-zone.ts +++ b/src/constructs/hosted-zone.ts @@ -6,6 +6,7 @@ import { Fn, NestedStack, NestedStackProps, + Tags, } from 'aws-cdk-lib'; import { Construct } from 'constructs'; @@ -13,38 +14,39 @@ export interface HostedZoneConfig { readonly zoneName: string; readonly publicZone: boolean; readonly createAcmCertificate?: boolean; + readonly certificateValidationZoneName?: string; + readonly tags?: Record; } export interface HostedZoneStackProps extends NestedStackProps { readonly hostedZones: HostedZoneConfig[]; readonly vpc?: ec2.IVpc; + readonly hostedZoneTags?: Record; } export class HostedZoneStack extends NestedStack { public readonly hostedZoneOutputs: { [key: string]: route53.IHostedZone } = {}; public readonly certificateOutputs: { [key: string]: acm.Certificate } = {}; + private readonly publicValidationHostedZones: { [key: string]: route53.IHostedZone } = {}; constructor(scope: Construct, id: string, props: HostedZoneStackProps) { super(scope, id, props); props.hostedZones.forEach((hostedZoneConfig) => { - this.addHostedZone(hostedZoneConfig, props.vpc); + this.addHostedZone(hostedZoneConfig, props.vpc, props.hostedZoneTags); + }); + props.hostedZones.forEach((hostedZoneConfig) => { + this.addCertificate(hostedZoneConfig, props.hostedZoneTags); }); } - private addHostedZone(hostedZoneConfig: HostedZoneConfig, vpc?: ec2.IVpc) { + private addHostedZone(hostedZoneConfig: HostedZoneConfig, vpc?: ec2.IVpc, hostedZoneTags?: Record) { const idPrefix = hostedZoneConfig.zoneName.replace(/[^A-Za-z0-9]/g, ''); if (!hostedZoneConfig.publicZone && !vpc) { throw new Error( `Private hosted zone requires a VPC association: ${hostedZoneConfig.zoneName}`, ); } - if (!hostedZoneConfig.publicZone && hostedZoneConfig.createAcmCertificate) { - throw new Error( - `ACM certificate generation is supported only for public hosted zones: ${hostedZoneConfig.zoneName}`, - ); - } - const hostedZone = hostedZoneConfig.publicZone ? new route53.PublicHostedZone(this, `${idPrefix}PublicHostedZone`, { zoneName: hostedZoneConfig.zoneName, @@ -55,23 +57,30 @@ export class HostedZoneStack extends NestedStack { }); this.hostedZoneOutputs[hostedZoneConfig.zoneName] = hostedZone; + this.applyTags(hostedZone, this.resolveTags(hostedZoneTags, hostedZoneConfig.tags)); new CfnOutput(this, `${idPrefix}HostedZoneId`, { value: hostedZone.hostedZoneId, description: `Hosted zone ID for ${hostedZoneConfig.zoneName}`, }); if (hostedZoneConfig.publicZone) { + this.publicValidationHostedZones[hostedZoneConfig.zoneName] = hostedZone; new CfnOutput(this, `${idPrefix}NameServers`, { value: Fn.join(',', hostedZone.hostedZoneNameServers ?? []), description: `Name servers for ${hostedZoneConfig.zoneName}`, }); } + } + private addCertificate(hostedZoneConfig: HostedZoneConfig, hostedZoneTags?: Record) { if (hostedZoneConfig.createAcmCertificate) { + const idPrefix = hostedZoneConfig.zoneName.replace(/[^A-Za-z0-9]/g, ''); + const validationHostedZone = this.getCertificateValidationHostedZone(hostedZoneConfig); const certificate = new acm.Certificate(this, `${idPrefix}Certificate`, { domainName: hostedZoneConfig.zoneName, subjectAlternativeNames: [`*.${hostedZoneConfig.zoneName}`], - validation: acm.CertificateValidation.fromDns(hostedZone), + validation: acm.CertificateValidation.fromDns(validationHostedZone), }); + this.applyTags(certificate, this.resolveTags(hostedZoneTags, hostedZoneConfig.tags)); this.certificateOutputs[hostedZoneConfig.zoneName] = certificate; new CfnOutput(this, `${idPrefix}CertificateArn`, { value: certificate.certificateArn, @@ -79,4 +88,45 @@ export class HostedZoneStack extends NestedStack { }); } } + + private getCertificateValidationHostedZone(hostedZoneConfig: HostedZoneConfig): route53.IHostedZone { + const validationZoneName = hostedZoneConfig.publicZone + ? hostedZoneConfig.zoneName + : hostedZoneConfig.certificateValidationZoneName; + + if (!validationZoneName) { + throw new Error( + `ACM certificate generation for private hosted zone requires certificateValidationZoneName: ${hostedZoneConfig.zoneName}`, + ); + } + + if (this.publicValidationHostedZones[validationZoneName]) { + return this.publicValidationHostedZones[validationZoneName]; + } + + const idPrefix = validationZoneName.replace(/[^A-Za-z0-9]/g, ''); + const validationHostedZone = route53.HostedZone.fromLookup(this, `${idPrefix}CertificateValidationHostedZone`, { + domainName: validationZoneName, + privateZone: false, + }); + this.publicValidationHostedZones[validationZoneName] = validationHostedZone; + return validationHostedZone; + } + + private applyTags(scope: Construct, tags?: Record) { + if (!tags) { + return; + } + + Object.entries(tags).forEach(([key, value]) => { + Tags.of(scope).add(key, value); + }); + } + + private resolveTags(hostedZoneTags?: Record, hostedZoneConfigTags?: Record) { + return { + ...hostedZoneTags, + ...hostedZoneConfigTags, + }; + } } diff --git a/src/constructs/network.ts b/src/constructs/network.ts index 48fde69..73c722e 100644 --- a/src/constructs/network.ts +++ b/src/constructs/network.ts @@ -79,6 +79,7 @@ export interface VPCProps { readonly natEipAllocationIds?: string[]; readonly subnets: ISubnetsProps[]; readonly hostedZones?: HostedZoneConfig[]; + readonly hostedZoneTags?: Record; readonly vpcEndpointServices?: VpcEndpontServiceConfig[]; // List of VPC endpoint Service to configure readonly useNestedStacks?: boolean; } @@ -210,6 +211,7 @@ export class Network extends Construct { if (props.hostedZones) { this.hostedZoneStack = new HostedZoneStack(this, 'HostedZones', { hostedZones: props.hostedZones, + hostedZoneTags: props.hostedZoneTags, vpc: this.vpc, }); }