Keep up with the latest announcements from Google Cloud Next '21. Click here.

Developers & Practitioners

Deploying the Cloud Spanner Emulator locally

Welcome to the second part of our series on the Cloud Spanner Emulator. In the first part, we got an overview of Cloud Spanner and the emulator, as well as the various ways that it can be provisioned. 

Today, we will learn about running the Cloud Spanner emulator locally and containerizing + deploying the backend of our sample application (OmegaTrade) on the local emulator. 

Starting the emulator locally 

The emulator has two processes, emulator_main which contains a gRPC server and gateway_main which is the REST gateway that also starts emulator_main as a subprocess. If your application only uses client libraries or the RPC API, you can just start the emulator_main process via Linux prebuilt binaries. If your application uses the REST API, then you should start gateway_main. The default ports are 9010 and 9020 for the gRPC server and REST server respectively, but they can be changed while starting the emulator via the docker / Linux pre-built libraries.

There are various options to start the emulator locally. Here we will cover the gcloud instructions, but instructions for other methods can be found within the Cloud Spanner emulator GitHub repository

We begin by updating our gcloud components and starting the emulator:

  gcloud components update 
gcloud emulators spanner start &

NOTE - If you are getting an error like:

Emulator_Error

You can try the following commands (or their equivalents for your operating system): 

  sudo apt-get update
sudo apt-get install google-cloud-sdk-spanner-emulator
gcloud emulators spanner start &

NOTE - This command starts the emulator in the background. To stop the emulator_main and gateway_main processes on a Linux/UNIX machine you may execute the following commands

  pidof emulator_main | xargs kill
pidof gateway_main  | xargs kill

Let’s create a configuration for the emulator. This is a one-time setup and can be reused subsequently. Run the following commands:

  gcloud config configurations create emulator
gcloud config set auth/disable_credentials true
gcloud config set project test-project
gcloud config set api_endpoint_overrides/spanner http://localhost:9020/

In your development environment, you might want to switch between using a local emulator or connecting to a production Cloud Spanner instance. You can manage this by having multiple gcloud configurations and switching between configurations by using the following command

  gcloud config configurations activate default
# to switch to the emulator configuration
gcloud config configurations activate emulator

# to verify it is actually connected to the emulator
gcloud config list

Let’s create an instance, database and some tables on the local emulator. 

Create an instance in the emulator

  gcloud spanner instances create omegatrade-instance --config=emulator-config \
--description="OmegaTrade Instance - Cloud Spanner emulator" --nodes=3

Create a database

  gcloud spanner databases create omegatrade-db --instance omegatrade-instance

Create tables

  gcloud spanner databases ddl update omegatrade-db --instance=omegatrade-instance --ddl='CREATE TABLE users (userId STRING(36) NOT NULL, businessEmail STRING(50), fullName STRING(36), password STRING(100), photoUrl STRING(250), provider STRING(20),
forceChangePassword BOOL) PRIMARY KEY(userId);
CREATE UNIQUE NULL_FILTERED INDEX usersByBusinessEmail ON users (businessEmail);' #user table

gcloud spanner databases ddl update omegatrade-db --instance=omegatrade-instance --ddl='CREATE TABLE companies (companyId STRING(36) NOT NULL, companyName STRING(30), companyShortCode STRING(15), created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)) PRIMARY KEY(companyId);
CREATE UNIQUE NULL_FILTERED INDEX companiesByCompanyName ON companies (companyName);
CREATE UNIQUE NULL_FILTERED INDEX companiesByShortCode ON companies (companyShortCode);'  
#companies table


gcloud spanner databases ddl update omegatrade-db --instance=omegatrade-instance --ddl='CREATE TABLE companyStocks (companyStockId STRING(36) NOT NULL, companyId STRING(36) NOT NULL, open NUMERIC, volume NUMERIC, currentValue NUMERIC, date FLOAT64, close NUMERIC,
dayHigh NUMERIC, dayLow NUMERIC, timestamp TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true), CONSTRAINT FK_CompanyStocks FOREIGN KEY (companyId) REFERENCES companies (companyId)) PRIMARY KEY(companyStockId);'  #companyStocks table

gcloud spanner databases ddl update omegatrade-db --instance=omegatrade-instance --ddl='CREATE TABLE simulations (sId STRING(36) NOT NULL, companyId STRING(36) NOT NULL,
status STRING(36), createdAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
CONSTRAINT FK_CompanySimulation FOREIGN KEY (companyId) REFERENCES companies (companyId)
) PRIMARY KEY(sId);'   #simulations table

Verify that the tables were successfully created by querying INFORMATION_SCHEMA in the emulator instance

  gcloud spanner databases execute-sql omegatrade-db  --instance=omegatrade-instance  --sql='SELECT * FROM information_schema.tables WHERE table_schema <> "INFORMATION_SCHEMA"'

Now that the emulator is up and running, let’s clone the sample app repo and deploy the backend service of OmegaTrade with the emulator backend.

  git clone https://github.com/cloudspannerecosystem/omegatrade.git && 
cd omegatrade/backend

Create the .env file and ensure the project id, instance name and database name match the ones we created above.

  PROJECTID = test-project
INSTANCE = omegatrade-instance
DATABASE = omegatrade-db
JWT_KEY = w54p3Y?4dj%8Xqa2jjVC84narhe5Pk
EXPIRE_IN = 2d

Build and run the application

  npm install
export SPANNER_EMULATOR_HOST="localhost:9010"
node seed-data.js
node server.js &

The app should be up and running on http://localhost:3000

Emulator App Running

The OmegaTrade app is using the Node.js Cloud Spanner client and interacting with the emulator via the gRPC endpoint. 

