Cloud Functions로 Cloud Spanner 사용

Cloud Spanner

목표

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

비용

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

시작하기 전에

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

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

    API 사용 설정

  3. Cloud SDK를 설치하고 초기화합니다.

    Cloud SDK가 이미 설치되어 있으면 다음 명령어를 실행하여 업데이트하세요.

    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 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');
    
    // 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.
     */
    exports.get = 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

    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)
    
    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"
    	"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"
    
    // 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 get \
--runtime nodejs10 --trigger-http
--runtime 플래그에 다음 값을 사용하여 원하는 Node.js 버전을 지정할 수 있습니다.
  • nodejs10
  • nodejs12
  • nodejs14

Python

gcloud functions deploy spanner_read_data \
--runtime python38 --trigger-http
--runtime 플래그에 다음 값을 사용하여 원하는 Python 버전을 지정할 수 있습니다.
  • python37
  • python38
  • python39

Go

gcloud functions deploy HelloSpanner \
--runtime go113 --trigger-http
--runtime 플래그에 다음 값을 사용하여 원하는 Go 버전을 지정할 수 있습니다.
  • go111(지원 중단됨)
  • go113

자바

gcloud functions deploy java-spanner-function \
--entry-point functions.HelloSpanner \
--runtime java11 \
--memory 512MB --trigger-http

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

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

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

함수 트리거

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

Node.js

curl "https://REGION-PROJECT_ID.cloudfunctions.net/get" 

Python

curl "https://REGION-PROJECT_ID.cloudfunctions.net/spanner_read_data" 

Go

curl "https://REGION-PROJECT_ID.cloudfunctions.net/HelloSpanner" 

자바

curl "https://REGION-PROJECT_ID.cloudfunctions.net/java-spanner-function" 

REGIONPROJECT_ID는 함수 배포가 끝난 후 터미널에 표시되는 값과 일치합니다. 시작하기 가이드를 따라 작업을 수행하고 데이터베이스를 채웠다면 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 Functions 리소스의 추가 비용이 Google Cloud 계정에 청구되지 않도록 하려면 다음 안내를 따르세요.

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

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

    Node.js

    gcloud functions delete get 

    Python

    gcloud functions delete spanner_read_data 

    Go

    gcloud functions delete HelloSpanner 

    자바

    gcloud functions delete java-spanner-function 

다음 단계