Cloud Run for Anthos에서 Pub/Sub 사용


이 튜토리얼에서는 Cloud Run for Anthos 서비스를 작성, 배포하고 Pub/Sub 푸시 구독으로부터 호출하는 방법을 보여줍니다.

목표

  • Cloud Run for Anthos에 서비스 작성, 빌드, 배포
  • Pub/Sub 주제에 메시지를 게시하여 서비스 호출

비용

이 문서에서는 비용이 청구될 수 있는 다음과 같은 Google Cloud 구성요소를 사용합니다.

프로젝트 사용량을 기준으로 예상 비용을 산출하려면 가격 계산기를 사용하세요. Google Cloud를 처음 사용하는 사용자는 무료 체험판을 사용할 수 있습니다.

시작하기 전에

  1. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  2. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  3. Cloud Run for Anthos API 사용 설정
  4. gcloud CLI 설치 및 초기화
  5. kubectl 구성요소를 설치합니다.
    gcloud components install kubectl
  6. 구성요소 업데이트
    gcloud components update
  7. Cloud Run for Anthos를 사용하는 경우 Cloud Run for Anthos 설정의 안내에 따라 새 클러스터를 만듭니다.

gcloud 기본값 설정

Cloud Run for Anthos 서비스의 기본값으로 gcloud를 구성하려면 다음 안내를 따르세요.

  1. 기본 프로젝트를 설정합니다.

    gcloud config set project PROJECT_ID

    PROJECT_ID를 이 튜토리얼에서 사용하는 프로젝트의 이름으로 바꿉니다.

  2. 클러스터에 gcloud를 구성합니다.

    gcloud config set run/platform gke
    gcloud config set run/cluster CLUSTER-NAME
    gcloud config set run/cluster_location REGION

    다음과 같이 바꿉니다.

    • CLUSTER-NAME을 클러스터에 사용한 이름으로 바꿉니다.
    • REGION을 지원되는 클러스터 위치 중 원하는 위치로 바꿉니다.

Pub/Sub 주제 만들기

샘플 서비스가 Pub/Sub 주제에 게시된 메시지로 트리거되므로, Pub/Sub에 주제를 만들어야 합니다.

새 Pub/Sub 주제를 만들려면 다음 명령어를 사용합니다.

gcloud pubsub topics create myRunTopic

myRunTopic을 사용하거나 Cloud 프로젝트 내에서 고유한 주제 이름으로 바꿀 수 있습니다.

코드 샘플 검색

사용할 코드 샘플을 검색하려면 다음 안내를 따르세요.

  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. Cloud Run for Anthos 샘플 코드가 있는 디렉터리로 변경합니다.

    Node.js

    cd nodejs-docs-samples/run/pubsub/

    Python

    cd python-docs-samples/run/pubsub/

    Go

    cd golang-samples/run/pubsub/

    자바

    cd java-docs-samples/run/pubsub/

코드 보기

