Tutorial HTTP Endpoints with GCP Function (2nd Gen) and Go
Cloud Functions provide a lightweight execution environment for code in various languages. They are the serverless compute solutions in Google Cloud Platform (GCP) that is similar to AWS Lambda functions in AWS.
There are two different versions of Cloud Functions available: 1st Gen and 2nd Gen. 2nd Gen is recommended for all new projects.
In this tutorial, we are developing a very simple Hello World application that renders and serves the following web page:
You can open the page here: https://hello-http-v2-ndzxp34coq-uc.a.run.app/.
The source code is provided on GitHub: mxro / gcp-serverless-http-example.
The function code is developed using Go.
Part 1: Project Setup
First, ensure that you have Go installed. If not, find instructions here: Go - Download and Install
Then we set up a simple Go project:
go mod init example.com/cfv2
Part 2: Define Function Source Code
Create a new file hello.go
and paste the following content:
// Sends a simple message to the client
package hello_v2
import (
"html/template"
"net/http"
"time"
"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)
func init() {
functions.HTTP("hello-http-v2", HelloHTTPV2)
}
var htmlTemplate = `<html>
<head>
<title>Hello from Go (v2)</title>
</head>
<body>
<p>Hello there!</p>
<p>The time on the server is {{ .Date }}</p>
</body>
</html>
`
// HelloHTTP is an HTTP Cloud Function with a request parameter.
func HelloHTTPV2(w http.ResponseWriter, r *http.Request) {
var my_template *template.Template = template.New("hello")
my_template.Parse(htmlTemplate)
dt := time.Now()
render_data := struct {
Date string
}{
Date: dt.Local().Format("02-01-2006 15:04:05"),
}
my_template.Execute(w, render_data)
}
We are using the Functions Framework for Go. Key is the init()
function that registers the handler function for the API:
func init() {
functions.HTTP("hello-http-v2", HelloHTTPV2)
}
Note the name we define for our function "hello-http-v2"
.
All handler functions defined using the framework need to provide the following method signature:
func HelloHTTPV2(w http.ResponseWriter, r *http.Request) { }
Since we are just rendering some content and sending it back to the client, we won't need to access the request.
Instead, we write a rendered template to the ResponseWriter:
my_template.Execute(w, render_data)
Ensure that all our dependencies are accounted for by running:
go mod tidy
This should create a go.sum
file with locked down dependency versions and also update go.mod
. For reference, go.mod
should now have the following contents:
module example.com/cfv2
go 1.19
require github.com/GoogleCloudPlatform/functions-framework-go v1.6.1
require (
github.com/cloudevents/sdk-go/v2 v2.6.1 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
go.uber.org/atomic v1.4.0 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.10.0 // indirect
)
The versions of dependencies may be different in your project.
Part 3: Deploy Function
In order to deploy our function, we need the gcloud CLI.
If you don't have the gcloud CLI installed, find instructions to install it here: Install the gcloud CLI.
Once the gcloud CLI is installed, we need to login and select the GCP project we want to use:
gcloud init
gcloud config set project [PROJECT ID]
If you don't have a project set up, see the following to create a project: Google Cloud - Creating and Managing Projects.
Once everything is set up, you can deploy your function with the following command:
gcloud functions deploy hello-http-v2 --gen2 --runtime=go119 --source=functionsv2/hello --entry-point=hello-http-v2 --region=us-central1 --trigger-http --allow-unauthenticated
After the deployment is complete, run the following command to obtain the public HTTP endpoint:
gcloud functions describe hello-http-v2
This will provide an output like the following:
buildConfig:
build: xxx
entryPoint: hello-http-v2
runtime: go119
source:
storageSource:
bucket: xxx
object: hello-http-v2/function-source.zip
sourceProvenance:
resolvedStorageSource:
bucket: xxx
generation: 'xxx'
object: hello-http-v2/function-source.zip
environment: GEN_2
labels:
deployment-tool: cli-gcloud
name: projects/go-serverless-http-example/locations/us-central1/functions/hello-http-v2
serviceConfig:
allTrafficOnLatestRevision: true
availableCpu: '0.1666'
availableMemory: 256M
ingressSettings: ALLOW_ALL
maxInstanceCount: 100
maxInstanceRequestConcurrency: 1
revision: hello-http-v2-00005-zaj
service: projects/go-serverless-http-example/locations/us-central1/services/hello-http-v2
serviceAccountEmail: xxx
timeoutSeconds: 60
uri: https://hello-http-v2-ndzxp34coq-uc.a.run.app
state: ACTIVE
updateTime: '2023-01-28T01:37:25.903696730Z'
Take note here of the uri
property towards the end. This is the public endpoint you can open in your browser to test the page.
https://hello-http-v2-ndzxp34coq-uc.a.run.app
Part 4: Test function
No project is complete without some unit testing. Thankfully, this is very easy to set up in Go using the httptest
package.
Add another file to your project called hello_test.go
and provide the following contents:
package hello_v2
import (
"net/http/httptest"
"strings"
"testing"
)
func TestHelloHTTP(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Add("Content-Type", "text/html")
rr := httptest.NewRecorder()
HelloHTTPV2(rr, req)
if got := rr.Body.String(); !strings.Contains(got, "Hello there!") {
t.Errorf("HelloHTTPV2() = %q, want %q", got, "Hello There")
}
}
We are simply calling the handler function defined in hello.go
and ensure the correct response is provided.
You can run this test with the following command:
go test
If everything worked successfully, you should receive an output as follows:
$ go test
PASS
ok example.com/cfv2 0.348s
Conclusion
It is very easy to create a public API endpoint using Cloud Functions. The result is returned very fast. Even when a cold start is required, the response should be rendered in under 200 ms. This compares quite well to serverless API performance on AWS.
Unfortunately, the API we created is not suitable for anything but the most basic prototyping. For a real API, we will likely want to use our own domain. This appears to be quite complicated in GCP. We will need a Load Balancer, a serverless NEG and an API Gateway among some other components. See Getting started with HTTP(S) Load Balancing for API Gateway and HTTP(S) Load Balancing for API Gateway.