Networking & Content Delivery

Automating DNS infrastructure using Route 53 Resolver endpoints

Introduction

DNS name resolution is a fundamental part of all on-premises and cloud networks. For customers with hybrid networks, additional infrastructure and configuration are needed for private DNS resolution to work seamlessly across environments. However, building this type of DNS infrastructure in a multi-account environment is complex.

In this post, we show how to automate the construction of DNS infrastructure with Route 53 Resolver endpoints using AWS CloudFormation. We also show how to update your DNS infrastructure on an ongoing basis to simplify management and reduce the cost of running conditional DNS forwarders on EC2 instances.

Solution overview

This solution uses Route 53 Resolver endpoints. It creates Route 53 Resolver endpoints in a Virtual Private Cloud (VPC) that is used for DNS management.

The appropriate Route 53 resolver rules are created in the same AWS account where Resolver endpoints are present. The Route 53 resolver rules are shared via AWS Resource Access Manager (RAM) with other AWS accounts (spoke accounts). VPCs in spoke accounts are then associated with the Route 53 resolver rules.

In our example, we have two DNS domain names – cloud.dev.example.com and onprem.dev.example.com. The domain cloud.dev.example.com is a private hosted zone in Route 53. The domain onprem.dev.example.com is a zone hosted within an on-premises DNS server. We have an outbound endpoint and an inbound endpoint created in the VPC. We also have a Route 53 resolver rule created and shared via RAM to the two spoke accounts.

Rule 1 for onprem.dev.example.com is to associate the outbound endpoint with target IPs of the on-premises DNS server. For the private hosted zone cloud.dev.example.com, we directly associate the VPCs that require resolutions of records in it.

Figure 1: A DNS architecture setup with Route 53 Resolver endpoints and rules shared via Resource Access Manager

The DNS query for any record that belongs to onprem.dev.example.com goes to outbound endpoint by the way of AmazonProvidedDNS server. The DNS query then gets forwarded to the on-premises DNS server, authoritative for resolving those records. The DNS query for any records that belong to cloud.dev.example.com goes to the AmazonProvidedDNS server. Since the VPC is associated to private hosted zone of cloud.dev.example.com, the DNS query gets forwarded to the authoritative name servers of private hosted zone for resolution of those records. Similarly, any DNS query for records under cloud.dev.example.com from on-premises servers gets forwarded to the inbound endpoint. The VPC is associated with the private hosted zone. Hence the DNS query gets forwarded to authoritative name servers of private hosted zone by the way of AmazonProvidedDNS server for resolution of those set of records. This whitepaper provides details on how Route 53 Resolver works and outlines several different hybrid DNS architectures possible on AWS.

Note: Avoid DNS loops that occur due to wildcard Route 53 resolver rule “.” . For example, consider a scenario where you have Route 53 resolver rule “.” with forwarding IPs set up to be on-premises DNS server. You also have a forwarding rule on the on-premises DNS server to forward queries for cloud.dev.example.com to inbound endpoint that reside in AWS VPC. The query for records that belong to cloud.dev.example.com from a spoke account gets forwarded to an on-premises DNS server. The on-premises DNS server again forwards it back to the AWS going into a DNS loop.

Assumptions:

  • Network connectivity between the DNS-VPC and the on-premises is in place. Connectivity is by way of VPN or DX.
  • VPC attribute enableDNShostnames is set to true.
  • If your workload performs 10,000 DNS queries per second or above to a Resolver endpoint IP, create additional endpoint ENIs to scale your Queries Per Second (QPS).

Our solution uses AWS CloudFormation to build the DNS infrastructure required to solve three primary use-cases for private domain resolution:

  • Resolving on-premises private domains from workloads running in your VPCs.
  • Resolving private domains in your AWS environment from workloads running on premises.
  • Resolving private domains between workloads running in different AWS accounts.
    • Using VPC associations to the private hosted zone.

Five CloudFormation templates are used:

  • Resolver.yaml – creates the Route 53 resolver endpoints.
AWSTemplateFormatVersion: 2010-09-09
Description: >-
  AWS CloudFormation Template to deploy Resolver endpoints   
