Posted: Saturday, January 27, 2024

Word Count: 3046

Reading Time: 14 minutes



Any organization requires their DNS records, private and personal, somewhere, why not leverage Azure DNS. The lab below, if you choose to leverage it, provides base terraform code that you can leverage to deploy Azure Public DNS.

Pricing Estimate

This lab is relatively low cost and should only set you back a buck if the resources hang out for a while. As long as you do not surpass 10 million DNS queries in a billing cycle you shouldn’t experience a dramatic increase in your Azure spend.

ResourceMonthly Cost
Price per Zone$0.50
Price Per Million Queries$0.40
Monthly Lab Estimate$0.90
TLDR Summary
Repo LocationGitHub Repo
Terraform CommandsTerraform Init
Terraform Plan -out=PubDNSLab
Terraform Apply -out=PubDNSLab
Global Variables and Locals

Variables and Locals that are not relevant to the creation of DNS Records or zones are stored here. This file sets, resource location, subscription ID, and a few tags for testing purposes.

Terraform – variables.tf
## Vars

variable "location" {
  default = "centralus"
}

variable "department" {
  default = "awesome"
}

variable "environment" {
  default = "nonprod"
}

variable "subscription_id" {
  default = "SUBSCRIPTION_ID_HERE"
}

## Locals

data "external" "me" {
  program = ["az", "account", "show", "--query", "user"]
}

locals {
  dns_resource_group_name = "my-publicdns-lab"
  Owner                   = lookup(data.external.me.result, "name")
  localuser               = split("@", local.Owner)
  tags = {
    Environment  = var.environment
    Createdby    = lookup(data.external.me.result, "name")
    LastModifiedDate = formatdate("DD MMM YYYY hh:mm ZZZ", timestamp())
    Department = var.department
  }
}
Bits and That Technology Blog

Terraform providers are plugins that Terraform uses to interact with remote systems. Each provider offers a collection of resource types and data sources that Terraform can manage. Providers are responsible for understanding API interactions and exposing resources. They are usually developed by vendors of the service or by the Terraform community.

Terraform – providers.tf
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0.2"
    }
  }
  required_version = ">= 1.1.0"
}
provider "azurerm" {
  subscription_id = var.subscription_id
  features {}
}
Bits and That Technology Blog
Modules

As you would expect the modules are configured in a similar fashion. Variables, Resources and outputs are all consolidated into a single tf file. The resources are leveraging a for_each statement to loop through an array of variables to create the related resources. Additionally, you will notice a key has been added to each variable file.

Resource Groups

This module is foundational to everything else , as it creates the resource group that will host everything in this lab.

Terraform – resourceGroup.tf
## Variables

variable "location" {}

variable "DnsResourceGroup" {}

variable "tags" {}

## Resources

resource "azurerm_resource_group" "public_dns_zone" {
  name     = var.DnsResourceGroup
  location = var.location
  tags     = var.tags
}

## Outputs

output "name" {
  value = azurerm_resource_group.public_dns_zone.name
}

output "id" {
  value = azurerm_resource_group.public_dns_zone.id
}

output "location" {
  value = azurerm_resource_group.public_dns_zone.location
}

output "tags" {
  value = azurerm_resource_group.public_dns_zone.tags
}
Bits and That Technology Blog

DNS Zone Module and Variables File

The DNS zone module is leveraged to create all required DNS zones in the azure subscription. In this lab, we create three public DNS zones: massivedynamics.com, umbrellacorp.com and initech.com. The zones are fed into an output array which is referenced by the modules that create the corresponding records in each zone.

Terraform – dnszones_module.tf
variable resource_group_name {}

variable "dnszones" {
  type = list(object({
    key       = string
    name      = string
  }))
}

resource "azurerm_dns_zone" "zones" {
  for_each = { for idx, record in var.dnszones : idx => record }
  name                = each.value.name
  resource_group_name = var.resource_group_name
}

