튜토리얼: Cloud Run 서비스의 로컬 문제해결


이 튜토리얼에서는 서비스 개발자가 Google Cloud Observability 도구를 사용하여 발견된 Cloud Run 서비스의 문제를 해결하고 조사를 위한 로컬 개발 워크플로를 보여줍니다.

문제 해결 가이드의 단계별 '우수사례' 컴패니언은 배포 시 런타임 오류가 발생하는 샘플 프로젝트를 사용하여 문제를 찾고 해결합니다.

목표

  • Cloud Run에 서비스 작성, 빌드, 배포
  • Error Reporting 및 Cloud Logging을 사용하여 오류 식별
  • 근본 원인 분석을 위해 Container Registry에서 컨테이너 이미지 검색
  • '프로덕션' 서비스 수정 후 향후 문제 해결을 위한 서비스 개선

비용

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

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

시작하기 전에

  1. Google Cloud 계정에 로그인합니다. Google Cloud를 처음 사용하는 경우 계정을 만들고 Google 제품의 실제 성능을 평가해 보세요. 신규 고객에게는 워크로드를 실행, 테스트, 배포하는 데 사용할 수 있는 $300의 무료 크레딧이 제공됩니다.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

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

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

    Go to project selector

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

  6. Cloud Run Admin API 사용 설정
  7. gcloud CLI를 설치하고 초기화합니다.
  8. 구성요소를 업데이트합니다.
    gcloud components update
  9. 안내에 따라 Docker를 로컬로 설치합니다.

필요한 역할

튜토리얼을 완료하는 데 필요한 권한을 얻으려면 관리자에게 프로젝트에 대한 다음 IAM 역할을 부여해 달라고 요청하세요.

역할 부여에 대한 자세한 내용은 액세스 관리를 참조하세요.

커스텀 역할이나 다른 사전 정의된 역할을 통해 필요한 권한을 얻을 수도 있습니다.

gcloud 기본값 설정

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

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

    gcloud config set project PROJECT_ID

    PROJECT_ID를 이 튜토리얼용으로 만든 프로젝트 이름으로 바꿉니다.

  2. 선택한 리전에 맞게 gcloud를 구성합니다.

    gcloud config set run/region REGION

    REGION을 지원되는 Cloud Run 리전 중 원하는 리전으로 바꿉니다.

Cloud Run 위치

Cloud Run은 리전을 기반으로 합니다. 즉, Cloud Run 서비스를 실행하는 인프라가 특정 리전에 위치해 있으며 해당 리전 내의 모든 영역에서 중복으로 사용할 수 있도록 Google이 관리합니다.

Cloud Run 서비스를 실행하는 리전을 선택하는 데 있어 중요한 기준은 지연 시간, 가용성 또는 내구성 요구사항입니다. 일반적으로 사용자와 가장 가까운 리전을 선택할 수 있지만 Cloud Run 서비스에서 사용하는 다른 Google Cloud 제품 위치도 고려해야 합니다. 여러 위치에서 Google Cloud 제품을 함께 사용하면 서비스 지연 시간과 비용에 영향을 미칠 수 있습니다.

Cloud Run은 다음 리전에서 사용할 수 있습니다.

등급 1 가격 적용

  • asia-east1(타이완)
  • asia-northeast1(도쿄)
  • asia-northeast2(오사카)
  • europe-north1(핀란드) 잎 아이콘 낮은 CO2
  • europe-southwest1(마드리드)
  • europe-west1(벨기에) 잎 아이콘 낮은 CO2
  • europe-west4(네덜란드)
  • europe-west8(밀라노)
  • europe-west9(파리) 잎 아이콘 낮은 CO2
  • me-west1(텔아비브)
  • us-central1(아이오와) 잎 아이콘 낮은 CO2
  • us-east1(사우스캐롤라이나)
  • us-east4(북 버지니아)
  • us-east5(콜럼버스)
  • us-south1(댈러스)
  • us-west1(오리건) 잎 아이콘 낮은 CO2

