Published on

Cloud Run 503·컨테이너 미기동 원인 7가지

Authors

서버리스처럼 보이지만 Cloud Run 장애의 상당수는 컨테이너 런타임/네트워크/헬스체크/리소스 같은 전통적인 운영 이슈로 귀결됩니다. 특히 “503”은 증상일 뿐이고, 실제 원인은 보통 다음 둘 중 하나입니다.

  • 인스턴스가 준비(ready) 상태가 되지 못함: 컨테이너가 기동에 실패하거나, 포트를 열지 못하거나, 시작 시간이 너무 길거나, OOM 등으로 즉시 죽음
  • 준비는 됐지만 트래픽 라우팅이 실패: 잘못된 포트/프로토콜, 인증/권한, VPC 경로 문제, 동시성/타임아웃 등

이 글은 Cloud Run에서 자주 만나는 503·컨테이너 미기동 원인 7가지를 “어디를 보면 빨리 끝나는지” 중심으로 정리합니다.

> 참고로 503 트러블슈팅은 인그레스/프록시 계층에서도 비슷한 패턴이 반복됩니다. Kubernetes 환경의 503 점검 흐름이 궁금하면 EKS Ingress 503인데 Pod 정상일 때 점검 가이드도 함께 보면 사고 대응 속도가 빨라집니다.

먼저: 3분 안에 하는 1차 분리(로그·이벤트·리비전)

1) 리비전 단위로 실패인지 확인

Cloud Run은 “서비스”가 아니라 리비전이 실제 배포 단위입니다. 503이 나면 “최신 리비전이 트래픽을 받는지”, “해당 리비전이 Ready인지”부터 확인합니다.

# 서비스 상태
gcloud run services describe my-svc --region=asia-northeast3 \
  --format='value(status.url)'

# 리비전 목록(최근 실패 리비전 찾기)
gcloud run revisions list --service=my-svc --region=asia-northeast3

# 특정 리비전 상세(Ready/Conditions 확인)
gcloud run revisions describe my-svc-00042-abc --region=asia-northeast3

2) Cloud Logging에서 “컨테이너 시작 실패”를 먼저 찾기

대부분은 애플리케이션 로그가 아니라 플랫폼 로그(컨테이너 런타임, 헬스체크, 스타트업)에서 단서가 나옵니다.

# 최근 30분, 서비스 기준 로그 필터링
gcloud logging read \
 'resource.type="cloud_run_revision" AND resource.labels.service_name="my-svc"' \
 --limit=200 --freshness=30m --format=json

로그에서 자주 보이는 키워드:

  • Container failed to start / Failed to start and then listen on the port
  • Health check failed / startup probe failed (간접적으로)
  • Killed / OOMKilled (메모리)
  • permission denied (파일/소켓/바이너리 실행)

3) “직접 컨테이너 실행”으로 재현

Cloud Run 문제의 절반은 로컬에서 컨테이너를 돌리면 바로 드러납니다.

# Cloud Run은 기본적으로 PORT 환경변수로 포트를 주입합니다.
docker run --rm -e PORT=8080 -p 8080:8080 gcr.io/PROJECT/IMAGE:TAG

# 기동 직후 health endpoint가 있다면 즉시 확인
curl -v http://localhost:8080/healthz

원인 1) 애플리케이션이 $PORT로 리스닝하지 않음(가장 흔함)

Cloud Run은 컨테이너에 PORT 환경변수를 주고, 그 포트로 리스닝해야 인스턴스가 Ready가 됩니다. 로컬에선 3000으로 띄웠는데 Cloud Run에선 8080을 기대하는 식의 불일치가 대표적입니다.

증상

  • 503
  • 로그에 Failed to start and then listen on the port defined by the PORT environment variable 류 메시지

해결

  • 앱이 process.env.PORT(Node), os.environ["PORT"](Python) 등으로 포트를 읽도록 수정
  • Dockerfile/엔트리포인트에서 하드코딩한 포트 제거

Node.js(Express) 예시

import express from "express";

const app = express();
const port = process.env.PORT || 8080;

app.get("/healthz", (req, res) => res.status(200).send("ok"));

app.listen(port, "0.0.0.0", () => {
  console.log(`listening on ${port}`);
});

Dockerfile에서 EXPOSE는 힌트일 뿐(필수는 아님)

