Usa Spanner con funciones de Cloud Run

Spanner

Objetivos

Escribe, implementa y activa una función de HTTP de Cloud Functions que tenga acceso a Spanner.

Costos

En este documento, se usan funciones de Spanner y Cloud Run, que son componentes facturables de Google Cloud.

Antes de comenzar

  1. En este documento, se supone que tienes una instancia de Spanner llamada test-instance y una base de datos llamada example-db que usa el esquema de la aplicación de música. A fin de obtener instrucciones sobre cómo crear una instancia y una base de datos con el esquema de la aplicación de música, consulta la Guía de inicio rápido sobre el uso de Console o los instructivos para comenzar a usar Go, Java, Node.js o Python.

  2. Habilita las funciones de Cloud Run y las APIs de Cloud Build.

    Habilitar las API

  3. Instala e inicializa la CLI de gcloud.

    Si ya tienes instalada la CLI de gcloud, ejecuta el siguiente comando para actualizarla:

    gcloud components update
    
  4. Prepara tu entorno de desarrollo:

Prepara la aplicación

  1. Clona el repositorio de la app de muestra en tu máquina local:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    De manera opcional, puedes descargar la muestra como un archivo zip y extraerla.

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    De manera opcional, puedes descargar la muestra como un archivo zip y extraerla.

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

    De manera opcional, puedes descargar la muestra como un archivo ZIP y extraerla.

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

    De manera opcional, puedes descargar la muestra como un archivo zip y extraerla.

  2. Ve al directorio que contiene la muestra de funciones de Cloud Run para acceder a Spanner:

    Node.js

    cd nodejs-docs-samples/functions/spanner/

    Python

    cd python-docs-samples/functions/spanner/

    Go

    cd golang-samples/functions/spanner/

    Java

    cd java-docs-samples/functions/spanner/
  3. Ve el código de muestra:

    Node.js

    // Imports the Google Cloud client library
    const {Spanner} = require('@google-cloud/spanner');
    
    // Imports the functions framework to register your HTTP function
    const functions = require('@google-cloud/functions-framework');
    
    // Instantiates a client
    const spanner = new Spanner();
    
    // Your Cloud Spanner instance ID
    const instanceId = 'test-instance';
    
    // Your Cloud Spanner database ID
    const databaseId = 'example-db';
    
    /**
     * HTTP Cloud Function.
     *
     * @param {Object} req Cloud Function request context.
     * @param {Object} res Cloud Function response context.
     */
    functions.http('spannerQuickstart', async (req, res) => {
      // Gets a reference to a Cloud Spanner instance and database
      const instance = spanner.instance(instanceId);
      const database = instance.database(databaseId);
    
      // The query to execute
      const query = {
        sql: 'SELECT * FROM Albums',
      };
    
      // Execute the query
      try {
        const results = await database.run(query);
        const rows = results[0].map(row => row.toJSON());
        rows.forEach(row => {
          res.write(
            `SingerId: ${row.SingerId}, ` +
              `AlbumId: ${row.AlbumId}, ` +
              `AlbumTitle: ${row.AlbumTitle}\n`
          );
        });
        res.status(200).end();
      } catch (err) {
        res.status(500).send(`Error querying Spanner: ${err}`);
      }
    });

    Python

    import functions_framework
    from google.cloud import spanner
    
    instance_id = "test-instance"
    database_id = "example-db"
    
    client = spanner.Client()
    instance = client.instance(instance_id)
    database = instance.database(database_id)
    
    
    @functions_framework.http
    def spanner_read_data(request):
        query = "SELECT * FROM Albums"
    
        outputs = []
        with database.snapshot() as snapshot:
            results = snapshot.execute_sql(query)
    
            for row in results:
                output = "SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row)
                outputs.append(output)
    
        return "\n".join(outputs)
    
    

    Go

    
    // Package spanner contains an example of using Spanner from a Cloud Function.
    package spanner
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"net/http"
    	"sync"
    
    	"cloud.google.com/go/spanner"
    	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
    	"google.golang.org/api/iterator"
    )
    
    // client is a global Spanner client, to avoid initializing a new client for
    // every request.
    var client *spanner.Client
    var clientOnce sync.Once
    
    // db is the name of the database to query.
    var db = "projects/my-project/instances/my-instance/databases/example-db"
    
    func init() {
    	functions.HTTP("HelloSpanner", HelloSpanner)
    }
    
    // HelloSpanner is an example of querying Spanner from a Cloud Function.
    func HelloSpanner(w http.ResponseWriter, r *http.Request) {
    	clientOnce.Do(func() {
    		// Declare a separate err variable to avoid shadowing client.
    		var err error
    		client, err = spanner.NewClient(context.Background(), db)
    		if err != nil {
    			http.Error(w, "Error initializing database", http.StatusInternalServerError)
    			log.Printf("spanner.NewClient: %v", err)
    			return
    		}
    	})
    
    	fmt.Fprintln(w, "Albums:")
    	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
    	iter := client.Single().Query(r.Context(), stmt)
    	defer iter.Stop()
    	for {
    		row, err := iter.Next()
    		if err == iterator.Done {
    			return
    		}
    		if err != nil {
    			http.Error(w, "Error querying database", http.StatusInternalServerError)
    			log.Printf("iter.Next: %v", err)
    			return
    		}
    		var singerID, albumID int64
    		var albumTitle string
    		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
    			http.Error(w, "Error parsing database response", http.StatusInternalServerError)
    			log.Printf("row.Columns: %v", err)
    			return
    		}
    		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
    	}
    }
    

    Java

    import com.google.api.client.http.HttpStatusCodes;
    import com.google.cloud.functions.HttpFunction;
    import com.google.cloud.functions.HttpRequest;
    import com.google.cloud.functions.HttpResponse;
    import com.google.cloud.spanner.DatabaseClient;
    import com.google.cloud.spanner.DatabaseId;
    import com.google.cloud.spanner.LazySpannerInitializer;
    import com.google.cloud.spanner.ResultSet;
    import com.google.cloud.spanner.SpannerException;
    import com.google.cloud.spanner.SpannerOptions;
    import com.google.cloud.spanner.Statement;
    import com.google.common.annotations.VisibleForTesting;
    import com.google.common.base.MoreObjects;
    import java.io.PrintWriter;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    // HelloSpanner is an example of querying Spanner from a Cloud Function.
    public class HelloSpanner implements HttpFunction {
      private static final Logger logger = Logger.getLogger(HelloSpanner.class.getName());
    
      // TODO<developer>: Set these environment variables.
      private static final String SPANNER_INSTANCE_ID =
          MoreObjects.firstNonNull(System.getenv("SPANNER_INSTANCE"), "my-instance");
      private static final String SPANNER_DATABASE_ID =
          MoreObjects.firstNonNull(System.getenv("SPANNER_DATABASE"), "example-db");
    
      private static final DatabaseId databaseId =
          DatabaseId.of(
              SpannerOptions.getDefaultProjectId(),
              SPANNER_INSTANCE_ID,
              SPANNER_DATABASE_ID);
    
      // The LazySpannerInitializer instance is shared across all instances of the HelloSpanner class.
      // It will create a Spanner instance the first time one is requested, and continue to return that
      // instance for all subsequent requests.
      private static final LazySpannerInitializer SPANNER_INITIALIZER = new LazySpannerInitializer();
    
      @VisibleForTesting
      DatabaseClient getClient() throws Throwable {
        return SPANNER_INITIALIZER.get().getDatabaseClient(databaseId);
      }
    
      @Override
      public void service(HttpRequest request, HttpResponse response) throws Exception {
        var writer = new PrintWriter(response.getWriter());
        try {
          DatabaseClient client = getClient();
          try (ResultSet rs =
              client
                  .singleUse()
                  .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) {
            writer.printf("Albums:%n");
            while (rs.next()) {
              writer.printf(
                  "%d %d %s%n",
                  rs.getLong("SingerId"), rs.getLong("AlbumId"), rs.getString("AlbumTitle"));
            }
          } catch (SpannerException e) {
            writer.printf("Error querying database: %s%n", e.getMessage());
            response.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, e.getMessage());
          }
        } catch (Throwable t) {
          logger.log(Level.SEVERE, "Spanner example failed", t);
          writer.printf("Error setting up Spanner: %s%n", t.getMessage());
          response.setStatusCode(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, t.getMessage());
        }
      }
    }

    La función envía una consulta de SQL para recuperar todos los datos de Albums de tu base de datos. La función se ejecuta cuando haces una solicitud HTTP al extremo de la función.