등급 2 가격 적용

  • africa-south1(요하네스버그)
  • asia-east2(홍콩)
  • asia-northeast3(대한민국 서울)
  • asia-southeast1(싱가포르)
  • asia-southeast2 (자카르타)
  • asia-south1(인도 뭄바이)
  • asia-south2(인도 델리)
  • australia-southeast1(시드니)
  • australia-southeast2(멜버른)
  • europe-central2(폴란드 바르샤바)
  • europe-west10(베를린)
  • europe-west12(토리노)
  • europe-west2(영국 런던) 잎 아이콘 낮은 CO2
  • europe-west3(독일 프랑크푸르트) 잎 아이콘 낮은 CO2
  • europe-west6(스위스 취리히) 잎 아이콘 낮은 CO2
  • me-central1(도하)
  • me-central2(담맘)
  • northamerica-northeast1(몬트리올) 잎 아이콘 낮은 CO2
  • northamerica-northeast2(토론토) 잎 아이콘 낮은 CO2
  • southamerica-east1(브라질 상파울루) 잎 아이콘 낮은 CO2
  • southamerica-west1(칠레 산티아고) 잎 아이콘 낮은 CO2
  • us-west2(로스앤젤레스)
  • us-west3(솔트레이크시티)
  • us-west4(라스베이거스)

Cloud Run 서비스를 이미 만들었다면 Google Cloud 콘솔의 Cloud Run 대시보드에서 리전을 확인할 수 있습니다.

코드 구성

