arrow_back

Deploying Spanner Applications with Cloud Functions and Cloud Run

Join Sign in
Test and share your knowledge with our community!
done
Get access to over 700 hands-on labs, skill badges, and courses

Deploying Spanner Applications with Cloud Functions and Cloud Run

Lab 1 hour 30 minutes universal_currency_alt 1 Credit show_chart Introductory
Test and share your knowledge with our community!
done
Get access to over 700 hands-on labs, skill badges, and courses

SCBL007

Overview

In this lab, you write applications that utilize Spanner databases, and deploy them to both Cloud Functions and Cloud Run. You also install, configure, and enable the Spanner emulator for use in development environments.

Objectives

In this lab, you learn how to:

  • Deploy Cloud Functions that read and write to Spanner databases.
  • Set up and use the Spanner emulator for development.
  • Build a REST API that allows you to read and write Spanner data.
  • Deploy a REST API to Google Cloud Run.

Setup and Requirements

Before you click the Start Lab button

Read these instructions. Labs are timed and you cannot pause them. The timer, which starts when you click Start Lab, shows how long Google Cloud resources will be made available to you.

This hands-on lab lets you do the lab activities yourself in a real cloud environment, not in a simulation or demo environment. It does so by giving you new, temporary credentials that you use to sign in and access Google Cloud for the duration of the lab.

To complete this lab, you need:

  • Access to a standard internet browser (Chrome browser recommended).
Note: Use an Incognito or private browser window to run this lab. This prevents any conflicts between your personal account and the Student account, which may cause extra charges incurred to your personal account.
  • Time to complete the lab---remember, once you start, you cannot pause a lab.
Note: If you already have your own personal Google Cloud account or project, do not use it for this lab to avoid extra charges to your account.

Activate Cloud Shell

Cloud Shell is a virtual machine that is loaded with development tools. It offers a persistent 5GB home directory and runs on the Google Cloud. Cloud Shell provides command-line access to your Google Cloud resources.

  1. Click Activate Cloud Shell Activate Cloud Shell icon at the top of the Google Cloud console.

When you are connected, you are already authenticated, and the project is set to your PROJECT_ID. The output contains a line that declares the PROJECT_ID for this session:

Your Cloud Platform project in this session is set to YOUR_PROJECT_ID

gcloud is the command-line tool for Google Cloud. It comes pre-installed on Cloud Shell and supports tab-completion.

  1. (Optional) You can list the active account name with this command:
gcloud auth list
  1. Click Authorize.

  2. Your output should now look like this:

Output:

ACTIVE: * ACCOUNT: student-01-xxxxxxxxxxxx@qwiklabs.net To set the active account, run: $ gcloud config set account `ACCOUNT`
  1. (Optional) You can list the project ID with this command:
gcloud config list project

Output:

[core] project = <project_ID>

Example output:

[core] project = qwiklabs-gcp-44776a13dea667a6 Note: For full documentation of gcloud, in Google Cloud, refer to the gcloud CLI overview guide.

Task 1. Create a database with test data

  1. On the Google Cloud Console title bar, click Activate Cloud Shell (cloud shell icon). If prompted, click Continue.

  2. Run the following command to enable the Cloud Build API.

    If you are asked to Authorize the command, then do so.
gcloud services enable cloudbuild.googleapis.com
  1. Run the following Bash command to create a schema file for the Pets database.
mkdir ~/lab-files cd ~/lab-files cat > ./pets-db-schema.sql << ENDOFFILE CREATE TABLE Owners ( OwnerID STRING(36) NOT NULL, OwnerName STRING(MAX) NOT NULL ) PRIMARY KEY (OwnerID); CREATE TABLE Pets ( OwnerID STRING(36) NOT NULL, PetID STRING(MAX) NOT NULL, PetType STRING(MAX) NOT NULL, PetName STRING(MAX) NOT NULL, Breed STRING(MAX) NOT NULL, ) PRIMARY KEY (OwnerID,PetID), INTERLEAVE IN PARENT Owners ON DELETE CASCADE ENDOFFILE
  1. Run the following commands to create a Spanner instance and database.
