How to setup custom domain for your Google cloud functions

09 Mar 2020

This post is mainly a reminder to myself. I recently created a SaaS that I wanted to deploy to Google Cloud Platform to use its cloud functions and datastore services.

I wanted to have URL like this for my functions:

https://app.mydomain.com/api/v1/FunctionName

The application’s frontend uses Firebase as the deployment target. I’m only using the hosting part of Firebase. The marketing website and the single-page app use Firebase’s custom domain and CDN hosting.

The domain name is already setup on both Firebase hosting targets.

The backend API is written in Go and uses GCP cloud functions. The issue is I wanted to have the custom domain and URL above for my functions and not use the default one.

If you search for how to use custom domain for Google cloud functions, you’ll see articles showing that you can use Firebase’s configuration to rewrites the path to your functions.

What they are not saying is that to work, you have to deploy the functions in the us-central1 region since Firebase is deployed there. I was deploying to other regions and was not understanding why I could not reach my functions from Firebase rewrites. They have to be in us-central1.

Here’s my Firebase config file:

{
  "hosting": [
    {
      "target": "web",
      "public": "site/public",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ]
    },
    {
      "target": "app",
      "public": "app",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**",
        "**/*.ts",
        "package*.json",
        "tsconfig.json"
      ],
      "rewrites": [
        {
          "source": "/api/v1/dosomething",
          "function": "DoSomething"
        }
      ]
    }
  ]
}

For all my functions I have a rewrtes block indicating the URL I want to rewrite to which function. The key point here is that my functions are not Node Firebase functions. They are Google cloud functions deployed in my GCP project. Turns out I prefer writting them in Go ;).

You can see that I have my marketing site in the site/public directory. This is a static site generated by Hugo. The other target named app is the single-page application and is an Elm application.

I wanted a mono repository for this project. The marketing site, the single-page application, and the backend functions are all in the same GitHub repository.

Here’s the structure of the directories:

Shameless plug, if you're interested in learning how to build API in Go for SaaS/side projects, please check out my book: Build SaaS apps in Go. It helps me continue creating more content.
.
├── app
│   ├── css
│   ├── img
│   └── js
├── functions
├── site
│   ├── archetypes
│   ├── content
│   ├── data
│   ├── layouts
│   ├── public
│   ├── resources
│   ├── static
│   ├── tailwind
│   └── themes
└── src
    ├── Api
    └── Page

The app directory is the single-page application. The elm make output bundle in the js directory of app.

The functions directory is where all my Go functions are. This is a standard Go package that exposes the functions. For instance:

package functions

import (
  "fmt"
  "net/http"
)

func DoSomething(w http.ResponseWriter, r *http.Request) {
  fmt.Println("this is the functions")
}

The site directory is the Hugo static site.

The src directory is the Elm source files.

Using make to deploy functions

The quickest way I found to deploy functions when they change was by using make. I’ve been using make more and more for the past 2 years. It’s very good and you wonder why so many rebuilt build tool instead of using that.

Here’s an example of a deploy code for a specific function.

dosomething-deploy:
  cd functions; \
    gcloud functions deploy DoSomething --allow-unauthenticated --region=us-central1 \
    --env-vars-file .env.yaml --runtime go111 --trigger-http --memory=128MB

I copy-paste this target for all of my functions. If you’re deploying Go function setting the memory to 128MB will save you some amount of money.

I’m also passing an .env.yaml file to all my functions with shared environment variables.

The most important parameter is the --region=us-central making the function available in the rewrite section of the Firebase configuration.

That way your functions use the custom domain you set on your Firebase target and the rewrites section makes sure the function gets called via the custom route you specified.

Is the Makefile approach scales?

I would dare say yes, unless you have more than 150 functions. Once you create a good naming convention in your Makefile it become intuitive to type:

$> make dosome+TAB

And you get auto-complete for the function you want to deploy.

I know there’s tools out there that help or handle the deployment. I prefer having control over what I’m doing and understand what’s going on. Especially when make is such a proven and powerful tool.

As always YMMV, the point of this post was to clearly indicates that you have to deploy your functions to us-central1 to have them available in your Firebase rewrites section.

Back to posts

You may reach me here: dstpierre on GitHub @dominicstpierre on Twitter