새 Cloud Run greeter 서비스를 단계별로 빌드합니다. 이 서비스는 문제해결 연습을 위해 런타임 오류를 일으킵니다.

  1. 새 프로젝트를 만듭니다.

    Node.js

    서비스 패키지, 초기 종속 항목, 일부 공통 작업을 정의하여 Node.js 프로젝트를 만듭니다.

    1. hello-service 디렉터리를 만듭니다.

      mkdir hello-service
      cd hello-service
      
    2. package.json 파일을 생성하여 새 Node.js 프로젝트를 만듭니다.

      npm init --yes
      npm install --save express@4
      
    3. 편집기에서 새 package.json 파일을 열고 node index.js를 실행하도록 start 스크립트를 구성합니다. 완료되면 파일이 다음과 같이 표시됩니다.

      {
        "name": "hello-broken",
        "description": "Broken Cloud Run service for troubleshooting practice",
        "version": "1.0.0",
        "private": true,
        "main": "index.js",
        "scripts": {
          "start": "node index.js",
          "test": "echo \"Error: no test specified\" && exit 0",
          "system-test": "NAME=Cloud c8 mocha -p -j 2 test/system.test.js --timeout=360000 --exit"
        },
        "engines": {
          "node": ">=16.0.0"
        },
        "author": "Google LLC",
        "license": "Apache-2.0",
        "dependencies": {
          "express": "^4.17.1"
        },
        "devDependencies": {
          "c8": "^8.0.0",
          "google-auth-library": "^9.0.0",
          "got": "^11.0.0",
          "mocha": "^10.0.0"
        }
      }
      

    중간 튜토리얼을 넘어서 이 서비스를 계속 발전시키려면 설명, 저자 입력과 라이선스 평가를 고려하세요. 자세한 내용은 package.json 문서를 참조하세요.

    Python

    1. hello-service 디렉터리를 만듭니다.

      mkdir hello-service
      cd hello-service
      
    2. requirements.txt 파일을 만들고 여기에 종속 항목을 복사합니다.

      Flask==3.0.3
      pytest==8.2.0; python_version > "3.0"
      # pin pytest to 4.6.11 for Python2.
      pytest==4.6.11; python_version < "3.0"
      gunicorn==22.0.0
      Werkzeug==3.0.3
      

    Go

    1. hello-service 디렉터리를 만듭니다.

      mkdir hello-service
      cd hello-service
      
    2. go 모듈을 초기화하여 Go 프로젝트를 만듭니다.

      go mod init example.com/hello-service
      

    특정 이름은 원하는 대로 업데이트할 수 있습니다. 코드가 웹에 도달할 수 있는 코드 저장소에 게시된 경우 이름을 업데이트해야 합니다.

    Java

    1. 새 Maven 프로젝트를 만듭니다.

      mvn archetype:generate \
        -DgroupId=com.example.cloudrun \
        -DartifactId=hello-service \
        -DarchetypeArtifactId=maven-archetype-quickstart \
        -DinteractiveMode=false
      
    2. 종속 항목을 pom.xml 종속 항목 목록(<dependencies> 요소 사이)에 복사합니다.

      <dependency>
        <groupId>com.sparkjava</groupId>
        <artifactId>spark-core</artifactId>
        <version>2.9.4</version>
      </dependency>
      <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.0.12</version>
      </dependency>
      <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.12</version>
      </dependency>
      
    3. 빌드 설정을 pom.xml(<dependencies> 요소 아래)에 복사합니다.

      <build>
        <plugins>
          <plugin>
            <groupId>com.google.cloud.tools</groupId>
            <artifactId>jib-maven-plugin</artifactId>
            <version>3.4.0</version>
            <configuration>
              <to>
                <image>gcr.io/PROJECT_ID/hello-service</image>
              </to>
            </configuration>
          </plugin>
        </plugins>
      </build>
      

  2. 새로 추가되는 요청을 처리할 HTTP 서비스를 만듭니다.

    Node.js

    const express = require('express');
    const app = express();
    
    app.get('/', (req, res) => {
      console.log('hello: received request.');
    
      const {NAME} = process.env;
      if (!NAME) {
        // Plain error logs do not appear in Stackdriver Error Reporting.
        console.error('Environment validation failed.');
        console.error(new Error('Missing required server parameter'));
        return res.status(500).send('Internal Server Error');
      }
      res.send(`Hello ${NAME}!`);
    });
    const port = parseInt(process.env.PORT) || 8080;
    app.listen(port, () => {
      console.log(`hello: listening on port ${port}`);
    });

    Python

    import json
    import os
    
    from flask import Flask
    
    
    app = Flask(__name__)
    
    
    @app.route("/", methods=["GET"])
    def index():
        """Example route for testing local troubleshooting.
    
        This route may raise an HTTP 5XX error due to missing environment variable.
        """
        print("hello: received request.")
    
        NAME = os.getenv("NAME")
    
        if not NAME:
            print("Environment validation failed.")
            raise Exception("Missing required service parameter.")
    
        return f"Hello {NAME}"
    
    
    if __name__ == "__main__":
        PORT = int(os.getenv("PORT")) if os.getenv("PORT") else 8080
    
        # This is used when running locally. Gunicorn is used to run the
        # application on Cloud Run. See entrypoint in Dockerfile.
        app.run(host="127.0.0.1", port=PORT, debug=True)

    Go

    
    // Sample hello demonstrates a difficult to troubleshoot service.
    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    )
    
    func main() {
    	log.Print("hello: service started")
    
    	http.HandleFunc("/", helloHandler)
    
    
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    		log.Printf("Defaulting to port %s", port)
    	}
    
    	log.Printf("Listening on port %s", port)
    	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
    }
    
    func helloHandler(w http.ResponseWriter, r *http.Request) {
    	log.Print("hello: received request")
    
    	name := os.Getenv("NAME")
    	if name == "" {
    		log.Printf("Missing required server parameter")
    		// The panic stack trace appears in Cloud Error Reporting.
    		panic("Missing required server parameter")
    	}
    
    	fmt.Fprintf(w, "Hello %s!\n", name)
    }
    

    Java

    import static spark.Spark.get;
    import static spark.Spark.port;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class App {
    
      private static final Logger logger = LoggerFactory.getLogger(App.class);
    
      public static void main(String[] args) {
        int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080"));
        port(port);
    
        get(
            "/",
            (req, res) -> {
              logger.info("Hello: received request.");
              String name = System.getenv("NAME");
              if (name == null) {
                // Standard error logs do not appear in Stackdriver Error Reporting.
                System.err.println("Environment validation failed.");
                String msg = "Missing required server parameter";
                logger.error(msg, new Exception(msg));
                res.status(500);
                return "Internal Server Error";
              }
              res.status(200);
              return String.format("Hello %s!", name);
            });
      }
    }

  3. Dockerfile을 만들어 서비스를 배포하는 데 사용되는 컨테이너 이미지를 정의합니다.

    Node.js

    
    # Use the official lightweight Node.js image.
    # https://hub.docker.com/_/node
    FROM node:20-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 copying both package.json AND package-lock.json (when available).
    # Copying this first prevents re-running npm install on every code change.
    COPY package*.json ./
    
    # Install dependencies.
    # if you need a deterministic and repeatable build create a
    # package-lock.json file and use npm ci:
    # RUN npm ci --omit=dev
    # if you need to include development dependencies during development
    # of your application, use:
    # RUN npm install --dev
    
    RUN npm install --omit=dev
    
    # 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.11
    
    # 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.21-bookworm 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:bookworm-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"]
    

    Java

    이 샘플은 Jib를 사용해서 일반적인 Java 도구로 Docker 이미지를 빌드합니다. Jib는 Dockerfile을 사용하거나 Docker를 설치할 필요 없이 컨테이너 빌드를 최적화합니다. Jib로 자바 컨테이너 빌드에 대해 자세히 알아보세요.

    <plugin>
      <groupId>com.google.cloud.tools</groupId>
      <artifactId>jib-maven-plugin</artifactId>
      <version>3.4.0</version>
      <configuration>
        <to>
          <image>gcr.io/PROJECT_ID/hello-service</image>
        </to>
      </configuration>
    </plugin>
    

