Pod 완벽 마스터
Pod의 핵심 개념과 실전 활용법
학습 항목
이미지 로딩 중...
Kubernetes Pod 관리 완벽 가이드
Kubernetes의 가장 기본이 되는 Pod의 개념부터 실전 운영까지, 초급 개발자를 위한 친절한 가이드입니다. Pod 생성, 관리, 디버깅, 그리고 실무에서 꼭 알아야 할 베스트 프랙티스를 모두 담았습니다.
목차
- Pod 기본 개념
- Pod 생성과 YAML 작성
- Multi-Container Pod
- Pod 생명주기와 상태 관리
- Resources와 Limits
- Health Check 설정
- Environment Variables
- Volume 마운트
- Pod 디버깅
- Pod 업데이트 전략
1. Pod 기본 개념
시작하며
여러분이 Docker 컨테이너로 애플리케이션을 개발했는데, 이걸 실제 운영 환경에서 수백 개, 수천 개로 확장해야 한다면 어떻게 하시겠어요? 각 컨테이너를 일일이 시작하고, 모니터링하고, 문제가 생기면 재시작하는 작업을 손으로 할 수는 없겠죠.
이런 문제는 실제 개발 현장에서 매일 발생합니다. 컨테이너 하나는 관리하기 쉽지만, 수십 개의 서비스가 각각 여러 개의 컨테이너로 실행되면 복잡도가 기하급수적으로 증가합니다.
자동 복구, 로드 밸런싱, 스케일링까지 고려하면 더욱 어려워집니다. 바로 이럴 때 필요한 것이 Kubernetes Pod입니다.
Pod는 Kubernetes에서 배포할 수 있는 가장 작은 단위로, 하나 이상의 컨테이너를 묶어서 관리할 수 있게 해줍니다.
개요
간단히 말해서, Pod는 하나 이상의 컨테이너를 담는 그릇입니다. Kubernetes에서 컨테이너를 직접 실행하는 것이 아니라, 항상 Pod 안에서 실행됩니다.
왜 굳이 Pod라는 개념이 필요할까요? 실무에서는 하나의 애플리케이션이 여러 개의 밀접하게 연관된 프로세스로 구성되는 경우가 많습니다.
예를 들어, 메인 웹 애플리케이션과 로그 수집기, 또는 애플리케이션과 프록시 서버가 항상 함께 실행되어야 하는 경우가 있습니다. 기존에는 이런 컨테이너들을 각각 관리하고 네트워크로 연결해야 했다면, 이제는 하나의 Pod로 묶어서 동일한 네트워크와 스토리지를 공유하며 함께 스케줄링할 수 있습니다.
Pod의 핵심 특징은 다음과 같습니다: 첫째, Pod 내의 모든 컨테이너는 같은 노드에 배치됩니다. 둘째, 같은 네트워크 네임스페이스를 공유하여 localhost로 통신할 수 있습니다.
셋째, 같은 볼륨을 공유할 수 있습니다. 이러한 특징들이 밀접하게 결합된 애플리케이션 구성 요소를 효율적으로 관리할 수 있게 해줍니다.
코드 예제
# 가장 간단한 Pod 정의 예제
apiVersion: v1
kind: Pod
metadata:
name: my-first-pod
labels:
app: web
spec:
containers:
- name: nginx-container
image: nginx:1.21
ports:
- containerPort: 80
protocol: TCP
설명
이것이 하는 일: 위 YAML 파일은 nginx 웹 서버를 실행하는 가장 기본적인 Pod를 정의합니다. 이 파일 하나로 Kubernetes 클러스터에 컨테이너를 배포할 수 있습니다.
첫 번째로, metadata 섹션은 Pod의 이름과 라벨을 정의합니다. name은 Pod를 식별하는 고유한 이름이고, labels는 나중에 이 Pod를 찾거나 그룹화할 때 사용하는 키-값 쌍입니다.
예를 들어, "app: web" 라벨을 가진 모든 Pod를 한 번에 조회할 수 있습니다. 그 다음으로, spec.containers 부분이 실행되면서 실제 컨테이너를 정의합니다.
image 필드는 Docker Hub에서 가져올 이미지를 지정하고, ports는 컨테이너가 사용할 포트를 선언합니다. containerPort는 컨테이너 내부에서 애플리케이션이 리스닝하는 포트를 의미합니다.
마지막으로, 이 YAML을 kubectl apply -f 명령으로 적용하면 Kubernetes가 자동으로 적절한 노드를 선택하고, 이미지를 다운로드하며, 컨테이너를 시작합니다. Pod가 실패하면 자동으로 재시작되고, 노드에 문제가 생기면 다른 노드로 이동됩니다.
여러분이 이 코드를 사용하면 수동으로 서버에 접속해서 Docker 명령을 실행할 필요가 없습니다. 선언적 방식으로 원하는 상태를 정의하면, Kubernetes가 알아서 그 상태를 유지해줍니다.
또한 버전 관리 시스템에 YAML 파일을 저장하여 인프라를 코드로 관리할 수 있고, 팀원들과 쉽게 공유할 수 있습니다.
실전 팁
💡 Pod 이름은 DNS 호환 형식이어야 합니다. 소문자, 숫자, 하이픈(-)만 사용하고, 하이픈으로 시작하거나 끝나지 않도록 주의하세요.
💡 labels는 나중에 Service나 Deployment에서 Pod를 선택할 때 필수적입니다. 일관된 라벨링 전략을 팀 내에서 정하는 것이 중요합니다.
💡 운영 환경에서는 Pod를 직접 생성하기보다 Deployment나 StatefulSet을 사용하는 것이 좋습니다. Pod는 일회성이고 재생성되지 않지만, Deployment는 자동으로 Pod를 재생성해줍니다.
💡 컨테이너 이미지에 :latest 태그보다는 구체적인 버전 번호를 사용하세요. 예측 가능한 배포와 롤백을 위해 필수적입니다.
💡 kubectl get pods 명령으로 Pod 상태를 확인하고, kubectl describe pod <pod-name>으로 상세 정보와 이벤트를 볼 수 있습니다.
2. Pod 생성과 YAML 작성
시작하며
여러분이 처음 Kubernetes를 접하면 "YAML 파일을 어떻게 작성해야 하지?"라는 고민에 빠지게 됩니다. 공식 문서를 보면 수십 가지 필드가 있고, 뭐가 필수고 뭐가 선택인지 헷갈리기 시작합니다.
이런 혼란은 자연스러운 것입니다. Kubernetes YAML은 매우 유연하고 강력하지만, 그만큼 처음 접하는 사람에게는 복잡해 보입니다.
잘못된 들여쓰기 하나로 에러가 나기도 하고, 필수 필드를 빠뜨려서 Pod가 시작되지 않을 수도 있습니다. 바로 이럴 때 필요한 것이 체계적인 YAML 작성 방법입니다.
Pod YAML의 기본 구조를 이해하고, 자주 사용하는 패턴을 익히면 훨씬 쉽게 작성할 수 있습니다.
개요
간단히 말해서, Pod YAML은 네 가지 최상위 필드로 구성됩니다: apiVersion, kind, metadata, spec입니다. 이 구조는 모든 Kubernetes 리소스에 공통적으로 적용됩니다.
왜 YAML 형식을 사용할까요? YAML은 사람이 읽고 쓰기 쉬운 데이터 직렬화 형식으로, 계층 구조를 명확하게 표현할 수 있습니다.
JSON도 가능하지만, YAML이 주석을 지원하고 가독성이 더 좋아서 선호됩니다. 예를 들어, 복잡한 설정을 팀원에게 전달할 때 주석으로 설명을 추가할 수 있어 매우 유용합니다.
기존에는 긴 커맨드라인 옵션으로 컨테이너를 실행했다면, 이제는 YAML 파일 하나로 모든 설정을 관리할 수 있습니다. 또한 Git으로 버전 관리하고, 리뷰하고, 롤백할 수 있습니다.
YAML 작성의 핵심 규칙은 다음과 같습니다: 첫째, 들여쓰기는 반드시 스페이스 2칸을 사용합니다(탭 금지). 둘째, 리스트는 하이픈(-)으로 표시합니다.
셋째, 키-값 쌍은 콜론(:) 다음에 반드시 스페이스를 넣습니다. 이러한 규칙을 지키지 않으면 파싱 에러가 발생합니다.
코드 예제
apiVersion: v1
kind: Pod
metadata:
name: web-app-pod
labels:
app: web
tier: frontend
environment: production
annotations:
description: "Main web application pod"
spec:
containers:
- name: web-app
image: myapp:v1.2.3
ports:
- containerPort: 8080
name: http
env:
- name: DATABASE_URL
value: "postgresql://db:5432/mydb"
설명
이것이 하는 일: 위 YAML은 프로덕션 환경에서 실행될 웹 애플리케이션 Pod를 정의합니다. 라벨, 어노테이션, 환경 변수 등 실무에서 자주 사용하는 설정들을 포함하고 있습니다.
첫 번째로, apiVersion과 kind는 이 리소스의 타입을 지정합니다. Pod는 핵심 API에 속하므로 v1을 사용합니다.
만약 Deployment를 만든다면 apps/v1을 사용하게 됩니다. kubectl api-resources 명령으로 모든 리소스 타입과 API 버전을 확인할 수 있습니다.
그 다음으로, metadata 섹션이 Pod의 메타데이터를 정의합니다. labels는 Service나 Deployment가 이 Pod를 선택할 때 사용하는 중요한 식별자입니다.
예를 들어, "app: web"인 모든 Pod에 트래픽을 분산하도록 Service를 설정할 수 있습니다. annotations는 라벨과 비슷하지만, 선택자로 사용되지 않고 설명이나 도구 설정 같은 임의의 메타데이터를 저장합니다.
마지막으로, spec.containers 배열이 실제 컨테이너를 정의합니다. 각 컨테이너는 고유한 이름이 필요하고, image는 정확한 버전 태그를 포함해야 합니다.
ports의 name 필드는 선택사항이지만, Service에서 포트를 참조할 때 유용합니다. env는 환경 변수를 주입하는데, 실무에서는 보안을 위해 Secret을 사용하는 것이 좋습니다.
여러분이 이 코드를 사용하면 일관된 형식으로 Pod를 정의할 수 있고, 팀 내에서 같은 패턴을 공유할 수 있습니다. labels와 annotations를 잘 활용하면 수백 개의 Pod를 효율적으로 관리하고 모니터링할 수 있습니다.
또한 환경 변수를 통해 같은 이미지를 다른 환경(개발, 스테이징, 프로덕션)에서 다른 설정으로 실행할 수 있습니다.
실전 팁
💡 YAML 작성 시 온라인 YAML 검증 도구나 IDE 플러그인(VS Code의 Kubernetes 확장)을 사용하면 문법 오류를 미리 발견할 수 있습니다.
💡 kubectl create 명령으로 기본 YAML 템플릿을 생성할 수 있습니다: kubectl create pod test --image=nginx --dry-run=client -o yaml > pod.yaml
💡 labels는 계층적으로 구성하세요. app, component, tier, environment 같은 표준 라벨을 사용하면 나중에 필터링과 그룹화가 쉽습니다.
💡 주석을 활용하여 복잡한 설정의 이유를 설명하세요. 몇 달 후에 다시 봤을 때도 쉽게 이해할 수 있습니다.
💡 kubectl apply -f <file> --dry-run=client로 실제 적용 전에 YAML이 유효한지 검증할 수 있습니다.
3. Multi-Container Pod
시작하며
여러분이 웹 애플리케이션을 운영하면서 "로그를 수집해서 중앙 서버로 보내야 하는데, 어떻게 해야 하지?"라고 고민한 적 있나요? 메인 애플리케이션 코드에 로그 전송 로직을 넣는 것은 관심사의 분리 원칙에 어긋나고, 별도의 서버로 실행하면 네트워크 설정이 복잡해집니다.
이런 문제는 마이크로서비스 아키텍처에서 특히 흔합니다. 하나의 주요 기능과 여러 개의 보조 기능(로깅, 모니터링, 프록시 등)이 긴밀하게 협력해야 하는 경우가 많습니다.
각각을 별도의 Pod로 만들면 관리가 복잡하고, 네트워크 레이턴시도 증가합니다. 바로 이럴 때 필요한 것이 Multi-Container Pod입니다.
하나의 Pod에 여러 컨테이너를 넣어 localhost로 통신하고, 같은 볼륨을 공유하며, 항상 함께 스케줄링되도록 할 수 있습니다.
개요
간단히 말해서, Multi-Container Pod는 하나의 Pod에 두 개 이상의 컨테이너를 실행하는 패턴입니다. 이 컨테이너들은 같은 네트워크 네임스페이스를 공유하므로 localhost로 통신할 수 있습니다.
왜 이런 패턴이 필요할까요? 실무에서는 사이드카(Sidecar), 앰배서더(Ambassador), 어댑터(Adapter) 같은 디자인 패턴을 구현할 때 자주 사용됩니다.
예를 들어, 메인 애플리케이션 옆에 로그 수집 사이드카를 두거나, SSL 종료를 처리하는 프록시 컨테이너를 함께 실행하는 경우입니다. 기존에는 이런 기능들을 별도의 서비스로 분리하거나 메인 애플리케이션에 포함시켜야 했다면, 이제는 관심사를 분리하면서도 긴밀하게 통합할 수 있습니다.
Multi-Container Pod의 핵심 특징은 다음과 같습니다: 첫째, 모든 컨테이너가 같은 IP 주소를 공유합니다. 둘째, 포트 충돌을 피해야 합니다(한 컨테이너가 80번 포트를 쓰면 다른 컨테이너는 사용 불가).
셋째, emptyDir 볼륨으로 파일 시스템을 공유할 수 있습니다. 이러한 특징들이 복잡한 애플리케이션 패턴을 우아하게 구현할 수 있게 해줍니다.
코드 예제
apiVersion: v1
kind: Pod
metadata:
name: web-with-sidecar
spec:
# 공유 볼륨 정의
volumes:
- name: shared-logs
emptyDir: {}
containers:
# 메인 애플리케이션 컨테이너
- name: web-app
image: nginx:1.21
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
# 로그 수집 사이드카 컨테이너
- name: log-collector
image: fluent/fluent-bit:1.8
volumeMounts:
- name: shared-logs
mountPath: /logs
설명
이것이 하는 일: 위 YAML은 nginx 웹 서버와 Fluent Bit 로그 수집기를 하나의 Pod에서 실행합니다. nginx가 생성하는 로그를 Fluent Bit가 수집하여 중앙 로그 서버로 전송하는 사이드카 패턴입니다.
첫 번째로, volumes 섹션에서 shared-logs라는 emptyDir 볼륨을 정의합니다. emptyDir은 Pod가 생성될 때 빈 디렉토리로 시작하고, Pod가 삭제되면 함께 사라지는 임시 스토리지입니다.
이 볼륨은 같은 Pod의 모든 컨테이너가 접근할 수 있어, 컨테이너 간 파일 공유에 완벽합니다. 그 다음으로, web-app 컨테이너가 /var/log/nginx 경로에 shared-logs 볼륨을 마운트합니다.
nginx는 기본적으로 이 디렉토리에 access.log와 error.log를 생성합니다. 동시에 log-collector 컨테이너도 같은 볼륨을 /logs 경로에 마운트하여 nginx가 생성하는 로그 파일을 실시간으로 읽을 수 있습니다.
마지막으로, 두 컨테이너는 같은 네트워크 네임스페이스를 공유하므로, 필요하다면 localhost:80으로 nginx에 접근할 수 있습니다. Kubernetes는 이 두 컨테이너를 항상 같은 노드에 배치하고, 둘 중 하나라도 실패하면 Pod 전체를 재시작합니다.
이렇게 생명주기가 함께 관리됩니다. 여러분이 이 코드를 사용하면 애플리케이션 코드를 수정하지 않고도 로그 수집, 모니터링, 프록시 같은 인프라 기능을 추가할 수 있습니다.
각 컨테이너는 단일 책임 원칙을 따르면서도 긴밀하게 협력합니다. 또한 로그 수집기를 업그레이드할 때 애플리케이션 이미지는 그대로 두고 사이드카만 교체하면 됩니다.
실전 팁
💡 Init Container를 사용하면 메인 컨테이너가 시작되기 전에 초기화 작업을 수행할 수 있습니다. 예를 들어, 설정 파일 다운로드나 데이터베이스 마이그레이션 등에 활용하세요.
💡 컨테이너 간 통신이 필요하면 localhost를 사용하되, 포트 번호는 환경 변수로 전달하여 유연성을 높이세요.
💡 사이드카 컨테이너의 리소스 제한을 별도로 설정하세요. 메인 컨테이너가 사이드카 때문에 리소스 부족을 겪지 않도록 해야 합니다.
💡 로그는 stdout/stderr로 출력하는 것이 Kubernetes 권장사항이지만, 레거시 애플리케이션이 파일 로그를 사용한다면 사이드카 패턴이 좋은 해결책입니다.
💡 여러 컨테이너가 같은 볼륨에 쓰기를 한다면 파일 잠금과 동시성 문제를 고려해야 합니다.
4. Pod 생명주기와 상태 관리
시작하며
여러분이 kubectl get pods를 실행했을 때 "Pending", "Running", "CrashLoopBackOff" 같은 상태를 본 적 있나요? 처음에는 이 상태들이 무슨 의미인지, 왜 Pod가 Running 상태로 가지 않는지 헷갈립니다.
이런 혼란은 Pod의 생명주기를 이해하지 못해서 발생합니다. Pod는 생성부터 종료까지 여러 단계를 거치며, 각 단계마다 다른 상태를 가집니다.
문제가 생겼을 때 현재 어느 단계에서 막혔는지 알아야 적절한 조치를 취할 수 있습니다. 바로 이럴 때 필요한 것이 Pod 생명주기에 대한 깊은 이해입니다.
각 상태의 의미와 전이 조건을 알면, 문제를 빠르게 진단하고 해결할 수 있습니다.
개요
간단히 말해서, Pod는 Pending → Running → Succeeded/Failed 단계로 진행됩니다. 각 단계는 Pod가 현재 무엇을 하고 있는지, 무엇을 기다리고 있는지를 나타냅니다.
왜 이런 상태 관리가 필요할까요? Kubernetes는 선언적 시스템으로, 여러분이 원하는 상태를 선언하면 시스템이 현재 상태를 원하는 상태로 맞춰갑니다.
이 과정에서 Pod가 어느 단계에 있는지 추적해야 하고, 문제가 있으면 적절히 대응해야 합니다. 예를 들어, 이미지를 다운로드 중이라면 Pending, 컨테이너가 실행 중이라면 Running, 애플리케이션이 정상 종료되었다면 Succeeded 상태가 됩니다.
기존에는 프로세스 관리자가 간단한 상태만 제공했다면, 이제는 훨씬 세밀한 상태 정보와 조건(Conditions)을 제공하여 정확한 진단이 가능합니다. Pod 상태의 핵심 개념은 다음과 같습니다: 첫째, Phase는 Pod의 전체적인 생명주기 단계입니다.
둘째, Conditions는 PodScheduled, Initialized, ContainersReady, Ready 같은 세부 조건들입니다. 셋째, ContainerStatus는 각 컨테이너의 상태를 개별적으로 추적합니다.
이러한 정보들이 종합적으로 Pod의 건강 상태를 알려줍니다.
코드 예제
# Pod 상태 확인 스크립트
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
containers:
- name: app
image: busybox:1.34
command: ['sh', '-c', 'echo Starting...; sleep 30; echo Done']
# 컨테이너 생명주기 훅
lifecycle:
postStart:
exec:
command: ['/bin/sh', '-c', 'echo PostStart hook executed']
preStop:
exec:
command: ['/bin/sh', '-c', 'echo PreStop hook; sleep 5']
restartPolicy: OnFailure
설명
이것이 하는 일: 위 YAML은 Pod 생명주기를 보여주는 간단한 예제입니다. 컨테이너가 시작하고 종료할 때 실행되는 훅(hook)을 정의하여, 생명주기 이벤트를 추적할 수 있습니다.
첫 번째로, command는 컨테이너가 시작될 때 실행할 명령을 지정합니다. 이 예제에서는 메시지를 출력하고 30초 대기한 후 종료합니다.
이 과정에서 Pod는 Pending(스케줄링 및 이미지 풀링) → Running(실행 중) → Succeeded(정상 종료) 상태로 전이합니다. 그 다음으로, lifecycle.postStart 훅이 컨테이너의 메인 프로세스가 시작된 직후 실행됩니다.
주의할 점은 이 훅이 비동기적으로 실행되므로, 훅이 완료되기 전에 메인 프로세스가 시작될 수 있다는 것입니다. 이 훅은 초기화 작업이나 알림 전송에 유용합니다.
lifecycle.preStop 훅은 컨테이너가 종료되기 직전에 실행됩니다. 이 예제에서는 5초 동안 대기하는데, 이런 패턴은 graceful shutdown을 구현할 때 사용됩니다.
예를 들어, 현재 처리 중인 요청을 완료하고 새 요청은 받지 않도록 할 수 있습니다. 마지막으로, restartPolicy는 컨테이너가 종료되었을 때 어떻게 할지 결정합니다.
OnFailure는 비정상 종료(exit code != 0)일 때만 재시작하고, Always는 항상 재시작하며, Never는 재시작하지 않습니다. Job 같은 배치 작업에는 OnFailure나 Never가 적합합니다.
여러분이 이 코드를 사용하면 애플리케이션의 시작과 종료를 세밀하게 제어할 수 있습니다. postStart 훅으로 설정 파일 검증이나 캐시 워밍을 수행하고, preStop 훅으로 graceful shutdown을 구현하여 데이터 손실을 방지할 수 있습니다.
또한 kubectl describe pod 명령으로 각 생명주기 이벤트의 타임라인을 확인하여 성능 병목을 찾을 수 있습니다.
실전 팁
💡 kubectl get pods --watch로 Pod 상태 변화를 실시간으로 모니터링할 수 있습니다. 디버깅할 때 매우 유용합니다.
💡 CrashLoopBackOff는 컨테이너가 반복적으로 실패한다는 의미입니다. kubectl logs <pod> --previous로 이전 컨테이너의 로그를 확인하세요.
💡 ImagePullBackOff는 이미지를 가져올 수 없다는 뜻입니다. 이미지 이름, 태그, 레지스트리 인증을 확인하세요.
💡 preStop 훅의 실행 시간은 terminationGracePeriodSeconds(기본 30초)에 포함됩니다. 시간이 부족하면 강제 종료됩니다.
💡 Init Container는 메인 컨테이너보다 먼저 순차적으로 실행되며, 모두 성공해야 메인 컨테이너가 시작됩니다. 의존성 체크나 초기화에 활용하세요.
5. Resources와 Limits
시작하며
여러분이 Pod를 배포했는데 갑자기 노드 전체가 느려지거나, 반대로 Pod가 메모리 부족으로 강제 종료된 경험이 있나요? 리소스 관리를 제대로 하지 않으면 이런 문제가 자주 발생합니다.
이런 문제는 멀티테넌트 환경에서 특히 심각합니다. 한 Pod가 CPU나 메모리를 독점하면 같은 노드의 다른 Pod들이 영향을 받습니다.
반대로 너무 적은 리소스를 할당하면 애플리케이션이 제대로 동작하지 않습니다. 바로 이럴 때 필요한 것이 적절한 Resources와 Limits 설정입니다.
requests로 최소 필요 리소스를 보장하고, limits로 최대 사용량을 제한하여 안정적인 운영이 가능합니다.
개요
간단히 말해서, resources.requests는 Pod가 필요로 하는 최소 리소스이고, resources.limits는 Pod가 사용할 수 있는 최대 리소스입니다. Kubernetes 스케줄러는 requests를 기반으로 Pod를 배치합니다.
왜 두 가지 값이 필요할까요? requests는 스케줄링 결정과 리소스 보장에 사용됩니다.
노드에 충분한 requests만큼의 여유가 있어야 Pod가 배치될 수 있습니다. limits는 실행 중인 컨테이너가 과도하게 리소스를 소비하는 것을 막습니다.
예를 들어, 메모리 누수가 있는 애플리케이션이 전체 노드의 메모리를 소진하는 것을 방지할 수 있습니다. 기존에는 각 애플리케이션이 필요한 만큼 리소스를 사용했다면, 이제는 명시적으로 선언하고 제한하여 예측 가능한 성능과 안정성을 확보할 수 있습니다.
리소스 관리의 핵심 개념은 다음과 같습니다: 첫째, CPU는 압축 가능한(compressible) 리소스로, limit을 초과하면 throttling됩니다. 둘째, 메모리는 압축 불가능한 리소스로, limit을 초과하면 Pod가 OOMKilled됩니다.
셋째, QoS 클래스(Guaranteed, Burstable, BestEffort)는 리소스 설정에 따라 자동으로 결정됩니다. 이러한 메커니즘들이 공정하고 효율적인 리소스 분배를 가능하게 합니다.
코드 예제
apiVersion: v1
kind: Pod
metadata:
name: resource-demo
spec:
containers:
- name: web-app
image: nginx:1.21
# 리소스 요청과 제한 설정
resources:
requests:
# CPU: 0.25 코어 보장
cpu: "250m"
# 메모리: 256MB 보장
memory: "256Mi"
limits:
# CPU: 최대 0.5 코어까지 사용 가능
cpu: "500m"
# 메모리: 최대 512MB까지 사용 가능
memory: "512Mi"
설명
이것이 하는 일: 위 YAML은 nginx Pod에 CPU와 메모리 리소스를 할당합니다. 최소 250m CPU와 256Mi 메모리를 보장하고, 최대 500m CPU와 512Mi 메모리까지 사용할 수 있도록 설정합니다.
첫 번째로, CPU requests "250m"은 0.25 CPU 코어를 의미합니다(m은 밀리코어, 1000m = 1 코어). Kubernetes 스케줄러는 이 Pod를 배치할 때 최소 0.25 코어의 여유가 있는 노드를 선택합니다.
이 값은 스케줄링 결정에만 사용되며, 실제로 CPU를 예약하지는 않습니다. 그 다음으로, CPU limits "500m"은 이 컨테이너가 최대 0.5 코어까지 사용할 수 있다는 뜻입니다.
만약 컨테이너가 더 많은 CPU를 사용하려 하면, 커널의 CFS(Completely Fair Scheduler)가 throttling하여 사용량을 제한합니다. 이는 다른 Pod에 영향을 주지 않으면서도 burst 성능을 허용하는 좋은 방법입니다.
메모리 requests "256Mi"는 256 메비바이트를 보장합니다(Mi는 메비바이트, 1Mi = 1024 * 1024 bytes). 메모리 limits "512Mi"를 초과하면, 리눅스 OOM Killer가 컨테이너를 종료시킵니다.
이때 Pod는 OOMKilled 상태가 되고, restartPolicy에 따라 재시작됩니다. 마지막으로, 이 설정으로 Pod는 Burstable QoS 클래스를 받습니다(requests < limits인 경우).
노드에 리소스 압박이 있을 때, Kubernetes는 BestEffort → Burstable → Guaranteed 순서로 Pod를 축출합니다. Guaranteed QoS를 받으려면 requests와 limits를 동일하게 설정하고, 모든 컨테이너에 명시해야 합니다.
여러분이 이 코드를 사용하면 안정적인 애플리케이션 성능을 보장할 수 있습니다. 과도한 리소스 할당으로 노드 자원을 낭비하지 않으면서도, 필요할 때는 burst 성능을 활용할 수 있습니다.
또한 메모리 누수가 있어도 전체 노드가 다운되지 않고 해당 Pod만 재시작됩니다.
실전 팁
💡 프로덕션에서는 반드시 requests와 limits를 설정하세요. 설정하지 않으면 BestEffort QoS를 받아 가장 먼저 축출됩니다.
💡 CPU limits를 너무 낮게 설정하면 애플리케이션 시작 시간이 느려질 수 있습니다. 특히 Java 같은 JVM 애플리케이션은 시작할 때 CPU를 많이 사용합니다.
💡 메모리 limits는 신중하게 설정하세요. 너무 낮으면 OOMKilled가 자주 발생하고, 너무 높으면 노드 리소스를 낭비합니다.
💡 kubectl top pod <pod-name>으로 실제 리소스 사용량을 모니터링하고, 이를 기반으로 적절한 값을 찾으세요(Metrics Server 필요).
💡 LimitRange와 ResourceQuota를 네임스페이스에 설정하여 팀 차원에서 리소스 관리 정책을 강제할 수 있습니다.
6. Health Check 설정
시작하며
여러분이 배포한 Pod가 Running 상태인데도 실제로는 요청을 처리하지 못하는 경우를 경험한 적 있나요? 애플리케이션이 시작은 되었지만 데이터베이스 연결에 실패했거나, 내부적으로 데드락에 빠진 경우입니다.
이런 상황은 매우 흔합니다. 컨테이너 프로세스가 실행 중이라고 해서 애플리케이션이 정상적으로 동작한다는 보장은 없습니다.
사용자는 타임아웃이나 에러를 경험하지만, Kubernetes는 Pod가 건강하다고 판단하여 아무 조치도 취하지 않습니다. 바로 이럴 때 필요한 것이 Health Check입니다.
Liveness Probe로 애플리케이션이 살아있는지 확인하고, Readiness Probe로 트래픽을 받을 준비가 되었는지 검증하며, Startup Probe로 느린 시작을 처리할 수 있습니다.
개요
간단히 말해서, Probe는 컨테이너의 상태를 주기적으로 검사하는 메커니즘입니다. Liveness는 재시작 여부를, Readiness는 트래픽 전송 여부를, Startup은 초기화 완료 여부를 결정합니다.
왜 세 가지 Probe가 필요할까요? 각각 다른 목적을 가지고 있기 때문입니다.
Liveness Probe는 애플리케이션이 데드락이나 무한 루프에 빠졌을 때 재시작하여 복구합니다. Readiness Probe는 일시적으로 부하가 높거나 외부 의존성에 문제가 있을 때 트래픽을 차단하여 사용자 경험을 보호합니다.
Startup Probe는 시작이 느린 레거시 애플리케이션이 초기화 중에 Liveness 체크로 죽지 않도록 보호합니다. 기존에는 외부 모니터링 도구로 애플리케이션을 감시했다면, 이제는 Kubernetes에 내장된 메커니즘으로 자동 복구와 트래픽 관리가 가능합니다.
Health Check의 핵심 개념은 다음과 같습니다: 첫째, HTTP GET, TCP Socket, Exec 세 가지 체크 방식이 있습니다. 둘째, initialDelaySeconds로 시작 지연을, periodSeconds로 체크 주기를 설정합니다.
셋째, failureThreshold만큼 연속 실패하면 액션을 취합니다. 이러한 설정들이 애플리케이션의 특성에 맞는 정교한 헬스 체크를 가능하게 합니다.
코드 예제
apiVersion: v1
kind: Pod
metadata:
name: healthcheck-demo
spec:
containers:
- name: web-app
image: myapp:v1.0
ports:
- containerPort: 8080
# Startup Probe: 애플리케이션 시작 확인
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30 # 30번 실패까지 허용
periodSeconds: 10 # 10초마다 체크
# Liveness Probe: 애플리케이션이 살아있는지 확인
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 3
periodSeconds: 3
# Readiness Probe: 트래픽 받을 준비가 되었는지 확인
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
설명
이것이 하는 일: 위 YAML은 웹 애플리케이션에 대한 포괄적인 헬스 체크를 설정합니다. 시작 확인, 생존 확인, 준비 확인을 각각 다른 엔드포인트와 설정으로 구현합니다.
첫 번째로, startupProbe가 애플리케이션 시작을 감지합니다. 이 Probe가 성공할 때까지 Liveness와 Readiness Probe는 비활성화됩니다.
failureThreshold 30 * periodSeconds 10 = 최대 300초(5분) 동안 시작을 기다립니다. 이는 데이터베이스 마이그레이션이나 캐시 워밍 같은 긴 초기화 과정을 가진 애플리케이션에 필수적입니다.
그 다음으로, livenessProbe가 3초마다 /healthz 엔드포인트를 호출합니다. 이 엔드포인트는 애플리케이션의 핵심 기능이 동작하는지 확인해야 합니다.
예를 들어, 내부 상태를 체크하거나 간단한 쿼리를 실행합니다. 연속으로 failureThreshold(기본 3)번 실패하면, Kubernetes가 컨테이너를 재시작합니다.
readinessProbe는 /ready 엔드포인트를 5초마다 체크합니다. 이 엔드포인트는 더 엄격한 검사를 수행합니다.
예를 들어, 데이터베이스 연결, 캐시 연결, 의존 서비스의 가용성을 모두 확인합니다. 실패하면 이 Pod는 Service의 엔드포인트에서 제거되어 트래픽을 받지 않습니다.
다시 성공하면 자동으로 엔드포인트에 추가됩니다. 마지막으로, 각 Probe는 HTTP 200-399 응답을 성공으로 간주합니다.
타임아웃(기본 1초)이나 연결 실패도 실패로 처리됩니다. 애플리케이션은 이런 Probe 엔드포인트를 가볍고 빠르게 구현해야 합니다.
복잡한 로직이나 외부 API 호출은 피하세요. 여러분이 이 코드를 사용하면 자동 복구와 무중단 배포가 가능합니다.
애플리케이션이 응답하지 않으면 자동으로 재시작되고, 배포 중에는 준비된 Pod만 트래픽을 받아 사용자 경험이 보호됩니다. 또한 롤링 업데이트 시 새 버전이 준비될 때까지 이전 버전이 트래픽을 처리하여 다운타임이 없습니다.
실전 팁
💡 Liveness Probe는 재시작으로 복구 가능한 문제만 체크하세요. 외부 의존성(DB, API) 문제는 재시작해도 해결되지 않으므로 Readiness에서만 체크합니다.
💡 Probe 타임아웃은 기본 1초로 짧습니다. 느린 애플리케이션은 timeoutSeconds를 늘리세요. 하지만 너무 길면 문제 감지가 늦어집니다.
💡 initialDelaySeconds를 너무 짧게 설정하면 아직 시작하지 않은 애플리케이션이 실패 처리됩니다. 애플리케이션의 평균 시작 시간보다 약간 길게 설정하세요.
💡 /healthz와 /ready 엔드포인트는 인증을 요구하지 않아야 합니다. kubelet이 호출하므로 인증 토큰을 전달할 방법이 없습니다.
💡 TCP Socket Probe는 HTTP를 지원하지 않는 애플리케이션(예: Redis, MySQL)에 유용합니다. Exec Probe는 최후의 수단으로, 성능 오버헤드가 있습니다.
7. Environment Variables
시작하며
여러분이 같은 애플리케이션을 개발, 스테이징, 프로덕션 환경에서 실행해야 할 때 어떻게 하나요? 각 환경마다 다른 이미지를 빌드하는 것은 비효율적이고, 하드코딩된 설정은 보안 문제를 일으킵니다.
이런 문제는 12 Factor App 방법론에서도 강조하는 중요한 주제입니다. 코드는 환경에 독립적이어야 하고, 설정은 환경 변수로 주입되어야 합니다.
그래야 동일한 아티팩트를 모든 환경에서 사용할 수 있습니다. 바로 이럴 때 필요한 것이 Environment Variables와 ConfigMap/Secret입니다.
환경에 따라 다른 설정을 외부에서 주입하여, 코드 변경 없이 다양한 환경에서 실행할 수 있습니다.
개요
간단히 말해서, 환경 변수는 컨테이너 실행 시 주입되는 키-값 쌍입니다. Kubernetes는 직접 값을 지정하거나, ConfigMap이나 Secret에서 값을 가져오는 여러 방법을 제공합니다.
왜 환경 변수를 사용할까요? 첫째, 설정과 코드를 분리하여 동일한 이미지를 여러 환경에서 재사용할 수 있습니다.
둘째, 민감한 정보(API 키, 비밀번호)를 이미지에 포함시키지 않아 보안이 향상됩니다. 예를 들어, 데이터베이스 URL, API 엔드포인트, 로그 레벨 같은 설정을 환경마다 다르게 주입할 수 있습니다.
기존에는 설정 파일을 이미지에 포함시키거나 볼륨으로 마운트했다면, 이제는 Kubernetes의 네이티브 메커니즘으로 더 쉽고 안전하게 관리할 수 있습니다. 환경 변수 주입의 핵심 패턴은 다음과 같습니다: 첫째, 단순 값은 env.value로 직접 지정합니다.
둘째, ConfigMap의 값은 valueFrom.configMapKeyRef로 참조합니다. 셋째, Secret의 값은 valueFrom.secretKeyRef로 참조합니다.
넷째, envFrom으로 전체 ConfigMap/Secret을 한 번에 주입할 수 있습니다. 이러한 패턴들이 유연하고 안전한 설정 관리를 가능하게 합니다.
코드 예제
apiVersion: v1
kind: Pod
metadata:
name: env-demo
spec:
containers:
- name: app
image: myapp:v1.0
env:
# 직접 값 지정
- name: ENVIRONMENT
value: "production"
# ConfigMap에서 값 가져오기
- name: DATABASE_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: db.host
# Secret에서 값 가져오기
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db.password
# ConfigMap 전체를 환경 변수로 주입
envFrom:
- configMapRef:
name: app-config
설명
이것이 하는 일: 위 YAML은 다양한 방법으로 환경 변수를 컨테이너에 주입합니다. 직접 값, ConfigMap 참조, Secret 참조를 모두 사용하여 실무에서 자주 쓰이는 패턴을 보여줍니다.
첫 번째로, env.value는 가장 간단한 방법으로 하드코딩된 값을 주입합니다. "ENVIRONMENT=production"처럼 명확한 값은 이 방식이 적합합니다.
하지만 민감한 정보나 환경마다 다른 값은 이 방법을 피해야 합니다. YAML 파일이 Git에 저장되므로 보안 위험이 있습니다.
그 다음으로, configMapKeyRef는 ConfigMap에서 특정 키의 값을 가져옵니다. 먼저 kubectl create configmap app-config --from-literal=db.host=postgres.example.com 같은 명령으로 ConfigMap을 생성해야 합니다.
이 방법의 장점은 설정을 중앙화하고, 여러 Pod가 같은 ConfigMap을 공유할 수 있다는 것입니다. secretKeyRef는 민감한 정보를 안전하게 주입합니다.
Secret은 base64로 인코딩되고(암호화는 아님), RBAC으로 접근을 제한할 수 있으며, etcd 암호화를 활성화하면 저장 시 암호화됩니다. kubectl create secret generic app-secrets --from-literal=db.password=mypassword로 생성합니다.
주의할 점은 base64는 암호화가 아니므로, Secret을 Git에 저장하면 안 됩니다. 마지막으로, envFrom.configMapRef는 ConfigMap의 모든 키-값 쌍을 환경 변수로 주입합니다.
예를 들어, ConfigMap에 LOG_LEVEL=info, MAX_CONNECTIONS=100이 있으면, 컨테이너에 두 환경 변수가 모두 생성됩니다. 이는 많은 설정을 한 번에 주입할 때 편리하지만, 키 이름 충돌에 주의해야 합니다.
여러분이 이 코드를 사용하면 환경별로 다른 이미지를 빌드할 필요가 없습니다. 동일한 이미지를 dev, staging, prod에서 사용하고, ConfigMap과 Secret만 다르게 설정하면 됩니다.
또한 설정을 변경할 때 이미지를 다시 빌드하지 않아도 되고, ConfigMap을 업데이트하고 Pod를 재시작하면 됩니다.
실전 팁
💡 Secret은 base64 인코딩일 뿐 암호화가 아닙니다. 진짜 암호화가 필요하면 etcd 암호화를 활성화하거나, Sealed Secrets, External Secrets 같은 도구를 사용하세요.
💡 ConfigMap과 Secret을 변경해도 이미 실행 중인 Pod의 환경 변수는 업데이트되지 않습니다. Pod를 재시작해야 합니다. 파일로 마운트하면 자동 업데이트됩니다.
💡 envFrom 사용 시 키 이름이 유효한 환경 변수 이름인지 확인하세요. 점(.)이나 하이픈(-)은 언더스코어(_)로 변환되거나 무시됩니다.
💡 애플리케이션 코드에서 환경 변수가 없을 때의 기본값을 설정하여, 설정 누락으로 인한 크래시를 방지하세요.
💡 Kustomize나 Helm을 사용하면 환경별 ConfigMap/Secret을 더 쉽게 관리할 수 있습니다. 템플릿과 오버레이로 중복을 줄이세요.
8. Volume 마운트
시작하며
여러분이 컨테이너에서 생성한 파일이 Pod가 재시작되면 사라진다는 사실을 알고 계신가요? 컨테이너의 파일 시스템은 ephemeral(일시적)이어서, 로그나 데이터베이스 파일 같은 중요한 데이터를 저장할 수 없습니다.
이런 문제는 스테이트풀 애플리케이션을 운영할 때 치명적입니다. 데이터베이스가 재시작되면 모든 데이터가 사라지고, 업로드된 파일도 유실됩니다.
또한 Multi-Container Pod에서 컨테이너 간 데이터를 공유할 방법도 필요합니다. 바로 이럴 때 필요한 것이 Volume입니다.
emptyDir로 임시 공유 스토리지를, PersistentVolume로 영속적인 데이터 저장을, hostPath로 노드 파일 시스템 접근을 구현할 수 있습니다.
개요
간단히 말해서, Volume은 Pod의 생명주기와 독립적인 스토리지입니다. 컨테이너가 재시작되어도 Volume의 데이터는 유지되고, 같은 Pod의 여러 컨테이너가 공유할 수 있습니다.
왜 Volume이 필요할까요? 첫째, 데이터 영속성이 필요한 애플리케이션(데이터베이스, 파일 스토리지)을 실행할 수 있습니다.
둘째, 컨테이너 간 데이터 공유가 가능합니다(사이드카 패턴). 셋째, 설정 파일이나 시크릿을 파일로 주입할 수 있습니다.
예를 들어, nginx 설정 파일을 ConfigMap에서 Volume으로 마운트하여 동적으로 변경할 수 있습니다. 기존에는 외부 스토리지를 수동으로 마운트하거나, 데이터를 외부로 복사했다면, 이제는 Kubernetes가 자동으로 스토리지를 프로비저닝하고 관리합니다.
Volume의 핵심 타입은 다음과 같습니다: 첫째, emptyDir은 Pod 생명주기 동안만 존재하는 임시 스토리지입니다. 둘째, hostPath는 노드의 파일 시스템을 직접 마운트합니다(보안 위험, 운영 환경에서 비추천).
셋째, PersistentVolumeClaim은 영속적인 스토리지를 요청합니다. 넷째, configMap과 secret은 설정을 파일로 마운트합니다.
이러한 타입들이 다양한 스토리지 요구사항을 충족시킵니다.
코드 예제
apiVersion: v1
kind: Pod
metadata:
name: volume-demo
spec:
# Volume 정의
volumes:
# 임시 공유 스토리지
- name: shared-data
emptyDir: {}
# ConfigMap을 파일로 마운트
- name: config-volume
configMap:
name: app-config
# Secret을 파일로 마운트
- name: secret-volume
secret:
secretName: app-secrets
containers:
- name: app
image: myapp:v1.0
volumeMounts:
# 공유 데이터 마운트
- name: shared-data
mountPath: /data
# 설정 파일 마운트 (읽기 전용)
- name: config-volume
mountPath: /etc/config
readOnly: true
# 시크릿 파일 마운트 (읽기 전용)
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
설명
이것이 하는 일: 위 YAML은 여러 타입의 Volume을 Pod에 마운트합니다. 임시 데이터 공유, 설정 파일 주입, 시크릿 파일 주입을 모두 보여주는 실용적인 예제입니다.
첫 번째로, volumes 섹션에서 사용할 Volume들을 정의합니다. shared-data는 emptyDir 타입으로, Pod가 생성될 때 빈 디렉토리가 만들어지고 Pod가 삭제되면 함께 사라집니다.
기본적으로 노드의 디스크를 사용하지만, emptyDir: { medium: "Memory" }로 설정하면 tmpfs(RAM)를 사용하여 더 빠른 I/O가 가능합니다. 그 다음으로, config-volume은 app-config ConfigMap을 Volume으로 변환합니다.
ConfigMap의 각 키가 파일 이름이 되고, 값이 파일 내용이 됩니다. 예를 들어, ConfigMap에 "app.conf: server { listen 80; }"가 있으면, /etc/config/app.conf 파일이 생성됩니다.
이 Volume은 ConfigMap이 업데이트되면 자동으로 갱신됩니다(최대 60초 지연). secret-volume도 비슷하게 동작하지만, Secret의 내용을 마운트합니다.
Secret은 민감한 정보를 포함하므로, readOnly: true로 마운트하여 컨테이너가 수정하지 못하도록 합니다. 또한 Secret Volume은 tmpfs에 저장되어 디스크에 기록되지 않습니다.
마지막으로, volumeMounts에서 정의한 Volume을 컨테이너의 파일 시스템에 마운트합니다. mountPath는 컨테이너 내부 경로이고, name은 volumes에서 정의한 이름과 일치해야 합니다.
한 Volume을 여러 컨테이너에 서로 다른 경로로 마운트할 수도 있습니다. 여러분이 이 코드를 사용하면 설정을 코드와 분리하여 관리할 수 있습니다.
ConfigMap을 업데이트하면 파일이 자동으로 갱신되고(환경 변수는 안 됨), 애플리케이션이 설정을 다시 로드할 수 있습니다. 또한 민감한 정보를 환경 변수 대신 파일로 주입하여, 프로세스 목록에 노출되지 않도록 할 수 있습니다.
실전 팁
💡 emptyDir은 Pod 재시작 시 데이터가 유지되지만, Pod가 삭제되면 사라집니다. 영속적인 데이터는 PersistentVolumeClaim을 사용하세요.
💡 ConfigMap/Secret을 Volume으로 마운트할 때, subPath를 사용하면 특정 키만 특정 파일로 마운트할 수 있습니다. 디렉토리 전체를 덮어쓰지 않아도 됩니다.
💡 hostPath는 노드에 종속되어 Pod가 다른 노드로 이동하면 데이터에 접근할 수 없습니다. 테스트나 시스템 접근 용도로만 사용하세요.
💡 PersistentVolumeClaim을 사용하면 스토리지를 동적으로 프로비저닝할 수 있습니다. StorageClass를 지정하여 SSD, HDD, NFS 등 다양한 백엔드를 선택하세요.
💡 Volume 마운트 시 기존 디렉토리 내용이 가려집니다. 이미지의 /etc/config에 파일이 있어도, Volume을 마운트하면 보이지 않습니다.
9. Pod 디버깅
시작하며
여러분이 배포한 Pod가 CrashLoopBackOff 상태에 빠졌거나, Running인데 요청을 처리하지 못할 때 어떻게 디버깅하시나요? 처음에는 어디서부터 시작해야 할지 막막합니다.
이런 상황은 매우 자주 발생합니다. 로컬에서는 잘 동작하던 애플리케이션이 Kubernetes에서는 실패하고, 에러 메시지도 명확하지 않습니다.
로그를 보려고 해도 컨테이너가 계속 재시작되어 로그가 사라집니다. 바로 이럴 때 필요한 것이 체계적인 디버깅 기법입니다.
kubectl의 다양한 명령어와 옵션을 활용하여, 문제를 빠르게 진단하고 해결할 수 있습니다.
개요
간단히 말해서, Pod 디버깅은 상태 확인 → 로그 조회 → 이벤트 분석 → 컨테이너 접속의 순서로 진행됩니다. 각 단계마다 kubectl이 제공하는 강력한 명령어들을 사용합니다.
왜 체계적인 접근이 필요할까요? Pod 문제는 다양한 원인이 있습니다.
이미지 풀링 실패, 리소스 부족, 설정 오류, 애플리케이션 버그, 네트워크 문제 등입니다. 각 원인마다 다른 증상과 해결 방법이 있으므로, 단계적으로 좁혀가야 합니다.
예를 들어, ImagePullBackOff는 이미지 이름이나 인증 문제이고, OOMKilled는 메모리 부족입니다. 기존에는 서버에 직접 SSH로 접속해서 로그 파일을 찾았다면, 이제는 kubectl 명령어로 원격에서 모든 정보에 접근할 수 있습니다.
디버깅의 핵심 도구는 다음과 같습니다: 첫째, kubectl get pods는 Pod 상태를 빠르게 확인합니다. 둘째, kubectl describe pod는 상세 정보와 이벤트를 보여줍니다.
셋째, kubectl logs는 컨테이너의 stdout/stderr를 확인합니다. 넷째, kubectl exec는 실행 중인 컨테이너에 접속합니다.
다섯째, kubectl debug는 문제 있는 Pod를 복제하여 안전하게 디버깅합니다. 이러한 도구들이 빠른 문제 해결을 가능하게 합니다.
코드 예제
# Pod 상태 확인
kubectl get pods
kubectl get pods -o wide # 노드와 IP 정보 포함
# 상세 정보와 이벤트 확인
kubectl describe pod <pod-name>
# 로그 확인
kubectl logs <pod-name>
kubectl logs <pod-name> -c <container-name> # 특정 컨테이너
kubectl logs <pod-name> --previous # 이전 컨테이너 로그 (재시작된 경우)
kubectl logs <pod-name> --tail=100 # 마지막 100줄
kubectl logs <pod-name> -f # 실시간 스트리밍
# 컨테이너 접속
kubectl exec -it <pod-name> -- /bin/sh
kubectl exec -it <pod-name> -c <container-name> -- /bin/bash
# 임시 디버그 컨테이너 실행
kubectl debug <pod-name> -it --image=busybox --target=<container-name>
# 리소스 사용량 확인
kubectl top pod <pod-name>
설명
이것이 하는 일: 위 명령어들은 Pod 문제를 진단하고 해결하는 전체 워크플로우를 보여줍니다. 간단한 상태 확인부터 심층 디버깅까지 단계적으로 접근합니다.
첫 번째로, kubectl get pods로 Pod의 현재 상태를 확인합니다. STATUS 컬럼에서 Pending, Running, CrashLoopBackOff, ImagePullBackOff 같은 상태를 볼 수 있습니다.
RESTARTS 컬럼은 컨테이너가 몇 번 재시작되었는지 보여주는데, 숫자가 계속 증가하면 Liveness Probe 실패나 애플리케이션 크래시를 의미합니다. 그 다음으로, kubectl describe pod는 가장 유용한 디버깅 명령어입니다.
출력의 하단에 있는 Events 섹션에서 Pod의 전체 생명주기 이벤트를 시간순으로 볼 수 있습니다. "Failed to pull image", "Back-off restarting failed container", "Insufficient memory" 같은 메시지가 문제의 근본 원인을 알려줍니다.
kubectl logs는 애플리케이션의 표준 출력과 에러를 보여줍니다. --previous 옵션이 특히 중요한데, 컨테이너가 재시작되면 현재 로그는 비어있지만 이전 컨테이너의 로그에 크래시 원인이 남아있습니다.
-f 옵션으로 실시간 로그를 보면서 요청을 테스트할 수 있습니다. kubectl exec는 실행 중인 컨테이너 안에 쉘로 접속합니다.
설정 파일을 확인하거나, 네트워크 연결을 테스트하거나, 프로세스를 확인할 수 있습니다. 예를 들어, exec로 접속해서 curl localhost:8080으로 애플리케이션이 로컬에서는 응답하는지 확인할 수 있습니다.
마지막으로, kubectl debug는 Kubernetes 1.23+에서 사용 가능한 강력한 도구입니다. 문제 있는 Pod를 복제하고, 다른 이미지(busybox, alpine)로 디버그 컨테이너를 추가하여, 원본 Pod를 건드리지 않고 안전하게 조사할 수 있습니다.
여러분이 이 명령어들을 사용하면 대부분의 Pod 문제를 스스로 해결할 수 있습니다. describe로 빠르게 원인을 파악하고, logs로 애플리케이션 에러를 확인하며, exec로 직접 조사하여 정확한 해결책을 찾을 수 있습니다.
또한 -o yaml이나 -o json으로 전체 Pod 정의를 추출하여, 설정 오류를 찾을 수 있습니다.
실전 팁
💡 kubectl get events --sort-by='.lastTimestamp'로 클러스터 전체 이벤트를 시간순으로 볼 수 있습니다. 여러 Pod의 문제를 한눈에 파악할 때 유용합니다.
💡 컨테이너 이미지에 쉘이 없으면 exec가 실패합니다. 이런 경우 kubectl debug로 다른 이미지의 디버그 컨테이너를 추가하세요.
💡 kubectl get pod <pod-name> -o yaml로 실행 중인 Pod의 전체 정의를 확인할 수 있습니다. 환경 변수, 볼륨 마운트 등 모든 정보를 볼 수 있습니다.
💡 stern이나 kubetail 같은 서드파티 도구를 사용하면 여러 Pod의 로그를 동시에 스트리밍할 수 있습니다.
💡 Pending 상태가 지속되면 kubectl describe node로 노드 리소스를 확인하세요. CPU나 메모리 부족으로 스케줄링되지 못할 수 있습니다.
10. Pod 업데이트 전략
시작하며
여러분이 운영 중인 서비스를 새 버전으로 업데이트해야 할 때, 다운타임 없이 어떻게 배포하시나요? 모든 Pod를 한 번에 교체하면 서비스가 중단되고, 하나씩 수동으로 교체하면 시간이 오래 걸립니다.
이런 문제는 고가용성 서비스에서 매우 중요합니다. 사용자는 24/7 서비스를 기대하고, 배포 중에도 중단 없이 요청을 처리해야 합니다.
또한 새 버전에 문제가 있으면 빠르게 롤백할 수 있어야 합니다. 바로 이럴 때 필요한 것이 롤링 업데이트와 블루-그린 배포 같은 업데이트 전략입니다.
Deployment를 사용하면 Kubernetes가 자동으로 무중단 배포를 수행하고, 문제 발생 시 롤백도 쉽게 할 수 있습니다.
개요
간단히 말해서, 롤링 업데이트는 Pod를 점진적으로 새 버전으로 교체하는 방식입니다. 일부 Pod는 새 버전을 실행하고, 일부는 이전 버전을 실행하면서, 전체 서비스는 계속 가용합니다.
왜 Deployment를 사용할까요? Pod를 직접 생성하면 업데이트가 어렵습니다.
Deployment는 ReplicaSet을 관리하고, 선언적 업데이트, 롤백, 스케일링을 자동으로 처리합니다. 예를 들어, 이미지 버전을 v1에서 v2로 변경하면, Deployment가 새 ReplicaSet을 만들고 점진적으로 트래픽을 이동시킵니다.
기존에는 블루-그린 배포를 수동으로 구현하거나, 로드 밸런서를 직접 조작했다면, 이제는 Kubernetes가 자동으로 처리합니다. 업데이트 전략의 핵심 개념은 다음과 같습니다: 첫째, RollingUpdate 전략은 maxUnavailable과 maxSurge로 속도를 조절합니다.
둘째, Recreate 전략은 모든 Pod를 먼저 삭제하고 새로 생성합니다(다운타임 발생). 셋째, Readiness Probe가 성공해야 새 Pod가 트래픽을 받습니다.
넷째, kubectl rollout 명령으로 배포를 제어하고 롤백합니다. 이러한 메커니즘들이 안전하고 예측 가능한 배포를 가능하게 합니다.
코드 예제
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
# 동시에 업데이트할 수 있는 최대 Pod 수
maxUnavailable: 1
# 원하는 Pod 수를 초과할 수 있는 최대 수
maxSurge: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: myapp:v2.0 # 새 버전으로 업데이트
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
설명
이것이 하는 일: 위 YAML은 5개의 Pod를 실행하는 Deployment를 정의하고, 롤링 업데이트 전략으로 새 버전을 배포합니다. 한 번에 최대 1개 Pod가 중단되고, 최대 1개 추가 Pod를 생성할 수 있습니다.
첫 번째로, replicas: 5는 항상 5개의 Pod를 유지하도록 지정합니다. Deployment는 ReplicaSet을 통해 이 상태를 보장하고, Pod가 실패하면 자동으로 재생성합니다.
kubectl scale deployment web-app --replicas=10으로 쉽게 스케일 아웃할 수 있습니다. 그 다음으로, strategy.rollingUpdate 설정이 업데이트 속도를 제어합니다.
maxUnavailable: 1은 업데이트 중 최대 1개 Pod(20%)가 사용 불가 상태여도 된다는 뜻입니다. 5개 중 4개는 항상 서비스해야 하므로, 한 번에 하나씩만 교체됩니다.
maxSurge: 1은 일시적으로 6개(replicas + 1)까지 Pod를 실행할 수 있다는 의미입니다. 업데이트 프로세스는 이렇게 진행됩니다: 1) 새 ReplicaSet이 생성되고 1개 Pod가 시작됩니다.
-
새 Pod의 Readiness Probe가 성공할 때까지 기다립니다. 3) 이전 ReplicaSet에서 1개 Pod를 삭제합니다.
-
이 과정을 모든 Pod가 교체될 때까지 반복합니다. Readiness Probe가 중요한 이유는, 준비되지 않은 Pod에 트래픽을 보내지 않기 위함입니다.
마지막으로, kubectl set image deployment/web-app web=myapp:v3.0으로 이미지를 업데이트하거나, YAML을 수정하고 kubectl apply -f를 실행하면 자동으로 롤링 업데이트가 시작됩니다. kubectl rollout status deployment/web-app로 진행 상황을 모니터링하고, kubectl rollout undo deployment/web-app으로 이전 버전으로 롤백할 수 있습니다.
여러분이 이 코드를 사용하면 안전한 무중단 배포가 가능합니다. 새 버전의 Readiness Probe가 실패하면 롤아웃이 자동으로 중단되어, 불량 버전이 전체 서비스를 망가뜨리지 않습니다.
또한 배포 히스토리가 저장되어 kubectl rollout history로 확인하고, 특정 리비전으로 롤백할 수 있습니다.
실전 팁
💡 maxUnavailable과 maxSurge를 백분율로도 지정할 수 있습니다(예: 25%). 큰 Deployment에서 더 유연합니다.
💡 kubectl rollout pause/resume으로 롤아웃을 일시 중지하고 재개할 수 있습니다. 문제를 발견했을 때 즉시 중단하세요.
💡 minReadySeconds를 설정하면 새 Pod가 준비된 후 일정 시간 기다렸다가 다음 Pod를 교체합니다. 초기 버그를 감지할 시간을 줍니다.
💡 progressDeadlineSeconds(기본 600초)를 초과하면 롤아웃이 실패로 표시됩니다. Readiness가 계속 실패하는 경우 자동으로 멈춥니다.
💡 Canary 배포를 구현하려면 두 개의 Deployment를 만들고(v1과 v2), Service의 라벨 셀렉터로 트래픽 비율을 조절하거나, Istio 같은 Service Mesh를 사용하세요.