Let’s make a curl request to insert some data into the emulator via the OmegaTrade app

  curl --location --request POST 'http://localhost:3000/api/v1/users/register-user' \
--header 'Content-Type: application/json' \
--data-raw '{
    "fullName": "user01",
    "businessEmail": "user01@acme.com",
    "password": "qwerty",
    "confirmPassword": "qwerty",
    "provider": "",
    "photoUrl": ""
}'

Verify if the data was inserted with a query

  gcloud spanner databases execute-sql omegatrade-db --instance=omegatrade-instance  --sql='SELECT * FROM users'

The output should look something like this:

Emulator Output Sample

Using the emulator with containerized applications

Bundling the emulator as one of the application components

While developing and testing containerized applications, you can either run an emulator as a standalone container locally or point to a remote host where the emulator is running using its public IP or hostname in the gcloud configuration. 

NOTE - Ensure that your firewall allows ingress TCP traffic on ports 9010 and 9020.

Example:

  # Override REST endpoint to connect to remote emulator
gcloud config set api_endpoint_overrides/spanner http://x.x.x.x:9020/


# Override gRPC endpoint to connect to remote emulator
export SPANNER_EMULATOR_HOST="x.x.x.x:9010"

NOTE - If you are running the emulator on a remote host, this will send data from the client (local) to the emulator (remote) over the internet as plain text. That is not a problem for test data, but if you are using this for testing/development/debugging, you should be careful to use only synthetic data and not privacy-sensitive data.

Another option is to build and execute the emulator as one of the components of the application image. We will do that now, by building a new Docker image with both the application code and the emulator, creating tables and then running the service.

  git clone https://github.com/cloudspannerecosystem/omegatrade.git && cd omegatrade/backend

Take a look at dockerfile.local.emulator here. You will notice that we are using the prebuilt emulator docker image from GCR as the static source to copy emulator libraries to our image. Alternatively, you can also run wget to download these binaries in your Docker container. Since the OmegaTrade backend is a Node.js app, we will start with google/cloud-sdk:slim as the base image and install Cloud SDK using a RUN command. Finally, the init script start_spanner.bash here does quite a few things: it starts the emulator, creates an instance, database and tables on the local emulator, starts the application, and acts as the entry point. 

Before building and deploying the Docker image, we need to create a .env file with the environment variables that we need for running the application.

  PROJECTID = test-project
INSTANCE = omegatrade-instance
DATABASE = omegatrade-db
JWT_KEY = w54p3Y?4dj%8Xqa2jjVC84narhe5Pk
EXPIRE_IN = 2d

Note - The project ID in the .env file must be the same as in the start_spanner.bash file.

Let’s build and deploy

  docker build -t omegatrade-backend-emulator -f dockerfile.local.emulator . \
--no-cache --progress=plain
docker run -it -d -p 3000:3000 omegatrade-backend-emulator

After running the ‘docker run’ command, wait for 15-20 seconds while everything is being set up in the container.

You can verify the functionality of the app by inserting some data via the application into the emulator

  curl --location --request POST 'http://localhost:3000/api/v1/users/register-user' \
--header 'Content-Type: application/json' \
--data-raw '{
    "fullName": "user01",
    "businessEmail": "user01@acme.com",
    "password": "qwerty",
    "confirmPassword": "qwerty",
    "provider": "",
    "photoUrl": ""
}'

Again, you can verify if the data was inserted with a query

  gcloud spanner databases execute-sql omegatrade-db --instance=omegatrade-instance  --sql='SELECT * FROM users'

Running a standalone emulator and deploying the app

We can run the app and the emulator in separate containers and test the application locally. In order to do so, we need to create a Docker network and start the application and emulator containers attached to this network. Take a look at dockerfile.standalone.emulator here. The start_spanner_standalone.bash script here acts as the entry point, which connects to the emulator and creates an instance, database and schema. Now, let’s go ahead and deploy. 

Create a Docker network

  docker network create emulator-net

Start the emulator in a container named “emulator” (because the app container will connect to the emulator by this name) and specify the network

  docker run -d --net=emulator-net -p 9010:9010 -p 9020:9020 --name emulator gcr.io/cloud-spanner-emulator/emulator

If you’ve already followed the steps in the previous section, you may skip cloning the repository and creating the .env file below.

  git clone https://github.com/cloudspannerecosystem/omegatrade.git && cd omegatrade/backend

Before building and deploying the Docker image, we need to create a .env file with the environment variables that we need for running the application.

  PROJECTID = test-project
INSTANCE = omegatrade-instance
DATABASE = omegatrade-db
JWT_KEY = w54p3Y?4dj%8Xqa2jjVC84narhe5Pk
EXPIRE_IN = 2d

Note - The project ID in the .env file must be the same as in the start_spanner.bash file.

Start the application container, specifying the network we created

  docker build -t omegatrade-service -f dockerfile.standalone.emulator .
docker run -it -d --net=emulator-net -p 3001:3000 --name omegatrade omegatrade-service

After running the ‘docker run’ command, you may need to wait for 15-20 seconds as everything is being set up in the container.

If you’re curious, you can view the logs via

  docker logs omegatrade

You can now test the application with a curl command

  curl --location --request POST 'http://localhost:3000/api/v1/users/register-user' \
--header 'Content-Type: application/json' \
--data-raw '{
    "fullName": "user02",
    "businessEmail": "user02@acme.com",
    "password": "qwerty",
    "confirmPassword": "qwerty",
    "provider": "",
    "photoUrl": ""
}'

In the next and final part of the series, we will learn about running the emulator on a remote GCE instance. Stay tuned!