코드 제공

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

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

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

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/hello-service

    여기서 PROJECT_ID는 Google Cloud 프로젝트 ID입니다. gcloud config get-value project로 현재 프로젝트 ID를 확인할 수 있습니다.

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

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/hello-service

    여기서 PROJECT_ID는 Google Cloud 프로젝트 ID입니다. gcloud config get-value project로 현재 프로젝트 ID를 확인할 수 있습니다.

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

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/hello-service

    여기서 PROJECT_ID는 Google Cloud 프로젝트 ID입니다. gcloud config get-value project로 현재 프로젝트 ID를 확인할 수 있습니다.

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

    Java

    1. gcloud 사용자 인증 정보 도우미를 사용하여 Docker가 Container Registry로 내보내도록 승인합니다.
      gcloud auth configure-docker
    2. Jib Maven 플러그인을 사용하여 컨테이너를 빌드하고 Container Registry로 내보냅니다.
      mvn compile jib:build -Dimage=gcr.io/PROJECT_ID/hello-service

    여기서 PROJECT_ID는 Google Cloud 프로젝트 ID입니다. gcloud config get-value project로 현재 프로젝트 ID를 확인할 수 있습니다.

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

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

    gcloud run deploy hello-service --image gcr.io/PROJECT_ID/hello-service

    PROJECT_ID를 Google Cloud 프로젝트 ID로 바꿉니다. hello-service는 컨테이너 이미지 이름이자 Cloud Run 서비스의 이름입니다. 컨테이너 이미지는 이전에 gcloud 설정에서 구성한 서비스 및 리전에 배포됩니다.

    인증되지 않은 호출 허용 프롬프트에 y, '예'라고 응답합니다. IAM 기반 인증에 대한 자세한 내용은 액세스 관리를 참조하세요.

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

사용해 보기

성공적으로 배포되었는지 확인하기 위해 서비스를 시도합니다. 요청이 HTTP 500 또는 503 오류(5xx 서버 오류 중 일부)로 실패해야 합니다. 이 튜토리얼에서는 이 오류 응답의 문제 해결 방법을 단계별로 살펴봅니다.

서비스에 탐색 가능한 URL이 자동으로 할당됩니다.

  1. 웹브라우저에서 다음 URL로 이동합니다.

    1. 웹브라우저를 엽니다.

    2. 이전의 배포 명령어로 서비스 URL 출력을 찾습니다.

      deploy 명령어로 URL이 제공되지 않으면 문제가 발생한 것입니다. 오류 메시지를 검토하고 그에 따라 조치를 취합니다. 사용 가능한 지침이 없으면 문제 해결 가이드를 검토하고 배포 명령어를 다시 시도합니다.

    3. 이 URL을 브라우저의 주소 표시줄에 복사하고 Enter 키를 눌러 이동합니다.

  2. HTTP 500 또는 HTTP 503 오류를 확인하세요.

    HTTP 403 오류가 수신되면 배포 프롬프트에서 allow unauthenticated invocations를 거절했을 수 있습니다. 이 문제를 해결하려면 서비스에 인증되지 않은 액세스 권한을 부여합니다.

    gcloud run services add-iam-policy-binding hello-service \
      --member="allUsers" \
      --role="roles/run.invoker"
    

자세한 내용은 공개(인증되지 않은) 액세스 허용을 참조하세요.

문제 조사

사용해 보기에서 위에 표시된 HTTP 5xx 오류가 프로덕션 런타임 오류로 발생한 것을 시각적으로 표시합니다. 이 튜토리얼에서는 이를 처리하기 위한 공식 프로세스를 단계별로 살펴봅니다. 프로덕션 오류 해결 프로세스가 매우 다양하지만 이 튜토리얼에서는 일련의 단계에 따라 유용한 도구와 기법을 적용하는 방법을 보여줍니다.