Implementa la función

Ejecuta el siguiente comando en el directorio spanner para implementar la función con un activador HTTP:

Node.js

gcloud functions deploy nodejs-spanner-function \
--gen2 \
--runtime=nodejs20 \
--region=REGION \
--source=. \
--entry-point=spannerQuickstart 
--trigger-http

Usa la marca --runtime para especificar el ID del entorno de ejecución de una versión compatible de Node.js para ejecutar la función.

Python

gcloud functions deploy python-spanner-function \
--gen2 \
--runtime=python312 \
--region=REGION \
--source=. \
--entry-point=spanner_read_data 
--trigger-http

Usa la marca --runtime para especificar el ID de entorno de ejecución de una versión de Python compatible para ejecutar la función.

Go

gcloud functions deploy go-spanner-function \
--gen2 \
--runtime=go121 \
--region=REGION \
--source=. \
--entry-point=HelloSpanner 
--trigger-http

Usa la marca --runtime para especificar el ID de entorno de ejecución de una versión de Go compatible para ejecutar tu función.

Java

gcloud functions deploy java-spanner-function \
--gen2 \
--runtime=java17 \
--region=REGION \
--source=. \
--entry-point=functions.HelloSpanner \
--memory=512MB 
--trigger-http

Usa la marca --runtime para especificar el ID del entorno de ejecución de una versión de Java compatible para ejecutar tu función.

