DEV Community

Cover image for Allowing an AWS account to delegate DNS subdomains to another account in two simple CDK stacks

Allowing an AWS account to delegate DNS subdomains to another account in two simple CDK stacks

The Problem

I had an issue this week. I was initializing AWS accounts for our developers and they required a subdomain per account. Think user.blah.com.

I wanted the developer account to be able to manage all records inside this subdomain, so they could create api.user.blah.com, staging.user.blah.com and so on. And if you manage your own subdomain, you can also generate SSL certificates for them as well.

Route 53 Logo

To achieve this with AWS, you simply need to create the hosted zone for the subdomain in Route53, find out which AWS nameservers it was allocated, and then add a 'NS' record for your subdomain using those nameservers in the DNS zone for the parent domain, ie blah.com.

Now, it sounds simple, but I don't want to do this every time I onboard a new developer. I want it to happen automatically when I deploy the developers initialization stack. Fortunately, it's possible, and not very difficult to implement!

The Solution

The solution here requires you to be using AWS Organizations to create AWS accounts for your developers.

The Parent Account

Firstly, we create a new CDK project and start editing our lib/stack.ts file.

I use a parameter I can pass in to my CDK stack to define the domain that will be created in Route53, eg. "blah.com". Lets capture that with CDK first:



    // Get the domain from the user. 
    const domain = new cdk.CfnParameter(this, "domain", {
      type: "String",
      description: "The domain to be created. "
    }).value.toString();


Enter fullscreen mode Exit fullscreen mode

Next we're going to create the Route53 Hosted Zone:



    // Create the Route53 zone
    const hostedZone = new r53.PublicHostedZone(this, 'zone', {
       zoneName: domain,
    });


Enter fullscreen mode Exit fullscreen mode

Now, this is where the magic happens. We're going to create a role that allows every other account which is part of your AWS organization to be able to add a NS record to the hosted zone we just created. Then, we grant the delegation access to the role. Be sure to change the o-xxxxxxx's to your Organization ID, of course.



    // Create the role
    const role = new Role(this, 'RootZoneOrganizationRole', {
      assumedBy: new OrganizationPrincipal('o-xxxxxxxxxx'),
      roleName: 'HostedZoneDelegationRole',
    });

    // Grant the delegation
    hostedZone.grantDelegation(role);


Enter fullscreen mode Exit fullscreen mode

CDK is doing a lot of heavy lifting under the hood here, it creates a role with correct permissions and a trust relationship that allows all the other accounts to do what we need it to do. Anyway, that's all we need for our parent account CDK stack! Save it and deploy it, probably something like this:



$ cdk deploy --parameters domain=user.blah.com --profile parent


Enter fullscreen mode Exit fullscreen mode

The Child Account

Now we're going to create a stack for the child account, do your cdk init and so on to create another new stack project and get ready to add some code.

Lets capture the child account domain, in this case it we'll expect user.blah.com



    // Get the subdomain from the user
    const domain = new cdk.CfnParameter(this, "domain", {
      type: "String",
      description: "The subdomain to be created. "
    }).value.toString();


Enter fullscreen mode Exit fullscreen mode

Create the hosted zone in Route53



    // Create the Route53 zone
    const hostedZone = new r53.PublicHostedZone(this, 'zone', {
       zoneName: domain,
    });


Enter fullscreen mode Exit fullscreen mode

Generate the delegation role ARN, specifying the parent account which hosts the blah.com domain



    const delegationRoleArn = Stack.of(this).formatArn({
      account: '1234567890',
      region: '',
      resource: 'role',
      resourceName: 'HostedZoneDelegationRole',
      service: 'iam',
    });


Enter fullscreen mode Exit fullscreen mode

Create the delegation role from this ARN



    const delegationRole = iam.Role.fromRoleArn(this, 'DelegationRole', delegationRoleArn);


Enter fullscreen mode Exit fullscreen mode

And now we can add the zone as a delegation record to the parent account Route53:



    new r53.CrossAccountZoneDelegationRecord(this, 'DelegationRecord', {
      delegationRole,
      delegatedZone: hostedZone,
      parentHostedZoneName: 'blah.com',
    });


Enter fullscreen mode Exit fullscreen mode

and that's it! that's all you need! any records added to the hostedZone from this point on will be completely valid, because the parent account now recognizes the child account's route53 configuration as the authority for that subdomain.

Huge shoutout to AWS Community Builder Matt Morgan who has created an awesome AWS Organizations for devs GitHub repository where you can learn about deploying full AWS Organizations!

Top comments (2)

Collapse
 
johnbeech profile image
John Beech

I'm trying this approach with all my cdk libs updated to 2.87.0 but am getting hostedZone.grantDelegation is not a function... any ideas?

Found similar docs here: docs.aws.amazon.com/cdk/api/v2/doc...

import { Stack } from 'aws-cdk-lib'
import { OrganizationPrincipal, Role } from 'aws-cdk-lib/aws-iam'
import { PublicHostedZone } from 'aws-cdk-lib/aws-route53'

export class DNSStack extends Stack {
  constructor (scope, id, props) {
    super(scope, id, props)

    const { awsAccountConfig } = props
    console.log('Creating DNS stack for', { awsAccountConfig })

    const hostedZone = PublicHostedZone.fromPublicHostedZoneAttributes(this, 'Zone', { domainName: 'mywebsite.com' })

    const orgId = 'o-xxxxxxxxxx'

    const role = new Role(this, 'RootZoneOrganizationRole', {
      roleName: 'HostedZoneDelegationRole',
      assumedBy: new OrganizationPrincipal(orgId)
    })

    hostedZone.grantDelegation(role)
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
xelfer profile image
Nicolas Triantafillou • Edited

Hmm, try import all from aws-iam instead? or at least Grant

import * as iam from 'aws-cdk-lib/aws-iam';
Enter fullscreen mode Exit fullscreen mode

The method is:

(method) PublicHostedZone.grantDelegation(grantee: cdk.aws_iam.IGrantable): cdk.aws_iam.Grant
Enter fullscreen mode Exit fullscreen mode