여기에서는 문제 조사를 위해 다음과 같은 단계를 수행합니다.

  • 추가 조사 지원 및 완화 전략 설정을 위해 보고된 오류에 대한 추가 세부정보를 수집합니다.
  • 수정 사항을 푸시할지 아니면 알려진 정상 버전으로 롤백할지 결정하여 사용자에 대한 영향을 완화시킵니다.
  • 오류를 재현하여 올바른 세부정보가 수집되었으며 오류가 일회성 결함이 아님을 확인합니다.
  • 버그에 대해 근본 원인 분석을 수행하여 오류를 일으킨 코드, 구성, 프로세스를 찾습니다.

조사를 시작할 때는 URL, 타임스탬프, '내부 서버 오류' 메시지로 시작합니다.

추가 세부정보 수집

문제에 대한 추가 정보를 수집하여 발생한 결과를 파악하고 수행할 다음 단계를 결정합니다.

사용 가능한 Google Cloud Observability 도구를 사용하여 세부정보를 수집합니다.

  1. 세부정보가 포함된 대시보드와 인식된 스택 트레이스로 오류 반복 추적 기능을 제공하는 Error Reporting 콘솔을 사용합니다.

    Error Reporting 콘솔로 이동

    &#39;Resolution Status&#39;, Occurrences, Error, &#39;Seen in&#39; 열이 포함된 오류 목록의 스크린샷입니다.
    기록된 오류 목록입니다. 오류는 버전, 서비스, 플랫폼 간의 메시지로 그룹화됩니다.
  2. 스택 트레이스 세부정보를 보려면 오류를 클릭하고 오류 직전의 함수 호출을 확인합니다.

    파싱된 단일 스택 트레이스의 스크린샷으로, 이 오류의 일반적인 프로필을 보여줍니다.
    오류 세부정보 페이지의 '스택 트레이스 샘플'에는 해당 오류의 단일 인스턴스가 표시됩니다. 개별 인스턴스를 검토할 수 있습니다.
  3. Cloud Logging을 사용하여 인식된 오류 스택 트레이스가 없기 때문에 Error Reporting 콘솔에 포함되지 않은 오류 메시지를 포함하여 문제를 일으키는 작업 순서를 검토합니다.

    Cloud Logging 콘솔로 이동

    첫 번째 드롭다운 메뉴에서 Cloud Run 버전 > hello-service를 선택합니다. 이렇게 하면 로그 항목이 서비스에서 생성된 항목으로 필터링됩니다.

Cloud Run에서 로그 보기에 대해 자세히 알아보기

정상 버전으로 롤백

작동 상태로 확인된 기존 서비스의 경우, Cloud Run에 해당 서비스의 이전 버전이 존재합니다. 이 튜토리얼에서는 이전 버전이 없는 새 서비스가 사용되므로, 롤백을 수행할 수 없습니다.

하지만 롤백할 수 있는 이전 버전의 서비스가 있는 경우 버전 세부정보 보기에 따라 서비스의 새 작업 배포를 만드는 데 필요한 컨테이너 이름과 구성 세부정보를 추출합니다.

오류 재현

이전에 가져온 세부정보를 사용하여 테스트 조건에서 문제가 일관적으로 발생하는지 확인합니다.

사용해 보기로 동일한 HTTP 요청을 보내고 동일한 오류와 세부정보가 보고되는지 확인합니다. 오류 세부정보가 표시되는 데 다소 시간이 걸릴 수 있습니다.

이 튜토리얼의 샘플 서비스가 읽기 전용이고 복잡한 부작용을 일으키지 않기 때문에 프로덕션에서 오류를 재현해도 안전합니다. 하지만 많은 실제 서비스의 경우에는 이와 달리, 테스트 환경에서 오류를 재현하거나 지역 조사로만 이 단계를 제한해야 할 수 있습니다.

오류를 재현하면 추가 작업을 위한 컨텍스트가 설정됩니다. 예를 들어 개발자가 오류를 재현할 수 없는 경우 추가 조사를 위해 서비스의 추가 계측이 필요할 수 있습니다.

근본 원인 분석 수행

근본 원인 분석은 효과적인 문제 해결에서 증상 대신 문제 자체를 해결할 수 있게 해주는 중요한 단계입니다.

