Using a Custom Domain with Google Cloud Functions for Firebase

5/17/17 - DON'T DO THIS ANYMORE

As of I/O 2017 Firebase has released a native Firebase Hosting + Functions integration which gives you this exact same functionality without leaving the Firebase ecosystem.

It's way easier so you should read about Firebase Hosting + Functions.

DON'T DO THIS ANYMORE

So you're starting to dive into Google Cloud Functions for Firebase, you've set up some functions which react to Realtime Database and Firebase Auth events - it's all great.

Now you decide you want a webhook-type Cloud Function which gets exposed via an HTTPS endpoint. Good thing Cloud Functions supports that.

You write your function, you deploy it, and you see your new HTTPS endpoint is...

https://us-central1-oceanic-night.cloudfunctions.net/log_pls  

Woah. That's a mouthful. Wouldn't it be great if we could use a Custom Domain so our function would be something memorable like...

https://func.example.com/log_pls  

That would be a lot easier to remember.

Sadly, Google Cloud Functions do not support custom domains out of the box. It's okay though, I can show you how to use a third-party service as a very cheap reverse-proxy which has the side benefit of acting as a CDN if we ever want cached content (which you likely will, as Cloud Functions HTTPS triggers can be a bit slow for some use cases).

I'll be using KeyCDN.com as my CDN / reverse-proxy, however most CDNs can do this job. However, don't be suprised if they charge you outrageous amounts for SSL (or other features). I compared a ton of CDNs for this project and KeyCDN was an obvious choice when it comes to cost and ease of use. (I have no affiliation with them, just a happy user)

Let's dive in!

Step 1. Sign Up for KeyCDN

You can head over to KeyCDN to register. You'll be in trial mode for your first month, then after that you only pay for your usage. If you're not expecting much traffic, it'll be pretty darn cheap.

Below is my dashboard (with a little usage from testing).

Step 2. Create a "Zone"

Once you're signed up, click on the "Zones" section on the right. A "zone" is basically a source for KeyCDN to pull from. So in our situation our zone will be the Cloud Functions server.

I have an existing zone already set up for my flickering-torch-4371 Firebase project named upsheet, so we'll add another for my oceanic-night project.

Note how the origin URL is set to my project's Cloud Function URL. This existing zone is served at that "Zone URL" and mirrors content from the "Origin URL".

Let's make a new zone. Hit the Add Zone button.

Give your zone a name, I named mine the same as my Firebase project ID oceanic-night but with the dash removed so it fit within the naming standards. Next make sure your Zone Status is "active and Zone Type is "pull.

If you want to read more about Pull / Push CDNs and these other options, then I'd check out KeyCDN's Getting Started they have a ton of information about using their service and CDNs / caching in general.

Now check the "Show Advanced Features" option. We're going to see a bunch of extra options under "Common Zone Settings".

Now we need to choose several settings for our zone. These are all "sane defaults" which means I'm guessing that this is what you want for your project, however it's worth reading about each setting to see if it's useful for you. For this tutorial to work, you'll need to set each of these options exactly.

SSL => "letsencrypt"

This will allow our custom domain to be served with a free LetsEncrypt SSL certificate

Force SSL => "enabled"

This will redirect any "http://" requests to "https://"

Origin URL => [See Below]

This URL will be the base URL for all your cloud functions. If you currently have a function at

# NOT YOUR ORIGIN URL
https://us-central1-FIREBASE_ID.cloudfunctions.net/FUNCTION_NAME  

Then your origin URL is...

# YOUR ORIGIN URL
https://us-central1-FIREBASE_ID.cloudfunctions.net/  

This is critical to get right.

Ignore Cache Control => "disabled"

This ensures that KeyCDN will listen to cache-control headers from our functions. This allows us to specify which functions should be cached and which functions should never be.

Ignore Query String => "disabled"

Ensure that we never reply with a cached response which was created with a different querystring.

Forward Host Header => "disabled"

This needs to be disabled, otherwise KeyCDN will forward their hostname to Google Cloud's servers and Cloud wont know how to handle that URL.

Cache Cookies => "disabled"

If our Cloud Function is setting a cookie, you almost definitely don't want it cached.

Strip Cookies => "disabled"

There may be a situation where your Cloud Function sets a cookie, so don't strip them out.

And... we're done with setting. Hit the "Save" button.

Fantastic, KeyCDN is now rolling out our CDN / reverse-proxy to all of their servers. While this works we can get on to the next step - setting up a Zonealias.

Step 3. Create a Zonealias

A zonealias is just another URL which connects to a KeyCDN Zone. Our goal is to serve our Cloud Functions from a custom URL - this is where we'll configure that URL.