Metadata:
  'AWS::CloudFormation::Interface':
    ParameterGroups:
      - Label:
          default: VPC Configuration
        Parameters:
          - endpointtype
          - vpcId
          - privatesubnet1
          - privatesubnet2
          - endpointcidr

Parameters:
  vpcId:
    Type: AWS::EC2::VPC::Id
    Description: VPC ID that hosts resolver endpoints
  privatesubnet1:
    Type: AWS::EC2::Subnet::Id
    Description: Chose the private subnet in AZ-1
  privatesubnet2:
    Type: AWS::EC2::Subnet::Id
    Description: Chose the private subnet in AZ-2
  endpointtype: 
    Type: String
    Default: 'OutboundEndpoint'
    AllowedValues:
      - OutboundEndpoint
      - InboundEndpoint
    Description: Name for Route53 resolver security group
  endpointcidr: 
    Type: String
    Description: Provide the CIDRs of resources in on-prem that will be accessed from AWS via outbound endpoint or CIDR of resources in on-prem accessing AWS Private Hosted Zones via inbound endpoints
Conditions:
  CreateOutboundEndpoint: !Equals [ !Ref endpointtype, OutboundEndpoint ]
  CreateInboundEndpoint: !Equals [ !Ref endpointtype, InboundEndpoint ]

Resources:

  resolverSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: 
        !If 
          - CreateOutboundEndpoint
          - 'OutboundResolverEndpointSecurityGroup'
          - 'InboundResolverEndpointSecurityGroup'
      GroupDescription: Security group controlling Route53 Endpoint access
      SecurityGroupEgress:
        !If
          - CreateOutboundEndpoint
          - - IpProtocol: tcp
              FromPort: 53
              ToPort: 53
              CidrIp: !Ref endpointcidr
            - IpProtocol: udp
              FromPort: 53
              ToPort: 53
              CidrIp: !Ref endpointcidr
          - !Ref AWS::NoValue
      SecurityGroupIngress:
        !If
          - CreateInboundEndpoint
          - - IpProtocol: tcp
              FromPort: 53
              ToPort: 53
              CidrIp: !Ref endpointcidr
            - IpProtocol: udp
              FromPort: 53
              ToPort: 53
              CidrIp: !Ref endpointcidr
          - !Ref AWS::NoValue
      VpcId: !Ref vpcId

  resolverEndpoint:
    Type: AWS::Route53Resolver::ResolverEndpoint
    Properties :
      Name : 
        !If
        - CreateOutboundEndpoint
        - OutboundEndpoint
        - InboundEndpoint      
      Direction : 
        !If
        - CreateOutboundEndpoint
        - OUTBOUND
        - INBOUND
      IpAddresses : 
        - SubnetId: !Ref privatesubnet1
        - SubnetId: !Ref privatesubnet2    
      SecurityGroupIds : 
        - !GetAtt resolverSecurityGroup.GroupId

Outputs:
  ResolverEndpointId:
    Description: Route 53 Resolver Endpoint ID
    Value: !GetAtt resolverEndpoint.ResolverEndpointId

  
  • Rule-share.yaml – creates the Resolver rule and Resource Access Manager (RAM) share for sharing the rule.
AWSTemplateFormatVersion: 2010-09-09
Description: >-
  AWS CloudFormation Template to create the resolver rules and share the Resolver rules via Resource Access Manager  
Metadata:
  'AWS::CloudFormation::Interface':
    ParameterGroups:
      - Label:
          default: Resolver Endpoint Configuration
        Parameters:
          - outboundresolverendpointId
      - Label:
          default: Resolver Rule Configuration
        Parameters:
          - domainFQDN
          - domainTargets
      - Label:
          default: Account Details
        Parameters:
          - AccountIds