Reemplaza REGION por el nombre de la región de Google Cloud en la que deseas implementar tu función (por ejemplo, us-west1).

La implementación de la función puede tardar hasta dos minutos.

Ten en cuenta el valor de url que se muestra cuando tu función termina de implementarse. Lo usarás cuando actives la función.

Puedes ver las funciones implementadas en la página de Funciones de Cloud Run en la consola de Google Cloud. También puedes crear y editar funciones en esa página, en la que además puedes obtener detalles y diagnósticos de tus funciones.

Activa la función

Haz una solicitud HTTP a tu función:

curl URL

Reemplaza URL por el valor de la URL que se muestra cuando la función se termina de implementar.

Deberías ver una respuesta que muestre los resultados de la consulta de SQL, suponiendo que trabajaste con un instructivo de primeros pasos y propagaste la base de datos:

SingerId: 2, AlbumId: 2, AlbumTitle: Forever Hold Your Peace
SingerId: 1, AlbumId: 2, AlbumTitle: Go, Go, Go
SingerId: 2, AlbumId: 1, AlbumTitle: Green
SingerId: 2, AlbumId: 3, AlbumTitle: Terrified
SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk

También puedes visitar la URL de la función en tu navegador para ver los resultados de tu consulta de SQL.

Limpieza

Para evitar incurrir en cargos adicionales a tu cuenta de Google Cloud por los recursos de Spanner y Cloud Run que se usaron en este documento, haz lo siguiente:

  1. Borra la instancia:

    gcloud spanner instances delete test-instance
    
  2. Borra la función que implementaste:

    Node.js

    gcloud functions delete nodejs-spanner-function 

    Python

    gcloud functions delete python-spanner-function 

    Go

    gcloud functions delete go-spanner-function 

    Java

    gcloud functions delete java-spanner-function 

¿Qué sigue?