Multitenant wep applications detect current tenant usually by URL checking name of first level folder or subdomain. Usually tenants are defined by subdomain as it is easier to distribute them over data center, cloud services or hosting accounts. This blog post demonstrates how to build Azure DNS service client to create DNS records for multitenant application subdomains.
So, not everybody has domain these days? Yes, right you are! Private users and starting companies doesn’t often have domain. Modern era businesses sometimes even doesn’t bother having domain. They are doing sales and marketing in social media channels and this is where their customers are.
Those who don’t have domain and those who don’t know how to make DNS entries usually need some solution by us. Our service is hosted on some domain anyway and I’m confident we know how to create subdomains and DNS records. For users who need subdomain we have to set subdomain up also in our DNS server.
Creating DNS record for Azure DNS service is something we luckily can automate.
Azure DNS terms
Before we get started let’s define DNS and Azure DNS service related terms.
- DNS zone – any distinct, contiguous portion of the domain name space in the DNS for which administrative responsibility has been delegated to a single manager. By example, gunnarpeipman.com is one DNS zone in my Azure DNS service.
- DNS record set – collection of records in a zone that have the same name and are the same type. Record set can contain one or more mappings between host name and IP-addresses. blah.gunnarpeipman.com can be record set because it points to two different IP-addresses. Also jekyll.gunnarpeipman.com is record set although it contains one CNAME record.
- DNS record – mapping in record set. Every record matches host name with some IP address or other host name. There are also other types of records available but we don’t need these right here.
With first DNS wisdoms inside our heads it’s time to jump in make hands dirty.
Azure DNS service
Azure DNS service is for managing DNS zones and records you own. It’s simple, reliable, stable and performant. The service relies on Microsoft global network of DNS servers.
Microsoft is not domain registrar! Microsoft doesn’t provide domain registrar services on Azure cloud. If you need new domain then it must be registered with some ISP who provides domain registration service. Your domain will be there where you registered it. To use Azure DNS service you need to configure your domain DNS servers at the site where you registered the domain.
What we will build here is shown on the screenshot below. Suppose we are building administration application for our multitenant SaaS application. When new tenant is added by admin (or why not from self-service portal by new customer) we want to create subdomain for this new tenant automatically.
Screenshot below demonstrates how it looks. We have Azure app service with host name demomultitenant.azurewebsites.net with multiple DNS records pointing to it. If our multitenant service has host name bigsaasapp.com then we have subdomains like contoso.bigsaasapp.com, northwind.bigsaasapp.com etc.
We don’t need subdomain and CNAME record for customers who want to use their own subdomain (like bigsaas.bigcorp.com). We don’t control those domains and this is up to customer to set everything up on their side.
You need Azure AD service principal. To make this example work you need service principal for your web application. I don’t cover these steps here, so please jump to documentation page How to: Use the portal to create an Azure AD application and service principal that can access resources if you don’t know how to create service principal on Azure AD. After creating service principal you have to make sure it has permissions to Azure DNS service.
Configuration for Azure DNS
Before everything else we need Azure DNS service configuration. For this I created simple class.
public class DnsServerSettings
{
public string TenantId { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string SubscriptionId { get; set; }
public string ResourceGroupName { get; set; }
public string ZoneName { get; set; }
}
Here’s the example of configuration in appSettings.json file. ID-s here are random, of course.
"DnsServerSettings": {
"TenantId": "3f2ff00d-9465-4dca-8c2a-736ac4e94a55",
"ClientId": "2e60a18a-3760-4f09-9663-4dbe436b2bfc",
"ClientSecret": "a7s9d69h3denkscxAS7sjndkASJDH",
"SubscriptionId": "77a81331-9341-4e4f-9c65-e6f0051b52de",
"ResourceGroupName": "WebAppHosting",
"ZoneName": "demomultitenant.azurewebsites.net"
}
To make things more convenient I inject DNS service settings to framework-level dependency injection.
var dnsServerSettings = Configuration.GetSection("DnsServerSettings").Get<DnsServerSettings>();
services.AddSingleton(dnsServerSettings);
Single instance of settings is okay as we have one DNS service for all multitenant application subdomains.
DSN service client interface
To have flexibility on coming up with better or more advanced implementation of DNS service client, I created simple interface.
public interface IDnsServiceClient
{
Task<bool> HasCName(string cName);
Task CreateCName(string cName);
}
This interface is to carry out two operations on Azure DNS service:
- check if given CNAME record is already registered in DNS zone
- create new CNAME record to given DNS zone
It’s possible to extend this interface later if there’s need but for this post the interface shown is good enough.
NB! Before going further you need the following NuGet package:
- Microsoft.Azure.Management.Dns
DNS service client
As we already get Azure DNS configuration from framework-level dependency injection and we have thin interface to communicate with service, it’s time to build client class we can use in tenant admin application.
public class DnsServiceClient : IDnsServiceClient
{
private DnsServerSettings _serverSettings;
public DnsServiceClient(DnsServerSettings serverSettings)
{
_serverSettings = serverSettings;
}
public async Task CreateCName(string cName)
{
throw new NotImplementedException();
}
public async Task<bool> HasCName(string cName)
{
throw new NotImplementedException();
}
protected async Task<ServiceClientCredentials> GetCredentials()
{
return await ApplicationTokenProvider.LoginSilentAsync(
_serverSettings.TenantId,
_serverSettings.ClientId,
_serverSettings.ClientSecret
);
}
}
The code above is skeleton for Azure DNS client. It does almost nothing useful yet. Still it gets Azure DNS configuration and is able to generate credentials needed to communicate with service.
Adding CNAME record to DNS
First let’s implement CreateCName() method of Azure DNS service client. We habe to create new record set with CNAME record and save it using DnsManagementClient() class.
public async Task CreateCName(string cName)
{
var credentials = await GetCredentials();
using (var dnsClient = new DnsManagementClient(credentials))
{
dnsClient.SubscriptionId = _serverSettings.SubscriptionId;
var recordSet = new RecordSet {
CnameRecord = new CnameRecord("demoservice.azurewebapps.net"),
TTL = 14400
};
await dnsClient.RecordSets.CreateOrUpdateAsync(
_serverSettings.ResourceGroupName,
_serverSettings.ZoneName,
cName,
RecordType.CNAME,
recordSet
);
}
}
It was easy, although call to CreateOrUpdateAsync() may first seem a little strange.
Quering DNS service for CNAME
Querying for existing DNS record is not so easy and straightforward as adding new records. We may have tens of thousands DNS records and it would put too much load to service when it returns all records for zone with one shot. To keep load under control, it uses paging. We have to read next portion of records from service until NextPageLink property of paged result is empty or null.
public class DnsServiceClient : IDnsServiceClient
{
private DnsServerSettings _serverSettings;
public DnsServiceClient(DnsServerSettings serverSettings)
{
_serverSettings = serverSettings;
}
public async Task CreateCName(string cName)
{
var credentials = await GetCredentials();
using (var dnsClient = new DnsManagementClient(credentials))
{
dnsClient.SubscriptionId = _serverSettings.SubscriptionId;
var recordSet = new RecordSet {
CnameRecord = new CnameRecord("demoservice.azurewebapps.net"),
TTL = 14400
};
await dnsClient.RecordSets.CreateOrUpdateAsync(
_serverSettings.ResourceGroupName,
_serverSettings.ZoneName,
cName,
RecordType.CNAME,
recordSet
);
}
}
public async Task<bool> HasCName(string cName)
{
var credentials = await GetCredentials();
using (var dnsClient = new DnsManagementClient(credentials))
{
dnsClient.SubscriptionId = _serverSettings.SubscriptionId;
var page = await dnsClient.RecordSets.ListAllByDnsZoneAsync(
_serverSettings.ResourceGroupName,
_serverSettings.ZoneName
);
while (true)
{
foreach (var x in page)
{
if (x.Type == "Microsoft.Network/dnszones/CNAME" &&
x.Name.ToLower() == cName.ToLower())
{
return true;
}
}
if (string.IsNullOrEmpty(page.NextPageLink))
{
break;
}
page = await dnsClient.RecordSets.ListAllByDnsZoneNextAsync(page.NextPageLink);
}
return false;
}
}
Now we are done with Azure DNS service client and we can inject it to dependency injection.
Using DNS service client in ASP.NET Core application
Although you probably have some application service class with tenant adding workflow, I will keep things simple and illustrative. Here’s the Create() method of tenants controller in admin application that uses DNS service client through dependency injection to create new CName record with new tenant.
public async Task<IActionResult> Create(TenantEditModel model)
{
if(!ModelState.IsValid)
{
return View(model);
}
if(await _dnsClient.HasCName(model.CName))
{
ModelState.AddModelError("CName", "CName already exists");
return View(model);
}
return RedirectToAction(nameof(Index));
}
If CNAME is already taken then admin is taken back to Create view and error for CNAME record is shown.
Wrapping up
When building administrative interface for multitenant applications we usually need to communicate with bunch of external services. One of these is usually some DNS server or service where we may want to add new CNAME record with subdomain for new tenant. We used Azure DNS service for our multitenant application and it wasn’t complex to check for existing CNAME records and add new ones.
View Comments (1)
thanks, good articles