Cloud Run은 EXPOSE를 강제하지 않지만, 팀 내 오해를 줄이려면 맞춰두는 게 좋습니다.

EXPOSE 8080

원인 2) 컨테이너가 시작하자마자 크래시(엔트리포인트/의존성/권한)

컨테이너가 “뜬 것처럼 보였다가” 바로 종료되면 Cloud Run은 인스턴스를 반복 생성하다가 결국 503으로 보입니다.

대표 패턴

  • 엔트리포인트 파일 경로 오타
  • 런타임 의존성 누락(예: libssl, glibc)
  • 실행 권한/유저 권한 문제(예: /app/start.sh: permission denied)

진단 체크

  • Cloud Logging에서 exit code, permission denied, no such file를 확인
  • 로컬에서 동일 이미지 실행 시 즉시 재현되는지 확인
# 엔트리포인트/커맨드 확인
docker image inspect gcr.io/PROJECT/IMAGE:TAG \
  --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}'

# 셸 스크립트 권한/개행 문제 점검
# (CRLF면 실행 실패하는 경우가 많습니다)
file start.sh
ls -al start.sh

해결 팁

  • 스크립트는 chmod +x + LF 개행
  • distroless/alpine 사용 시 필요한 라이브러리 확인
  • 컨테이너 유저가 읽을 수 없는 경로에 로그/소켓을 쓰고 있지 않은지 확인

원인 3) Startup time이 길어 Cloud Run의 준비(Ready) 타임아웃에 걸림

Cloud Run은 인스턴스가 일정 시간 내에 Ready가 되지 않으면 실패로 간주합니다. “DB 마이그레이션을 부팅 시점에 수행”하거나, “외부 API/비밀키 로딩을 동기적으로 기다림” 같은 패턴이 특히 위험합니다.

증상

  • 배포 직후 트래픽이 503
  • 로그가 거의 없거나, 초기화 로그만 찍히고 그 뒤로 없음

해결

  • 부팅 시 무거운 작업 제거(마이그레이션/인덱싱/대용량 로딩)
  • 초기화는 lazy-load 또는 백그라운드로 전환
  • Cloud Run의 request timeout과는 별개로 “기동 준비” 시간을 고려

나쁜 예: 부팅 때 DB 마이그레이션

// 서버 시작 전에 migration을 동기 실행 -> 기동 지연/실패 위험
await runMigrations();
app.listen(port);

좋은 예: 별도 Job/파이프라인로 분리

  • 배포 파이프라인에서 migration을 먼저 수행
  • 또는 Cloud Run Jobs/Cloud Build 단계로 분리

원인 4) 메모리 부족(OOM) 또는 CPU 부족으로 기동 중 사망

Cloud Run은 리소스가 빡빡하면 기동 중 OOMKilled가 나거나, GC/초기화가 길어져 Ready 실패로 이어집니다. Node/Java/Chrome(headless) 같이 메모리 피크가 큰 워크로드에서 흔합니다.

진단

  • 로그에 Killed 또는 OOM 관련 흔적
  • Cloud Monitoring에서 메모리 사용량이 limit에 닿는지 확인

해결

  • Cloud Run 메모리 상향(예: 512Mi → 1Gi)
  • 초기화 단계에서 대용량 캐시/모델 로딩 제거
  • 언어 런타임 옵션 조정(예: Node --max-old-space-size)

리눅스 관점에서 OOM이 어떻게 프로세스를 죽이는지 원리까지 확인하고 싶다면 리눅스 OOM Killer로 프로세스 죽음 진단·방지가 도움이 됩니다.

원인 5) VPC Connector/Private egress 설정 후 외부 네트워크가 막힘

Cloud Run에서 VPC 커넥터를 붙이거나 egress를 all로 바꾸면, 트래픽 경로가 바뀌면서 DNS/외부 인터넷/서드파티 API 호출이 실패할 수 있습니다. 부팅 중 외부 의존성이 있으면 컨테이너가 Ready가 되지 못하고 503으로 보입니다.

흔한 실수

  • egress=all인데 NAT(Cloud NAT) 미구성 → 외부 인터넷 불가
  • 사설 DNS/라우팅 문제로 특정 도메인 resolve 실패

