Posted: Saturday, February 3, 2024

Word Count: 1447

Reading Time: 7 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 bicep 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
Azureaz deployment sub create –location centralus –template-file ./main.bicep –parameters ./vars.bicepparam
Modules

This lab leverages 7 modules to deploy the solution:

ModuleDescription
resourceGroupModule.bicepCreates the resource group that will host all resources
dnsZoneModule.bicepCreates DNS Zones that will host the records created by the other modules.
aRecordModule.bicepCreates all host (A) records across all DNS zones
cnameRecordModule.bicepCreates all canonical name (CNAME) records across all DNS zones
mxRecordModule.bicepCreates all mail exchanger (MX) records across all DNS zones
srvRecordModule.bicepCreates all service (SRV) records across all DNS zones
txtRecordModule.bicepCreates all service (TXT) records across all DNS zones
Module Table Summary

Resource Group Module

There’s nothing really overly complicated regarding this module. It creates simply creates resource groups.

BICEP
targetScope = 'subscription'
param rgName string
param location string

resource resourceGroup 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: rgName
  location: location
}

output id string = resourceGroup.id
BICEP

DNS Zone Module

The module below is solely responsible for creating. There is an output designated in the module, but in this exercise it’s not leveraged. The variables are fed into the module from main.tf using an array found in the vars.bicepparam file.

BICEP
@description('The name of the DNS zone to be created.  Must have at least 2 segments, e.g. hostname.org')
param zoneName string

resource zone 'Microsoft.Network/dnsZones@2018-05-01' = {
  name: zoneName
  location: 'global'
}

output nameServers array = zone.properties.nameServers
BICEP

Record Modules

The record modules are tasked to create the records in the DNS zones. Similar to the DNS zones module, the variables are fed into the module from main.tf using an array found in the vars.bicepparam file. The modules are created to create a specific record type, but across all DNS zones. The variable array determines the ttl, name, values and zone name.

BICEP – aRecordModule.bicep
param recordName string
param zoneName string
param ipv4Address string
param ttl int = 3600

resource zone 'Microsoft.Network/dnsZones@2018-05-01' existing = {
  name: zoneName
}

resource record 'Microsoft.Network/dnsZones/A@2018-05-01' = {
  parent: zone
  name: recordName
  properties: {
    TTL: ttl
    ARecords: [
      {
        ipv4Address: ipv4Address
      }
    ]
  }
}

output id string = record.id
output fqdn string = record.properties.fqdn
Bits and That Technology Blog

BICEP – cnameRecordModule.bicep
param recordName string
param zoneName string
param cname string
param ttl int = 3600

resource zone 'Microsoft.Network/dnsZones@2018-05-01' existing = {
  name: zoneName
}

resource record 'Microsoft.Network/dnsZones/CNAME@2018-05-01' = {
  parent: zone
  name: recordName
  properties: {
    TTL: ttl
    CNAMERecord: {
      cname: cname
    }
  }
}

output id string = record.id
output fqdn string = record.properties.fqdn
Bits and That Technology Blog

BICEP – mxRecordModule.bicep
param recordName string
param zoneName string
param mxRecords array
param ttl int

resource zone 'Microsoft.Network/dnsZones@2018-05-01' existing = {
  name: zoneName
}

resource mxrecord 'Microsoft.Network/dnsZones/MX@2023-07-01-preview' = {
  name: recordName
  parent: zone
  properties: {
    TTL: ttl
    MXRecords: mxRecords
  }

}

output id string = mxrecord.id
output fqdn string = mxrecord.properties.fqdn
Bits and That Technology Blog

BICEP – txtRecordModule.bicep
param recordName string
param zoneName string
param txtRecords array
param ttl int

resource zone 'Microsoft.Network/dnsZones@2018-05-01' existing = {
  name: zoneName
}

resource txtrecord 'Microsoft.Network/dnsZones/TXT@2023-07-01-preview' = {
  name: recordName
  parent: zone
  properties: {
    TTL: ttl
    TXTRecords:  txtRecords
  }

}

output id string = txtrecord.id
output fqdn string = txtrecord.properties.fqdn


Bits and That Technology Blog

BICEP – srvRecordModule.bicep
param recordName string
param zoneName string
param txtRecords array
param ttl int

