개요
심화 1편이 “Pod를 안정적으로 운영하기 위한 설정"이었다면, 이 글은 “여러 서비스를 효율적으로 관리하고, 문제가 생겼을 때 빠르게 찾는 방법"에 초점을 둡니다.
Helm으로 반복 배포를 줄이고, Ingress Controller로 외부 트래픽을 세밀하게 제어하고, 장애 상황에서 원인을 빠르게 좁히는 패턴을 정리합니다.
1. Helm — 패키지 관리
정리 보기
Helm = K8s의 패키지 매니저
환경별로 값이 다른 YAML을 각각 관리하면 파일 수가 환경 수만큼 늘어납니다. Helm은 이걸 하나의 패키지(Chart)로 묶어서 변수만 바꿔 배포할 수 있게 합니다.
핵심 용어
| 용어 | 뜻 |
|---|---|
| Chart | 패키지 (YAML 템플릿 묶음) |
| Release | Chart를 클러스터에 설치한 인스턴스 |
| Repository | Chart를 저장/공유하는 저장소 |
| Values | Chart에 주입하는 설정값 |
Chart 디렉터리 구조
my-app/
├── Chart.yaml # 이름, 버전, 설명
├── values.yaml # 기본 설정값
├── templates/ # K8s YAML 템플릿
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ └── _helpers.tpl # 공통 헬퍼 함수
└── charts/ # 의존 Chartvalues.yaml 예시
replicaCount: 3
image:
repository: myapp
tag: "1.0.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
ingress:
enabled: true
host: api.example.comtemplates/deployment.yaml (템플릿)
값은 {{ .Values.xxx }}로 values.yaml에서 가져옵니다. helm template 명령어로 실제 배포 전에 렌더링 결과를 미리 확인할 수 있습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "my-app.name" . }}
template:
metadata:
labels:
app: {{ include "my-app.name" . }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}# 렌더링 결과 미리보기 (배포 안 함)
helm template my-release ./my-app -f values-prod.yaml기본 명령어
# Chart 설치
helm install my-release ./my-app
# 환경별 values로 설치
helm install my-release ./my-app -f values-prod.yaml
# 업그레이드 (값 변경 or 이미지 태그 변경)
helm upgrade my-release ./my-app --set image.tag=2.0.0
# 롤백
helm rollback my-release 1
# 릴리즈 목록
helm list
# 릴리즈 상태
helm status my-release
# 삭제
helm uninstall my-release
# 템플릿 렌더링 미리보기 (실제 배포 안 함)
helm template my-release ./my-app -f values-prod.yaml공개 Chart 사용 (Prometheus, nginx 등)
# 레포지토리 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# 설치
helm install prometheus prometheus-community/kube-prometheus-stack -f custom-values.yaml
# 어떤 values가 있는지 확인
helm show values prometheus-community/kube-prometheus-stack > default-values.yaml환경별 관리 패턴
my-app/
├── values.yaml # 공통 기본값
├── values-dev.yaml # dev 환경 오버라이드
├── values-stg.yaml # staging
└── values-prod.yaml # production# dev 배포
helm upgrade --install my-app ./my-app -f values-dev.yaml -n dev
# prod 배포
helm upgrade --install my-app ./my-app -f values-prod.yaml -n prod-i, --install은 같은 이름의 릴리즈가 없으면 install을 수행하고 있으면 upgrade를 수행합니다. 설치와 업그레이드를 한 명령으로 처리합니다.
2. Ingress Controller 심화
정리 보기
기본편 복습: Ingress는 규칙이고, Ingress Controller가 실제로 동작한다
여기서는 nginx Ingress Controller를 예로 듭니다. 설치하면 nginx가 Pod로 뜨면서 Ingress 리소스의 규칙대로 트래픽을 라우팅합니다.
설치 (Helm으로)
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespaceTLS (HTTPS) 설정
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: api-tls-secret # TLS 인증서가 담긴 Secret
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80cert-manager로 인증서 자동 발급 (Let’s Encrypt)
helm repo add jetstack https://charts.jetstack.io --force-update
helm install cert-manager jetstack/cert-manager --set crds.enabled=true -n cert-manager --create-namespaceCRD 설치 옵션은 crds.enabled입니다. 예전 자료에 나오는 installCRDs도 동작하지만, 최신 차트(v1.16+)에서는 crds.enabled를 사용합니다.
# ClusterIssuer 생성
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
ingressClassName: nginxHTTP-01 solver에서 Ingress Controller를 지정하는 방법은 세 가지(ingressClassName, class, name)인데, 공식 문서는 ingressClassName을 권장합니다(cert-manager 1.12에 추가된 필드). class는 ingressClassName을 지원하지 않는 ingress-gce에 권장되는 방식입니다. 세 필드를 모두 비워두면 cert-manager가 ingress class 없이 Ingress를 만들기 때문에 클러스터의 모든 Ingress Controller가 챌린지 트래픽을 처리하게 됩니다.
# Ingress에 어노테이션 추가하면 자동 발급
metadata:
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"어노테이션
| 어노테이션 | 용도 |
|---|---|
nginx.ingress.kubernetes.io/proxy-body-size |
요청 본문 최대 크기 (ConfigMap proxy-body-size 기본값 1m) |
nginx.ingress.kubernetes.io/proxy-read-timeout |
백엔드 응답 대기 시간 (단위 없는 초) |
nginx.ingress.kubernetes.io/limit-rps |
IP당 초당 요청 수 제한 (컨트롤러 레플리카 단위로 적용) |
nginx.ingress.kubernetes.io/rewrite-target |
URL 경로 재작성 |
nginx.ingress.kubernetes.io/cors-allow-origin |
허용할 Origin 지정 (enable-cors: "true"와 함께 사용, 기본값 *) |
nginx.ingress.kubernetes.io/affinity |
세션 어피니티 (nginx에서 사용 가능한 값은 cookie) |
limit-rps는 컨트롤러 레플리카별로 적용되기 때문에, 레플리카가 여러 개면 전체 허용량은 레플리카 수만큼 곱해집니다. HPA를 붙여 놓으면 레플리카 수가 고정되지 않으므로 전체 허용량도 함께 변합니다.
# 파일 업로드 제한 늘리기 + 타임아웃 늘리기
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
nginx.ingress.kubernetes.io/proxy-send-timeout: "120"경로 기반 라우팅
spec:
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 3000하나의 도메인에서 /api는 백엔드로, /는 프론트로 보내는 구조입니다.
디버깅
# Ingress Controller 로그
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx -f
# Ingress 설정이 실제로 어떻게 반영됐는지
kubectl exec -n ingress-nginx <nginx-pod> -- cat /etc/nginx/nginx.conf | grep -A 20 "server_name"3. Troubleshooting
정리 보기
기본 디버깅 흐름
문제 발생
→ kubectl get pods (상태 확인)
→ kubectl describe pod (이벤트 확인)
→ kubectl logs (앱 로그 확인)
→ 원인별 대응Pod 상태와 확인 명령어
| 상태 | 원인 | 확인 방법 |
|---|---|---|
| Pending | 노드 자원 부족 or 스케줄링 조건 불일치 | kubectl describe pod → Events |
| CrashLoopBackOff | 컨테이너가 종료된 뒤 재시작을 반복하는 상태 | kubectl logs --previous |
| ImagePullBackOff | 이미지 못 가져옴 (이름 틀림, 권한 없음) | kubectl describe pod → Events |
| OOMKilled | 메모리 limit 초과 | kubectl describe pod → Last State |
| Evicted | 노드 디스크/메모리 부족으로 축출됨 | kubectl describe pod → Events |
| Terminating (멈춤) | graceful shutdown 실패, finalizer 걸림 | kubectl get pod -o yaml → finalizers, deletionTimestamp |
1. Pending — Pod가 안 뜸
kubectl describe pod <이름>
# Events 섹션 확인Events 메시지별 원인:
| Events 메시지 | 원인 |
|---|---|
Insufficient cpu/memory |
노드에 자원 없음 |
no nodes available |
스케줄 가능한 노드 없음 |
didn't match Pod's node selector |
nodeSelector 불일치 |
persistentvolumeclaim not found |
PVC 없거나 안 붙음 |
2. CrashLoopBackOff — 계속 죽음
# 현재 로그
kubectl logs <pod>
# 이전 크래시 로그 (죽기 직전)
kubectl logs <pod> --previous
# 컨테이너 안에서 직접 확인
kubectl run debug --image=<같은이미지> -it --rm -- /bin/shkubectl logs --previous는 재시작 이전 컨테이너 인스턴스의 로그를 출력하므로, 종료 시점에 남은 로그를 확인할 수 있습니다.
3. ImagePullBackOff — 이미지 못 가져옴
kubectl describe pod <이름>
# Events: Failed to pull image "myapp:1.0" ... unauthorizedEvents에 기록되는 원인:
- 이미지 이름/태그 오타
- private registry 인증 없음
- 이미지가 존재하지 않음
# imagePullSecrets 설정
spec:
imagePullSecrets:
- name: registry-secret# Secret 생성
kubectl create secret docker-registry registry-secret \
--docker-server=<registry-url> \
--docker-username=<user> \
--docker-password=<pass>4. Service 연결 안 됨
# 1. Pod가 실제로 떠있는지
kubectl get pods -l app=web
# 2. Service의 Endpoints에 Pod IP가 있는지
kubectl get endpoints web-service
# → Endpoints가 비어있으면 selector가 Pod label과 불일치
# 3. Service selector와 Pod label 비교
kubectl get svc web-service -o yaml | grep -A 3 selector
kubectl get pods --show-labels
# 4. Pod 안에서 직접 테스트
kubectl exec -it <다른pod> -- curl http://web-service:805. Ingress 접근 안 됨
# 1. Ingress 설정 확인
kubectl describe ingress <이름>
# 2. Ingress Controller 실행 중인지
kubectl get pods -n ingress-nginx
# 3. Ingress Controller 로그
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
# 4. Service가 정상인지 (위 4번 참고)
# 5. 인증서 문제인지
kubectl get certificate -A
kubectl describe certificate <이름>6. 노드 문제
# 노드 상태
kubectl get nodes
# STATUS 열에서 Ready / NotReady 확인
# 노드 상세 (Conditions 섹션)
kubectl describe node <이름>
# MemoryPressure, DiskPressure, PIDPressure 확인
# 노드의 Pod 목록
kubectl get pods --field-selector spec.nodeName=<노드이름> -A디버깅용 임시 Pod 띄우기
# 네트워크 테스트용
kubectl run debug-net --image=nicolaka/netshoot -it --rm -- /bin/bash
# 안에서: curl, dig, ping, traceroute 등 가능
# 특정 namespace에서
kubectl run debug -n production --image=busybox -it --rm -- /bin/sh4. 리소스 쿼터와 LimitRange
정리 보기
ResourceQuota = 네임스페이스 전체의 자원 상한선
팀별로 네임스페이스를 나눴을 때, 한 팀이 클러스터 자원을 다 쓰는 걸 방지합니다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: team-a
spec:
hard:
requests.cpu: "4" # 전체 requests CPU 합계 4코어까지
requests.memory: "8Gi" # 전체 requests 메모리 8Gi까지
limits.cpu: "8"
limits.memory: "16Gi"
pods: "20" # Pod 최대 20개
services: "10"LimitRange = 개별 Pod/컨테이너의 기본값/상한
requests/limits를 안 넣은 Pod에 기본값을 강제합니다.
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: team-a
spec:
limits:
- type: Container
default: # limits 기본값
cpu: "500m"
memory: "256Mi"
defaultRequest: # requests 기본값
cpu: "100m"
memory: "128Mi"
max: # 최대 허용
cpu: "2"
memory: "2Gi"
min: # 최소 허용
cpu: "50m"
memory: "64Mi"적용 범위 차이
- ResourceQuota: 네임스페이스 전체의 requests/limits 합계와 오브젝트 개수에 상한을 둠
- LimitRange: requests/limits가 없는 컨테이너에 기본값을 채우고, 컨테이너별 최소/최대 값을 검증함
# 네임스페이스 쿼터 확인
kubectl get resourcequota -n team-a
kubectl describe resourcequota team-quota -n team-a
# LimitRange 확인
kubectl get limitrange -n team-a5. Pod Disruption Budget (PDB)
정리 보기
PDB = “최소 몇 개는 항상 떠있어야 한다"는 보호 규칙
노드 유지보수(kubectl drain), 클러스터 업그레이드 시 K8s가 Pod를 내리는데, PDB를 설정하면 동시에 너무 많이 내리지 못하도록 제한합니다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 2 # 최소 2개는 항상 살아있어야 함
# 또는
# maxUnavailable: 1 # 동시에 최대 1개까지만 내릴 수 있음
selector:
matchLabels:
app: web언제 동작하냐
| 상황 | PDB 적용 |
|---|---|
kubectl drain node (노드 유지보수) |
적용됨 |
| 클러스터 오토스케일러가 노드 축소 | 적용됨 |
| Pod가 스스로 죽음 (OOM, 앱 에러) | 적용 안 됨 |
kubectl delete pod |
적용 안 됨 |
PDB는 **자발적 축출(voluntary disruption)**에만 적용됩니다. 앱 자체 에러로 죽는 건 PDB와 관계없습니다.
# PDB 상태 확인
kubectl get pdb
# 상세 (현재 몇 개 available인지)
kubectl describe pdb web-pdb6. 멀티 클러스터와 환경 분리
정리 보기
클러스터를 어떻게 나누냐
| 전략 | 구조 | 격리 범위 |
|---|---|---|
| 단일 클러스터 + 네임스페이스 분리 | 1 클러스터, dev/stg/prod 네임스페이스 | 네임스페이스 단위 |
| 환경별 클러스터 | dev 클러스터, prod 클러스터 | 컨트롤 플레인과 노드까지 분리 |
| 팀별 + 환경별 | 팀A-prod, 팀B-prod 등 | 팀 단위로 클러스터 분리 |
kubectl context로 클러스터 전환
# 현재 context 확인
kubectl config current-context
# context 목록
kubectl config get-contexts
# context 전환
kubectl config use-context prod-cluster
# 특정 context로 명령어 실행 (전환 없이)
kubectl --context=dev-cluster get podskubeconfig 구조 (~/.kube/config)
apiVersion: v1
kind: Config
clusters:
- name: dev-cluster
cluster:
server: https://dev-k8s.example.com
certificate-authority-data: ...
- name: prod-cluster
cluster:
server: https://prod-k8s.example.com
certificate-authority-data: ...
contexts:
- name: dev
context:
cluster: dev-cluster
user: dev-user
namespace: default
- name: prod
context:
cluster: prod-cluster
user: prod-user
namespace: default
users:
- name: dev-user
user:
token: ...
current-context: devkubectx + kubens (편의 도구)
# 클러스터 전환 (kubectx)
kubectx prod-cluster
# 네임스페이스 전환 (kubens)
kubens productionkubectx는 context 전환, kubens는 네임스페이스 전환을 짧은 명령으로 수행하는 별도 도구입니다.
7. 보안 강화 — Pod Security
정리 보기
securityContext = Pod/컨테이너 레벨 보안 설정
root로 실행하지 않기, 파일시스템 읽기 전용 등 컨테이너 보안을 강제합니다.
spec:
securityContext:
runAsNonRoot: true # root 실행 금지
runAsUser: 1000 # UID 1000으로 실행
fsGroup: 2000 # 볼륨 그룹 ID
containers:
- name: app
image: myapp:1.0
securityContext:
allowPrivilegeEscalation: false # 권한 상승 금지
readOnlyRootFilesystem: true # 루트 파일시스템 읽기 전용
capabilities:
drop: ["ALL"] # 모든 Linux capability 제거각 필드의 동작
runAsNonRoot: true: 컨테이너가 UID 0으로 실행되면 kubelet이 시작을 거부함readOnlyRootFilesystem: true: 컨테이너 루트 파일시스템을 읽기 전용으로 마운트capabilities.drop: ["ALL"]: 컨테이너에 부여되는 Linux capability를 모두 제거
Pod Security Standards (PSS)
PSS는 정책 3단계를 정의한 문서이고, 이걸 실제로 강제하는 건 Pod Security Admission(PSA) 컨트롤러입니다. PSA는 1.22 알파, 1.23부터 기본 제공(베타), 1.25부터 GA입니다. 정책은 네임스페이스 라벨로 적용됩니다.
| 레벨 | 제한 |
|---|---|
| Privileged | 제한 없는 정책. 모드 라벨이 없을 때 적용되는 기본값 |
| Baseline | 알려진 권한 상승을 막는 최소 제한 (hostNetwork, hostPID, privileged, hostPath 등 차단) |
| Restricted | 가장 엄격 (non-root, allowPrivilegeEscalation: false, capabilities drop ALL, seccomp 지정 강제) |
모드는 enforce(위반 시 Pod 거부), audit(감사 로그에만 기록), warn(경고만) 세 가지이고, 라벨이 없을 때 쓰이는 기본 레벨은 세 모드 모두 privileged입니다.
# 네임스페이스에 보안 레벨 적용
kubectl label namespace production pod-security.kubernetes.io/enforce=restricted