gcloud spanner instances create test-spanner-instance --config=regional-{{{project_0.default_region|place_holder_text}}} --description="test-spanner-instance" --processing-units=100 gcloud spanner databases create pets-db --instance=test-spanner-instance --database-dialect=GOOGLE_STANDARD_SQL --ddl-file=./pets-db-schema.sql
  1. Add a few test records to the database with the following commands.
owner_uuid=$(cat /proc/sys/kernel/random/uuid) gcloud spanner rows insert --table=Owners --database=pets-db --instance=test-spanner-instance --data=OwnerID=$owner_uuid,OwnerName=Doug gcloud spanner rows insert --table=Pets --database=pets-db --instance=test-spanner-instance --data=PetID=$(cat /proc/sys/kernel/random/uuid),OwnerID=$owner_uuid,PetName='Noir',PetType='Dog',Breed='Schnoodle' gcloud spanner rows insert --table=Pets --database=pets-db --instance=test-spanner-instance --data=PetID=$(cat /proc/sys/kernel/random/uuid),OwnerID=$owner_uuid,PetName='Bree',PetType='Dog',Breed='Mutt' gcloud spanner rows insert --table=Pets --database=pets-db --instance=test-spanner-instance --data=PetID=$(cat /proc/sys/kernel/random/uuid),OwnerID=$owner_uuid,PetName='Gigi',PetType='Dog',Breed='Retriever'

Task 2. Create a Cloud Function to read from Spanner

  1. Create a folder for your first Cloud Function with the following command.
mkdir ~/lab-files/spanner_get_pets cd ~/lab-files/spanner_get_pets
  1. Create 2 files for your application: main.py and requirements.txt.
touch main.py requirements.txt
  1. Click the Open Editor button. In the lab-files/spanner_get_pets/requirements.txt file you just created, add the following code.
google-cloud-spanner==3.15.0
  1. In the lab-files/spanner_get_pets/main.py file add the following code that reads from the database and returns the pets.
from google.cloud import spanner instance_id = 'test-spanner-instance' database_id = 'pets-db' client = spanner.Client() instance = client.instance(instance_id) database = instance.database(database_id) def spanner_get_pets(request): query = """SELECT OwnerName, PetName, PetType, Breed FROM Owners JOIN Pets ON Owners.OwnerID = Pets.OwnerID;""" outputs = [] with database.snapshot() as snapshot: results = snapshot.execute_sql(query) output = '<div>OwnerName,PetName,PetType,Breed</div>' outputs.append(output) for row in results: output = '<div>{},{},{},{}</div>'.format(*row) outputs.append(output) return '\n'.join(outputs)
  1. Click the Open Terminal button. Then, deploy the Cloud Function with the following command. Note, the trigger will be an HTTP trigger, which means a URL will generated for invoking the function. (It will take a couple minutes for the command to complete.)
gcloud functions deploy spanner_get_pets --runtime python310 --trigger-http --region={{{project_0.default_region|place_holder_text}}} --quiet
  1. When the command completes to deploy the Cloud Function, test the Cloud Function using the following command. The test data should be returned.
curl -m 70 -X GET https://{{{project_0.default_region|place_holder_text}}}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/spanner_get_pets -H "Authorization: bearer $(gcloud auth print-identity-token)" -H "Content-Type: application/json"

Task 3. Create a Cloud Function to Write to Spanner

  1. Create a folder for your second Cloud Function with the following command.
mkdir ~/lab-files/spanner_save_pets cd ~/lab-files/spanner_save_pets
  1. Create two files for your application: main.py and requirements.txt.
touch main.py requirements.txt
  1. Click the Open Editor button. In the lab-files/spanner_save_pets/requirements.txt file you just created, add the following code.