진단

  • 부팅 로그에 ENOTFOUND, ETIMEDOUT, EHOSTUNREACH 등 네트워크 오류
  • (가능하면) 애플리케이션에 “외부 의존성 체크” 로그 추가

Node에서 DNS/HTTP 오류 로그 예시

import fetch from "node-fetch";

async function warmup() {
  try {
    const r = await fetch("https://example.com/health", { timeout: 3000 });
    console.log("warmup status", r.status);
  } catch (e) {
    console.error("warmup failed", e);
  }
}

warmup();

해결

  • 외부 인터넷이 필요하면 Cloud NAT 구성 또는 egress를 필요한 범위로 제한
  • 부팅 시 외부 호출을 필수로 두지 말고, 실패해도 서버는 뜨게 설계(Graceful degradation)

원인 6) 잘못된 헬스 엔드포인트/프로토콜(HTTP/2, gRPC, WebSocket) 오해

Cloud Run은 기본적으로 HTTP 요청을 라우팅하지만, 서비스가 실제로는 gRPC를 기대하거나(HTTP/2), 프록시 뒤에서 특정 헤더/프로토콜을 요구하면 “응답이 이상해서 503처럼 보이는” 케이스가 나옵니다.

진단

  • 브라우저/curl -v로 응답 헤더 확인
  • Cloud Run 로그에 502/503과 함께 upstream reset 류 메시지가 있는지 확인

해결

  • gRPC라면 Cloud Run의 gRPC 지원 방식(HTTP/2)과 클라이언트 설정을 재점검
  • 프레임워크의 바인딩 주소(0.0.0.0)와 포트 확인
  • WebSocket/스트리밍 사용 시 타임아웃/프록시 호환성 점검

네트워크 계층에서 “서버가 끊긴다” 류의 증상 분석은 다른 스택에서도 동일합니다. HTTP 클라이언트 관점의 원인 분류는 Python httpx RemoteProtocolError 서버 끊김 원인과 해결도 참고할 만합니다.

원인 7) 최소 인스턴스 0 + 콜드스타트 + 동시성/타임아웃 설정 부조화

컨테이너가 “안 뜬다”는 제보 중 일부는 실제로는 콜드스타트 지연입니다. 특히 다음 조합에서 체감이 커집니다.

  • min instances = 0
  • 무거운 초기화(빌드된 번들 로딩, JVM warmup, headless 브라우저)
  • concurrency가 너무 높아 첫 인스턴스에 요청이 몰림
  • 클라이언트 타임아웃이 짧아 503/504처럼 보임(중간 프록시/클라이언트가 끊음)

해결

  • 트래픽이 상시 있는 서비스면 min instances를 1 이상으로
  • 초기화 최적화(원인 3과 동일)
  • concurrency를 낮춰 첫 인스턴스 과부하 방지
  • 클라이언트/게이트웨이 타임아웃을 Cloud Run 타임아웃과 정렬

실전 체크리스트: “503 + 컨테이너 미기동”을 15분 안에 끝내는 순서

  1. 리비전 Ready 여부 확인(최신 리비전이 Ready=FALSE면 거의 100% 기동 문제)
  2. Cloud Logging에서 Container failed to start / PORT / permission denied / Killed 키워드 탐색
  3. 동일 이미지를 로컬에서 -e PORT=8080로 실행해 재현
  4. PORT 리스닝/바인딩 주소 0.0.0.0 확인
  5. 부팅 시 외부 의존성(Secrets, DB, 외부 API) 동기 호출 제거
  6. 메모리/CPU 상향 또는 런타임 옵션 조정
  7. VPC 커넥터/egress 변경 이력 확인(Cloud NAT, DNS, 라우팅)

마무리: 503은 “라우팅 실패”가 아니라 “준비 실패”일 때가 더 많다

Cloud Run에서 503을 보면 로드밸런서나 네트워크부터 의심하기 쉽지만, 실제로는 컨테이너가 Ready가 되지 못해 트래픽을 받을 인스턴스가 없는 상태가 더 흔합니다.

가장 빠른 접근은 “리비전 상태 → 플랫폼 로그 → 로컬 재현” 3단 콤보입니다. 이 흐름으로 보면, 위 7가지 원인 중 어디에 속하는지가 금방 좁혀지고(대개 1번 PORT 또는 2번 크래시), 해결도 설정/코드 몇 줄로 끝나는 경우가 많습니다.