resource zone 'Microsoft.Network/dnsZones@2018-05-01' existing = {
  name: zoneName
}

resource txtrecord 'Microsoft.Network/dnsZones/TXT@2023-07-01-preview' = {
  name: recordName
  parent: zone
  properties: {
    TTL: ttl
    TXTRecords:  txtRecords
  }

}

output id string = txtrecord.id
output fqdn string = txtrecord.properties.fqdn


Bits and That Technology Blog

Variables

The variable file below controls the deployment of the DNS zones and records. With the exception of the resource group module, there is an array for every module listed above. Each record is enclosed in brackets and it’s a simple matter of copying what’s between the brackets an d modifying appropriately. The zone name value is determined by the order they are listed in the dnsZones parameter starting with 0. For example, to reference mypublicwish-1.com, you would reference dnsZones[0].name.

In each parameter, I personally added comments to group the records related to each dns zone. This isn’t a requirement, but it makes it easier on us humans.

BICEP – vars.bicepparam
using 'main.bicep'
////////////
// DNS Zones
/////////////

param dnsZones = [
  {
    name: 'mypublicwish-1.com'
  }
  {
    name: 'mypublicwish-2.com'
  }
]

//////////////
//  A REcords
/////////////

param aRecords = [
// dnsZones[0].name = mypublicwish-1.com
  {
  key: 'a1'
  ip: '1.1.1.1'
  name: 'hosta'
  zoneName: dnsZones[0].name
  ttl: 60
  }
  {
  key: 'a2'
  ip: '2.2.2.2'
  name: 'hostb'
  zoneName: dnsZones[0].name
  ttl: 60
  }

  // dnsZones[1].name = mypublicwish-1.com
  {
  key: 'a3'
  ip: '3.3.3.3'
  name: 'hostb2'
  zoneName: dnsZones[1].name
  ttl: 60
  }
]

//////////
// CNAMES
//////////

param cNameRecords = [
// dnsZones[0].name = mypublicwish-1.com
  {
  key: 'c1'
  name: '419k0hzlooiuoj3b0lkjlkjg0w541wjjydf53txx1hjlgdk'
  cname: 'dcv1.digitallycertly.com'
  zoneName: dnsZones[0].name
  ttl: 60
  }
  {
  key: 'c2'
  cname: 'verify.cuboidspace.com'
  name: '790kyaq7jlulk3g98dqypd16jlkjlkb3bvgb2htmbh9'
  zoneName: dnsZones[0].name
  ttl: 60
  }

  // dnsZones[1].name = mypublicwish-1.com
  {
  key: 'c3'
  cname: 'www.google.com'
  name: 'www'
  zoneName: dnsZones[1].name
  ttl: 60
  }
]

//////////////
// MX Records
/////////////


param mxRecords  = [
  // dnsZones[0].name = mypublicwish-1.com
  {
    key: 'mx1'
    recordName: '@'
    zoneName: dnsZones[0].name
    ttl: 60
    values: [
      {
        preference: 113
        exchange: 'mxa.hypnospp.org'
      }
      {
        preference: 140
        exchange: 'mxb.hypnospq.org'
      }
    ]
  }
  // dnsZones[0].name = mypublicwish-1.com
  {
    key: 'mx2'
    recordName: '@'
    zoneName: dnsZones[1].name
    ttl: 60
    values: [
      {
        preference: 113
        exchange: 'mxa.hypnosnb.org'
      }
      {
        preference: 140
        exchange: 'mxb.hypnosnc.org'
      }
    ]
  }
]


//////////////
// txtRecords
//////////////


param txtRecords  = [
  // dnsZones[0].name = mypublicwish-1.com
  {
    key: 'txt1'
    recordName: '@'
    zoneName: dnsZones[0].name
    ttl: 60
    targets: [
      {
      value: ['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']
      }
      {
      value: ['uc=ucJupER9Uyg']
      }


  ]
  }
  // dnsZones[0].name = mypublicwish-1.com
  {
    key: 'txt2'
    recordName: '@'
    zoneName: dnsZones[1].name
    ttl: 60
    targets: [
      {
      value: ['elppa-domain-verification=MGNcyKAUTC0rrZ']
      }
      {
      value: ['MOOZ_verify_649uu9bgwXh7NLKibBgouYcfD5ieT']
      }
  ]
  }

]

