Repository: cdklabs/cdk-enterprise-iac (language: python)

Issue:
feat: Allow for Enterprise DNS to be used while still resolving AWS internal DNS names
### Description

Need the ability to use Amazon Provided DNS when interacting with AWS services, otherwise use Enterprise provided DNS

### Use Case

In some enterprises, VPCs are vended to developers with DHCP option sets that point to the enterprise DNS servers. Sometimes the Enterprise DNS won't resolve AWS urls such as EFS file systems (e.g. `fs-abcde091234.efs.us-east-1.amazonaws.com`).



### Proposed Solution

Add a construct that leverages Route 53 resolvers to use Amazon Provided DNS when interacting with AWS, and the enterprise DNS otherwise

### Other information

_No response_

### Acknowledge

- [X] I may be able to implement this feature request
- [ ] This feature might incur a breaking change


Discussion / hints:

Fix patch:
diff --git a/src/constructs/enterpriseDnsResolver/enterpriseDnsResolver.ts b/src/constructs/enterpriseDnsResolver/enterpriseDnsResolver.ts
new file mode 100644
index 00000000..4cd1f466
--- /dev/null
+++ b/src/constructs/enterpriseDnsResolver/enterpriseDnsResolver.ts
@@ -0,0 +1,104 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import { IVpc, Peer, Port, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2';
+import {
+  CfnResolverEndpoint,
+  CfnResolverRule,
+  CfnResolverRuleAssociation,
+} from 'aws-cdk-lib/aws-route53resolver';
+import { Construct } from 'constructs';
+
+export interface enterpriseDnsResolverProps {
+  /**
+   * Vpc or IVpc to associate resolver rules with
+   */
+  readonly vpc: Vpc | IVpc;
+  /**
+   * List of IPs for enterprise DNS servers
+   */
+  readonly enterpriseDnsIpAddresses: string[];
+}
+
+export class enterpriseDnsResolver extends Construct {
+  private readonly _vpc: Vpc | IVpc;
+  private readonly _enterpriseDnsIpAddresses: string[];
+
+  constructor(scope: Construct, id: string, props: enterpriseDnsResolverProps) {
+    super(scope, id);
+
+    this._vpc = props.vpc;
+    this._enterpriseDnsIpAddresses = props.enterpriseDnsIpAddresses;
+
+    const sg = new SecurityGroup(this, 'resolverEndpointSecurityGroup', {
+      vpc: this._vpc,
+    });
+    sg.addIngressRule(
+      Peer.ipv4(this._vpc.vpcCidrBlock),
+      Port.tcp(53),
+      'tcp port 53 for route53 resolver'
+    );
+    sg.addIngressRule(
+      Peer.ipv4(this._vpc.vpcCidrBlock),
+      Port.udp(53),
+      'udp port 53 for route53 resolver'
+    );
+
+    let ipAddresses: CfnResolverEndpoint.IpAddressRequestProperty[] = [];
+    const combinedSubnets = [
+      ...this._vpc.publicSubnets,
+      ...this._vpc.privateSubnets,
+      ...this._vpc.isolatedSubnets,
+    ];
+
+    for (let i = 0; i < combinedSubnets.length; i++) {
+      const subnet = combinedSubnets[i];
+      ipAddresses.push({
+        subnetId: subnet.subnetId,
+      });
+    }
+
+    const resolverEndpoint = new CfnResolverEndpoint(
+      this,
+      'EnterpriseDnsResolverEndpoint',
+      {
+        direction: 'OUTBOUND',
+        securityGroupIds: [sg.securityGroupId],
+        ipAddresses,
+      }
+    );
+
+    // system rule
+    const systemRule = new CfnResolverRule(this, 'AmazonInternalRule', {
+      domainName: 'amazon.internal',
+      ruleType: 'SYSTEM',
+    });
+
+    let targetIps: CfnResolverRule.TargetAddressProperty[] = [];
+    for (let i = 0; i < this._enterpriseDnsIpAddresses.length; i++) {
+      const ip = this._enterpriseDnsIpAddresses[i];
+      targetIps.push({
+        ip,
+        port: '53',
+      });
+    }
+    // Enterprise DNS rule
+    const enterpriseRule = new CfnResolverRule(this, 'EnterpriseDnsRule', {
+      domainName: '.',
+      resolverEndpointId: resolverEndpoint.attrResolverEndpointId,
+      ruleType: 'FORWARD',
+      targetIps,
+    });
+
+    new CfnResolverRuleAssociation(this, 'SystemRuleAssociation', {
+      resolverRuleId: systemRule.attrResolverRuleId,
+      vpcId: this._vpc.vpcId,
+    });
+
+    new CfnResolverRuleAssociation(this, 'EnterpriseDnsRuleAssociation', {
+      resolverRuleId: enterpriseRule.attrResolverRuleId,
+      vpcId: this._vpc.vpcId,
+    });
+  }
+}


Test patch:
diff --git a/test/constructs/enterpriseDnsResolver/enterpriseDnsResolver.test.ts b/test/constructs/enterpriseDnsResolver/enterpriseDnsResolver.test.ts
new file mode 100644
index 00000000..f74105b8
--- /dev/null
+++ b/test/constructs/enterpriseDnsResolver/enterpriseDnsResolver.test.ts
@@ -0,0 +1,102 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import { Stack } from 'aws-cdk-lib';
+import { Match, Template } from 'aws-cdk-lib/assertions';
+import { Vpc } from 'aws-cdk-lib/aws-ec2';
+
+import { enterpriseDnsResolver } from '../../../src/constructs/enterpriseDnsResolver/enterpriseDnsResolver';
+
+let stack: Stack;
+
+describe('Enterprise DNS resolver', () => {
+  stack = new Stack();
+  const enterpriseDnsIpAddresses = ['1.1.1.1', '2.2.2.2', '3.3.3.3', '4.4.4.4'];
+  const vpc = new Vpc(stack, 'TestVpc', { maxAzs: 2 });
+
+  new enterpriseDnsResolver(stack, 'EnterpriseDnsResolver', {
+    vpc,
+    enterpriseDnsIpAddresses,
+  });
+
+  const template = Template.fromStack(stack);
+
+  test('Security group created in the right vpc', () => {
+    template.resourcePropertiesCountIs(
+      'AWS::EC2::SecurityGroup',
+      {
+        VpcId: {
+          Ref: Match.anyValue(),
+        },
+      },
+      1
+    );
+  });
+  test('Resolver endpoint is created in all vpc subnets', () => {
+    template.resourcePropertiesCountIs(
+      'AWS::Route53Resolver::ResolverEndpoint',
+      {
+        IpAddresses: [
+          // 4 subnets
+          {
+            SubnetId: {
+              Ref: Match.anyValue(),
+            },
+          },
+          {
+            SubnetId: {
+              Ref: Match.anyValue(),
+            },
+          },
+          {
+            SubnetId: {
+              Ref: Match.anyValue(),
+            },
+          },
+          {
+            SubnetId: {
+              Ref: Match.anyValue(),
+            },
+          },
+        ],
+      },
+      1
+    );
+  });
+
+  test('Two system rules are created', () => {
+    template.resourceCountIs('AWS::Route53Resolver::ResolverRule', 2);
+  });
+
+  test('Enterprise Dns Rule includes all provided dns IPs', () => {
+    let targetIpMatch: any[] = [];
+
+    for (let i = 0; i < enterpriseDnsIpAddresses.length; i++) {
+      targetIpMatch.push(Match.anyValue());
+    }
+
+    template.resourcePropertiesCountIs(
+      'AWS::Route53Resolver::ResolverRule',
+      {
+        DomainName: '.',
+        TargetIps: [
+          {
+            Ip: enterpriseDnsIpAddresses[0],
+          },
+          {
+            Ip: enterpriseDnsIpAddresses[1],
+          },
+          {
+            Ip: enterpriseDnsIpAddresses[2],
+          },
+          {
+            Ip: enterpriseDnsIpAddresses[3],
+          },
+        ],
+        RuleType: 'FORWARD',
+      },
+      1
+    );
+  });
+});