Docker와 Kubernetes 쓰면서 진짜 자주 만나는 인프라 장애 4가지와 해결책

Docker와 Kubernetes 쓰면서 진짜 자주 만나는 인프라 장애 4가지와 해결책


인프라를 다루다 보면 “로컬 컨테이너에서는 잘 뜨던 파드(Pod)가 왜 클러스터에만 올리면 CrashLoopBackOff를 뿜으며 무한 재부팅될까?”, “배포 이미지가 왜 1.5GB나 될까?” 같은 문제들로 밤을 지새우곤 합니다.

도커와 쿠버네티스는 컨테이너 오케스트레이션을 혁신적으로 단순화해 주었지만, 런타임 리소스 한계와 헬스체크 설정이 삐끗하면 오히려 디버깅하기 훨씬 까다로운 블랙박스가 되기도 합니다.

이번 글에서는 현업에서 컨테이너와 클라우드 인프라를 운영하면서 실제로 마주치는 대표적인 4대 장애 유형과 이를 깔끔하게 풀어낸 실전 경험을 공유합니다.


1. Docker 이미지 다이어트: 멀티 스테이지 빌드

개발 의존성(TypeScript 컴파일러, 테스트 러너 등)이 최종 프로덕션 이미지에 그대로 포함되면 이미지 크기가 수 기가바이트(GB)로 불어나고 배포 시간도 한없이 늘어납니다.

멀티 스테이지 빌드(Multi-stage Build) 적용 전후

# 1단계: 의존성 설치 및 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 2단계: 가벼운 런타임 배포용 최종 이미지
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# 빌드 산출물과 프로덕션 의존성만 쏙 빼옵니다
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist

USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

빌드 환경과 실행 환경을 물리적으로 분리하는 것만으로 이미지 크기가 1.2GB에서 150MB 수준으로 85% 이상 감량됩니다. 보안 취약점 공격 표면이 줄어드는 것은 덤입니다.


2. Kubernetes Pod 장애: OOMKilled와 CrashLoopBackOff

쿠버네티스 대시보드에서 빨간 불이 들어오는 단골 원인 2가지입니다.

💡 1. Exit Code 137 (OOMKilled)

파드가 갑자기 죽고 재시작 횟수가 늘어난다면 kubectl describe pod <pod-name> 명령을 쳐보세요. 종료 사유에 OOMKilled가 찍혀 있을 확률이 높습니다.

  • 원인: 파드가 resources.limits.memory에 지정된 한계보다 많은 메모리를 쓰려고 해서 리눅스 커널의 OOM 킬러가 프로세스에 SIGKILL(9)을 날린 것입니다. (128 + 9 = 137)
  • 해결책:
    1. 단기적으로는 매니페스트의 메모리 limit 값을 현실적인 수준으로 올려줍니다.
    2. 장기적으로는 힙 덤프나 프로파일러를 붙여 애플리케이션 내 메모리 누수(Memory Leak) 로직을 찾아내야 합니다.

💡 2. CrashLoopBackOff (헬스체크 타임아웃)

프로세스가 떴다 죽기를 반복하는 상태입니다.

  • 자주 놓치는 원인: 컨테이너 내부 애플리케이션이 완전히 부팅되기까지 15초가 걸리는데, 쿠버네티스의 readinessProbe나 livenessProbe가 5초 만에 헬스체크를 찔러보고 “응답 없네? 죽이자!” 하며 컨테이너를 강제로 재부팅시키는 실수입니다.
  • 해결책: initialDelaySeconds나 최신 startupProbe를 도입하여 애플리케이션이 초기화되고 DB 풀을 맺을 때까지 쿠버네티스가 충분히 기다려주도록 설정합니다.

3. GCP Cloud Run 콜드 스타트(Cold Start) 줄이기

서버리스 컨테이너 플랫폼인 Cloud Run은 트래픽이 없을 때 인스턴스를 0으로 줄여 비용을 아껴주지만, 첫 요청이 들어올 때 2~5초가량 멈칫하는 콜드 스타트가 발생합니다.

  • 경량 베이스 이미지 사용: Alpine이나 Distroless 같은 가벼운 이미지를 써서 컨테이너 다운로드 시간을 단축합니다.
  • 최소 인스턴스(Min Instances) 유지: 예산이 허락한다면 프로덕션 환경의 min-instances를 최소 1로 세팅하여 적어도 하나의 웜(Warm) 인스턴스가 상시 대기하도록 만듭니다.
  • 무거운 DB 연결 지연 초기화: 모듈 최상단에서 전역으로 무겁게 맺던 커넥션을 첫 요청이 들어오는 시점으로 미루거나 가볍게 유지합니다.

4. Supabase와 Cloudflare를 엮을 때 RLS 보안 챙기기

Next.js나 모바일 앱에서 백엔드로 Supabase를 쓰고 DNS로 Cloudflare를 물리는 조합이 요즘 대세입니다. 이때 보안에서 가장 중요한 것은 **Row Level Security (RLS)**입니다.

  • RLS 켜기: 테이블을 만들고 RLS를 활성화하지 않으면, 공개 익명 키(anon key)를 가진 누구나 브라우저 개발자 도구에서 supabase.from('users').select('*')를 쳐서 전 회원의 데이터를 긁어갈 수 있습니다.
  • 보안 정책(Policy) 명시:
    -- 오직 로그인한 본인의 데이터만 조회/수정할 수 있도록 정책 강제
    ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
    
    CREATE POLICY "Users can only view own profile" 
    ON user_profiles 
    FOR SELECT 
    USING (auth.uid() = user_id);

인프라가 아무리 탄탄해도 데이터베이스 보안 룰 하나가 비어있으면 순식간에 정보 유출 사고로 이어집니다. RLS는 항상 “기본 차단, 명시적 허용” 원칙을 유지하세요.

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.