google-cloud-spanner==3.15.0
  1. In the lab-files/spanner_save_pets/main.py file, add the following code that reads from the database and returns the pets.
from google.cloud import spanner import base64 import uuid import json instance_id = 'test-spanner-instance' database_id = 'pets-db' client = spanner.Client() instance = client.instance(instance_id) database = instance.database(database_id) def spanner_save_pets(event, context): pubsub_message = base64.b64decode(event['data']).decode('utf-8') data = json.loads(pubsub_message) # Check to see if the Owner already exists with database.snapshot() as snapshot: results = snapshot.execute_sql(""" SELECT OwnerID FROM OWNERS WHERE OwnerName = @owner_name""", params={"owner_name": data["OwnerName"]}, param_types={"owner_name": spanner.param_types.STRING}) row = results.one_or_none() if row != None: owner_exists = True owner_id = row[0] else: owner_exists = False owner_id = str(uuid.uuid4()) # Need a UUID for the new pet pet_id = str(uuid.uuid4()) def insert_owner_pet(transaction, data, owner_exists): try: row_ct = 0 params = { "owner_id": owner_id, "owner_name": data["OwnerName"], "pet_id": pet_id, "pet_name": data["PetName"], "pet_type": data["PetType"], "breed": data["Breed"], } param_types = { "owner_id": spanner.param_types.STRING, "owner_name": spanner.param_types.STRING, "pet_id": spanner.param_types.STRING, "pet_name": spanner.param_types.STRING, "pet_type": spanner.param_types.STRING, "breed": spanner.param_types.STRING, } # Only add the Owner if they don't exist already if not owner_exists: row_ct = transaction.execute_update( """INSERT Owners (OwnerID, OwnerName) VALUES (@owner_id, @owner_name)""", params=params, param_types=param_types,) # Add the pet row_ct += transaction.execute_update( """INSERT Pets (PetID, OwnerID, PetName, PetType, Breed) VALUES (@pet_id, @owner_id, @pet_name, @pet_type, @breed) """, params=params, param_types=param_types,) except: row_ct = 0 return row_ct row_ct = database.run_in_transaction(insert_owner_pet, data, owner_exists) print("{} record(s) inserted.".format(row_ct))
  1. Click the Open Terminal button. This Cloud Function triggers on a Pub/Sub message. For it to work, you first need to create the Pub/Sub topic with the following command.
gcloud pubsub topics create new-pet-topic
  1. Deploy the Cloud Function with the following command. (It will take a few minutes for the command to complete.)
gcloud functions deploy spanner_save_pets --runtime python310 --trigger-topic=new-pet-topic --region={{{project_0.default_region|place_holder_text}}} --quiet
  1. When the command completes, in the Console, navigate to the Pub/Sub service. Click the new-pet-topic topic to view its details.
    On the Details page, click the Messages tab, then click the Publish Message button.
    Enter the message below, then click the Publish button. Note: the message is in JSON format and must use the correct schema as shown for the function to work.
{"OwnerName": "Jean", "PetName": "Sally", "PetType": "Frog", "Breed": "Green"}
  1. Trigger the spanner_gets_pets Cloud Function using the curl command you ran before. Check if the new owner, Jean, and her frog, Sally, were added.
curl -m 70 -X GET https://{{{project_0.default_region|place_holder_text}}}-${GOOGLE_CLOUD_PROJECT}.cloudfunctions.net/spanner_get_pets -H "Authorization: bearer $(gcloud auth print-identity-token)" -H "Content-Type: application/json"

Task 4. Starting the Spanner emulator

  1. In the Cloud Shell terminal, run the following command to install and start the Spanner emulator.
sudo apt-get install google-cloud-sdk-spanner-emulator gcloud emulators spanner start
  1. The emulator takes over the terminal, so in the Cloud Shell toolbar, click the + icon to open a new terminal tab. Run the following commands to configure the Cloud SDK to use the emulator.
