Published on

GCP Cloud Run 503 해결 - VPC 커넥터·NAT 점검

Authors

서버리스인 Cloud Run에서 503 Service Unavailable은 흔히 “컨테이너가 죽었나?”로 오해되지만, 실제 현장에서는 **VPC 커넥터(Serverless VPC Access)**와 Cloud NAT가 얽힌 네트워크 경로 문제로 발생하는 경우가 많습니다. 특히 다음과 같은 조건이 겹치면 503이 간헐/지속적으로 재현됩니다.

  • Cloud Run이 VPC 커넥터를 통해 사설망으로 나가도록 설정되어 있음
  • 외부 API 호출/패키지 다운로드/DB 접속 등 egress 트래픽이 존재함
  • 서브넷 라우팅, 방화벽, NAT 포트/세션, DNS 경로가 복잡함

이 글은 “Cloud Run 503”을 네트워크 관점에서 쪼개서 진단하는 체크리스트와, 실제로 많이 밟는 해결 루트를 코드/명령어와 함께 제공합니다.

> NAT 비용/트래픽 폭증까지 같이 겪고 있다면 VPC NAT Gateway 비용 폭증 10분 진단·절감도 함께 보면 원인-비용을 한 번에 정리할 수 있습니다.

1) Cloud Run 503의 의미를 먼저 분해하기

Cloud Run에서 보이는 503은 크게 두 범주로 나뉩니다.

1.1 요청이 컨테이너까지 못 가는 503

  • 인그레스/라우팅/리비전/스케일링/인증 계층에서 실패
  • 예: 잘못된 URL, 권한, 리비전 준비 안 됨, 지나친 콜드스타트 등

1.2 컨테이너는 떴지만 “응답을 못 만든” 503

  • 앱이 외부 의존성(DB/API) 호출에서 막혀 타임아웃 → 프록시가 503으로 반환
  • 이때 원인이 VPC 커넥터/NAT/DNS일 가능성이 큽니다.

이 글의 초점은 1.2 유형입니다. 즉 Cloud Run 컨테이너 내부에서 outbound가 막혀서 생기는 503을 목표로 합니다.

2) 가장 먼저 확인할 설정: egress 라우팅 모드

Cloud Run에서 VPC 커넥터를 붙이면 egress는 보통 둘 중 하나입니다.

  • private-ranges-only: RFC1918(사설 대역)만 커넥터로 나감. 인터넷은 기본 경로로 나감(대체로 문제 적음)
  • all-traffic: 모든 egress가 커넥터로 나감. 인터넷도 VPC로 들어가므로 Cloud NAT 또는 프록시가 없으면 외부 통신이 죽습니다.

2.1 gcloud로 현재 설정 확인

gcloud run services describe YOUR_SERVICE \
  --region=YOUR_REGION \
  --format="value(spec.template.metadata.annotations)" | sed 's/,/\n/g'

출력에서 아래 키를 확인합니다.

  • run.googleapis.com/vpc-access-connector
  • run.googleapis.com/vpc-access-egress

만약 vpc-access-egress=all-traffic인데 Cloud NAT가 없다면, 외부 API 호출이 실패하고 앱이 타임아웃 → 503으로 보일 수 있습니다.

2.2 해결 방향

  • 인터넷이 꼭 VPC를 통해 나갈 이유가 없다면 private-ranges-only로 변경
  • 반드시 all-traffic이 필요하다면 Cloud NAT + 라우트 + 방화벽 + DNS를 함께 점검

3) VPC 커넥터에서 가장 많이 터지는 3가지

Serverless VPC Access 커넥터는 “서버리스 ↔ VPC”를 이어주는 다리인데, 아래가 자주 문제를 만듭니다.

3.1 커넥터 서브넷 IP 대역이 너무 작다

커넥터는 내부적으로 연결을 만들며, 동시성/스케일이 커지면 IP 소모가 늘어납니다. 서브넷이 작으면 연결 생성이 실패하거나 지연될 수 있습니다.

  • 증상: 트래픽 증가 시에만 503/타임아웃이 급증
  • 해결: 커넥터 전용 서브넷을 넉넉하게(/28보다 크게) 구성