이 튜토리얼의 코드는 다음과 같이 구성됩니다.

  • 새로 추가되는 요청을 처리하는 서버

    Node.js

    Node.js 서비스를 쉽게 테스트할 수 있도록 서버 구성이 서버 시작과 구분됩니다.

    Node.js 웹 서버가 app.js에 설정됩니다.

    const express = require('express');
    const app = express();
    
    // This middleware is available in Express v4.16.0 onwards
    app.use(express.json());
    웹 서버가 index.js에서 시작됩니다.
    const app = require('./app.js');
    const PORT = parseInt(parseInt(process.env.PORT)) || 8080;
    
    app.listen(PORT, () =>
      console.log(`nodejs-pubsub-tutorial listening on port ${PORT}`)
    );

    Python

    import base64
    import os
    
    from flask import Flask, request
    
    app = Flask(__name__)

    Go

    
    // Sample run-pubsub is a Cloud Run service which handles Pub/Sub messages.
    package main
    
    import (
    	"encoding/json"
    	"io/ioutil"
    	"log"
    	"net/http"
    	"os"
    )
    
    func main() {
    	http.HandleFunc("/", HelloPubSub)
    	// Determine port for HTTP service.
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    		log.Printf("Defaulting to port %s", port)
    	}
    	// Start HTTP server.
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    

    자바

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class PubSubApplication {
      public static void main(String[] args) {
        SpringApplication.run(PubSubApplication.class, args);
      }
    }

  • Pub/Sub 메시지를 처리하고 인사말을 로깅하는 핸들러입니다.

    Node.js

    app.post('/', (req, res) => {
      if (!req.body) {
        const msg = 'no Pub/Sub message received';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
      if (!req.body.message) {
        const msg = 'invalid Pub/Sub message format';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      const pubSubMessage = req.body.message;
      const name = pubSubMessage.data
        ? Buffer.from(pubSubMessage.data, 'base64').toString().trim()
        : 'World';
    
      console.log(`Hello ${name}!`);
      res.status(204).send();
    });

    Python

    @app.route("/", methods=["POST"])
    def index():
        envelope = request.get_json()
        if not envelope:
            msg = "no Pub/Sub message received"
            print(f"error: {msg}")
            return f"Bad Request: {msg}", 400
    
        if not isinstance(envelope, dict) or "message" not in envelope:
            msg = "invalid Pub/Sub message format"
            print(f"error: {msg}")
            return f"Bad Request: {msg}", 400
    
        pubsub_message = envelope["message"]
    
        name = "World"
        if isinstance(pubsub_message, dict) and "data" in pubsub_message:
            name = base64.b64decode(pubsub_message["data"]).decode("utf-8").strip()
    
        print(f"Hello {name}!")
    
        return ("", 204)
    
    

    Go

    
    // PubSubMessage is the payload of a Pub/Sub event.
    // See the documentation for more details:
    // https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
    type PubSubMessage struct {
    	Message struct {
    		Data []byte `json:"data,omitempty"`
    		ID   string `json:"id"`
    	} `json:"message"`
    	Subscription string `json:"subscription"`
    }
    
    // HelloPubSub receives and processes a Pub/Sub push message.
    func HelloPubSub(w http.ResponseWriter, r *http.Request) {
    	var m PubSubMessage
    	body, err := ioutil.ReadAll(r.Body)
    	if err != nil {
    		log.Printf("ioutil.ReadAll: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    	// byte slice unmarshalling handles base64 decoding.
    	if err := json.Unmarshal(body, &m); err != nil {
    		log.Printf("json.Unmarshal: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	name := string(m.Message.Data)
    	if name == "" {
    		name = "World"
    	}
    	log.Printf("Hello %s!", name)
    }
    

    자바

    import com.example.cloudrun.Body;
    import java.util.Base64;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    // PubsubController consumes a Pub/Sub message.
    @RestController
    public class PubSubController {
      @RequestMapping(value = "/", method = RequestMethod.POST)
      public ResponseEntity receiveMessage(@RequestBody Body body) {
        // Get PubSub message from request body.
        Body.Message message = body.getMessage();
        if (message == null) {
          String msg = "Bad Request: invalid Pub/Sub message format";
          System.out.println(msg);
          return new ResponseEntity(msg, HttpStatus.BAD_REQUEST);
        }
    
        String data = message.getData();
        String target =
            !StringUtils.isEmpty(data) ? new String(Base64.getDecoder().decode(data)) : "World";
        String msg = "Hello " + target + "!";
    
        System.out.println(msg);
        return new ResponseEntity(msg, HttpStatus.OK);
      }
    }

    정확한 HTTP 응답 코드를 반환하도록 서비스를 코딩해야 합니다. HTTP 200 또는 204와 같은 성공 코드는 Pub/Sub 메시지 처리가 완료되었음을 나타냅니다. HTTP 400 또는 500과 같은 오류 코드는 푸시를 사용하여 메시지 수신 가이드에 설명된 것처럼 메시지가 재시도됨을 나타냅니다.

  • 서비스의 작동 환경을 정의하는 Dockerfile입니다. Dockerfile 콘텐츠는 언어별로 다릅니다.

    Node.js

    
    # Use the official lightweight Node.js 12 image.
    # https://hub.docker.com/_/node
    FROM node:18-slim
    
    # Create and change to the app directory.
    WORKDIR /usr/src/app
    
    # Copy application dependency manifests to the container image.
    # A wildcard is used to ensure both package.json AND package-lock.json are copied.
    # Copying this separately prevents re-running npm install on every code change.
    COPY package*.json ./
    
    # Install dependencies.
    # If you add a package-lock.json speed your build by switching to 'npm ci'.
    # RUN npm ci --only=production
    RUN npm install --production
    
    # Copy local code to the container image.
    COPY . .
    
    # Run the web service on container startup.
    CMD [ "npm", "start" ]
    

    Python

    
    # Use the official Python image.
    # https://hub.docker.com/_/python
    FROM python:3.10
    
    # Allow statements and log messages to immediately appear in the Cloud Run logs
    ENV PYTHONUNBUFFERED True
    
    # Copy application dependency manifests to the container image.
    # Copying this separately prevents re-running pip install on every code change.
    COPY requirements.txt ./
    
    # Install production dependencies.
    RUN pip install -r requirements.txt
    
    # Copy local code to the container image.
    ENV APP_HOME /app
    WORKDIR $APP_HOME
    COPY . ./
    
    # Run the web service on container startup.
    # Use gunicorn webserver with one worker process and 8 threads.
    # For environments with multiple CPU cores, increase the number of workers
    # to be equal to the cores available.
    # Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
    CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
    

    Go

    
    # Use the offical golang image to create a binary.
    # This is based on Debian and sets the GOPATH to /go.
    # https://hub.docker.com/_/golang
    FROM golang:1.17-buster as builder
    
    # Create and change to the app directory.
    WORKDIR /app
    
    # Retrieve application dependencies.
    # This allows the container build to reuse cached dependencies.
    # Expecting to copy go.mod and if present go.sum.
    COPY go.* ./
    RUN go mod download
    
    # Copy local code to the container image.
    COPY . ./
    
    # Build the binary.
    RUN go build -v -o server
    
    # Use the official Debian slim image for a lean production container.
    # https://hub.docker.com/_/debian
    # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
    FROM debian:buster-slim
    RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
        ca-certificates && \
        rm -rf /var/lib/apt/lists/*
    
    # Copy the binary to the production image from the builder stage.
    COPY --from=builder /app/server /server
    
    # Run the web service on container startup.
    CMD ["/server"]
    

    자바

    이 샘플은 Jib를 사용해서 일반적인 자바 도구로 Docker 이미지를 빌드합니다. Jib는 Dockerfile을 사용하거나 Docker를 설치할 필요 없이 컨테이너 빌드를 최적화합니다. Jib로 자바 컨테이너 빌드에 대해 자세히 알아보세요.
    <plugin>
      <groupId>com.google.cloud.tools</groupId>
      <artifactId>jib-maven-plugin</artifactId>
      <version>3.3.1</version>
      <configuration>
        <to>
          <image>gcr.io/PROJECT_ID/pubsub</image>
        </to>
      </configuration>
    </plugin>
    

Pub/Sub 요청 원본을 인증하는 방법은 Pub/Sub와 통합 아래의 섹션을 참조하세요.

코드 제공

코드 제공은 Cloud Build로 컨테이너 이미지를 빌드하고, Container Registry에 컨테이너 이미지를 업로드하고, 컨테이너 이미지를 Cloud Run for Anthos에 배포하는 세 단계로 구성됩니다.

코드를 제공하려면 다음 안내를 따르세요.

  1. 컨테이너를 빌드하고 Container Registry에 게시합니다.

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    여기서 PROJECT_ID는 Cloud 프로젝트 ID이고 pubsub는 서비스에 지정할 이름입니다.

    성공하면 ID, 생성 시간, 이미지 이름이 포함된 성공 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    여기서 PROJECT_ID는 Cloud 프로젝트 ID이고 pubsub는 서비스에 지정할 이름입니다.

    성공하면 ID, 생성 시간, 이미지 이름이 포함된 성공 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    여기서 PROJECT_ID는 Cloud 프로젝트 ID이고 pubsub는 서비스에 지정할 이름입니다.

    성공하면 ID, 생성 시간, 이미지 이름이 포함된 성공 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

    자바

    mvn compile jib:build -Dimage=gcr.io/PROJECT_ID/pubsub

    여기서 PROJECT_ID는 Cloud 프로젝트 ID이고 pubsub는 서비스에 지정할 이름입니다.

    성공하면 BUILD SUCCESS 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

  2. 다음 명령어를 실행하여 앱을 배포합니다.

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub

    PROJECT_ID를 Cloud 프로젝트 ID로 바꿉니다. pubsub는 컨테이너 이름이고 pubsub-tutorial은 서비스 이름입니다. 컨테이너 이미지는 이전에 gcloud 설정에서 구성한 서비스 및 클러스터에 배포됩니다.

    배포가 완료될 때까지 기다립니다. 이 작업은 30초 정도 걸릴 수 있습니다. 성공하면 명령줄에 서비스 URL이 표시됩니다. 이 URL은 Pub/Sub 구독을 구성하는 데 사용됩니다.

  3. 서비스에 코드 업데이트를 배포하려면 이전 단계를 반복합니다. 서비스에 배포할 때마다 새 버전이 생성되고 준비가 되면 자동으로 트래픽 제공이 시작됩니다.

Pub/Sub와 통합

이제 Cloud Run for Anthos 서비스가 배포되었으므로 여기에 메시지를 게시하도록 Pub/Sub를 구성합니다.

Pub/Sub와 서비스를 통합하려면 다음 안내를 따르세요.

  1. 프로젝트에서 인증 토큰을 만들도록 Pub/Sub를 사용 설정합니다.

    gcloud projects add-iam-policy-binding PROJECT_ID \
         --member=serviceAccount:service-PROJECT-NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
         --role=roles/iam.serviceAccountTokenCreator

    다음과 같이 바꿉니다.

    • PROJECT_ID를 Cloud 프로젝트 ID로 바꿉니다.
    • PROJECT-NUMBER를 Cloud 프로젝트 번호로 바꿉니다.
  2. 서비스 계정을 만들거나 선택하여 Pub/Sub 구독 ID를 나타냅니다.

    gcloud iam service-accounts create cloud-run-pubsub-invoker \
         --display-name "Cloud Run for Anthos Pub/Sub Invoker"

    cloud-run-pubsub-invoker를 사용하거나 Cloud 프로젝트 내에서 고유한 이름으로 바꿀 수 있습니다.

  3. 서비스 계정으로 Pub/Sub 구독을 만듭니다.

    1. 클러스터에 자동 TLS 및 HTTPS를 사용 설정하고 서비스에 도메인 매핑을 추가합니다.

    2. Pub/Sub에 대한 도메인 소유권을 등록합니다.

    3. Pub/Sub 메시지에 연결된 인증 코드를 검증하는 코드를 추가합니다. 샘플 코드는 푸시 엔드포인트에 의한 인증 및 승인에 제공되어 있습니다.

      인증을 위해서는 토큰이 유효하고 예상된 서비스 계정과 연결되어 있는지 확인해야 합니다. Cloud Run과 달리 Cloud Run for Anthos에는 토큰이 유효한지 확인하거나 Cloud Run for Anthos 서비스를 호출하도록 서비스 계정이 승인되었는지 확인하는 기본 제공되는 승인 검사가 없습니다.

    4. 서비스 계정으로 Pub/Sub 구독을 만듭니다.

      gcloud pubsub subscriptions create myRunSubscription --topic myRunTopic \
           --push-endpoint=SERVICE-URL/ \
           --push-auth-service-account=cloud-run-pubsub-invoker@PROJECT_ID.iam.gserviceaccount.com

      다음과 같이 바꿉니다.

      • myRunTopic이전에 만든 주제로 바꿉니다.
      • SERVICE-URL을 커스텀 서비스 URL로 바꿉니다. https를 프로토콜로 지정합니다.
      • PROJECT_ID를 Cloud 프로젝트 ID로 바꿉니다.

      --push-auth-service-account 플래그는 인증 및 승인을 위한 Pub/Sub 푸시 기능을 활성화합니다.

이제 서비스가 Pub/Sub와 완전히 통합되었습니다.

사용해 보기

엔드 투 엔드 솔루션을 테스트하려면 다음 안내를 따르세요.

  1. 주제에 Pub/Sub 메시지를 전송합니다.

    gcloud pubsub topics publish myRunTopic --message "Runner"

    또한 이 튜토리얼에 표시된 것처럼 명령줄을 사용하는 대신 프로그래매틱 방식으로 메시지를 게시할 수도 있습니다. 자세한 내용은 메시지 게시를 참조하세요.

  2. 서비스 로그로 이동합니다.

    1. Google Cloud 콘솔에서 Cloud Run for Anthos 페이지로 이동합니다.

      Cloud Run for Anthos로 이동

    2. pubsub-tutorial 서비스를 클릭합니다.

    3. 로그 탭을 선택합니다.

      로그가 나타나려면 시간이 약간 걸릴 수 있습니다. 즉시 표시되지 않으면 잠시 후 다시 확인하세요.

  3. 'Hello Runner!' 메시지를 찾습니다.

정리

Pub/Sub에서 Cloud Run for Anthos를 사용하는 보다 세부적인 심화 사용 사례를 연습하려면 지금 삭제 단계를 건너뛰고 계속해서 이미지 처리 튜토리얼을 진행하세요.

이 튜토리얼용으로 새 프로젝트를 만든 경우 이 프로젝트를 삭제합니다. 기존 프로젝트를 사용한 경우 이 튜토리얼에 추가된 변경사항은 제외하고 보존하려면 튜토리얼용으로 만든 리소스를 삭제합니다.

프로젝트 삭제

비용이 청구되지 않도록 하는 가장 쉬운 방법은 튜토리얼에서 만든 프로젝트를 삭제하는 것입니다.

프로젝트를 삭제하려면 다음 안내를 따르세요.

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

튜토리얼 리소스 삭제

  1. 이 튜토리얼에서 배포한 Cloud Run for Anthos 서비스를 삭제합니다.

    gcloud run services delete SERVICE-NAME

    여기서 SERVICE-NAME은 선택한 서비스 이름입니다.

    Google Cloud 콘솔에서 Cloud Run for Anthos 서비스를 삭제할 수도 있습니다.

    Cloud Run for Anthos로 이동

  2. 튜토리얼 설정 중에 추가한 gcloud 기본 구성을 삭제합니다.

     gcloud config unset run/platform
     gcloud config unset run/cluster
     gcloud config unset run/cluster_location
    
  3. 프로젝트 구성을 삭제합니다.

     gcloud config unset project
    
  4. 이 튜토리얼에서 만든 다른 Google Cloud 리소스를 삭제합니다.

다음 단계