Parameters:
  outboundresolverendpointId:
    Type: String
    Description: Provide the Outbound Resolver Endpoint Id
  AccountIds:
    Type: List<Number>
    Description: List of account ids with which this rule will be shared.
  RuleName:
    Type: String
    Description: resolver rule name
  ResourceShareName:
    Type: String
    Description: resource share name
  domainFQDN:
    Type: String
    Description: Provide FQDN for domain
  domainTargetCount:
    Type: String
    Description: count for number targets ip for the resolver rule
    AllowedValues:
      - 1
      - 2
      - 3
      - 4
      - 5
      - 6
  domainTargets:
    Type: List<String>
    Default: 192.168.1.13:53, 192.168.2.14:53
    Description: A comma separated list of IP:port targets (two targets) for example1.com domain resolution. Please change the default IPs as per your environment.

Conditions:
  isCountOne: !Equals [!Ref domainTargetCount, 1]
  isCountTwo: !Equals [!Ref domainTargetCount, 2]
  isCountThree: !Equals [!Ref domainTargetCount, 3]
  isCountFour: !Equals [!Ref domainTargetCount, 4]
  isCountFive: !Equals [!Ref domainTargetCount, 5]
  isCountSix: !Equals [!Ref domainTargetCount, 6]


Resources:
  domainRuleWithOneTargets:
    Condition: isCountOne
    Type: AWS::Route53Resolver::ResolverRule
    Properties:
      DomainName: !Ref domainFQDN
      Name: !Ref RuleName
      ResolverEndpointId: !Ref outboundresolverendpointId
      RuleType: FORWARD
      TargetIps:
        - Ip: !Select [ 0, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]

  domainRuleWithTwoTargets:
    Condition: isCountTwo
    Type : AWS::Route53Resolver::ResolverRule
    Properties : 
      DomainName : !Ref domainFQDN
      Name : !Ref RuleName
      ResolverEndpointId : !Ref outboundresolverendpointId
      RuleType : FORWARD 
      TargetIps :
        - Ip: !Select [ 0, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]

  domainRuleWithThreeTargets:
    Condition: isCountThree
    Type : AWS::Route53Resolver::ResolverRule
    Properties :
      DomainName : !Ref domainFQDN
      Name : !Ref RuleName
      ResolverEndpointId : !Ref outboundresolverendpointId
      RuleType : FORWARD
      TargetIps :
        - Ip: !Select [ 0, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 2, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 2, !Ref domainTargets ] ] ]

  domainRuleWithFourTargets:
    Condition: isCountFour
    Type : AWS::Route53Resolver::ResolverRule
    Properties :
      DomainName : !Ref domainFQDN
      Name : !Ref RuleName
      ResolverEndpointId : !Ref outboundresolverendpointId
      RuleType : FORWARD
      TargetIps :
        - Ip: !Select [ 0, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 2, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 2, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 3, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 3, !Ref domainTargets ] ] ]

  domainRuleWithFiveTargets:
    Condition: isCountFive
    Type : AWS::Route53Resolver::ResolverRule
    Properties :
      DomainName : !Ref domainFQDN
      Name : !Ref RuleName
      ResolverEndpointId : !Ref outboundresolverendpointId
      RuleType : FORWARD
      TargetIps :
        - Ip: !Select [ 0, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 2, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 2, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 3, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 3, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 4, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 4, !Ref domainTargets ] ] ]

  domainRuleWithSixTargets:
    Condition: isCountSix
    Type : AWS::Route53Resolver::ResolverRule
    Properties :
      DomainName : !Ref domainFQDN
      Name : !Ref RuleName
      ResolverEndpointId : !Ref outboundresolverendpointId
      RuleType : FORWARD
      TargetIps :
        - Ip: !Select [ 0, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 0, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 1, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 2, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 2, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 3, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 3, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 4, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 4, !Ref domainTargets ] ] ]
        - Ip: !Select [ 0, !Split [ ":", !Select [ 5, !Ref domainTargets ] ] ]
          Port: !Select [ 1, !Split [ ":", !Select [ 5, !Ref domainTargets ] ] ]

  resolverruleshare:
    Type: AWS::RAM::ResourceShare
    Properties:
      Name: !Ref ResourceShareName
      ResourceArns:
        - !If
          - isCountOne
          - !GetAtt domainRuleWithOneTargets.Arn
          - !Ref AWS::NoValue
        - !If
          - isCountTwo
          - !GetAtt domainRuleWithTwoTargets.Arn
          - !Ref AWS::NoValue
        - !If
          - isCountThree
          - !GetAtt domainRuleWithThreeTargets.Arn
          - !Ref AWS::NoValue
        - !If
          - isCountFour
          - !GetAtt domainRuleWithFourTargets.Arn
          - !Ref AWS::NoValue
        - !If
          - isCountFive
          - !GetAtt domainRuleWithFiveTargets.Arn
          - !Ref AWS::NoValue
        - !If
          - isCountSix
          - !GetAtt domainRuleWithSixTargets.Arn
          - !Ref AWS::NoValue
      Principals: !Ref AccountIds