이 튜토리얼의 앞 부분에서는 Cloud Run에서 문제를 재현하여 서비스가 Cloud Run에서 호스팅될 때 문제가 발생하는지 확인했습니다. 이제는 문제를 로컬로 재현하여 문제가 해당 코드로 격리되는지 또는 프로덕션 호스팅에서만 발생하는지 확인합니다.

  1. 지금까지 Container Registry에서 Docker CLI를 로컬로 사용하지 않았으면 gcloud에서 인증합니다.

    gcloud auth configure-docker

    다른 접근 방식은 Container Registry 인증 방법을 참조하세요.

  2. 가장 최근에 사용한 컨테이너 이미지 이름을 사용할 수 없으면 서비스 설명에 가장 최근에 배포된 컨테이너 이미지의 정보가 포함됩니다.

    gcloud run services describe hello-service

    spec 객체에서 컨테이너 이미지 이름을 찾습니다. 명령어에 대상을 더 많이 지정하면 이를 직접 검색할 수 있습니다.

    gcloud run services describe hello-service \
       --format="value(spec.template.spec.containers.image)"

    이 명령어는 gcr.io/PROJECT_ID/hello-service와 같은 컨테이너 이미지 이름을 보여줍니다.

  3. Container Registry에서 환경으로 컨테이너 이미지를 가져옵니다. 이 단계에서는 컨테이너 이미지를 다운로드하는 데 몇 분 정도 걸릴 수 있습니다.

    docker pull gcr.io/PROJECT_ID/hello-service

    나중에 동일한 명령어를 사용해서 이 이름을 다시 사용하는 컨테이너 이미지 업데이트를 검색할 수 있습니다. 이 단계를 건너뛰면 아래 docker run 명령어가 로컬 머신에 없는 경우 컨테이너 이미지를 가져옵니다.

  4. 로컬로 실행하여 문제가 Cloud Run에 고유한 문제가 아닌지 확인합니다.

    PORT=8080 && docker run --rm -e PORT=$PORT -p 9000:$PORT \
       gcr.io/PROJECT_ID/hello-service

    위 명령어의 각 요소들에 대한 설명은 다음과 같습니다.

    • PORT 환경 변수는 서비스에서 컨테이너 내부에서 리슨할 포트를 결정하는 데 사용됩니다.
    • run 명령어는 Dockerfile 또는 상위 컨테이너 이미지에 정의된 진입점 명령어로 컨테이너를 시작합니다.
    • --rm 플래그는 종료 시 컨테이너 인스턴스를 삭제합니다.
    • -e 플래그는 환경 변수에 값을 할당합니다. -e PORT=$PORT는 로컬 시스템에서 변수 이름이 동일한 컨테이너로 PORT 변수를 전달합니다.
    • -p 플래그는 localhost 포트 9000에서 사용 가능한 서비스로 컨테이너를 게시합니다. localhost:9000에 대한 요청은 포트 8080의 컨테이너로 라우팅됩니다. 즉, 사용 중인 포트 번호에 대한 서비스 출력이 서비스 액세스 방법과 일치하지 않습니다.
    • 마지막 인수인 gcr.io/PROJECT_ID/hello-service는 컨테이너 이미지의 sha256 해시 식별자에 대한 인간이 읽을 수 있는 라벨인 컨테이너 이미지 tag입니다. 로컬에서 사용할 수 없는 경우 docker는 원격 레지스트리에서 이미지를 검색합니다.

    브라우저에서 http://localhost:9000을 엽니다. 터미널 출력에서 {ops_name}}의 오류 메시지와 일치하는 오류 메시지를 확인하세요.

    로컬에서 문제를 재현할 수 없는 경우 Cloud Run 환경에서만 발생할 수 있습니다. 조사할 특정 영역은 Cloud Run 문제 해결 가이드를 참조하세요.

    여기에서는 오류가 로컬로 재현됩니다.

이제 오류가 영구적으로 확인되었으며 호스팅 플랫폼 대신 서비스 코드로 인해 발생했으므로 코드를 더 자세히 조사해야 합니다.

이 튜토리얼에서는 컨테이너 내부의 코드와 로컬 시스템에 있는 코드가 동일하다고 간주해도 안전합니다.

오류 보고서의 스택 트레이스 및 코드 상호 참조를 다시 확인하여 오류가 있는 특정 줄을 찾습니다.

Node.js

