Create AWS Route53 Hosted Zones

The aws:route53/zone:Zone resource, part of the Pulumi AWS provider, defines a Route53 hosted zone that contains DNS records for a domain or subdomain. This guide focuses on three capabilities: public zone creation for internet-facing domains, subdomain delegation with NS records, and private zones for VPC-internal DNS resolution.

Hosted zones are containers for DNS records. Private zones require VPCs with DNS support enabled; subdomain zones require parent zones and delegation records. The examples are intentionally small. Combine them with your own Record resources and VPC infrastructure.

Create a public zone for internet-facing DNS

Most deployments start with a public hosted zone that manages DNS records for a domain accessible from the internet.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const primary = new aws.route53.Zone("primary", {name: "example.com"});
import pulumi
import pulumi_aws as aws

primary = aws.route53.Zone("primary", name="example.com")
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/route53"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := route53.NewZone(ctx, "primary", &route53.ZoneArgs{
			Name: pulumi.String("example.com"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var primary = new Aws.Route53.Zone("primary", new()
    {
        Name = "example.com",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.route53.Zone;
import com.pulumi.aws.route53.ZoneArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var primary = new Zone("primary", ZoneArgs.builder()
            .name("example.com")
            .build());

    }
}
resources:
  primary:
    type: aws:route53:Zone
    properties:
      name: example.com

The name property specifies the domain this zone manages. Route53 automatically creates NS and SOA records. You’ll use the zone’s nameServers output to update your domain registrar’s name server configuration, directing DNS queries to Route53.

Delegate a subdomain to its own hosted zone

Organizations often split DNS management by creating separate zones for subdomains, allowing different teams to manage records independently.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const main = new aws.route53.Zone("main", {name: "example.com"});
const dev = new aws.route53.Zone("dev", {
    name: "dev.example.com",
    tags: {
        Environment: "dev",
    },
});
const dev_ns = new aws.route53.Record("dev-ns", {
    zoneId: main.zoneId,
    name: "dev.example.com",
    type: aws.route53.RecordType.NS,
    ttl: 30,
    records: dev.nameServers,
});
import pulumi
import pulumi_aws as aws

main = aws.route53.Zone("main", name="example.com")
dev = aws.route53.Zone("dev",
    name="dev.example.com",
    tags={
        "Environment": "dev",
    })
dev_ns = aws.route53.Record("dev-ns",
    zone_id=main.zone_id,
    name="dev.example.com",
    type=aws.route53.RecordType.NS,
    ttl=30,
    records=dev.name_servers)
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/route53"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		main, err := route53.NewZone(ctx, "main", &route53.ZoneArgs{
			Name: pulumi.String("example.com"),
		})
		if err != nil {
			return err
		}
		dev, err := route53.NewZone(ctx, "dev", &route53.ZoneArgs{
			Name: pulumi.String("dev.example.com"),
			Tags: pulumi.StringMap{
				"Environment": pulumi.String("dev"),
			},
		})
		if err != nil {
			return err
		}
		_, err = route53.NewRecord(ctx, "dev-ns", &route53.RecordArgs{
			ZoneId:  main.ZoneId,
			Name:    pulumi.String("dev.example.com"),
			Type:    pulumi.String(route53.RecordTypeNS),
			Ttl:     pulumi.Int(30),
			Records: dev.NameServers,
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var main = new Aws.Route53.Zone("main", new()
    {
        Name = "example.com",
    });

    var dev = new Aws.Route53.Zone("dev", new()
    {
        Name = "dev.example.com",
        Tags = 
        {
            { "Environment", "dev" },
        },
    });

    var dev_ns = new Aws.Route53.Record("dev-ns", new()
    {
        ZoneId = main.ZoneId,
        Name = "dev.example.com",
        Type = Aws.Route53.RecordType.NS,
        Ttl = 30,
        Records = dev.NameServers,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.route53.Zone;
import com.pulumi.aws.route53.ZoneArgs;
import com.pulumi.aws.route53.Record;
import com.pulumi.aws.route53.RecordArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var main = new Zone("main", ZoneArgs.builder()
            .name("example.com")
            .build());

        var dev = new Zone("dev", ZoneArgs.builder()
            .name("dev.example.com")
            .tags(Map.of("Environment", "dev"))
            .build());

        var dev_ns = new Record("dev-ns", RecordArgs.builder()
            .zoneId(main.zoneId())
            .name("dev.example.com")
            .type("NS")
            .ttl(30)
            .records(dev.nameServers())
            .build());

    }
}
resources:
  main:
    type: aws:route53:Zone
    properties:
      name: example.com
  dev:
    type: aws:route53:Zone
    properties:
      name: dev.example.com
      tags:
        Environment: dev
  dev-ns:
    type: aws:route53:Record
    properties:
      zoneId: ${main.zoneId}
      name: dev.example.com
      type: NS
      ttl: '30'
      records: ${dev.nameServers}

This configuration creates two zones: a parent zone for example.com and a child zone for dev.example.com. The NS record in the parent zone delegates queries for dev.example.com to the child zone’s name servers. The nameServers output from the child zone provides the values for the NS record’s records property.

Create a private zone for VPC-internal DNS

Applications running in VPCs often need internal DNS resolution that isn’t exposed to the internet.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const primary = new aws.ec2.Vpc("primary", {
    cidrBlock: "10.6.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
});
const secondary = new aws.ec2.Vpc("secondary", {
    cidrBlock: "10.7.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
});
const _private = new aws.route53.Zone("private", {
    name: "example.com",
    vpcs: [
        {
            vpcId: primary.id,
        },
        {
            vpcId: secondary.id,
        },
    ],
});
import pulumi
import pulumi_aws as aws

primary = aws.ec2.Vpc("primary",
    cidr_block="10.6.0.0/16",
    enable_dns_hostnames=True,
    enable_dns_support=True)
secondary = aws.ec2.Vpc("secondary",
    cidr_block="10.7.0.0/16",
    enable_dns_hostnames=True,
    enable_dns_support=True)
private = aws.route53.Zone("private",
    name="example.com",
    vpcs=[
        {
            "vpc_id": primary.id,
        },
        {
            "vpc_id": secondary.id,
        },
    ])
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/route53"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		primary, err := ec2.NewVpc(ctx, "primary", &ec2.VpcArgs{
			CidrBlock:          pulumi.String("10.6.0.0/16"),
			EnableDnsHostnames: pulumi.Bool(true),
			EnableDnsSupport:   pulumi.Bool(true),
		})
		if err != nil {
			return err
		}
		secondary, err := ec2.NewVpc(ctx, "secondary", &ec2.VpcArgs{
			CidrBlock:          pulumi.String("10.7.0.0/16"),
			EnableDnsHostnames: pulumi.Bool(true),
			EnableDnsSupport:   pulumi.Bool(true),
		})
		if err != nil {
			return err
		}
		_, err = route53.NewZone(ctx, "private", &route53.ZoneArgs{
			Name: pulumi.String("example.com"),
			Vpcs: route53.ZoneVpcArray{
				&route53.ZoneVpcArgs{
					VpcId: primary.ID(),
				},
				&route53.ZoneVpcArgs{
					VpcId: secondary.ID(),
				},
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var primary = new Aws.Ec2.Vpc("primary", new()
    {
        CidrBlock = "10.6.0.0/16",
        EnableDnsHostnames = true,
        EnableDnsSupport = true,
    });

    var secondary = new Aws.Ec2.Vpc("secondary", new()
    {
        CidrBlock = "10.7.0.0/16",
        EnableDnsHostnames = true,
        EnableDnsSupport = true,
    });

    var @private = new Aws.Route53.Zone("private", new()
    {
        Name = "example.com",
        Vpcs = new[]
        {
            new Aws.Route53.Inputs.ZoneVpcArgs
            {
                VpcId = primary.Id,
            },
            new Aws.Route53.Inputs.ZoneVpcArgs
            {
                VpcId = secondary.Id,
            },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.Vpc;
import com.pulumi.aws.ec2.VpcArgs;
import com.pulumi.aws.route53.Zone;
import com.pulumi.aws.route53.ZoneArgs;
import com.pulumi.aws.route53.inputs.ZoneVpcArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var primary = new Vpc("primary", VpcArgs.builder()
            .cidrBlock("10.6.0.0/16")
            .enableDnsHostnames(true)
            .enableDnsSupport(true)
            .build());

        var secondary = new Vpc("secondary", VpcArgs.builder()
            .cidrBlock("10.7.0.0/16")
            .enableDnsHostnames(true)
            .enableDnsSupport(true)
            .build());

        var private_ = new Zone("private", ZoneArgs.builder()
            .name("example.com")
            .vpcs(            
                ZoneVpcArgs.builder()
                    .vpcId(primary.id())
                    .build(),
                ZoneVpcArgs.builder()
                    .vpcId(secondary.id())
                    .build())
            .build());

    }
}
resources:
  primary:
    type: aws:ec2:Vpc
    properties:
      cidrBlock: 10.6.0.0/16
      enableDnsHostnames: true
      enableDnsSupport: true
  secondary:
    type: aws:ec2:Vpc
    properties:
      cidrBlock: 10.7.0.0/16
      enableDnsHostnames: true
      enableDnsSupport: true
  private:
    type: aws:route53:Zone
    properties:
      name: example.com
      vpcs:
        - vpcId: ${primary.id}
        - vpcId: ${secondary.id}

Private zones respond only to queries from associated VPCs. The vpcs property lists VPC IDs where this zone resolves. Each VPC must have enableDnsHostnames and enableDnsSupport set to true. Private zones conflict with delegationSetId since delegation sets only work for public zones.

Beyond these examples

These snippets focus on specific zone-level features: public and private zone creation, subdomain delegation with NS records, and VPC associations for private DNS. They’re intentionally minimal rather than full DNS configurations.

The examples may reference pre-existing infrastructure such as VPCs with DNS support enabled (for private zones) and parent zones (for subdomain delegation). They focus on configuring the zone rather than provisioning all DNS records.

To keep things focused, common zone patterns are omitted, including:

  • Reusable delegation sets (delegationSetId)
  • Force destroy for zones with records (forceDestroy)
  • Accelerated recovery configuration (enableAcceleratedRecovery)
  • DNSSEC configuration (KeySigningKey, HostedZoneDnsSec resources)

These omissions are intentional: the goal is to illustrate how each zone feature is wired, not provide drop-in DNS modules. See the Route53 Zone resource reference for all available configuration options.

Let's create AWS Route53 Hosted Zones

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

VPC & Private Zones
Why am I seeing a perpetual diff with my VPC associations?
You cannot use inline vpc blocks and aws.route53.ZoneAssociation resources for the same zone simultaneously. Choose one approach or use ignoreChanges on the vpcs property to manage additional associations via aws.route53.ZoneAssociation.
Can I create a private hosted zone without VPC associations?
No, private zones require at least one VPC association at all times. You must include at least one VPC in the vpcs array.
How do I create a private hosted zone?
Configure the vpcs property with at least one VPC ID. The VPC must have DNS support and DNS hostnames enabled.
Subdomain Configuration
How do I set up a subdomain zone?
Create the subdomain zone with its full name (e.g., dev.example.com), then create an NS record in the parent zone pointing to the subdomain zone’s nameServers.
Zone Properties & Lifecycle
What properties can't I change after creating a zone?
The name and delegationSetId properties are immutable and cannot be changed after zone creation.
What happens to my DNS records when I delete the zone?
By default, deletion fails if records exist. Set forceDestroy to true to automatically delete all records (including those managed outside Pulumi) when destroying the zone.
How do I disable accelerated recovery after enabling it?
You must explicitly set enableAcceleratedRecovery to false. Simply removing the property from your configuration won’t disable it.
Delegation Sets
Can I use delegation sets with private hosted zones?
No, delegationSetId conflicts with vpc because delegation sets can only be used for public zones.

Using a different cloud?

Explore networking guides for other cloud providers: