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.
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.
| Resource | Monthly Cost | 
|---|---|
| Price per Zone | $0.50 | 
| Price Per Million Queries | $0.40 | 
| Monthly Lab Estimate | $0.90 | 
| Repo Location | GitHub Repo | 
| Azure | az deployment sub create –location centralus –template-file ./main.bicep –parameters ./vars.bicepparam | 
This lab leverages 7 modules to deploy the solution:
| Module | Description | 
|---|---|
| resourceGroupModule.bicep | Creates the resource group that will host all resources | 
| dnsZoneModule.bicep | Creates DNS Zones that will host the records created by the other modules. | 
| aRecordModule.bicep | Creates all host (A) records across all DNS zones | 
| cnameRecordModule.bicep | Creates all canonical name (CNAME) records across all DNS zones | 
| mxRecordModule.bicep | Creates all mail exchanger (MX) records across all DNS zones | 
| srvRecordModule.bicep | Creates all service (SRV) records across all DNS zones | 
| txtRecordModule.bicep | Creates all service (TXT) records across all DNS zones | 
There’s nothing really overly complicated regarding this module. It creates simply creates resource groups.
targetScope = 'subscription'
param rgName string
param location string
resource resourceGroup 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: rgName
  location: location
}
output id string = resourceGroup.idBICEPThe 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.
@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.nameServersBICEPThe 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.
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.fqdnBits and That Technology Blogparam 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.fqdnBits and That Technology Blogparam 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.fqdnBits and That Technology Blogparam 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 Blogparam 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 BlogThe 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.
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 BlogThe 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.
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 BlogI 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