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..7a3e927 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
@@ -120,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.* |
@@ -179,6 +1152,16 @@ public readonly vpc: Vpc;
---
+##### `hostedZoneStack`Optional
+
+```typescript
+public readonly hostedZoneStack: HostedZoneStack;
+```
+
+- *Type:* HostedZoneStack
+
+---
+
##### `natSubnets`Required
```typescript
@@ -1280,6 +2263,216 @@ 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.* |
+| certificateValidationZoneName | string | *No description.* |
+| createAcmCertificate | boolean | *No description.* |
+| tags | {[ key: string ]: string} | *No description.* |
+
+---
+
+##### `publicZone`Required
+
+```typescript
+public readonly publicZone: boolean;
+```
+
+- *Type:* boolean
+
+---
+
+##### `zoneName`Required
+
+```typescript
+public readonly zoneName: string;
+```
+
+- *Type:* string
+
+---
+
+##### `certificateValidationZoneName`Optional
+
+```typescript
+public readonly certificateValidationZoneName: string;
+```
+
+- *Type:* string
+
+---
+
+##### `createAcmCertificate`Optional
+
+```typescript
+public readonly createAcmCertificate: boolean;
+```
+
+- *Type:* boolean
+
+---
+
+##### `tags`Optional
+
+```typescript
+public readonly tags: {[ key: string ]: string};
+```
+
+- *Type:* {[ key: string ]: string}
+
+---
+
+### 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.* |
+| hostedZoneTags | {[ key: string ]: string} | *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[]
+
+---
+
+##### `hostedZoneTags`Optional
+
+```typescript
+public readonly hostedZoneTags: {[ key: string ]: string};
+```
+
+- *Type:* {[ key: string ]: string}
+
+---
+
+##### `vpc`Optional
+
+```typescript
+public readonly vpc: IVpc;
+```
+
+- *Type:* aws-cdk-lib.aws_ec2.IVpc
+
+---
+
### LoadBalancerConfig
#### Initializer
@@ -2054,6 +3247,8 @@ 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.* |
@@ -2082,6 +3277,26 @@ public readonly vpc: VpcProps;
---
+##### `hostedZones`Optional
+
+```typescript
+public readonly hostedZones: HostedZoneConfig[];
+```
+
+- *Type:* HostedZoneConfig[]
+
+---
+
+##### `hostedZoneTags`Optional
+
+```typescript
+public readonly hostedZoneTags: {[ key: string ]: string};
+```
+
+- *Type:* {[ key: string ]: string}
+
+---
+
##### `natEipAllocationIds`Optional
```typescript
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..d280af9
--- /dev/null
+++ b/src/constructs/hosted-zone.ts
@@ -0,0 +1,132 @@
+import {
+ aws_certificatemanager as acm,
+ aws_ec2 as ec2,
+ aws_route53 as route53,
+ CfnOutput,
+ Fn,
+ NestedStack,
+ NestedStackProps,
+ Tags,
+} from 'aws-cdk-lib';
+import { Construct } from 'constructs';
+
+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, props.hostedZoneTags);
+ });
+ props.hostedZones.forEach((hostedZoneConfig) => {
+ this.addCertificate(hostedZoneConfig, props.hostedZoneTags);
+ });
+ }
+
+ 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}`,
+ );
+ }
+ 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;
+ 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(validationHostedZone),
+ });
+ this.applyTags(certificate, this.resolveTags(hostedZoneTags, hostedZoneConfig.tags));
+ this.certificateOutputs[hostedZoneConfig.zoneName] = certificate;
+ new CfnOutput(this, `${idPrefix}CertificateArn`, {
+ value: certificate.certificateArn,
+ description: `ACM certificate ARN for ${hostedZoneConfig.zoneName}`,
+ });
+ }
+ }
+
+ 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 bbb57e4..73c722e 100644
--- a/src/constructs/network.ts
+++ b/src/constructs/network.ts
@@ -1,6 +1,13 @@
-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 { HostedZoneConfig, HostedZoneStack } from './hosted-zone';
import { SubnetStack } from './subnet-stack';
import { VpcEndpointServiceNestedStack, VpcEndpontServiceConfig } from './vpc-endpoint-service';
import { ObjToStrMap } from '../utils/common';
@@ -71,6 +78,8 @@ export interface VPCProps {
readonly vpcEndpoints?: VpcEndpointConfig[]; // List of VPC endpoints to configure
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;
}
@@ -118,6 +127,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) {
@@ -198,6 +208,13 @@ 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,
+ hostedZoneTags: props.hostedZoneTags,
+ vpc: this.vpc,
+ });
+ }
// Add VPC endpoints if specified in the props
if (props?.vpcEndpoints) {
for (const endpointConfig of props.vpcEndpoints) {
@@ -500,5 +517,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';