gcloud config configurations create emulator gcloud config set auth/disable_credentials true gcloud config set project $GOOGLE_CLOUD_PROJECT gcloud config set api_endpoint_overrides/spanner http://localhost:9020/
  1. Create the instance and database using gcloud, but note that these commands are using the emulator now, not Spanner in the cloud. Run each separately, not as a whole.
cd ~/lab-files gcloud spanner instances create emulator-instance --config=emulator-config --description="EmulatorInstance" --nodes=1 gcloud spanner databases create pets-db --instance=emulator-instance --database-dialect=GOOGLE_STANDARD_SQL --ddl-file=./pets-db-schema.sql
  1. To have the code from the Python client library use the emulator, the SPANNER_EMULATOR_HOST environment variable must be set. Run the following code now to do that.
export SPANNER_EMULATOR_HOST=localhost:9010

Task 5. Writing a REST API for the Spanner Pets database

  1. Create create a folder for the Cloud Run program files and add the files needed.
mkdir ~/lab-files/cloud-run cd ~/lab-files/cloud-run touch Dockerfile main.py requirements.txt
  1. Click the Open Editor button. In the lab-files/cloud-run/requirements.txt file you just created, add the following code.
google-cloud-spanner==3.15.0 Flask Flask-RESTful
  1. In main.py, add the following. This code uses Python Flask and the Flask-RESTful library to build a REST API for the Pets database.
    Note: the use of the environment variables near the top of the file (lines 11 to 20). When you deploy to Cloud Run, you set these variables to point to the real Spanner database. When the variables are not set, it defaults to the emulator.
