Cloud Run Functions에서 Spanner 사용

Cloud Spanner

목표

Spanner에 액세스하는 HTTP Cloud 함수를 작성, 배포, 트리거합니다.

비용

이 문서에서는 비용이 청구될 수 있는 Google Cloud 구성요소인 Spanner와 Cloud Run Functions를 사용합니다.

시작하기 전에

  1. 이 문서에서는 test-instance라는 Spanner 인스턴스와 음악 애플리케이션 스키마를 사용하는 example-db라는 데이터베이스가 있다고 가정합니다. 음악 애플리케이션 스키마를 사용하여 인스턴스와 데이터베이스를 만드는 방법에 대한 자세한 내용은 빠른 시작: 콘솔 사용 또는 Go, 자바, Node.js, Python 시작하기 튜토리얼을 참조하세요.

  2. Cloud Run Functions 및 Cloud Build API를 사용 설정합니다.

    API 사용 설정

  3. gcloud CLI를 설치하고 초기화합니다.

    gcloud CLI가 이미 설치되어 있으면 다음 명령어를 실행하여 업데이트합니다.

    gcloud components update
    
  4. 개발 환경을 준비합니다.

애플리케이션 준비

  1. 샘플 앱 저장소를 로컬 머신에 클론합니다.

    Node.js

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

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

    Python

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

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

    Go

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

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

    자바

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

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

  2. Spanner에 액세스하기 위한 Cloud Run Functions 샘플 코드가 있는 디렉터리로 변경합니다.

    Node.js

    cd nodejs-docs-samples/functions/spanner/

    Python

    cd python-docs-samples/functions/spanner/

    Go

    cd golang-samples/functions/spanner/

    자바

    cd java-docs-samples/functions/spanner/
  3. 다음 샘플 코드를 살펴봅니다.

    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)
    	}
    }
    

    자바

    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());
        }
      }
    }

    함수는 SQL 쿼리를 전송하여 데이터베이스의 모든 Albums 데이터를 가져옵니다. 이 함수는 함수의 엔드포인트로 HTTP 요청을 보낼 때 실행됩니다.

함수 배포

HTTP 트리거를 사용하여 함수를 배포하려면 spanner 디렉터리에서 다음 명령어를 실행합니다.

Node.js

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

--runtime 플래그를 사용하여 함수를 실행할 지원되는 Node.js 버전의 런타임 ID를 지정합니다.

Python

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

--runtime 플래그를 사용하여 함수를 실행할 지원되는 Python 버전의 런타임 ID를 지정합니다.

Go

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

--runtime 플래그를 사용하여 함수를 실행할 지원되는 Go 버전의 런타임 ID를 지정합니다.

자바

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

--runtime 플래그를 사용하여 함수를 실행할 지원되는 Java 버전의 런타임 ID를 지정합니다.

REGION을 함수를 배포할 Google Cloud 리전 이름으로 바꿉니다(예: us-west1).

함수 배포에 최대 2분이 소요될 수 있습니다.

함수 배포가 끝나면 반환된 url 값을 기록합니다. 이는 함수를 트리거할 때 사용됩니다.

Google Cloud 콘솔의 Cloud Run Functions 페이지에서 배포한 함수를 확인할 수 있습니다. 이 페이지에서 함수를 만들고 수정할 수도 있으며 함수에 관한 세부정보와 진단 정보를 확인할 수 있습니다.

함수 트리거

함수에 HTTP 요청을 보냅니다.

curl URL

URL을 함수 배포가 끝난 후 반환된 URL 값으로 바꿉니다.

시작하기 가이드를 따라 작업을 수행하고 데이터베이스를 채웠다면 SQL 쿼리 결과를 나타내는 출력이 표시됩니다.

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

브라우저에서 함수의 URL을 방문하여 SQL 쿼리 결과를 볼 수도 있습니다.

삭제

이 문서에서 사용한 Spanner 및 Cloud Run Functions 리소스에 대한 Google Cloud 계정에 추가 요금이 부과되지 않도록 하려면 다음을 수행합니다.

  1. 인스턴스를 삭제합니다.

    gcloud spanner instances delete test-instance
    
  2. 배포한 함수를 삭제합니다.

    Node.js

    gcloud functions delete nodejs-spanner-function 

    Python

    gcloud functions delete python-spanner-function 

    Go

    gcloud functions delete go-spanner-function 

    자바

    gcloud functions delete java-spanner-function 

다음 단계