새로운 애플리케이션 버전을 Cloud Run에 배포할 때, 기대와 달리 예상치 못한 오류 메시지와 마주하는 답답함은 많은 개발자가 경험해 보았을 것입니다. 원인을 알 수 없는 배포 실패는 개발 흐름을 끊고 귀중한 시간을 앗아갑니다. Cloud Run은 서버리스의 간결함과 컨테이너의 유연성을 제공하지만 높은 수준의 추상화가 때로는 문제의 근본 원인을 파악하기 어렵게 만들기도 합니다.
다행히도 Cloud Run의 경우 별도의 로깅 라이브러리나 에이전트 설정 없이도 기본적으로 연동하는 Cloud Logging을 통해 대부분의 배포 및 런타임 문제의 원인을 밝혀낼 결정적인 단서를 찾을 수 있습니다. 문제 해결의 열쇠는 복잡한 도구를 추가하는 것이 아니라 이미 주어진 도구를 제대로 활용하는 데 있습니다.
이번 포스팅에서는 개발자들이 Cloud Run 환경에서 가장 흔하게 마주치는 세 가지 유형의 오류 시나리오 예로 Cloud Logging을 활용하여 문제를 빠르고 정확하게 해결하는 방법을 알아보겠습니다.
컨테이너 빌드 실패
개발자가 로컬 환경에서 완벽하게 작동하는 코드를 gcloud run deploy –source 명령어를 사용하여 소스 코드에서 직접 배포할 때 명령어 실행 직후 Build failed라는 짧은 메시지와 함께 배포가 실패하는 경우가 있습니다.
예를 들어 볼까요. Go 코드에서 선언되지 않은 변수를 표준 출력으로 내보내려고 시도했다면, 이는 명백한 컴파일 오류입니다. 이 오류의 근본 원인은 컨테이너 이미지를 만드는 단계, 즉 Cloud Build 단계에서 발생합니다. gcloud run deploy –source 명령어는 내부적으로 Cloud Build 서비스를 호출하여 소스 코드를 컨테이너 이미지로 변환합니다. 이 과정에서 컴파일 오류, 의존성 누락, 또는Dockerfile 구문 오류 등이 발생하면 컨테이너 자체가 만들어지지 않아 배포가 중단되는 것입니다.
이 실패는 Cloud Run의 실행 환경이 아니라 격리된 Cloud Build 환경에서 발생한 것입니다. 따라서 가장 먼저 Cloud Build 로그를 확인해야 합니다. gcloud 명령어는 실패 시 터미널에 Cloud Build 로그 페이지로 바로 연결되는 URL을 제공해 줍니다. 로그를 살펴보면 undefined: message와 같이 무엇이 잘못되었는지 명확하게 알려주는 오류 메시지를 즉시 발견할 수 있습니다. 이처럼 빌드 실패 시Cloud Build 로그를 확인하는 것은 문제의 근본 원인을 가장 빠르고 정확하게 파악할 수 있는 첫 번째 디버깅 단계입니다.
즉각적인 해결책은 로그에서 확인된 원인을 수정하고 다시 배포하는 것입니다. 더 중요한 것은 이러한 오류를 근본적으로 예방하는 것입니다. 모든 코드는 원격 저장소에 반영하기 전에 개발자의 로컬 환경에서 최소한의 컴파일 및 단위 테스트를 통과하도록 의무화해야 합니다. 또한, CI/CD 파이프라인의 첫 단계에 린팅(Linting)이나 정적 코드 분석을 통합하여, 빌드를 시작하기도 전에 구문 오류를 자동으로 걸러내는 것이 좋습니다.
포트 불일치로 인한 배포 지연
컨테이너 빌드는 성공했지만 Cloud Run이 새로운 리비전(revision)을 배포하는 과정에서 몇 분간 지연되다가 결국 Revision is not ready and cannot serve traffic라는 메시지와 함께 실패하는 경우가 있습니다.
이 문제의 근본 원인은 애플리케이션이 Cloud Run 컨테이너 런타임 계약을 위반했기 때문입니다. 이 계약의 핵심 규칙 중 하나는 Cloud Run이 지정하는 포트에서 요청을 수신 대기해야 한다는 것입니다. Cloud Run은 컨테이너를 시작할 때 애플리케이션이 수신 대기해야 할 포트 번호를 PORT라는 환경 변수를 통해 동적으로 주입합니다. 이 변수의 기본값은 8080입니다.
만약 애플리케이션이 이 PORT 환경 변수를 무시하고 코드 내에 8085와 같은 특정 포트 번호를 하드코딩했다면 어떻게 될까요? 애플리케이션은 8085번 포트에서 요청을 기다리지만 Cloud Run의 Startup Probe는 8080번 포트로 연결을 시도합니다. 이 연결이 계속 실패하면 Cloud Run 서비스의 전체 Startup Probe 타임아웃(기본값 4분)에 도달하게 됩니다. 개발자가 경험한 길고 답답한 대기 시간의 정체가 바로 이 4분입니다. 타임아웃이 발생하면 Cloud Run은 해당 컨테이너가 비정상이라고 최종 판단하고 배포를 실패로 처리합니다.
이러한 런타임 구성 오류를 진단하는 가장 빠른 방법은 구글 클라우드 콘솔에서 해당 Cloud Run 서비스의 LOGS 탭을 확인하는 것입니다. 이 탭의 가장 큰 장점은 Cloud Logging으로 이동하여 복잡한 쿼리를 작성할 필요 없이 해당 서비스와 관련된 로그만 자동으로 필터링하여 보여준다는 점입니다. LOGS 탭을 확인하면 배포 실패 시점에 The user-provided container failed to start and listen on the port defined by the PORT=8080 environment variable. 같은 명확한 오류 메시지를 발견할 수 있습니다.
이 오류의 해결책은 코드에 포트 번호를 하드코딩하는 대신에 Cloud Run이 환경 변수로 제공하는 PORT 값을 읽어와 동적으로 포트를 설정하도록 수정하는 것입니다. 이렇게 하면 Cloud Run이 어떤 포트를 지정하든 애플리케이션이 유연하게 대응할 수 있습니다. 또한, 포트 번호뿐만 아니라 데이터베이스 연결 문자열, API 키 등 환경에 따라 달라질 수 있는 모든 설정 값은 반드시 환경 변수를 통해 주입받도록 표준화하는 것이 바람직합니다.
예측 불가능한 애플리케이션 충돌
배포에 성공하여 정상 운영 중이던 서비스에서 특정 API 엔드포인트에 접근할 때마다 Service Unavailable 메시지와 함께 HTTP 503 오류가 발생하는 상황은 더욱 골치 아픕니다. 이는 서비스의 특정 기능에 대한 가용성을 상실시켜 직접적인 사용자 경험 저하를 유발합니다.
이 503 오류는 애플리케이션이 직접 반환한 것이 아닙니다. 이는 애플리케이션 코드 내에서 처리되지 않은 예외(unhandled exception), 예를 들어 Go 언어의 패닉이 발생했을 때 나타날 수 있습니다. 패닉이 발생하면 애플리케이션 프로세스가 비정상적으로 종료되고 해당 프로세스를 실행하던 컨테이너 또한 즉시 중지됩니다. Cloud Run은 Liveness Probe 등을 통해 컨테이너의 상태를 지속적으로 감시하다가 컨테이너가 비정상 종료된 것을 감지합니다. 이로 인해 Cloud Run의 프론트엔드 로드 밸런서는 해당 요청을 처리할 건강한 백엔드 인스턴스를 찾지 못해 클라이언트에게 HTTP 503 오류를 반환하는 것입니다.
이 문제 해결의 핵심은 Cloud Run과 Cloud Logging의 강력한 기본 통합 기능에 있습니다. 애플리케이션이 표준 출력(stdout)이나 표준 오류(stderr)로 내보내는 모든 내용은 Cloud Logging이 자동으로 수집합니다. Go 언어에서 패닉이 발생하면 Go 언어는 그 원인이 된 코드 위치와 함수 호출 스택을 담은 전체 stack trace를 기본적으로 표준 오류로 출력합니다. 즉, Cloud Logging이 패닉의 원인을 담은 결정적 증거를 자동으로 수집한다는 의미입니다.
조사의 시작점은 Cloud Logging에서 503 상태 코드 로그를 찾는 것입니다. 이 로그를 기준으로 충돌 직전에 애플리케이션이 표준 출력으로 내보낸 로그가 있는지 살펴보고 이어서 패닉이 발생한 원인과 코드 위치를 알려주는 전체 stack trace를 확인하면 됩니다.
문제 해결책은 stack trace를 통해 확인된 패닉 유발 코드를 수정하고 재배포하는 것입니다. 하지만 장기적으로 안정적인 서비스를 위해서는 방어적 프로그래밍과 견고한 예외 처리 메커니즘을 도입하는 것이 필요합니다. 예를 들어 Go 언어에서는 defer와 recover키워드를 사용한 복구 미들웨어를 구현하여 특정 요청의 패닉이 전체 컨테이너 인스턴스의 비정상 종료로 이어지는 것을 막고 서비스 안정성을 높일 수 있습니다.
로그는 모든 답을 알고 있다
앞서 Cloud Run 배포 및 운영 시 흔히 발생하는 세 가지 오류 시나리오를 살펴보았습니다. 그리고 이 모든 문제의 해결 열쇠가 Cloud Logging 안에 있다는 것을 확인했습니다. 이를 통해 전문 로깅 라이브러리를 도입하지 않더라도 Cloud Run과 Cloud Logging의 기본 통합 기능만으로도 대부분의 일반적인 문제를 신속하고 효율적으로 해결할 수 있다는 것을 알 수 있었습니다. 오류가 발생했을 때 가장 먼저 로그를 살펴보는 습관을 들이고, 각 오류 유형에 따라 어떤 로그를 확인해야 하는지 명확히 인지한다면 디버깅에 소요되는 시간을 극적으로 단축시킬 수 있을 것입니다.