- Published on
GCP Cloud Run 503 해결 - VPC 커넥터·NAT 점검
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스인 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-connectorrun.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이 드롭되는지/리셋되는지 단서를 얻습니다.
connection이DENIED로 나오면 방화벽/라우트- 아예 로그가 없다면 커넥터/라우팅이 의심
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)까지 포함해 구조적으로 해결하는 것이 가장 빠릅니다.