Again, you can see I already have an existing zonealias set up for my upsheet project. You can see how this connects the "alias" func.upsheet.co to my zone. This means that any request I make will go through KeyCDNs servers and hit the origin URL we configured in the last step.

You'll see on the top of this page that KeyCDN says we need to add a CNAME record to our domain's DNS so we'll do that next.

Open your domain registrar and find the DNS Settings for your domain.

Note: This is my specific registrar's DNS editing page, your will look different.

Now find where you can add a DNS record and create a CNAME record. The host field can be either a subdomain or left blank if you'd like to serve your functions on a naked domain. Then the value needs to be the Zone URL of the zone we created.

Once you've added that record (and made sure to save it!). Head back to KeyCDN and click on the "Add Zonealias" button.

Your alias will be the URL you want to access your functions at. I want my functions to be at..

https://func.pandri.com/FUNCTION_NAME  

So my alias is...

func.pandri.com  

Then pick the "Zone" you want associated with this alias - choose the one we made in the last step.

Click the "Add" button and you'll probably get an error.

If you got an error here, don't worry - it's totally expected.

DNS records take a little while to propegate (probably 15-20 min, but it can be up to a couple hours) so just go do something else then try hitting the "Add" button again in a little bit.

Once your Zonealias gets added you'll see this.

That means KeyCDN is ready to go. We're not quite done - but everything from KeyCDN is ready.

Step 5. Add Cache-Control headers to our Cloud Functions

The very last thing we need to do is to ensure that our functions are never cached. Remember, KeyCDN's main goal is to cache content on it's servers to speed up requests. That can be really useful, but if all we just want it to be a reverse-proxy, then we need to tell it not to cache our functions.

Let's open a terminal and do a curl FUNCTION_URL request to our new domain.

My test function responds successfully, great! If we run that same curl request again, you'll see it responds really fast. Why is going through KeyCDN so much faster? Well it's because it's caching our function's response.

We can prove that this is happening by running curl -v FUNCTION_URL

Note the highlighted line - that's the X-Cache header. This is a special header which KeyCDN appends to it's response to tell us whether or not the content is being served from it's cache. In this case it's X-Cache: HIT so it looked in it's cache, found a HIT and returned that instead of calling our function.

We'll need to modify our function to return a Cache-Control header which tells KeyCDN to never cache the result.

There are many situation where you may want the result of a function cached, in those situations just return a different Cache-Control header. You can check out the KeyCDN docs on Cache-Control for more information on these headers.

Let's look at our function's source code.

exports.log_pls = function (req, res) {  
  console.log("This is a log.");

  res.status(200).send('Success!');
};

The only thing this function does is send out a console.log to Cloud Logging and then send back a Success! message. If we don't want this to be cached we need to add a header.

exports.log_pls = function (req, res) {  
  console.log("This is a log.");

  // Add this header
  res.header({
    "Cache-Control": "private, max-age=0, no-cache"
  });

  res.status(200).send('Success!');
};

This Cache-Control header is a little overkill, but it will ensure that KeyCDN never caches this content. We tell KeyCDN that this item is private, the max age it can be before being refreshed is zero, and we then ask it nicely to never cache it.

Now you can redeploy all your HTTPS Cloud Functions with this header to ensure they don't get cached.

Once we've redeployed, we need to tell KeyCDN that the old version (the version which was okay to cache) is now obsolete.

Open up KeyCDN's Zone page again.

Find your zone and go to Manage > Purge. This will remove all cached content.

Finally, let's verify that our function will never be cached. We'll run curl -v FUNCTION_URL again.

Notice how the previous response had an X-Cache: HIT header and the new response has an X-Cache: MISS header. This means it looked for a cached entry, but couldn't find one (because we told it to never cache this URL).

You may get a Cache: MISS header because the cache was empty, then the cache used that MISS's response to populate the cache for future requests. Let's ensure that this isn't happening and that the response will always MISS and as a result always call our Cloud Function.

I ran a bunch of curl requests and pulled out just the X-Cache header. They're all X-Cache: MISS, this means we're good to go.

You can now start using your functions at their fancy new URLs for all your projects.

Closing Comments

This entire process may seem like overkill just to get a Custom URL in place, however the value of placing a CDN between your functions and your users is actually fairly significant. Should you choose to enable caching in the future, you're already all set up - you just need to return the right headers.

I've been using this setup in one of my projects for a bit now and it seems to work really well. I haven't had any problems. Just remember to add the Cache-Control headers. If you forget this, you'll see very odd behaviors as your "functions" return old results and never seem to log anything, haha.


Follow me on Twitter for more Cloud Functions Tips!