Simulating DNS ALIAS/ANAME records on Google Cloud DNS
If you are a Heroku and GCP user, chances are you’ve faced the need for a custom domain for your Heroku app and want to use a Cloud DNS zone, then this article is for you. But first, you guessed it, theory…
What is a DNS ALIAS/ANAME record?
DNS ALIAS/ANAME records are a non-standard invention to surpass the limitations of CNAME records. When you need to provide some service at a subdomain name level (www.example.com) where a third-party provider has control over the mapping from names to IP addresses (www-example-com.some-provider-dns-resolver.com), you would use a CNAME record (www) without a glance. But what would happen when it’s needed at a domain level? (example.com)
CNAME records cannot coexist with other records for the same name when you have at least a SOA record already there.
So some DNS providers have implemented their own method to solve this and called it ANAME (to resemble an A record) or ALIAS.
Enough theory, give me the code already!
What’s special about Heroku?
Heroku is a cloud platform service owned by Salesforce. Basically it takes your Python, Java, node.js, PHP, Go, Scala, etc. app, saves it in a git repository, compiles it and runs it inside some sort of containers called “Dynos”. They have a pretty fair free-tier for you to make experiments, if you haven’t used it already, give it a try (https://www.heroku.com)
When you publish a Dyno, a randomly generated hostname is created for you, like “your-app-name.herokuapp.com”. Of course you can go further and set a custom domain name for it and now is when you face the scenario I have mentioned before: a third-party provider that has control over the mapping from names to IP addresses.
Let’s get hands on the example.
The solution
For this, let’s assume you have a validated Heroku account (credit card needed), a GCP account, a GCP project with billing enabled, a public Cloud DNS zone and of course some sort of domain name pointing to Cloud DNS. I got a free domain at https://www.freenom.com (dns-alias-example-gcp.tk).
Cloud DNS is a paid product, but you can test this without incurring too much costs (~0,20 USD/month).
Don’t worry, every bit of code you’ll need is at the end of the article.
First we need a Heroku app, so let’s clone a node.js example from them and customize it a little:
> git clone https://github.com/heroku/node-js-getting-started.git
> cd node-js-getting-started
# Change some code with your favorite tool
> git commit -m 'Some changes'
We are going to use the Heroku CLI to speed up things… (https://devcenter.heroku.com/articles/heroku-cli)
> heroku create dns-alias-example-gcp
# This basically adds a git remote called "heroku" with a branch main and the repository for your freshly created app
> git push heroku main
And that’s it! You have a fully working app on http://dns-alias-example-gcp.herokuapps.com or whatever the name you chose/was assigned.
Now let’s add some custom domains to it, without leaving the app directory:
> heroku domains:add www.dns-alias-example-gcp.tkConfigure your app's DNS provider to point to the DNS Target mathematical-galangal-5xfoibzn3dqfw10ruz2w4e9d.herokudns.com.
For help, see https://devcenter.heroku.com/articles/custom-domainsThe domain www.dns-alias-example-gcp.tk has been enqueued for addition
Run heroku domains:wait 'www.dns-alias-example-gcp.tk' to wait for completion
Adding www.dns-alias-example-gcp.tk to ⬢ dns-alias-example-gcp... done
As you can see, Heroku gives us with a CNAME value to add to our DNS.
Because I’m a terminal fan, let’s use gcloud to speed up…
> gcloud dns record-sets transaction start --zone=dns-alias-exampleTransaction started [transaction.yaml].> gcloud dns record-sets transaction add “mathematical-galangal-5xfoibzn3dqfw10ruz2w4e9d.herokudns.com” \
--name=www.dns-alias-example-gcp.tk \
--ttl=300 \
--type=CNAME \
--zone=dns-alias-exampleRecord addition appended to transaction at [transaction.yaml].> gcloud dns record-sets transaction execute --zone=dns-alias-exampleExecuted transaction [transaction.yaml] for managed-zone [dns-alias-example]....
And voilá! http://www.dns-alias-example-gcp.tk takes us into our Heroku App…
Easy-peasy! That’s all folks. See you next time!
The real solution
Just joking… Now comes the interesting part.
Suppose that for some reason you want http://dns-alias-example-gcp.tk to point to your app, how can you do that?
Let’s add that domain to Heroku
> heroku domains:add dns-alias-example-gcp.tkConfigure your app's DNS provider to point to the DNS Target vertical-paintbrush-60mg95m1acditzsf87sizxv7.herokudns.com.
For help, see https://devcenter.heroku.com/articles/custom-domainsThe domain dns-alias-example-gcp.tk has been enqueued for addition
Run heroku domains:wait 'www.dns-alias-example-gcp.tk' to wait for completion
Adding dns-alias-example-gcp.tk to ⬢ dns-alias-example-gcp... done
But I can’t add a CNAME record pointing to the root domain… So we are going to use a Bucket, a Cloud Function and Cloud Scheduler to periodically translate that “DNS Target” to an standard DNS A record!
Begin by creating our configuration file:
cat <<EOF>config.json
{
"rules": [
{
"zone": "dns-alias-example",
"name": "dns-alias-example-gcp.tk.",
"host": "vertical-paintbrush-60mg95m1acditzsf87sizxv7.herokudns.com"
}
]
}
EOF
We are creating a json with an array of rules that specify the Cloud DNS Zone to update, the hostname (root) and the “third-party host” we use to resolve our ip addresses.
Then we store it in a bucket:
> gsutil mb -b on gs://dns-alias-example
> gsutil cp ./config.json gs://dns-alias-example
We will also need two service accounts with some permissions:
# This one will be for the Cloud Function
> gcloud iam service-accounts create dns-alias-admin# And this one for Cloud Scheduler
> gcloud iam service-accounts create dns-alias-cron# Allow the first one to read the bucket and admin Cloud DNS
> gsutil iam ch \
serviceAccount:dns-alias-admin@dns-alias-example.iam.gserviceaccount.com:roles/storage.objectViewer \
gs://dns-alias-example> gcloud projects add-iam-policy-binding dns-alias-example \
--member=serviceAccount:dns-alias-admin@dns-alias-
example.iam.gserviceaccount.com \
--role=roles/dns.admin
And here’s the magic JS code for the Cloud Function that will save (or doom) us all:
Basically, it works like this:
- Read the config file from the bucket (both file and bucket specified in the request)
- Loop through each rule
- Resolve the Ipv4 address(es) from the host specified for this rule
- Get all A records for the zone
- Compare the results between the dns resolve and the stored A records (if any)
- Update/create accordingly
- Enjoy
To deploy the Cloud Function (do NOT allow unauthenticated requests):
> gcloud functions deploy dns-alias-updater \
--region=us-central1 \
--runtime=nodejs12 \
--trigger-http \
--service-account=dns-alias-admin@dns-alias-example.iam.gserviceaccount.com \
--entry-point=updateRecords
Remember the Cloud Scheduler service account we created before? It will need permissions to run the function:
> gcloud alpha functions add-iam-policy-binding \
dns-alias-updater \
--region=us-central1 \
--member=serviceAccount:dns-alias-cron@dns-alias-example.iam.gserviceaccount.com \
--role=roles/cloudfunctions.invoker
And for the final touch:
> gcloud scheduler jobs create http update-alias-cron \
--schedule="*/5 * * * *" \
--uri="https://us-central1-dns-alias-example.cloudfunctions.net/dns-alias-updater" \
--headers="Content-Type=application/json" \
--http-method POST \
--message-body='{"bucket":"dns-alias-example","fileName":"config.json","ttl":300}' \
--oidc-service-account-email=dns-alias-cron@dns-alias-example.iam.gserviceaccount.com \
--oidc-token-audience="https://us-central1-dns-alias-example.cloudfunctions.net/dns-alias-updater"
Here we are creating a cron job on Cloud Scheduler that runs every 5 minutes invoking the Cloud Function with this parameters:
{
"bucket":"dns-alias-example",
"fileName":"config.json",
"ttl":300
}
We MUST do this with “gcloud”, because at the time of writing this article there is a “bug” with Cloud Scheduler and HTTP invocations. If you don’t set the “Content-Type” header, it will be “application/octet-stream” and your function will fail. Unfortunately you cannot set that parameter from the GUI.
Final words
If for some reason you NEED to use Cloud DNS with Heroku and acces your app at root domain level, this is a fairly easy and cheap solution to that problem until someone makes this a standard DNS functionality, as proposed here:
A working example of the work done here can be found at:
And the code for the Cloud Function can be found here:
Feel free to use my code as needed. Also you can contact me for some guidance. Any kind of feedback is appreciated!