Cloud Functions is a service on Google Cloud Platform that supports serverless deployments of functions. Much like serverless platforms with App Engine and serverless containers with Cloud Run, the infrastructure is hidden and is autoscaled to meet demand. Unlike App Engine and Cloud Run which typically run full applications, Cloud Functions are typically used to perform a single task and are implemented using a limited set of container environments provided by Google.

Previously, rendering of the Guestbook was done in Flask with Jinja templates returning HTML and CSS back to the browser upon each update. Modern, client-side rendering approaches, however, perform the rendering task on the front-end browser, leaving only the Model function to be implemented by the backend server. When split into this architecture, after the browser downloads the initial web content, the backend is then only accessed via a REST API to update Model state. Serverless functions are often used to implement REST APIs. As our Guestbook application is quite minimal, having only two functions, it can easily be adapted to this style of architecture.

To demonstrate this, we will now take our guestbook application and break it into a frontend and a backend and then create an API for directly accessing the Guestbook model backend. Our backend REST API will be done serverlessly on GCP using Cloud Functions while the frontend will be a simple webpage that calls out to the REST API to POST and GET guestbook entries. Note that because our Guestbook is a public API, we do not employ GCP's API Gateway to manage access to it in this lab.

To begin with, visit the code in Cloud Shell

cd guestbook-src/06_gcp_restapi_cloudfunctions

The gbmodel code is the same.

The main code change in this version is the removal of the server-side rendering code that generates HTML for the site. This has been replaced with a single file (main.py) that Cloud Function looks for to implement our two endpoints. The first endpoint is named entries and supports code for handling GET requests as shown below. It calls our Model's select() function to retrieve all of the entries in the Guestbook model. In previous instantiations, the web application took the results returned from the model (as a list of lists) and then rendered them in HTML using Jinja2. For the REST API, rather than return HTML, we instead need to return an object in Javascript Object Notation (JSON). The code takes the list of lists and creates a list of dictionaries (entries) that is then converted into JSON (json.dumps) and sent back to the client (make_response) with an appropriate response type (application/json).

main.py

from flask import make_response, abort
import gbmodel
import json
import functions_framework

@functions_framework.http
def entries(request):
    """ Guestbook API endpoint
        :param request: flask.Request object
        :return: flask.Response object (in JSON), HTTP status code
    """
    model = gbmodel.get_model()
    if request.method == 'GET':
        entries = [dict(name=row[0], email=row[1], signed_on=row[2].isoformat(), message=row[3] ) for row in model.select()]
        response = make_response(json.dumps(entries))
        response.headers['Content-Type'] = 'application/json'
        response.headers['Access-Control-Allow-Origin'] = '*'
        return response, 200

    return abort(403)

CORS aside

The entries function also contains code to handle HTTP OPTIONS request methods.

main.py

    if request.method == 'OPTIONS':
        return handle_cors(), 204

This is to support CORS (cross-origin resource sharing). CORS allows us (as a REST API provider) to restrict which web sites can access our APIs from Javascript. If your Guestbook front-end is hosted by foo.com, the web browser will query our REST API endpoint with a "pre-flight" OPTIONS request to check to see if Javascript code from foo.com is able to access the endpoint. The code for handling the CORS request is below. As the code shows, it simply sets the HTTP response headers to allow all origins '*' to perform the specified method request (in this case, GET). We use the wildcard since we don't know (in this codelab) where the frontend will be served from. If the front-end were served from foo.com, then we could specify 'https://foo.com' for the 'Access-Control-Allow-Origin' header value.

main.py

def handle_cors():
    response = make_response()
    response.headers['Access-Control-Allow-Origin'] = '*'
    response.headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
    response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
    response.headers['Access-Control-Max-Age'] = '3600'
    return response

The other endpoint implemented within main.py is named entry. As shown below, the endpoint only supports HTTP POST requests and checks to ensure that a JSON object specifying a Guestbook entry is included in the POST. It then gets the dictionary representation of the JSON object (request.get_json()), validating that all parts of the entry are included (via the all()), before calling the Model's insert()method to insert the entry into the Guestbook

main.py

@functions_framework.http
def entry(request):
    """ Guestbook API endpoint
        :param request: flask.Request object
        :return: flack.Response object (in JSON), HTTP status code
    """
    model = gbmodel.get_model()

    if request.method == 'POST' and request.headers['content-type'] == 'application/json':
        request_json = request.get_json(silent=True)

        if all(key in request_json for key in ('name', 'email', 'message')):
            model.insert(request_json['name'], request_json['email'], request_json['message'])
        else:
            raise ValueError("JSON missing name, email, or message property")
        return handle_cors(), 204

Note that, similar to the prior CORS support in the entries endpoint, we include similar code to handle the HTTP OPTIONS requests on this one.

It is possible to test the cloud function code locally without needing to deploy it. This is done with the help of the functions-framework.

Setup a local virtual environment for your project and install the pip requirements.

You can then run a specific function with:

$ functions-framework --target entries --debug

And access your entries on http://127.0.0.1:8080/entries

We will now deploy our API endpoints. Go back to the source directory containing main.py. The command below deploys the function that has been implemented specifying a Python 3.11 environment. Since Cloud Functions can be triggered by many different kinds of events, not just REST API requests, we must specify an HTTP trigger. In addition, our two functions only require access to Cloud Datastore so we will deploy both using the service account created previously in order to practice least-privileges. (Search for Service account in Google Cloud Console if you did not name it guestbook)

gcloud functions deploy entries --runtime python311 --trigger-http --service-account guestbook@<your_google_clour_project>.iam.gserviceaccount.com
gcloud functions deploy entry --runtime python311 --trigger-http --service-account guestbook@<your_google_clour_project>.iam.gserviceaccount.com

Go to the Cloud Functions console to view the functions.

Then, use the commands below to show the functions' settings along with the URL to access each (httpsTrigger).

gcloud functions describe entries
gcloud functions describe entry

Click on the URL for the REST API's entries endpoint (httpsTrigger) or copy and paste it into the web browser. The request will result in the entire contents of the Guestbook being returned to you as a JSON object by the endpoint.

Go back to the Cloud Functions UI and click on the entry endpoint for submitting a new entry into the Guestbook. Then click on "Testing". This will bring you to a page that allows you to interact with the endpoint. We wish to see that the endpoint can properly handle new entries being submitted. To test that the endpoint can properly handle new entries being submitted, format a JSON object using the necessary fields with your name, your e-mail address, and a message of "Hello Cloud Functions!".

Then, click on "Test the function" to send the JSON object via HTTP POST to the API.

Revisit the entries endpoint in the browser. The output should show the entry you've submitted.

REST APIs can be programmatically accessed via any popular language. As we have been using Python for the class, we can do so using its interpreter. On your Ubuntu VM, enter the interpreter:

$ python

Use python3 in linux

Within the interpreter, import the requests package and access the REST API's entries endpoint, saving the response.

import requests
resp = requests.get('https://<region_name>-<project_id>.cloudfunctions.net/entries')

Using the interpreter and the resp object, use Python's print() function to show the following for the response:

Then, assign the response JSON to a variable and use the Python interpreter to write a loop that individually prints the name, email, signed_on, and message of the first Guestbook entry returned.

We can also use Python to submit a new Guestbook entry via the REST API. Within the same interpreter session, import the JSON package and create a dictionary containing a Guestbook entry with your name, email, and message of "Hello Cloud Functions from Python Requests!". Note that, in Python, you can create a dictionary using syntax similar to JSON. For example, the snippet my_dict = {'foo':'bar'} creates a dictionary with a single entry with key 'foo' and value 'bar'.

import json
mydict = {
    'name' : 'Demo',
    'email' : '@he-arc.ch',
    'message' : 'Hello from python request',
}

Then, submit a POST request to the API's entry endpoint, passing the dictionary containing the Guestbook entry into the json keyword parameter to the post() method. The request package will convert the dictionary into the JSON format as part of the request.

resp = requests.post('https://<region_name>-<project_id>.cloudfunctions.net/entry', json=my_dict)

With a single page application (SPA), you download the entire site as a static bundle and then, just like an application, you interact with it seamlessly. As the application needs to send and retrieve data to the backend server, it does so asynchronously using Javascript and HTTP requests to and from the APIs it is programmed to access. There are many examples of single-page applications such as Google Mail and Google Docs.

This version of the Guestbook is implemented in a similar manner. A standard interface to interface with the backend model, our controller and presenter code is able to work with any new models without modification. In this case, one can consider the REST API specification as our new model interface as it standardizes our HTTP queries so that our front-end code does not have to change if we shift backends between AWS and GCP.

Visit the source directory containing the application.

cd guestbook-src/06_gcp_restapi_cloudfunctions/frontend-src

View index.html which contains the application. As the file shows, it is similar to prior versions with two exceptions. The first exception is that it now includes a Javascript file

frontend-src/index.html

   <script src="./static/guestbook.js"></script>

This file will execute code when we submit a new entry and update our page with the response. By operating on the DOM directly, it will allow us to view the results of our submission immediately without reloading the page.

The base page also implements the form used to sign the Guestbook. As the code below shows, each input field is labeled (name, email, message) so that it can be accessed by our Javascript code . In addition, when the "Sign" button is clicked, the sign() function that is defined in guestbook.js will be called (we will bind the listener to the button in the javascript code).

frontend-src/index.html

 <h2>Guestbook</h2>
      <div class="box">
        <label
          ><span>Name:</span><input type="text" name="name" required
        /></label>
        <label><span>Email:</span><input type="email" name="email" /></label>
        <label
          ><span>Message:</span><textarea name="message" required></textarea>
        </label>
        <label><span></span><input type="submit" value="Sign" /></label>
      </div>

Finally, at the bottom of the page is the definition of a