3.2 커넥터 리전/네트워크 불일치

Cloud Run 서비스 리전과 커넥터 리전이 맞아야 하고, 연결하려는 VPC 네트워크도 정확해야 합니다.

  • 증상: 배포는 되는데 호출 시 불안정, 특정 리비전만 실패
  • 해결: 동일 리전 커넥터 사용, 네트워크/서브넷 재확인

3.3 방화벽(egress/ingress) 착각

Cloud Run은 VPC 내부 VM처럼 “인스턴스 태그”로 방화벽을 붙이는 방식이 아닙니다. 커넥터가 사용하는 범위/경로를 기준으로 방화벽을 설계해야 합니다.

  • 증상: 사설 DB(예: Cloud SQL private IP) 접속 실패 → 앱 타임아웃 → 503
  • 해결: 대상(예: DB 서브넷/포트)에서 커넥터 대역 허용

4) all-traffic이면 Cloud NAT이 사실상 필수인 이유

all-traffic은 인터넷으로 향하는 패킷도 VPC로 들어옵니다. 하지만 VPC의 프라이빗 서브넷은 기본적으로 인터넷으로 나갈 수 없습니다.

이때 인터넷 egress를 만들려면 보통 다음 조합이 필요합니다.

  • Cloud Router
  • Cloud NAT
  • (커넥터가 붙은 VPC의) 올바른 라우팅

4.1 Cloud NAT이 없을 때의 전형적 증상

  • curl https://api.example.com이 컨테이너 내부에서 무한 대기/타임아웃
  • DNS는 되는데 TCP가 안 열리거나, TLS 핸드셰이크에서 멈춤
  • 앱 레벨에서는 “외부 API 장애”처럼 보여서 503으로 귀결

5) 컨테이너 내부에서 네트워크를 재현하는 최소 코드

Cloud Run은 디버깅이 제한적이므로, 헬스/진단 엔드포인트를 임시로 넣어 네트워크를 확인하는 방식이 효과적입니다.

아래는 Node.js(Express) 예시입니다.

import express from "express";
import dns from "node:dns/promises";

const app = express();

app.get("/diag", async (req, res) => {
  const target = req.query.target || "https://www.google.com";
  const host = new URL(target).hostname;

  const result = { target, host };

  try {
    const t0 = Date.now();
    const ips = await dns.resolve(host);
    result.dnsMs = Date.now() - t0;
    result.ips = ips;
  } catch (e) {
    result.dnsError = String(e);
  }

  try {
    const t1 = Date.now();
    const r = await fetch(target, { method: "GET" });
    result.httpMs = Date.now() - t1;
    result.status = r.status;
  } catch (e) {
    result.httpError = String(e);
  }

  res.json(result);
});

app.listen(process.env.PORT || 8080);
  • DNS는 되는데 HTTP가 안 되면: NAT/라우팅/방화벽/포트 고갈 가능성
  • DNS부터 안 되면: Cloud DNS 경로(서버리스 DNS), 프라이빗 DNS 설정, egress 경로를 우선 의심

6) Cloud Logging으로 “503이 어디서 났는지” 분리

Cloud Run 503은 앱 로그만 보면 애매할 수 있습니다. 아래를 같이 봅니다.

6.1 Cloud Run 요청 로그(프록시 레벨)

Log Explorer에서 리소스:

  • Cloud Run Revision

필터 예시:

resource.type="cloud_run_revision"
severity>=ERROR
httpRequest.status=503

여기서 latency가 길게 찍히면, 앱이 외부 의존성에서 오래 기다리다 실패했을 가능성이 큽니다.

6.2 VPC Flow Logs(서브넷)

커넥터가 붙은 VPC 서브넷에 Flow Logs를 켜면, 외부로 나가는 SYN이 드롭되는지/리셋되는지 단서를 얻습니다.

  • connectionDENIED로 나오면 방화벽/라우트
  • 아예 로그가 없다면 커넥터/라우팅이 의심

6.3 Cloud NAT 로그