Outputs:
  domainRuleWithOneTargets:
    Condition: isCountOne
    Description: Route 53 Resolver Rule ID for domain1fqdn
    Value: !Ref domainRuleWithOneTargets
  domainRuleWithTwoTargets:
    Condition: isCountTwo
    Description: Route 53 Resolver Rule ID for domain1fqdn
    Value: !Ref domainRuleWithTwoTargets
  domainRuleWithThreeTargets:
    Condition: isCountThree
    Description: Route 53 Resolver Rule ID for domain1fqdn
    Value: !Ref domainRuleWithThreeTargets
  domainRuleWithFourTargets:
    Condition: isCountFour
    Description: Route 53 Resolver Rule ID for domain1fqdn
    Value: !Ref domainRuleWithFourTargets
  domainRuleWithFiveTargets:
    Condition: isCountFive
    Description: Route 53 Resolver Rule ID for domain1fqdn
    Value: !Ref domainRuleWithFiveTargets
  domainRuleWithSixTargets:
    Condition: isCountSix
    Description: Route 53 Resolver Rule ID for domain1fqdn
    Value: !Ref domainRuleWithSixTargets
  • Rule-association.yaml – creates the association of VPC to the Route 53 Resolver rule.
AWSTemplateFormatVersion: 2010-09-09
Description: >-
  AWS CloudFormation Template to associate the spoke VPC to the resolver rules

Parameters:
  ruleId:
    Type: String
    Description: Provide the resolver rule ID for rule-1
  associationName:
    Type: String
    Description: name for the association
  vpcId:
    Type: AWS::EC2::VPC::Id
    Description: Provide the VPC ID with which Route 53 Resolver rules are associated

Resources:
  ruleAssociation:
    Type: AWS::Route53Resolver::ResolverRuleAssociation
    Properties:
      Name: !Ref associationName
      ResolverRuleId: !Ref ruleId
      VPCId: !Ref vpcId
  • AuthAndAssociationLambda.yaml – creates a Lambda function and necessary IAM role for it. The function is triggered through a custom resource defined in AuthOrAssociation.yaml template. The Lambda function makes necessary API calls to authorize, associate, disassociate, and delete authorization of a VPC to private hosted zone based on the action user choose.