output "ids" {
  value = { for key, zone in azurerm_dns_zone.zones : key => zone.id }
}

output "all_zone_names" {
  value = { for key, zone in azurerm_dns_zone.zones : key => zone.name }
}
Bits and That Technology Blog

Terraform – dnszones.tf
#Zones

locals {
  dnszones = [
    {
        key = "000"
        name                = "massivedynamics.com"
    },
    {
      key = "001"
        name                = "umbrellacorp.com"
    },
    {
      key = "002"
        name                = "initech.com"
    },
        # Add more dns zones as needed
  ]
}
Bits and That Technology Blog

Records Creations

Every module in this section is tasked with creating records in the zones listed above. There are modules for A, CNAME, MX, SRV and TXT records. Each modules created that related records for all zones.

Host (A) Records

This module and variable files in this section will create all host (a) records across each DNS zone.

Terraform – a_module.tf
## Variables

variable "resource_group_name" {}

variable "host_records" {
  type = list(object({
    key       = string
    a_record = string
    zone      = string
    ttl       = number
    values    = list(string)
  }))
}

## Resources

resource "azurerm_dns_a_record" "arecords" {
  for_each = { for record in var.host_records: record.key => record }
  name                = each.value.a_record
  zone_name           = each.value.zone
  resource_group_name = var.resource_group_name
  ttl                 = each.value.ttl
  records             = each.value.values
}

## Output

output "name" {
    value = { 
        for key, record in azurerm_dns_a_record.arecords: key => record.name}
}

output "zone_name" {
  value = { 
  for key, record in azurerm_dns_a_record.arecords: key => record.zone_name}
}

output "resource_group_names" {
    value = { 
    for key, record in azurerm_dns_a_record.arecords: key => record.resource_group_name
    }
}

output "ttls" {
    value = { 
    for key, record in azurerm_dns_a_record.arecords: key => record.ttl
    }
}

output "ttls" {
    value = { 
    for key, record in azurerm_dns_a_record.arecords: key => record.ttl
    }
}
Bits and That Technology Blog

Terraform – a_records.tf
#############
# A RECORDS
#############

locals {
  arecords_list = [

    # massivedynamics.com - 0

    {
      key     = "${module.public_zones.all_zone_names[0]}-0"
      a_record = "www"
      values  = ["3.0.0.3"]
      zone    = module.public_zones.all_zone_names[0]
      ttl     = 3600
    },
    {
      key     = "${module.public_zones.all_zone_names[0]}-1"
      a_record = "www2"
      values  = ["3.0.0.3"]
      zone    = module.public_zones.all_zone_names[0]
      ttl     = 3600
    },

    # umbrellacorp.com - 1


    {
      key     = "${module.public_zones.all_zone_names[1]}-0"
      a_record = "www"
      values  = ["2.0.0.2"]
      zone    = module.public_zones.all_zone_names[1]
      ttl     = 3600
    },

    #Initech.com - 2

    {
      key     = "${module.public_zones.all_zone_names[2]}-0"
      a_record = "www"
      values  = ["1.0.0.1"]
      zone    = module.public_zones.all_zone_names[2]
      ttl     = 3600
    },
        # Add more record sets as needed
  ]
}
JavaScript

CNAMES

The module and variable files in this section will create all canonical name (CNAME) records across each DNS zone.

Terraform – cname_module.tf
## Variables

variable "resource_group_name" {}

variable "cname_records" {
  type = list(object({
    key       = string
    cname_record = string
    zone      = string
    ttl       = number
    values    = list(string)
  }))
}

## Resources

resource "azurerm_dns_cname_record" "cnames" {
  name                = each.value.cname_record
  zone_name           = var.zone_name
  resource_group_name = var.resource_group_name
  ttl                 = var.ttl
  record              = var.values
}

## Outputs

output "name" {
    value = { 
        for key, record in azurerm_dns_a_record.cnames: key => record.name}
}
}