Cloud NAT 로깅을 켜면 “NAT이 할당됐는지/포트가 부족한지”를 볼 수 있습니다.

  • 포트 고갈/세션 과다 시: 간헐적 503, 특정 시간대 급증
  • 대량 egress가 있다면 비용도 같이 튀기 쉬움 → 위 NAT 비용 글 참고

7) 자주 쓰는 해결 시나리오 4가지

현장에서 가장 많이 쓰는 처방을 “상황 → 조치”로 정리합니다.

7.1 all-traffic인데 인터넷 호출이 필요함

  • 조치: Cloud NAT 구성 + 라우터 연결 + NAT 대상 서브넷 포함
  • 추가: NAT 로그로 포트/세션 상태 확인

7.2 외부 API를 짧은 주기로 많이 호출(포트 고갈)

  • 조치: HTTP keep-alive/커넥션 풀링 적용, 불필요한 새 연결 줄이기
  • 조치: NAT 포트 할당/엔드포인트 수(설계) 재검토

예: Node.js에서 keep-alive를 켜서 NAT 포트 사용량을 줄입니다.

import https from "https";

const agent = new https.Agent({ keepAlive: true, maxSockets: 50 });

const r = await fetch("https://api.example.com", { agent });

7.3 사설 DB(Private IP) 접속만 필요함

  • 조치: egress를 private-ranges-only로 돌려 인터넷은 기본 경로로 나가게 함
  • 조치: DB 방화벽에 커넥터 대역 허용

7.4 DNS가 꼬여서 특정 도메인만 실패

  • 조치: 컨테이너에서 resolve()로 DNS 실패 여부 확인
  • 조치: Private DNS/Cloud DNS 정책이 있는 경우, serverless egress 경로와 충돌 여부 점검

네트워크/DNS가 섞인 타임아웃 문제는 AWS 사례지만 원리(사설망 egress, NAT, DNS)가 유사합니다. 패턴 학습용으로 EKS Pod STS AssumeRole 타임아웃 - NAT·PrivateLink·DNS도 참고할 만합니다.

8) 배포 설정 예시: Cloud Run + VPC 커넥터 + egress

아래는 Cloud Run 서비스를 VPC 커넥터에 붙이고 egress를 설정하는 예시입니다.

gcloud run deploy YOUR_SERVICE \
  --image=REGION-docker.pkg.dev/PROJECT/REPO/IMAGE:TAG \
  --region=YOUR_REGION \
  --vpc-connector=YOUR_CONNECTOR \
  --vpc-egress=all-traffic \
  --set-env-vars=NODE_ENV=production

인터넷 egress가 꼭 필요 없다면:

gcloud run services update YOUR_SERVICE \
  --region=YOUR_REGION \
  --vpc-egress=private-ranges-only

9) 재발 방지 체크리스트

  • egress 모드가 왜 all-traffic인지 근거를 문서화(보안/감사 요구인지)
  • 커넥터 서브넷 크기와 스케일 상한을 트래픽 기준으로 산정
  • Cloud NAT 로깅/모니터링(포트 고갈, 드롭, 에러) 대시보드화
  • 외부 API 호출은 keep-alive/재시도(지수 백오프)/타임아웃을 명시적으로 설계
  • 장애 시 “앱 5xx”와 “네트워크 타임아웃”을 로그로 구분(진단 엔드포인트, 외부 의존성 헬스)

10) 결론: 503을 앱 문제가 아니라 ‘경로’로 보자

Cloud Run 503이 VPC 커넥터/NAT과 관련된 경우, 핵심은 단 하나입니다.

  • 내 요청이 어디로 나가야 하는지(사설/인터넷)
  • 그 경로가 실제로 열려 있는지(라우트/방화벽/NAT/DNS)

특히 all-traffic은 “보안상 깔끔해 보이지만” NAT/포트/비용/관측성까지 함께 책임져야 하는 모드입니다. 503이 보인다면 먼저 egress 설정을 확인하고, 그 다음 NAT 로그와 Flow Logs로 증거를 확보한 뒤, 커넥션 관리(keep-alive)까지 포함해 구조적으로 해결하는 것이 가장 빠릅니다.