AWSTemplateFormatVersion: 2010-09-09
Description: Custom Resource Lambda to associate & authorize VPC

Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: route53-access
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - route53:AssociateVPCWithHostedZone
                  - route53:DisassociateVPCFromHostedZone
                  - ec2:DescribeVpcs
                  - route53:CreateVPCAssociationAuthorization
                  - route53:DeleteVPCAssociationAuthorization
                  - logs:*
                Resource: '*'

  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.7
      Role: !GetAtt LambdaRole.Arn
      Handler: index.handler
      Code:
        ZipFile: |
          import os, boto3, json
          import logging
          import cfnresponse

          logger = logging.getLogger("__name__")
          logger.setLevel(os.environ.get("LOG_LEVEL", logging.INFO))
          r53_client = boto3.client('route53')

          def parameters(vpc_id, phz_id):
              return dict(
                  HostedZoneId=phz_id,
                  VPC={
                      'VPCRegion': os.environ['AWS_REGION'],
                      'VPCId': vpc_id
                  }
              )

          def associate_vpc_to_hosted_zone(vpc_ids, phz_id):
              for vpc_id in vpc_ids:
                  try:
                      logger.info("associating {} to hosted zone {}".format(vpc_id, phz_id))
                      response = r53_client.associate_vpc_with_hosted_zone(**dict(parameters(vpc_id, phz_id)))
                      logger.info("association is complete : \n {}".format(response))
                  except Exception as ex:
                      logger.error("Error associating %s to hosted zone %s : %s", vpc_id, phz_id, ex, exc_info=True)

          def authorize_vpc_to_hosted_zone(vpc_ids, phz_id):
              for vpc_id in vpc_ids:
                  try:
                      logger.info("authorizing {} to hosted zone {}".format(vpc_id, phz_id))
                      response = r53_client.create_vpc_association_authorization(**dict(parameters(vpc_id, phz_id)))
                      logger.info("authorization is complete :\n {}".format(response))
                  except Exception as ex:
                      logger.error("Error authorizing %s to hosted zone %s : %s", vpc_id, phz_id, ex, exc_info=True)

          def disassociate_vpc_from_hosted_zone(vpc_ids, phz_id):
              for vpc_id in vpc_ids:
                  try:
                      logger.info("disassociation {} to hosted zone {}".format(vpc_id, phz_id))
                      response = r53_client.disassociate_vpc_from_hosted_zone(**dict(parameters(vpc_id, phz_id)))
                      logger.info("disassociation is complete :\n {}".format(response))
                  except Exception as ex:
                      logger.error("Error disassociation %s to hosted zone %s : %s", vpc_id, phz_id, ex, exc_info=True)

          def deauthorize_vpc_to_hosted_zone(vpc_ids, phz_id):
              for vpc_id in vpc_ids:
                  try:
                      logger.info("delete authorization {} to hosted zone {}".format(vpc_id, phz_id))
                      response = r53_client.delete_vpc_association_authorization(**dict(parameters(vpc_id, phz_id)))
                      logger.info("delete authorization is complete:\n {}".format(response))
                  except Exception as ex:
                      logger.error("Error deleting authorization %s to hosted zone %s : %s", vpc_id, phz_id, ex,
                                   exc_info=True)

          def perform_action(action, vpc_ids, phz_id):
              if action == 'ASSOCIATE':
                  associate_vpc_to_hosted_zone(vpc_ids, phz_id)
              elif action == 'AUTHORIZE':
                  authorize_vpc_to_hosted_zone(vpc_ids, phz_id)
              elif action == 'DEAUTHORIZE':
                  deauthorize_vpc_to_hosted_zone(vpc_ids, phz_id)
              elif action == 'DISASSOCIATE':
                  disassociate_vpc_from_hosted_zone(vpc_ids, phz_id)

          def handler(event, context):
              try:
                  logger.info("custom resource triggered by {} request type: {}".format(event.get('StackId'), event.get('RequestType')))
                  logger.info("custom resource invoked with these properties: {}".format(event.get('ResourceProperties')))
                  properties = event.get("ResourceProperties")
                  vpc_ids = properties.get("VPCID")
                  phz_id = properties.get("HostedZoneID")
                  event_type = event.get('RequestType')
                  action = properties.get("Action")
                  if event_type == 'Create' or event_type == 'Update':
                      perform_action(action, vpc_ids, phz_id)
                  elif event_type == 'Delete':
                      perform_action(action, vpc_ids, phz_id)
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, phz_id)
              except Exception as ex:
                  logger.error("Error performing actions: %s", ex, exc_info=True)
                  if 'PhysicalResourceId' in event:
                      cfnresponse.send(event, context, cfnresponse.FAILED, {}, event.get("PhysicalResourceId"))
                  else:
                      cfnresponse.send(event, context, cfnresponse.FAILED, {})

Outputs:
  AuthorizeAssociateVPCLambda:
    Value: !GetAtt LambdaFunction.Arn
    Export:
      Name: AuthorizeAssociateVPCLambdaArn
  • AuthOrAssociation.yaml – creates the custom resource that triggers Lambda function for association, authorization, disassociation, or deleting authorization of VPCs to private hosted zone. The custom resource passes a list of VPCId, Private HostedZoneId and an Action (Association or Authorization) to the Lambda function.
AWSTemplateFormatVersion: 2010-09-09
Description: Custom Resource to authorize or associate VPC