output "zone_name" {
  value = { 
  for key, record in azurerm_dns_a_record.cnames: key => record.zone_name}
}

output "resource_group_names" {
    value = { 
    for key, record in azurerm_dns_a_record.cnames: key => record.resource_group_name
    }
}

output "ttls" {
    value = { 
    for key, record in azurerm_dns_a_record.cnames: key => record.ttl
    }
}
Bits and That Technology Blog

Terraform – cname_module.tf
################
# CNAMES
################

locals {
  cnamerecords_list = [

    # massivedynamics.com - 0

    {
      key         = "${module.public_zones.all_zone_names[0]}-1"
      cname_record = "autodiscover"
      values      = "autodiscover.massivelogistic.com"
      zone        = module.public_zones.all_zone_names[0]
      ttl         = 180
    },

    # umbrellacorp.com - 1

    {
      key         = "${module.public_zones.all_zone_names[1]}-2"
      cname_record = "cdnverify.zealots"
      values      = "cdnverify.umbrellacorp.cerberus.net"
      zone        = module.public_zones.all_zone_names[1]
      ttl         = 180
    },

    # Initech.com -2
    

    {
      key         = "${module.public_zones.all_zone_names[2]}-1"
      cname_record = "f6hkKeVcvvQ1CHVwca6kNWG8Gp1"
      values      = "dcv.Initechcert.com"
      zone        = module.public_zones.all_zone_names[2]
      ttl         = 3600
    },
        # Add more record sets as needed
  ]
}
Bits and That Technology Blog

MX

The module and variable files in this section will create all mail exchanger (MX) records across each DNS zone.

Terraform – mx_module.tf
## Variables

variable "resource_group_name" {
  type = string
}

variable "mx_records" {
  type = list(object({
    key         = string
    mxrecord    = string
    zone        = string
    ttl         = number
    preferences = list(number)
    values      = list(string)
  }))
}

## Resources

resource "azurerm_dns_mx_record" "mx_record" {
  for_each = { for record in var.mx_records :
    record.key => record
  }

  name                = each.value.mxrecord
  zone_name           = each.value.zone
  resource_group_name = var.resource_group_name
  ttl                 = each.value.ttl

  dynamic "record" {
    for_each = [for i in range(length(each.value.preferences)) : {
      preference = each.value.preferences[i]
      value      = each.value.values[i]
    }]

    content {
      preference = record.value.preference
      exchange   = record.value.value
    }
  }
}

## Outputs

output "name" {
    value = { 
        for key, record in azurerm_dns_mx_record.mx_record: key => record.name}
}

output "zone_name" {
  value = { 
  for key, record in azurerm_dns_mx_record.mx_record: key => record.zone_name}
}

output "resource_group_names" {
    value = { 
    for key, record in azurerm_dns_mx_record.mx_record: key => record.resource_group_name
    }
}

output "ttls" {
    value = { 
    for key, record in azurerm_dns_mx_record.mx_record: key => record.ttl
    }
}
Bits and That Technology Blog

Terraform – mx_records.tf
##############
# MX Records
##############