from google.cloud import spanner from flask import Flask, request from flask_restful import Resource, Api from flask import jsonify import uuid import os # Get the Instance ID and database ID from environment variables # Use the emulator if the env variables are not set. # Requires the start-spanner-emulator.sh script be run. if "INSTANCE_ID" in os.environ: instance_id = os.environ["INSTANCE_ID"] else: instance_id = 'emulator-instance' if "DATABASE_ID" in os.environ: database_id = os.environ["DATABASE_ID"] else: database_id = 'pets-db' client = spanner.Client() instance = client.instance(instance_id) database = instance.database(database_id) app = Flask(__name__) api = Api(app) class PetsList(Resource): def get(self): query = """SELECT Owners.OwnerID, OwnerName, PetID, PetName, PetType, Breed FROM Owners JOIN Pets ON Owners.OwnerID = Pets.OwnerID;""" pets = [] with database.snapshot() as snapshot: results = snapshot.execute_sql(query) for row in results: pet = {'OwnerID': row[0], 'OwnerName': row[1], 'PetID': row[2], 'PetName': row[3], 'PetType': row[4], 'Breed': row[5], } pets.append(pet) return pets, 200 def post(self): # Check to see if the Owner already exists data = request.get_json(True) with database.snapshot() as snapshot: results = snapshot.execute_sql(""" SELECT OwnerID FROM OWNERS WHERE OwnerName = @owner_name""", params={"owner_name": data["OwnerName"]}, param_types={"owner_name": spanner.param_types.STRING}) row = results.one_or_none() if row != None: owner_exists = True owner_id = row[0] else: owner_exists = False owner_id = str(uuid.uuid4()) # Need a UUID for the new pet pet_id = str(uuid.uuid4()) def insert_owner_pet(transaction, data, owner_exists): try: row_ct = 0 params = {"owner_id": owner_id, "owner_name": data["OwnerName"], "pet_id": pet_id, "pet_name": data["PetName"], "pet_type": data["PetType"], "breed": data["Breed"], } param_types = { "owner_id": spanner.param_types.STRING, "owner_name": spanner.param_types.STRING, "pet_id": spanner.param_types.STRING, "pet_name": spanner.param_types.STRING, "pet_type": spanner.param_types.STRING, "breed": spanner.param_types.STRING, } # Only add the Owner if they don't exist already if not owner_exists: row_ct = transaction.execute_update( """INSERT Owners (OwnerID, OwnerName) VALUES (@owner_id, @owner_name)""", params=params, param_types=param_types,) # Add the pet row_ct += transaction.execute_update( """INSERT Pets (PetID, OwnerID, PetName, PetType, Breed) VALUES (@pet_id, @owner_id, @pet_name, @pet_type, @breed)""", params=params, param_types=param_types,) except: row_ct = 0 return row_ct row_ct = database.run_in_transaction(insert_owner_pet, data, owner_exists) return "{} record(s) inserted.".format(row_ct), 201 def delete(self): # This delete all the Owners and Pets # Uses Cascading Delete on interleaved Pets table def delete_owners(transaction): row_ct = transaction.execute_update( "DELETE FROM Owners WHERE true = true") return row_ct row_ct = database.run_in_transaction(delete_owners) return "{} record(s) deleted.".format(row_ct), 201 class Pet(Resource): def get(self, pet_id): params = {"pet_id": pet_id} param_types = { "pet_id": spanner.param_types.STRING, } query = """SELECT Owners.OwnerID, OwnerName, PetID, PetName, PetType, Breed FROM Owners JOIN Pets ON Owners.OwnerID = Pets.OwnerID WHERE PetID = @pet_id; """ with database.snapshot() as snapshot: results = snapshot.execute_sql( query, params=params, param_types=param_types,) pet = None for row in results: pet = {'OwnerID': row[0], 'OwnerName': row[1], 'PetID': row[2], 'PetName': row[3], 'PetType': row[4], 'Breed': row[5], } if pet: return pet, 200 else: return "Not found", 404 def patch(self, pet_id): # This woud be for an Update return "Not Implemented", 500 def delete(self, pet_id): # This woud be for a Delete return "Not Implemented", 500 api.add_resource(PetsList, '/pets') api.add_resource(Pet, '/pets/<pet_id>') @app.errorhandler(404) def not_found(error): return jsonify({'error': 'Not found'}), 404 @app.errorhandler(500) def server_error(e): return 'An internal error occurred.', 500 if __name__ == "__main__": app.run(host='0.0.0.0', port=8080, debug=True)
  1. Click the Open Terminal button, and then run the following code to install the necessary packages.
pip install -r requirements.txt
  1. Now run the following code in terminal to start the program.
python main.py
  1. In the Cloud Shell toolbar, click the + icon again to open a third terminal tab.

  2. You can use curl to test the API. First, add some records using HTTP POST commands.

curl -X POST --data '{"OwnerName": "Sue", "PetName": "Sparky", "PetType": "Cat", "Breed": "Alley"}' http://localhost:8080/pets curl -X POST --data '{"OwnerName": "Sue", "PetName": "Pickles", "PetType": "Dog", "Breed": "Shepherd"}' http://localhost:8080/pets curl -X POST --data '{"OwnerName": "Doug", "PetName": "Noir", "PetType": "Dog", "Breed": "Schnoodle"}' http://localhost:8080/pets curl -X POST --data '{"OwnerName": "Doug", "PetName": "Bree", "PetType": "Dog", "Breed": "Mutt"}' http://localhost:8080/pets curl -X POST --data '{"OwnerName": "Joey", "PetName": "Felix", "PetType": "Cat", "Breed": "Tabby"}' http://localhost:8080/pets
  1. See if the records were added using an HTTP GET command. The records should be returned in JSON format.
curl http://localhost:8080/pets

Note: These records were added to the emulator. Next, you deploy to Cloud Run and use the real Spanner instance.

  1. Type exit to close the third terminal tab, then type Ctrl + C to stop the Python program in the second terminal tab. Close the second tab by typing exit.
    Return to the first terminal tab and stop the emulator by typing Ctrl + C.