Parameters:
  AddVPCID:
    Type: List<String>
  RemoveVPCID:
    Type: List<String>
  HostedZoneID:
    Type: String
  ActionType:
    Type: String
    AllowedValues:
      - AUTHORIZE
      - ASSOCIATE
      - DEAUTHORIZE
      - DISASSOCIATE

Conditions:
  isAssociateOrAuthorize: !Or [!Equals [!Ref ActionType, AUTHORIZE], !Equals [!Ref ActionType, ASSOCIATE] ]

Resources:
  HostedZoneVPC:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: !ImportValue AuthorizeAssociateVPCLambdaArn
      VPCID: !If [isAssociateOrAuthorize, !Ref AddVPCID, !Ref RemoveVPCID]
      HostedZoneID: !Ref HostedZoneID
      Action: !Ref ActionType

Note: Authorization is done in the account where the private hosted zone resides, and association is done in the spoke accounts from where you are trying to resolve the records of the private hosted zone.

To summarize – invoking these templates create the following resources:

  • In the AWS Account where you create DNS resources
    • Route 53 Resolver endpoints.
    • Security groups that are associated with the Resolver endpoints.
    • Route 53 resolver rules.
    • RAM share to share the resolver rules
  • In all the Spoke Accounts
    • Association of VPCs to resolver rules

Deployment steps

Resolving on-premises domains from workloads running in your VPCs.

  1. Launch the stack using (Resolver.yaml) template in the AWS CloudFormation console in AWS account where you create DNS resources.
  2. Select the type – “Outbound Endpoint” in the dropdown.
  3. Select at least two unique subnets from different Availability Zones in the VPC where you want to create the outbound endpoint.
  4. Launch the second stack using (Rule-share.yaml) template in the AWS CloudFormation console, again in the same AWS account.
  5. Provide the details of on-premises domain and corresponding authoritative DNS servers.
  6. Provide AWS Account IDs of spoke accounts with which the rule requires to be shared.
  7. Launch a stack using (Rule-association.yaml) template in the AWS CloudFormation console in all the spoke accounts.
  8. Select the resolver rule that is shared via RAM.
  9. Select the VPCId from where you want to resolve the on-premises domain.

Resolving private domains in your AWS environment from workloads running on-premises.

  1. Launch the stack using (Resolver.yaml) template in the AWS CloudFormation console in AWS account where you create DNS resources.
  2. Select the type – “Inbound Endpoint” in the dropdown.
  3. Select at least two unique subnets from different Availability Zones in the VPC where you want to create the inbound endpoint.
  4. Associate the VPC from the AWS account in which you have created inbound endpoints to private hosted zone for the domain in AWS being queried from on-premises.

Note: You are not required to create additional endpoint rules for inbound endpoints. Share and associate the spoke VPCs to them as mentioned in the previous case.

Resolving private domains between workloads running in different AWS accounts.

Route 53’s private hosted zone is a container that holds private domain records. You can make the records visible to your resources in VPCs in two ways – associate the VPCs to the private hosted zones or by using Route 53 Resolver endpoints. We recommend that you directly associate the VPCs to the private hosted zone.

To associate the VPCs to the private hosted zone that reside in the same or different AWS accounts, follow these steps:

Authorization of VPCs to a private hosted zone:

In the account, where the private hosted zone resides, follow the below steps:

  1. Launch a stack using AuthAndAssociationLambda.yaml template in the AWS CloudFormation console.
  2. Launch the next stack using AuthOrAssociation.yaml template in the AWS CloudFormation console.
  3. Select ActionType as AUTHORIZE.
  4. Provide the parameters for PrivateHostedZoneId and VPCId (a list of VPC Ids is also supported).

Association of VPCs to a private hosted zone:

In the spoke accounts that must be associated to the private hosted zone, follow these steps:

  1. Launch a stack using AuthAndAssociationLambda.yaml template in the AWS CloudFormation console.
  2. Launch the next stack using AuthOrAssociation.yaml in the AWS CloudFormation console.
  3. Select ActionType as ASSOCIATE.
  4. Provide the parameters for PrivateHostedZoneId and VPCId (a list of VPC Ids is also supported).