locals {
  mxrecords_list = [
    #massivedynamics Care - 0

    {
      key         = "${module.public_zones.all_zone_names[0]}-00"
      mxrecord    = "@"
      preferences = [100, 101]
      values      = ["usb-smtp-inbound-1.massivedynamics.com", "usb-smtp-inbound-2.massivedynamics.com"]
      zone        = module.public_zones.all_zone_names[0]
      ttl         = 60
    },

    # umbrellacorp.com -1

    {
      key         = "${module.public_zones.all_zone_names[0]}-00"
      mxrecord    = "@"
      preferences = [100, 200]
      values      = ["usb-smtp-inbound-1.nemesis.com", "usb-smtp-inbound-2.nemesis.com"]
      zone        = module.public_zones.all_zone_names[1]
      ttl         = 60
    },
    {
      key         = "${module.public_zones.all_zone_names[0]}-01"
      mxrecord    = "gh-mail"
      preferences = [113, 140]
      values      = ["mxa.hypnos.org", "mxb.hypnos.org"]
      zone        = module.public_zones.all_zone_names[1]
      ttl         = 60
    },

    # Initech.com - 2

    {
      key         = "${module.public_zones.all_zone_names[0]}-00"
      mxrecord    = "@"
      preferences = [200, 300]
      values      = ["usb-smtp-inbound-1.Initech.com", "usb-smtp-inbound-2.Initech.com"]
      zone        = module.public_zones.all_zone_names[2]
      ttl         = 60
    },


    # Add more MX record sets as needed
  ]
}
Bits and That Technology Blog

TXT Records

The module and variable files in this section will create all text (TXT) records across each DNS zone.

Terraform – txt_module.tf
## Variables

variable "resource_group_name" {}

variable "txt_records" {
  type = list(object({
    key       = string
    txtrecord = string
    zone      = string
    ttl       = number
    values    = list(string)
  }))
}

## Resources 

resource "azurerm_dns_txt_record" "txt_records" {
  for_each = { for idx, record in var.txt_records : idx => record }

  name                = each.value.txtrecord
  zone_name           = each.value.zone
  resource_group_name = var.resource_group_name
  ttl                 = 300

  dynamic "record" {
    for_each = each.value.values
    content {
      value = record.value
    }
  }
}

## Outputs

output "name" {
    value = { 
        for key, record in azurerm_dns_txt_record.txt_records: key => record.name}
}

output "zone_name" {
  value = { 
  for key, record in azurerm_dns_txt_record.txt_records: key => record.zone_name}
}

output "resource_group_names" {
    value = { 
    for key, record in azurerm_dns_txt_record.txt_records: key => record.resource_group_name
    }
}

output "ttls" {
    value = { 
    for key, record in azurerm_dns_txt_record.txt_records: key => record.ttl
    }
}
Bits and That Technology Blog

Terraform – txt_records.tf