index.js 파일에서 로그에 표시된 스택 트레이스 행 번호 주위에서 오류 메시지 소스를 찾습니다.
const {NAME} = process.env;
if (!NAME) {
  // Plain error logs do not appear in Stackdriver Error Reporting.
  console.error('Environment validation failed.');
  console.error(new Error('Missing required server parameter'));
  return res.status(500).send('Internal Server Error');
}

Python

main.py 파일에서 로그에 표시된 스택 트레이스 행 번호 주위에서 오류 메시지 소스를 찾습니다.
NAME = os.getenv("NAME")

if not NAME:
    print("Environment validation failed.")
    raise Exception("Missing required service parameter.")

Go

main.go 파일에서 로그에 표시된 스택 트레이스 행 번호 주위에서 오류 메시지 소스를 찾습니다.

name := os.Getenv("NAME")
if name == "" {
	log.Printf("Missing required server parameter")
	// The panic stack trace appears in Cloud Error Reporting.
	panic("Missing required server parameter")
}

Java

App.java 파일에서 로그에 표시된 스택 트레이스 행 번호 주위에서 오류 메시지 소스를 찾습니다.

String name = System.getenv("NAME");
if (name == null) {
  // Standard error logs do not appear in Stackdriver Error Reporting.
  System.err.println("Environment validation failed.");
  String msg = "Missing required server parameter";
  logger.error(msg, new Exception(msg));
  res.status(500);
  return "Internal Server Error";
}

이 코드를 검사할 때 NAME 환경 변수가 설정되어 있지 않으면 다음 작업이 수행됩니다.

  • Google Cloud Observability에 오류가 기록됩니다.
  • HTTP 오류 응답이 전송됩니다.

이 문제는 누락된 변수로 인해 발생하지만 근본 원인은 더 구체적입니다. 환경 변수에 하드 종속성을 추가하는 코드 변경에 배포 스크립트 및 런타임 요구사항 문서에 대한 변경사항이 포함되지 않았습니다.

근본 원인 해결

이제 코드를 수집하고 잠재적 근본 원인을 파악했으므로 이를 해결하기 위한 단계를 수행할 수 있습니다.

  • NAME 환경 변수를 사용해서 서비스가 로컬로 작동하는지 확인합니다.

    1. 환경 변수를 추가하여 컨테이너를 로컬로 실행합니다.

      PORT=8080 && docker run --rm -e PORT=$PORT -p 9000:$PORT \
       -e NAME="Local World!" \
       gcr.io/PROJECT_ID/hello-service
    2. 브라우저를 http://localhost:9000으로 이동합니다.

    3. 페이지에 'Hello Local World!'가 표시되는지 확인합니다.

  • 다음 변수를 포함하도록 실행 중인 Cloud Run 서비스 환경을 수정합니다.

    1. 서비스 업데이트 명령어를 실행하여 환경 변수를 추가합니다.

      gcloud run services update hello-service \
        --set-env-vars NAME=Override
      
    2. Cloud Run이 새로 추가된 환경 변수를 사용해서 이전 버전을 기준으로 새 버전을 만드는 동안 잠시 기다립니다.

  • 이제 서비스가 수정되었는지 확인합니다.

    1. 브라우저에서 Cloud Run 서비스 URL로 이동합니다.
    2. "Hello Override!"가 페이지에 표시되는지 확인합니다.
    3. Cloud Logging 또는 Error Reporting에 예상치 못한 메시지나 오류가 표시되지 않는지 확인합니다.

이후 문제해결 속도 개선

이 샘플 프로덕션 문제에서 오류는 운영 구성과 관련되어 있습니다. 이후에 이 문제의 영향을 최소화하는 코드 변경사항이 있습니다.

  • 보다 구체적인 세부정보를 포함하도록 오류 로그를 개선합니다.
  • 오류를 반환하는 대신 서비스를 안전한 기본값으로 대체합니다. 기본값을 사용할 때 정상 기능이 변경될 경우, 모니터링 목적으로 경고 메시지를 사용합니다.

