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를 설치하고 초기화합니다.

  4. gcloud 구성요소를 업데이트합니다.

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

애플리케이션 준비

  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

Python

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

Go

gcloud functions deploy HelloSpanner \
--runtime go111 --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 

다음 단계