Pulumi - DNS in AWS
What
This post explores two things. First it explores setting up a DNS domain, zone and records as part of route 53. Then, we will go further and explore using output of other services as records. For the purposes of demonstrtation this will be a new record that will be created if an EC2 instance exists.
Why
The pattern of route53 is the last step in the infrastructure journey. Setting up infrastructure, and assigning a friendly domain name, and www record to the infrastructure is a relatively normal thing to do.
The pulumi templates in this article will allow you to deploy this pattern quickly and easily.
What
I will re-use the EC2 instance that I have in another Pulumi stack in order to have a virtual machine up and running.
I will also delegate a domain to AWS at my hosting provider. Once done, I will show how to use both the EC2 IP address and create a www record. I will also show how to bypass doing this and create a default record.
Configuration
The configuration of the template is simple and standard. Import the relevant modules, and set the config imports to get the variables from the stack config.
Stack reference
There is a special variable called other_stack in my pulumi config file. This is used in my code as a reference to import a second stack. The second stack is the virtual machine stack. This is where I will get the IP address for the www record from if this variable is defined.
The variable is used as a boolean to determine if a stackreference is used or not. A stack reference is a reference to another stack that allows you to use the outputs of another stack in the current program.
The code snippet below has a standard python if statement that is run if the variable "other_stack" is true. If true, we will import the stack name and import as a stack reference. Once imported, we will then use the IP address of the EC2 instance created in the other stack, and assign it to a variable.
import pulumi
import pulumi_aws as aws
config = pulumi.Config()
project_name = config.get("project_nane", "svk")
project_purpose = config.get("purpose", "route53")
zone_name = config.get("zone_name", "example.com")
use_other_stack = config.get_bool("other_stack", False)
if use_other_stack == True:
source_stack_name = config.get("sourceStackName")
source_stack = pulumi.StackReference(source_stack_name)
vm_ip = source_stack.get_output("public_ip")
The stack reference here is a way of referencing another pulumi stack, and its output variables. This allows me to use the IP address of an EC2 instance in a completely different stack as part of my DNS zone.
DNS zones
The code below configures two DNS zones. These are traditional DNS zones. One is a "main" zone, and one is a sub zone.
The main zone definition is relatively simple. Once created, it returns the nameservers that AWS assigns to the zone. These can be used for redelegation of the zone to the AWS nameservers.
mainzone = aws.route53.Zone("mainzone",
name = zone_name,
)
dev = aws.route53.Zone("dev",
name="dev." + zone_name,
tags={
"Environment": "dev",
})
dev_ns = aws.route53.Record("dev-ns",
zone_id=mainzone.zone_id,
name="dev." + zone_name,
type=aws.route53.RecordType.NS,
ttl=30,
records=dev.name_servers)
Note the the code above also creates a sub zone. This is named dev. In all of my examples I am using one of my test zones, svkcode.com. This means that the sub zone will be named dev.svkcode.com
Once the zone has been created it looks like this in AWS:
DNS Records
The code below creates two DNS records. One for each zone. I have created a www record. This means that there will be two records
www.svkcode.com
www.dev.svkcode.com
The thing to note is that I am using a standard python if statement on one of the records. This means that the record will be conditionally created if we are using an external stack.
If the external stack variable is set, the IP address that is used is the IP address of the virtual machine from our external stack.
This is to cater for times when I do not have an external address from outside this stack that I want to use. The neat thing here is that it is a standard python if statement.
if use_other_stack == True:
www = aws.route53.Record("www",
zone_id=mainzone.zone_id,
name="www." + zone_name,
type=aws.route53.RecordType.A,
ttl=300,
#records=["1.2.3.4"]
records=[vm_ip]
)
else:
www = aws.route53.Record("www",
zone_id=mainzone.zone_id,
name="www." + zone_name,
type=aws.route53.RecordType.A,
ttl=300,
records=["1.2.3.4"]
)
devwww = aws.route53.Record("devwww",
zone_id=dev.zone_id,
name="www.dev." + zone_name,
type=aws.route53.RecordType.A,
ttl=300,
records=["1.1.1.1"]
)
When the zones and records have been created they look like the image below in the console.
Initial zone creation
On initial stack creation, we can see that two zones are created and the associated nameservers for each zone are output to the screen. These can be used for redelegation of the zone to AWS nameservers.
Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/XXX
Type Name Status
+ pulumi:pulumi:Stack aws-route53-dev created (80s)
+ ├─ aws:route53:Zone mainzone created (38s)
+ ├─ aws:route53:Zone dev created (39s)
+ ├─ aws:route53:Record www created (35s)
+ ├─ aws:route53:Record devwww created (35s)
+ ├─ aws:route53:Record dev-ns created (35s)
+ └─ aws:route53:HealthCheck healthcheck created (2s)
Outputs:
Dev Nameservers : [
[0]: "ns-1016.awsdns-63.net"
[1]: "ns-1231.awsdns-25.org"
[2]: "ns-1562.awsdns-03.co.uk"
[3]: "ns-378.awsdns-47.com"
]
Main Nameservers: [
[0]: "ns-1272.awsdns-31.org"
[1]: "ns-1706.awsdns-21.co.uk"
[2]: "ns-185.awsdns-23.com"
[3]: "ns-804.awsdns-36.net"
]
stack flag : false
Resources:
+ 7 created
Duration: 1m21s
Once the redelegation at the domain registrar is complete, we can see that the zone svkcode.org is correctly delegated to the AWS nameservers. The nameservers from my dig command match those that were output at the time of stack creation.
dig NS svkcode.com
; <<>> DiG 9.18.33 <<>> NS svkcode.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28036
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;svkcode.com. IN NS
;; ANSWER SECTION:
svkcode.com. 21600 IN NS ns-1272.awsdns-31.org.
svkcode.com. 21600 IN NS ns-185.awsdns-23.com.
svkcode.com. 21600 IN NS ns-1706.awsdns-21.co.uk.
svkcode.com. 21600 IN NS ns-804.awsdns-36.net.
;; Query time: 142 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
;; WHEN: Sat Nov 15 18:12:19 AEDT 2025
;; MSG SIZE rcvd: 177
Finally, when we use nslookup to test the records that were created we can see that the answer is 1.2.3.4. This means that the if statement has worked correctly and that we have elected not to use an external stack as the source for the www record.
nslookup www.svkcode.com
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
Name: www.svkcode.com
Address: 1.2.3.4
We can further validate that this is the expected behaviour by checking the stack configuration.
pulumi config
pulumi config setKEY VALUE
aws:region ap-southeast-2
other_stack false
sourceStackName codecowboydotio/aws-vm/dev
zone_name svkcode.com
pulumi:tags {"pulumi:template":"aws-python"}
We can see that the other_stack configuration is set to false. This means that the if statement in our code will fall through to the else statement and set a default record value of 1.2.3.4
Zone creation using other stack
When I change the stack configuration and set the other_stack variable to true, this should trigger creating a record using the IP address of an existing virtual machine from another stack.
The first thing I will do is create a virtual machine using my other stack.
Running the other stack, I get an output of the IP address of the virtual machne.
Outputs:
public_dns: [unknown]
public_ip : [unknown]
Resources:
+ 8 to create
Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/XXX
Type Name Status
+ pulumi:pulumi:Stack aws-vm-dev created (32s)
+ ├─ aws:ec2:Vpc svk-vm-vpc created (1s)
+ ├─ aws:ec2:SecurityGroup svk-vm-sg created (3s)
+ ├─ aws:ec2:InternetGateway svk-vm-vpc-igw created (1s)
+ ├─ aws:ec2:Subnet svk-vm-subnet created (11s)
+ ├─ aws:ec2:RouteTable svk-vm-vpc-rt created (1s)
+ ├─ aws:ec2:RouteTableAssociation svk-vm-vpc-rt created (1s)
+ └─ aws:ec2:Instance svk-vm-server created (13s)
Outputs:
public_ip : "3.26.192.12"
Resources:
+ 8 created
Duration: 35s
Next, I change the configuration of the other_stack variable to true. This effectively tells my code to use the IP address of the virtual machine that I just created for the record www.svkcode.com
pulumi config set other_stack true
pulumi config
KEY VALUE
aws:region ap-southeast-2
other_stack true
sourceStackName codecowboydotio/aws-vm/dev
zone_name svkcode.com
pulumi:tags {"pulumi:template":"aws-python"}
With the value now set to true, I can run the stack to create my DNS zones again. As this has already been run, only an update will be performed. The update impacts only the www record.
Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/XXX
Type Name Status Info
pulumi:pulumi:Stack aws-route53-dev
~ └─ aws:route53:Record www updated (40s) [diff: ~records]
Outputs:
Dev Nameservers : [
[0]: "ns-1016.awsdns-63.net"
[1]: "ns-1231.awsdns-25.org"
[2]: "ns-1562.awsdns-03.co.uk"
[3]: "ns-378.awsdns-47.com"
]
Main Nameservers: [
[0]: "ns-1272.awsdns-31.org"
[1]: "ns-1706.awsdns-21.co.uk"
[2]: "ns-185.awsdns-23.com"
[3]: "ns-804.awsdns-36.net"
]
~ stack flag : false => true
Resources:
~ 1 updated
6 unchanged
Duration: 50s
When I check the DNS configuration thi time by querying the records, I can see that the record has been updated to the IP address of the virtual machine from my other stack.
Bash
nslookup www.svkcode.com
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
Name: www.svkcode.com
Address: 3.26.192.12
Finally, if I hit the resource with a web browser, I should see the pacman game. This is because the stack that creates the virtual machine automatically installs a web server and a web based application.
Full Code
Below is the full end to end codebase
Python
import pulumi
import pulumi_aws as aws
config = pulumi.Config()
project_name = config.get("project_nane", "svk")
project_purpose = config.get("purpose", "route53")
zone_name = config.get("zone_name", "example.com")
use_other_stack = config.get_bool("other_stack", False)
if use_other_stack == True:
source_stack_name = config.get("sourceStackName")
source_stack = pulumi.StackReference(source_stack_name)
vm_ip = source_stack.get_output("public_ip")
mainzone = aws.route53.Zone("mainzone",
name = zone_name,
)
dev = aws.route53.Zone("dev",
name="dev." + zone_name,
tags={
"Environment": "dev",
})
dev_ns = aws.route53.Record("dev-ns",
zone_id=mainzone.zone_id,
name="dev." + zone_name,
type=aws.route53.RecordType.NS,
ttl=30,
records=dev.name_servers)
if use_other_stack == True:
www = aws.route53.Record("www",
zone_id=mainzone.zone_id,
name="www." + zone_name,
type=aws.route53.RecordType.A,
ttl=300,
#records=["1.2.3.4"]
records=[vm_ip]
)
else:
www = aws.route53.Record("www",
zone_id=mainzone.zone_id,
name="www." + zone_name,
type=aws.route53.RecordType.A,
ttl=300,
records=["1.2.3.4"]
)
devwww = aws.route53.Record("devwww",
zone_id=dev.zone_id,
name="www.dev." + zone_name,
type=aws.route53.RecordType.A,
ttl=300,
records=["1.1.1.1"]
)
pulumi.export("Main Nameservers", mainzone.name_servers)
pulumi.export("Dev Nameservers", dev.name_servers)
pulumi.export("stack flag", use_other_stack)
Summary
This is a simple and scalable way of creating DNS zones and making sure that the zones are running in AWS. I have also presented a simple and scalable way to create individual records based on other DNS resources. In addition to this, I have created a conditional way of creating records based on another Pulumi stack and its ouputs.
While DNS is a vast area, this particular example has been kept quite simple so that other more advanced topics can be explored at a later date.



Top comments (0)