이제 하드 종속 항목으로서 NAME 환경 변수 삭제 단계를 수행합니다.

  1. 기존 NAME 처리 코드를 삭제합니다.

    Node.js

    const {NAME} = process.env;
    if (!NAME) {
      // Plain error logs do not appear in Stackdriver Error Reporting.
      console.error('Environment validation failed.');
      console.error(new Error('Missing required server parameter'));
      return res.status(500).send('Internal Server Error');
    }

    Python

    NAME = os.getenv("NAME")
    
    if not NAME:
        print("Environment validation failed.")
        raise Exception("Missing required service parameter.")

    Go

    name := os.Getenv("NAME")
    if name == "" {
    	log.Printf("Missing required server parameter")
    	// The panic stack trace appears in Cloud Error Reporting.
    	panic("Missing required server parameter")
    }

    Java

    String name = System.getenv("NAME");
    if (name == null) {
      // Standard error logs do not appear in Stackdriver Error Reporting.
      System.err.println("Environment validation failed.");
      String msg = "Missing required server parameter";
      logger.error(msg, new Exception(msg));
      res.status(500);
      return "Internal Server Error";
    }

  2. 대체 값을 설정하는 새 코드를 추가합니다.

    Node.js

    const NAME = process.env.NAME || 'World';
    if (!process.env.NAME) {
      console.log(
        JSON.stringify({
          severity: 'WARNING',
          message: `NAME not set, default to '${NAME}'`,
        })
      );
    }

    Python

    NAME = os.getenv("NAME")
    
    if not NAME:
        NAME = "World"
        error_message = {
            "severity": "WARNING",
            "message": f"NAME not set, default to {NAME}",
        }
        print(json.dumps(error_message))

    Go

    name := os.Getenv("NAME")
    if name == "" {
    	name = "World"
    	log.Printf("warning: NAME not set, default to %s", name)
    }

    Java

    String name = System.getenv().getOrDefault("NAME", "World");
    if (System.getenv("NAME") == null) {
      logger.warn(String.format("NAME not set, default to %s", name));
    }

  3. 영향을 받는 구성 사례를 통해 컨테이너를 다시 빌드하고 실행하여 로컬로 테스트합니다.

    Node.js

    docker build --tag gcr.io/PROJECT_ID/hello-service .

    Python

    docker build --tag gcr.io/PROJECT_ID/hello-service .

    Go

    docker build --tag gcr.io/PROJECT_ID/hello-service .

    Java

    mvn compile jib:build

    NAME 환경 변수가 계속 작동하는지 확인합니다.

    PORT=8080 && docker run --rm -e PORT=$PORT -p 9000:$PORT \
     -e NAME="Robust World" \
     gcr.io/PROJECT_ID/hello-service

    NAME 변수 없이 서비스가 작동하는지 확인합니다.

    PORT=8080 && docker run --rm -e PORT=$PORT -p 9000:$PORT \
     gcr.io/PROJECT_ID/hello-service

    서비스가 결과를 반환하지 않으면 첫 번째 단계의 코드 삭제로 응답 작성에 사용되는 것과 같은 추가 행이 삭제되지 않았는지 확인합니다.

  4. 코드 배포 섹션을 다시 확인하여 이를 배포합니다.

    서비스에 배포할 때마다 새 버전이 생성되고 준비가 되면 자동으로 트래픽 제공이 시작됩니다.

    앞에서 설정한 환경 변수를 삭제하려면 다음 안내를 따르세요.

    gcloud run services update hello-service --clear-env-vars

기본값에 대해 새 기능을 서비스의 자동화된 테스트 범위에 추가합니다.

로그에서 다른 문제 찾기

이 서비스의 로그 뷰어에 다른 문제가 표시될 수 있습니다. 예를 들어 지원되지 않는 시스템 호출은 로그에 '컨테이너 샌드박스 제한'으로 표시됩니다.

예를 들어 Node.js 서비스로 인해 다음과 같은 로그 메시지가 표시될 수 있습니다.

Container Sandbox Limitation: Unsupported syscall statx(0xffffff9c,0x3e1ba8e86d88,0x0,0xfff,0x3e1ba8e86970,0x3e1ba8e86a90). Please, refer to https://gvisor.dev/c/linux/amd64/statx for more information.

이 경우 지원 부족은 hello-service 샘플 서비스에 영향을 주지 않습니다.

Terraform 문제 해결

Terraform 관련 문제 해결 또는 질문은 Terraform 정책 유효성 검사 문제 해결을 참조하거나 Terraform 지원에 문의하세요.

삭제

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

프로젝트 삭제

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

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

  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 서비스를 삭제합니다.

    gcloud run services delete SERVICE-NAME

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

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

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

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

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

다음 단계