Task 6. Deploying an app to Cloud Run

  1. To deploy to Cloud Run, you need a Docker image. First change directories to the cloud-run folder.
cd ~/lab-files/cloud-run
  1. To create the Docker image, you need to add instructions to the Dockerfile file. Click the Open Editor button and open the Dockerfile file. Paste the following code into it.
FROM python:3.9 WORKDIR /app COPY . . RUN pip install gunicorn RUN pip install -r requirements.txt ENV PORT=8080 CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 main:app
  1. Return to the terminal and run the following code to create the Docker image. (Make sure you are in the ~/lab-files/cloud-run folder.)
gcloud builds submit --tag=gcr.io/$GOOGLE_CLOUD_PROJECT/spanner-pets-api:v1.0 .
  1. Make sure Cloud Run is enabled in your project.
gcloud services enable run.googleapis.com
  1. Now, deploy the Cloud Run application using the following command. Note how the environment variables are set in the command, so the code will use the Cloud Spanner instance, not the emulator. It takes a couple minutes for the command to complete.
gcloud run deploy spanner-pets-api --image gcr.io/$GOOGLE_CLOUD_PROJECT/spanner-pets-api:v1.0 --update-env-vars INSTANCE_ID=test-spanner-instance,DATABASE_ID=pets-db --region={{{project_0.default_region|place_holder_text}}}
  1. When the command completes, note the service URL and copy it to the clipboard. As you did with the emulator, you can use curl commands to test the API. First, create a variable to store the URL as shown below. Make sure you paste your full URL including the https://... in place of the full placeholder - <YOUR_SERVICE_URL_HERE>.
pets_url=<YOUR_SERVICE_URL_HERE>
  1. Add some records using curl.
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" -X POST --data '{"OwnerName": "Sue", "PetName": "Sparky", "PetType": "Cat", "Breed": "Alley"}' $pets_url/pets curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" -X POST --data '{"OwnerName": "Sue", "PetName": "Pickles", "PetType": "Dog", "Breed": "Shepherd"}' $pets_url/pets curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" -X POST --data '{"OwnerName": "Doug", "PetName": "Noir", "PetType": "Dog", "Breed": "Schnoodle"}' $pets_url/pets curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" -X POST --data '{"OwnerName": "Doug", "PetName": "Bree", "PetType": "Dog", "Breed": "Mutt"}' $pets_url/pets curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" -X POST --data '{"OwnerName": "Joey", "PetName": "Felix", "PetType": "Cat", "Breed": "Tabby"}' $pets_url/pets
  1. See if the records were added.
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" $pets_url/pets
  1. In the Console, navigate to the Cloud Run service and click your service to view its details and look at the logs. You will see each request you made logged there.

  2. Navigate to Spanner and explore the instance and the database.

  3. The API implements DELETE as well. Return to the terminal and enter the following command to delete all the data.

curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" -X DELETE $pets_url/pets
  1. From the Console, delete the Spanner instance so you are no longer being charged for it.

Congratulations! You have written applications that utilize Spanner databases and deployed them to both Cloud Functions and Cloud Run. You also installed, configured, and enabled the Spanner emulator for use in development environments.

End your lab

When you have completed your lab, click End Lab. Your account and the resources you've used are removed from the lab platform.

You will be given an opportunity to rate the lab experience. Select the applicable number of stars, type a comment, and then click Submit.

The number of stars indicates the following:

  • 1 star = Very dissatisfied
  • 2 stars = Dissatisfied
  • 3 stars = Neutral
  • 4 stars = Satisfied
  • 5 stars = Very satisfied

You can close the dialog box if you don't want to provide feedback.

For feedback, suggestions, or corrections, please use the Support tab.

Copyright 2023 Google LLC All rights reserved. Google and the Google logo are trademarks of Google LLC. All other company and product names may be trademarks of the respective companies with which they are associated.