Repository: awslabs/aws-bootstrap-kit (language: python)

Issue:
feat: Make root HostedZone accessible
# Problem Statement

I am making a CDK app to manage an organization's infrastructure. The organization's domain name is used for multiple purposes, including the SDLC, the organization's email service, and domain ownership verification for external services. I am looking to add MX and other records to the root `HostedZone`. Unfortunately it is created by `AwsOrganizationStack` but is not accessible for other stacks.

I would like to be able to either/both:
- Provide a `HostedZone` via `AwsOrganizationStackProps`
- Access the `HostedZone` created by `AwsOrganizationStack` (created via `RootDns`)

As a workaround, I attempted to prevent `AwsOrganization` from creating a `RootDns` and intended to define my own `RootDns`, but `RootDnsProps` requires `stagesAccounts` which is also not accessible from `AwsOrganization`.


Discussion / hints:
Good point. will give both options.

On Tue, Feb 22, 2022, 19:45 Nate Tippie ***@***.***> wrote:

> Problem Statement
>
> I am making a CDK app to manage an organization's infrastructure. The
> organization's domain name is used for multiple purposes, including the
> SDLC, the organization's email service, and domain ownership verification
> for external services. I am looking to add MX and other records to the root
> HostedZone. Unfortunately it is created by AwsOrganizationStack but is
> not accessible for other stacks.
>
> I would like to be able to either/both:
>
>    - Provide a HostedZone via AwsOrganizationStackProps
>    - Access the HostedZone created by AwsOrganizationStack (created via
>    RootDns)
>
> As a workaround, I attempted to prevent AwsOrganization from creating a
> RootDns and intended to define my own RootDns, but RootDnsProps requires
> stagesAccounts which is also not accessible from AwsOrganization.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/awslabs/aws-bootstrap-kit/issues/89>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAJLIUKQK4T3BH7OIU6CNN3U4PKWBANCNFSM5PCG7BKA>
> .
> Triage notifications on the go with GitHub Mobile for iOS
> <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
> or Android
> <https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
>
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>