////////////////
// SRV Records
////////////////


param srvRecords  = [
  // dnsZones[0].name = mypublicwish-1.com
  {
    key: 'srv1'
    recordName: '_starssip._tcp.umbrellacorp.com'
    zoneName: dnsZones[0].name
    ttl: 60
    values: [
      {
        priority: 100
        weight: 10
        port: 443
        target: 'sipfed.online.umbrellacorp.com'
      }
    ]
  }
  {
    key: 'srv2'
    recordName: '_motleyfool._tcp.umbrellacorp.com'
    zoneName: dnsZones[0].name
    ttl: 60
    values: [
      {
        priority: 100
        weight: 10
        port: 443
        target: 'motelusmodules.online.uccreelsoft.com'
      }
    ]
  }
  // dnsZones[0].name = mypublicwish-1.com
  {
    key: 'srv3'
    recordName: '__starssip._tls.extenzaLife.com'
    zoneName: dnsZones[1].name
    ttl: 60
    values: [
      {
        priority: 100
        weight: 10
        port: 443
        target: 'sipdir.online.extenzaLife.com'
      }
      {
        priority: 105
        weight: 13
        port: 443
        target: 'sipdir.online.extenzaLife2.com'
      }
      
    ]
  }
]
Bits and That Technology Blog

Deployment

The main.bicep file puts everything together, the variables, the modules and any relevant outputs; however in this case, there are none. The dns and record modules leverage a for statement to loop the relevant values. Additionally, dependOn has been added to each module to ensure that zones are created after the zones. Since the loop does not create an implicit dependency, this is a requirement.

BICEP – main.bicep
targetScope = 'subscription'
param aRecords array
param mxRecords array
param txtRecords array
param cNameRecords array
param srvRecords array
param dnsZones array

module PubDnsRg 'Modules/resourceGroupModule.bicep' = {
  name: 'pubDNStest'
  params: {
    location: 'Central US'
    rgName: 'pubDNStest'
  }
}

module dnsZone 'Modules/dnsZoneModule.bicep' = [for zone in dnsZones:  {
  name: zone.name
  scope: resourceGroup(PubDnsRg.name)
  params: {
    zoneName: zone.name
  }

}]

module aRecord 'Modules/aRecordModule.bicep' = [for record in aRecords:  { 
 name: record.name
 scope: resourceGroup(PubDnsRg.name)
 params: {
  recordName: record.name
  zoneName: record.zoneName
  ipv4Address: record.ip
  ttl: record.ttl
 }
 dependsOn: [
  dnsZone
 ]
}]

module cNameRecord 'Modules/cnameRecordModule.bicep' = [for record in cNameRecords:  { 
  name: record.name
  scope: resourceGroup(PubDnsRg.name)
  params: {
   recordName: record.name
   zoneName: record.zoneName
   cname: record.target
   ttl: record.ttl
  }
  dependsOn: [
    dnsZone
   ]
 }]

module mxRecord 'Modules/mxRecordModule.bicep' = [for (record, index) in mxRecords: {
    name: record.key
    scope: resourceGroup(PubDnsRg.name)
    params: {
      ttl: record.ttl
      mxRecords: record.values
      recordName: record.recordName
      zoneName: record.zonename
    } 
    dependsOn: [
      dnsZone
     ]
}]

module txtRecord 'Modules/txtRecordModule.bicep' = [for (record, index) in txtRecords: {
  name: record.key
  scope: resourceGroup(PubDnsRg.name)
  params: {
    ttl: record.ttl
    txtRecords: record.targets
    recordName: record.recordName
    zoneName: record.zonename
  } 
  dependsOn: [
    dnsZone
   ]
}]

module srvRecord 'Modules/srvRecordModule.bicep' = [for (record, index) in srvRecords: {
  name: record.key
  scope: resourceGroup(PubDnsRg.name)
  params: {
    ttl: record.ttl
    srvRecords: record.values
    recordName: record.recordName
    zoneName: record.zonename
  } 
  dependsOn: [
    dnsZone
   ]
}]
Bits and That Technology Blog

Conclusion

I decided to leverage the .bicepparam format for the variables in this lab instead of the traditional JSON file. This is a newer format. If you run into an issue deploying this lab, you may need to update AZ CLI to the latest version.



Leave a Reply

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