element named "entries". This element will be used by our Javascript code to automatically update what is rendered for the based on the entries returned by the backend.

frontend-src/index.html

     <h2>Entries</h2>
     <div id="entries"></div>

The main file which implements our application is located at static/guestbook.js. In the file, we define a baseApiUrl for the REST API. This URL will support two endpoints: the entries endpoint supporting a GET request to obtain all of the entries in the Guestbook in JSON and the entry endpoint supporting a POST request to add an entry to the Guestbook using JSON. Begin by changing this URL to point to the base URL returned by the Cloud Function deployments (e.g. https://[region_name]-[project_id].cloudfunctions.net )

frontend-src/static/guestbook.js

const baseApiUrl = "<FMI>";

There are three main functions in this file: getEntries, sign, and viewEntries. The code for getEntries is shown below. It simply uses the browser's fetch() interface to access the entries endpoint asynchronously. It then parses the returned string into the gbentries array, before calling viewEntries. viewEntries will then update the page's DOM elements directly with the data obtained.

frontend-src/static/guestbook.js

const getEntries = async () => {
  const response = await fetch(baseApiUrl + "entries", {
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json"
    },
    method: "GET"
  });
  const gbentries = await response.json();
  renderEntries(gbentries);
};

The sign function collects form values via their names and formats a JSON object with them. It then issues a POST request using the browser's fetch() interface to the entry endpoint asynchronously. After receiving a response we call getEntries() to update the list .

frontend-src/static/guestbook.js

const sign = async () => {
  const name = document.querySelector("input[name=name]");
  const email = document.querySelector("input[name=email]");
  const message = document.querySelector("textarea[name=message]");
  const button  = document.querySelector("input[type=submit]");
  button.style.display = "none";

  await fetch(baseApiUrl + "entry", {
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json"
    },
    method: "POST",
    body: JSON.stringify({ name: name.value, email: email.value, message: message.value })
  });
  button.style.display = "block";
  name.value = "";
  email.value = "";
  message.value = "";
  getEntries();
};

To update the UI getEntries, call renderEntries with the gbentries returned by the API calls. As the code below shows, the renderEntries first clears out the DOM of previous guestbook entries. It then performs a map of a function across all of the gbentries it has been passed. The function mapped, appends the DOM elements that construct a single entry in the guestbook to the node.

frontend-src/static/guestbook.js

const renderEntries = entries => {
  const entriesNode = document.getElementById("entries");

  while (entriesNode.firstChild) {
    entriesNode.firstChild.remove();
  }

  entries.map(entry => {
    const entryNode = document.createElement("section");
    entryNode.classList.add("entry", "box");
    const email = entry.email ? `&lt;${entry.email}&gt;` : "";
    entryNode.innerHTML = `<pre>${entry.message}</pre>
    <header>
      ${ entry.name } ${email}<br>
      <em>signed on ${entry.signed_on}</em>
    </header>`
    entriesNode.appendChild(entryNode);
  });
};

Ensure that you have modified the static/guestbook.js file with the base URL of your endpoints. Bring up a web browser and go to File=>Open File to open a local HTML file. Navigate to the directory containing the index.html file and view it. Enter a message using your name, e-mail address, and the message "Hello Cloud Functions from SPA!".

Storage buckets are often used to serve static web sites. When configured as multi-region buckets, the content within them can be automatically forward deployed to locations near to where clients are requesting the content from. Distributing our client application can be done via Google SDK CLI. To begin with, bring up a terminal and change directories to the frontend client source code.

cd guestbook-src/06_gcp_restapi_cloudfunctions/frontend-src

Begin by changing the REST API base URL to point to the Cloud Function deployment previously (e.g. https://[region_name]-[project_id].cloudfunctions.net )

frontend-src/static/guestbook.js

const baseApiUrl = "<FMI>";

Then, create a bucket with your name formatted as below:

gsutil mb gs://restapi-<pnom>

(e.g. gs://restapi-bfritscher)

Since this will be a publicly accessible web site, we will assign an access policy allowing all users to access its content via IAM. In this case, the special identifier allUsers specifies everyone and objectViewer assigns read-access permissions.

gsutil iam ch allUsers:objectViewer gs://restapi-<pnom>

Finally, copy the entire contents of the directory over to the bucket.

gsutil cp -r . gs://restapi-<pnom>

Storage buckets by default are web accessible via the following URL

https://storage.googleapis.com/<BucketName>

Visit the index.html file in this bucket:

https://storage.googleapis.com/<BucketName>/index.html

Enter a message using your name, e-mail address, and the message "Hello Cloud Functions from SPA in GCS!".

Task Progress

Take a screenshot of the guestbook with the URL and all the messages from this lab and upload below.

You can try to find your bucket in the google cloud console by search bucket and delete it via the GUI or delete it via the command lines below.

Delete the storage bucket and the Cloud function either via the web console UI or from Cloud Shell via the CLI.

gsutil rm -r gs://restapi-<pnom>
gcloud functions delete entries
gcloud functions delete entry