locals {

  # massivedyamics.com - 0
  txtrecords = [
    {
      key       = "${module.public_zones.all_zone_names[0]}-0"
      txtrecord = "@"
      values = [
        "v=spf1 include:spf.protection.outlook.com include:usb._netblocks.mimecast.com include:nw026.com include:nw027.com include:nw028.com include:emailus.freshservice.com include:mg-spf.greenhouse.io include:rp.oracleemaildelivery.com ~all",
        "uc=ucJupER9Uyg",
        "elppa-domain-verification=MGNcyKAUTC0rrZ",
        "MOOZ_verify_649uu9bgwXh7NLKibBgouYcfD5ieT",
        "mdfoundation-domain-verification=JTi2sDRuraVFuwF7fxrzFA15zgQDqX0nTFY9x8Qo450qKtRAFhiy36LifU3Z8spksK"
      ]
      zone = module.public_zones.all_zone_names[0]
      ttl  = 3600
    },
    {
      key       = "${module.public_zones.all_zone_names[0]}-1"
      txtrecord = "_dmarc"
      values    = ["v=DMARC1; p=none; rua=mailto:dmarc_agg@massivelogistic.com; ruf=mailto:dmarc_for@massivelogistic.com; fo=1"]
      zone      = module.public_zones.all_zone_names[0]
      ttl       = 3600
    },

    # umbrellacorp.com - 1

    {
      key       = "${module.public_zones.all_zone_names[1]}-0"
      txtrecord = "9eqtue6uxumx0efgxrslq._domainkey"
      values = ["v=DKIM1;p=DDCyJiiTwoF3B3QLpcWYoXwynBKiC4CkTz5XjLG0rLH6xVhqKUVam0up5cXHyEMNWeruvGaPVeTN4AH4NV1j54TP0eXr1XNyvRER",
      "cQEPEA1LAJbLVTpiAbuBWZXKLeEC3yKWNcA0fAT46suFaWBWJFVjHfx4c9XgnUbDjBVXRm0o9uNnrpJXGZwuG3rCUY51ueRcqYs9"]
      zone = module.public_zones.all_zone_names[1]
      ttl  = 3600
    },
    {
      key       = "${module.public_zones.all_zone_names[1]}-1"
      txtrecord = "@"
      values = ["tvVjmvFbDEsDt3PaGmQUH3q7UEQjCbo", "neptune-site-verification=QWn6gJx5P1hcgJXekCczasmw2pMX6NQCZEUu6Fnu5RLbkNJb-r6z3vJaQ",
        "md=ucTvcNne0tU", "v=spf1 include:nyx.org include:spf.protection.garrador.com include:usb._netblocks.mimecast.com  include:umbrella-com.spf.smtp258.com ~all",
        "bandersnatch_verification_token=C24FE082863254F899D681B27847735825", "pluto-site-verification=b875ca0b846dc584f79a94ee1f990d63",
        "v=DMARC1; p=quarantine; rua=mailto:dmarc_agg@cockroach.com; ruf=mailto:dmarc_for@cockroach.com; fo=1"
      ]
      zone = module.public_zones.all_zone_names[1]
      ttl  = 3600
    },


    {
      key       = "${module.public_zones.all_zone_names[1]}-2"
      txtrecord = "@"
      values = ["tvVjmvFbDEsDt3PaGmQUH3q7UEQjCbo", "neptune-site-verification=QWn6gJx5P1hcgJXekCczasmw2pMX6NQCZEUu6Fnu5RLbkNJb-r6z3vJaQ",
        "uc=ucTvcNne0tU", "v=spf1 include:nyx.org include:spf.protection.garrador.com include:usb._netblocks.mimecast.com  include:umbrella-com.spf.smtp258.com ~all",
        "bandersnatch_verification_token=C24FE08286376254F899D68881B27847735825", "pluto-site-verification=b875ca0areiib846dgc584f79a94ee1f990d63",
        "v=DMARC1; p=quarantine; rua=mailto:dmarc_agg@cockroach.com; ruf=mailto:dmarc_for@cockroach.com; fo=1"
      ]
      zone = module.public_zones.all_zone_names[1]
      ttl  = 3600
    },

    # Initech -2

    {
      key       = "${module.public_zones.all_zone_names[2]}-0"
      txtrecord = "_dmarc"
      values    = ["v=DMARC1; p=quarantine; rua=mailto:dmarc_agg@Initech.com; ruf=mailto:dmarc_for@Initech.com; fo=1"]
      zone      = module.public_zones.all_zone_names[2]
      ttl       = 3600
    },
# add additional TXT Records Below

  ]
}
Bits and That Technology Blog

SRV Records

The module and variable files in this section will create all service (SRV) records across each DNS zone.

Terraform – srv_module.tf
## Variables

variable "resource_group_name" {
  type = string
}

variable "srv_records" {
  type = list(object({
    key       = string
    srvrecord = string
    zone      = string
    ttl       = number
    values    = list(string)
    priority  = list(string)
    weight    = list(string)
    ports     = list(string)

  }))
}

## Resources

resource "azurerm_dns_srv_record" "srvrecords" {
  for_each = { for record in var.srv_records :
    record.key => record
  }

  name                = each.value.srvrecord
  zone_name           = each.value.zone
  resource_group_name = var.resource_group_name
  ttl                 = each.value.ttl

  dynamic "record" {
    for_each = [for i in range(length(each.value.values)) : {
      priority = each.value.priority[i]
      weight   = each.value.weight[i]
      target   = each.value.values[i]
      ports    = each.value.ports[i]
    }]

    content {
      priority = record.value.priority
      weight   = record.value.weight
      target   = record.value.target
      port     = record.value.ports
    }
  }
}