How you can update your DNS infrastructure on an ongoing basis:

In this section, we show how to use our solution to update your DNS infrastructure on an ongoing basis by looking at three common tasks.

  1. Add/Remove accounts in your RAM share:
    • In the AWS CloudFormation console, Select the stack created using Rule-share.yaml
    • In the center pane, select action Update.
    • Select the first option – use current template.
    • In the parameters section, add or remove accounts from the AccountIds
    • Click Next and Update the stack.
  2. Add/Remove targets from an existing rule:
    • In the AWS CloudFormation console, Select the stack created using Rule-share.yaml
    • In the center pane, select action Update.
    • Select the first option – use current template.
    • To add a target, select the domainTargetCount parameter and select one of the allowed values.
    • Add the target IPs in the domainTargets based on the domainTargetCount.
    • Click Next and Update the stack.
    • Go to AWS CloudFormation console in spoke accounts.
    • Select the stack created using Rule-association.yaml
    • In the center pane, select action Update.
    • Select the first option – use current template.
    • Provide the new rule ID in ruleId.
    • Click Next and Update the stack.
  3. Authorize/Deauthorize and Associate/Disassociate VPCs:
    • To authorize more VPCs to private hosted zone:
      • In AWS account where you have private hosted zone, go to the AWS CloudFormation. Select the stack created using AuthOrAssociation.yaml template.
      • In the center pane, select action Update.
      • Select the first option – use current template.
      • To authorize more VPCs, add the VPC IDs into the AddVPCId parameter and select ActionType to AUTHORIZE.
      • Click Next and Update the stack.
    • To delete authorization from VPCs to private hosted zone:
      • In AWS account where you have private hosted zone, go to the AWS CloudFormation. Select the stack created using AuthOrAssociation.yaml template.
      • In the center pane, select action Update.
      • Select the first option – use current template.
      • Provide the VPC ID in the RemoveVPCId parameter and select ActionType to DEAUTHORIZE.
      • Click Next and Update the stack.
    • To associate more VPCs to Private Hosted Zone
      • Go to the spoke accounts.
      • Go to the AWS CloudFormation. Select the stack created using AuthOrAssociation.yaml template.
      • In the center pane, select action Update.
      • To associate more VPCs, pass the VPC IDs into the AddVPCId parameter and select ActionType to ASSOCIATE.
      • Click Next and Update the stack.
    • To delete association from VPCs to Private Hosted Zone:
      • Go to the spoke accounts.
      • Go to the AWS CloudFormation. Select the stack created using AuthOrAssociation.yaml template.
      • In the center pane, select action Update.
      • Select the first option – use current template.
      • Add the VPC ID into the RemoveVPCId parameter and select ActionType to DISASSOCIATE.
      • Click Next and Update the stack.

Note: View CloudWatch Logs for the Lambda function that was created through AuthAndAssociationLambda.yaml for more insights on the operations performed.

Cleanup Steps

On successful testing and validation, all the resources deployed through CloudFormation templates should be deleted in order to avoid any unwanted costs. Simply go to the CloudFormation console, identify the stacks appropriately, and delete them.

Note: If you use a multi account setup, you must navigate through account boundaries and follow the above mentioned steps as needed.

Conclusion

We’ve shown how easy it is to deploy a scalable and easily manageable DNS infrastructure using Route 53 Resolver endpoints. We remove the undifferentiated heavy lifting of deployment process by providing the necessary CloudFormation templates.

We hope that you’ve found this post informative and we look forward to hearing how you use this new feature!

 

Shiva Vaidyanathan

Shiva Vaidyanathan is a Cloud Infrastructure Architect at AWS. He provides technical guidance, design and lead implementation projects to customers ensuring their success on AWS. Prior to joining AWS, he has worked on several research projects on how to perform secure computing in public cloud infrastructures. He holds a MS in Computer Science from Rutgers University and a MS in Electrical Engineering from New York University.

 

Akhil Nayabu

Akhil Nayabu is a Service Engineering Consultant with AWS Professional Services in Seattle, WA. He is passionate about building solution using cloud native services. Outside of work he likes to play video games.