Fix patch:
diff --git a/source/aws-bootstrap-kit/API.md b/source/aws-bootstrap-kit/API.md
index a71d78f..9a44dd8 100644
--- a/source/aws-bootstrap-kit/API.md
+++ b/source/aws-bootstrap-kit/API.md
@@ -117,12 +117,21 @@ new AwsOrganizationsStack(scope: Construct, id: string, props: AwsOrganizationsS
   * **terminationProtection** (<code>boolean</code>)  Whether to enable termination protection for this stack. __*Default*__: false
   * **email** (<code>string</code>)  Email address of the Root account. 
   * **nestedOU** (<code>Array<[OUSpec](#aws-bootstrap-kit-ouspec)></code>)  Specification of the sub Organizational Unit. 
+  * **existingRootHostedZoneId** (<code>string</code>)  The (optional) existing root hosted zone id to use instead of creating one. __*Optional*__
   * **forceEmailVerification** (<code>boolean</code>)  Enable Email Verification Process. __*Optional*__
   * **rootHostedZoneDNSName** (<code>string</code>)  The main DNS domain name to manage. __*Optional*__
   * **thirdPartyProviderDNSUsed** (<code>boolean</code>)  A boolean used to decide if domain should be requested through this delpoyment or if already registered through a third party. __*Optional*__
 
 
 
+### Properties
+
+
+Name | Type | Description 
+-----|------|-------------
+**rootDns**? | <code>[RootDns](#aws-bootstrap-kit-rootdns)</code> | __*Optional*__
+
+
 
 ## class CrossAccountDNSDelegator  <a id="aws-bootstrap-kit-crossaccountdnsdelegator"></a>
 
@@ -199,6 +208,7 @@ new RootDns(scope: Construct, id: string, props: RootDnsProps)
 * **props** (<code>[RootDnsProps](#aws-bootstrap-kit-rootdnsprops)</code>)  *No description*
   * **rootHostedZoneDNSName** (<code>string</code>)  The top level domain name. 
   * **stagesAccounts** (<code>Array<[Account](#aws-bootstrap-kit-account)></code>)  The stages Accounts taht will need their subzone delegation. 
+  * **existingRootHostedZoneId** (<code>string</code>)  The (optional) existing root hosted zone id to use instead of creating one. __*Optional*__
   * **thirdPartyProviderDNSUsed** (<code>boolean</code>)  A boolean indicating if Domain name has already been registered to a third party or if you want this contruct to create it (the latter is not yet supported). __*Optional*__
 
 
@@ -209,6 +219,7 @@ new RootDns(scope: Construct, id: string, props: RootDnsProps)
 Name | Type | Description 
 -----|------|-------------
 **rootHostedZone** | <code>[aws_route53.IHostedZone](#aws-cdk-lib-aws-route53-ihostedzone)</code> | <span></span>
+**stagesHostedZones** | <code>Array<[aws_route53.HostedZone](#aws-cdk-lib-aws-route53-hostedzone)></code> | <span></span>
 
 ### Methods
 
@@ -232,16 +243,17 @@ __Returns__:
 
 
 ```ts
-createRootHostedZone(props: RootDnsProps): HostedZone
+createRootHostedZone(props: RootDnsProps): IHostedZone
 ```
 
 * **props** (<code>[RootDnsProps](#aws-bootstrap-kit-rootdnsprops)</code>)  *No description*
   * **rootHostedZoneDNSName** (<code>string</code>)  The top level domain name. 
   * **stagesAccounts** (<code>Array<[Account](#aws-bootstrap-kit-account)></code>)  The stages Accounts taht will need their subzone delegation. 
+  * **existingRootHostedZoneId** (<code>string</code>)  The (optional) existing root hosted zone id to use instead of creating one. __*Optional*__
   * **thirdPartyProviderDNSUsed** (<code>boolean</code>)  A boolean indicating if Domain name has already been registered to a third party or if you want this contruct to create it (the latter is not yet supported). __*Optional*__
 
 __Returns__:
-* <code>[aws_route53.HostedZone](#aws-cdk-lib-aws-route53-hostedzone)</code>
+* <code>[aws_route53.IHostedZone](#aws-cdk-lib-aws-route53-ihostedzone)</code>
 
 #### createStageSubZone(account, rootHostedZoneDNSName) <a id="aws-bootstrap-kit-rootdns-createstagesubzone"></a>
 
@@ -339,6 +351,7 @@ Name | Type | Description
 **analyticsReporting**?🔹 | <code>boolean</code> | Include runtime versioning information in this Stack.<br/>__*Default*__: `analyticsReporting` setting of containing `App`, or value of 'aws:cdk:version-reporting' context key
 **description**?🔹 | <code>string</code> | A description of the stack.<br/>__*Default*__: No description.
 **env**?🔹 | <code>[Environment](#aws-cdk-lib-environment)</code> | The AWS environment (account/region) where this stack will be deployed.<br/>__*Default*__: The environment of the containing `Stage` if available, otherwise create the stack will be environment-agnostic.
+**existingRootHostedZoneId**?🔹 | <code>string</code> | The (optional) existing root hosted zone id to use instead of creating one.<br/>__*Optional*__
 **forceEmailVerification**?🔹 | <code>boolean</code> | Enable Email Verification Process.<br/>__*Optional*__
 **rootHostedZoneDNSName**?🔹 | <code>string</code> | The main DNS domain name to manage.<br/>__*Optional*__
 **stackName**?🔹 | <code>string</code> | Name to deploy the stack with.<br/>__*Default*__: Derived from construct path.
@@ -414,6 +427,7 @@ Name | Type | Description
 -----|------|-------------
 **rootHostedZoneDNSName** | <code>string</code> | The top level domain name.
 **stagesAccounts** | <code>Array<[Account](#aws-bootstrap-kit-account)></code> | The stages Accounts taht will need their subzone delegation.
+**existingRootHostedZoneId**? | <code>string</code> | The (optional) existing root hosted zone id to use instead of creating one.<br/>__*Optional*__
 **thirdPartyProviderDNSUsed**? | <code>boolean</code> | A boolean indicating if Domain name has already been registered to a third party or if you want this contruct to create it (the latter is not yet supported).<br/>__*Optional*__
 
 
diff --git a/source/aws-bootstrap-kit/lib/aws-organizations-stack.ts b/source/aws-bootstrap-kit/lib/aws-organizations-stack.ts
index aff25d2..d1246b8 100644
--- a/source/aws-bootstrap-kit/lib/aws-organizations-stack.ts
+++ b/source/aws-bootstrap-kit/lib/aws-organizations-stack.ts
@@ -98,6 +98,11 @@ export interface AwsOrganizationsStackProps extends StackProps {
    */
   readonly rootHostedZoneDNSName?: string,
 
+  /**
+   * The (optional) existing root hosted zone id to use instead of creating one
+   */
+  readonly existingRootHostedZoneId?: string,
+
   /**
    * A boolean used to decide if domain should be requested through this delpoyment or if already registered through a third party
    */
@@ -117,6 +122,7 @@ export class AwsOrganizationsStack extends Stack {
   private readonly emailPrefix?: string;
   private readonly domain?: string;
   private readonly stageAccounts: Account[] = [];
+  public readonly rootDns?: RootDns;
 
   private createOrganizationTree(oUSpec: OUSpec, parentId: string, previousSequentialConstruct: IDependable): IDependable {
 
@@ -198,9 +204,10 @@ export class AwsOrganizationsStack extends Stack {
     }
 
     if(props.rootHostedZoneDNSName){
-      new RootDns(this, 'RootDNS', {
+      this.rootDns = new RootDns(this, 'RootDNS', {
         stagesAccounts: this.stageAccounts,
         rootHostedZoneDNSName: props.rootHostedZoneDNSName,
+        existingRootHostedZoneId: props.existingRootHostedZoneId,
         thirdPartyProviderDNSUsed: props.thirdPartyProviderDNSUsed?props.thirdPartyProviderDNSUsed:true
       });
     }
diff --git a/source/aws-bootstrap-kit/lib/dns.ts b/source/aws-bootstrap-kit/lib/dns.ts
index 52ad563..4925aaa 100644
--- a/source/aws-bootstrap-kit/lib/dns.ts
+++ b/source/aws-bootstrap-kit/lib/dns.ts
@@ -20,6 +20,11 @@ export interface RootDnsProps {
    */
   readonly rootHostedZoneDNSName: string;
 
+  /**
+   * The (optional) existing root hosted zone id to use instead of creating one
+   */
+  readonly existingRootHostedZoneId?: string;
+
   /**
    * A boolean indicating if Domain name has already been registered to a third party or if you want this contruct to create it (the latter is not yet supported)
    */
@@ -30,7 +35,8 @@ export interface RootDnsProps {
  * A class creating the main hosted zone and a role assumable by stages account to be able to set sub domain delegation
  */
 export class RootDns extends Construct {
-  rootHostedZone: route53.IHostedZone;
+  public readonly rootHostedZone: route53.IHostedZone;
+  public readonly stagesHostedZones: route53.HostedZone[] = [];
 
   constructor(scope: Construct, id: string, props: RootDnsProps) {
     super(scope, id);
@@ -42,6 +48,7 @@ export class RootDns extends Construct {
         account,
         props.rootHostedZoneDNSName
       );
+      this.stagesHostedZones.push(stageSubZone);
       this.createDNSAutoUpdateRole(account, stageSubZone);
       if (stageSubZone.hostedZoneNameServers) {
         new route53.RecordSet(
@@ -65,8 +72,10 @@ export class RootDns extends Construct {
         value: cdk.Fn.join(",", this.rootHostedZone.hostedZoneNameServers),
       });
     } else {
-      throw new Error("Creation of DNS domain is not yet supported");
-      // TODO: implement call to https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Route53Domains.html#registerDomain-property
+      if (!props.existingRootHostedZoneId) {
+        throw new Error("Creation of DNS domain is not yet supported");
+        // TODO: implement call to https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Route53Domains.html#registerDomain-property
+      }
     }
   }
 
@@ -111,8 +120,15 @@ export class RootDns extends Construct {
   }
 
   createRootHostedZone(props: RootDnsProps) {
-    return new route53.HostedZone(this, "RootHostedZone", {
-      zoneName: props.rootHostedZoneDNSName,
-    });
+    if (!props.existingRootHostedZoneId) {
+      return new route53.HostedZone(this, 'RootHostedZone', {
+        zoneName: props.rootHostedZoneDNSName,
+      });
+    } else {
+      return route53.HostedZone.fromHostedZoneAttributes(this, 'RootHostedZone', {
+        zoneName: props.rootHostedZoneDNSName,
+        hostedZoneId: props.existingRootHostedZoneId
+      });
+    }
   }
 }


Test patch:
diff --git a/source/aws-bootstrap-kit/test/aws-organizations-stack.test.ts b/source/aws-bootstrap-kit/test/aws-organizations-stack.test.ts
index 4e17d7d..bf0d2d4 100644
--- a/source/aws-bootstrap-kit/test/aws-organizations-stack.test.ts
+++ b/source/aws-bootstrap-kit/test/aws-organizations-stack.test.ts
@@ -212,6 +212,9 @@ test("should create root domain zone and stage based domain if rootHostedZoneDNS
     }
   );
 
+  expect(awsOrganizationsStack.rootDns?.rootHostedZone.zoneName).toEqual("yourdomain.com");
+  expect(awsOrganizationsStack.rootDns?.stagesHostedZones.length).toEqual(3);
+
   expect(awsOrganizationsStack).toHaveResource("AWS::Route53::HostedZone",{
     Name: "yourdomain.com."
   });
@@ -250,6 +253,26 @@ test("should not create any zone if no domain is provided", () => {
   expect(awsOrganizationsStack).toCountResources("AWS::Route53::HostedZone",0);
 });
 
+test("should not create root zone if existing root zone id is provided", () => {
+  const awsOrganizationsStack = new AwsOrganizationsStack(
+    new Stack(),
+    "AWSOrganizationsStack",
+    {
+      ...awsOrganizationsStackProps,
+      rootHostedZoneDNSName: "yourdomain.com",
+      existingRootHostedZoneId: "existing-root-zone-id"
+    }
+  );
+
+  // Check root zone not created
+  expect(awsOrganizationsStack).not.toHaveResource("AWS::Route53::HostedZone", {
+    Name: "yourdomain.com."
+  });
+
+  // Check child zones created
+  expect(awsOrganizationsStack).toCountResources("AWS::Route53::HostedZone", 3);
+});
+
 test("should have have email validation stack with forceEmailVerification set to true", () => {
 
   const awsOrganizationsStack = new AwsOrganizationsStack(