## Outputs

output "name" {
  value = azurerm_dns_srv_record.srvrecords
}

output "zone_name" {
  value = azurerm_dns_srv_record.srvrecords
}

output "resource_group_name" {
  value = azurerm_dns_srv_record.srvrecords
}

output "ttl" {
  value = azurerm_dns_srv_record.srvrecords
}
Bits and That Technology Blog

Terraform – srv_module.tf
##############
# SRV RECORDS
##############


locals {
  srv_recordsets = [
    
# Massive Dynamics - 0
    {
      key       = "${module.public_zones.all_zone_names[0]}-01"
      srvrecord = "__starssip._tls.extenzaLife.com"
      priority  = [100]
      weight    = [1]
      ports     = [443]
      values    = ["sipdir.online.extenzaLife.com"]
      zone      = module.public_zones.all_zone_names[0]
      ttl       = 60
    },
    {
      key       = "${module.public_zones.all_zone_names[0]}-02"
      srvrecord = "__starssip._tls.fleming-monroe.com"
      priority  = [100]
      weight    = [1]
      ports     = [443]
      values    = ["sipdir.online.fleming-monroe.com"]
      zone      = module.public_zones.all_zone_names[0]
      ttl       = 60
    },

# Umbrella Corp - 1

    {
      key       = "${module.public_zones.all_zone_names[1]}-01"
      srvrecord = "_starssip._tcp.umbrellacorp.com"
      priority  = [100]
      weight    = [1]
      ports     = [5061]
      values    = ["sipfed.online.umbrellacorp.com"]
      zone      = module.public_zones.all_zone_names[1]
      ttl       = 1800
    },
    {
      key       = "${module.public_zones.all_zone_names[1]}-02"
      srvrecord = "__starssip._tls.umbrellacorp.com"
      priority  = [100]
      weight    = [1]
      ports     = [443]
      values    = ["sipdir.online.umbrellacorp.com"]
      zone      = module.public_zones.all_zone_names[1]
      ttl       = 1800
    },
  ]
}
Bits and That Technology Blog

Deployment

All of the files above culminate into the main.tf file below. Each module is associated with the relevant locals array.

Terraform – main.tf
### Resource Group Creations

module "dns_resource_group" {
  source           = "./Modules/resourceGroup"
  DnsResourceGroup = local.dns_resource_group_name
  location         = var.location
  tags             = local.tags
}

### Zone Creations

module "public_zones" {
  source              = "./Modules/dnsZones"
  resource_group_name = module.dns_resource_group.name
  dnszones            = local.dnszones
}

### Record Creations

module "Arecords" {
  source              = "./Modules/Records/A"
  resource_group_name = module.dns_resource_group.name
  host_records        = local.a_records_list
}

module "CNAMErecords" {
  source              = "./Modules/Records/CNAME"
  resource_group_name = module.dns_resource_group.name
  cname_records       = local.cname_records_list
}



module "MXrecords" {
  source              = "./Modules/Records/MX"
  resource_group_name = module.dns_resource_group.name
  mx_records          = local.mxrecords_list
  depends_on          = [module.public_zones]
}

module "Srvrecords" {
  source              = "./Modules/Records/SRV"
  resource_group_name = module.dns_resource_group.name
  srv_records         = local.srv_recordsets
  depends_on          = [module.public_zones]
}

module "Txtrecords" {
  source              = "./Modules/Records/TXT"
  resource_group_name = module.dns_resource_group.name
  txt_records         = local.txtrecords
  depends_on          = [module.public_zones]
}
Bits and That Technology Blog

Conclusion

All done! You are encouraged to augment as you see fit. Also, don’t forget to clean up your subscription one you’re done tinkering. Although, $0.90 shouldn’t break the bank, every little bit helps.


Leave a Reply

Your email address will not be published. Required fields are marked *