Kubernetes 핵심 개념: Pod, Service, Ingress 이해하기
처음 Kubernetes를 접하면 압도된다. Pod, Node, Cluster, ReplicaSet, Deployment, Service, Ingress, ConfigMap, Secret... 용어가 너무 많다. 게다가 각각이 어떻게 연결되는지 설명해주는 글이 생각보다 없다.
이 글은 Kubernetes의 핵심 개념들을 도시 인프라 비유와 함께 설명한다. 아파트 건물, 도로, 신호등으로 K8s의 구조를 이해하면 YAML 파일이 훨씬 자연스럽게 읽힌다.
Kubernetes가 뭘 해결하나
Docker로 컨테이너를 만드는 건 쉽다. 근데 프로덕션에서 100개의 컨테이너를 관리하면? 이런 문제들이 생긴다.
- 컨테이너가 죽으면 누가 다시 살리나?
- 트래픽이 몰리면 어떻게 스케일 아웃하나?
- 여러 서버에 어떻게 분산 배포하나?
- 새 버전 배포할 때 다운타임 없이 하려면?
Kubernetes(K8s)는 이 모든 걸 자동으로 처리하는 컨테이너 오케스트레이션 플랫폼이다.
K8s 아키텍처 개요
┌─────────────────── Kubernetes Cluster ───────────────────┐
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Control Plane │ │
│ │ ┌─────────┐ ┌──────────┐ ┌────────────────────┐ │ │
│ │ │ API │ │ etcd │ │ Scheduler / │ │ │
│ │ │ Server │ │ (상태DB) │ │ Controller Manager│ │ │
│ │ └─────────┘ └──────────┘ └────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Worker │ │ Worker │ │ Worker │ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │ [Pod][Pod]│ │ [Pod][Pod]│ │ [Pod] │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└───────────────────────────────────────────────────────────┘
Control Plane: 클러스터 관리 뇌. 어디에 Pod를 배치할지 결정하고, 상태를 etcd에 저장하고, 원하는 상태와 현재 상태의 차이를 계속 맞춰나간다.
Worker Node: 실제 컨테이너가 실행되는 서버. kubelet이라는 에이전트가 Control Plane의 명령을 받아 Pod를 실행/종료한다.
비유하면: Control Plane은 시청, Worker Node는 구역이다. 시청이 "3구역에 아파트(Pod) 5채를 유지해라" 명령을 내리면, 3구역에서 그걸 실행한다.
Pod: 가장 작은 배포 단위
Pod가 뭔가
Pod는 K8s에서 배포의 최소 단위다. 하나 이상의 컨테이너를 묶는 그릇이다.
Pod
├── Container 1: nginx (메인 앱)
└── Container 2: log-shipper (사이드카, 로그 수집)
같은 Pod 안의 컨테이너는:
- 같은 네트워크 네임스페이스 공유 →
localhost로 통신 - 같은 볼륨 공유 가능
- 항상 같은 Node에 배치
비유: Pod는 아파트 한 호실이다. 한 호실에 여러 사람(컨테이너)이 살고, 같은 주소(IP)를 공유한다.
Pod YAML
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: my-app-pod
labels:
app: my-app
version: v1
spec:
containers:
- name: app
image: my-registry/my-app:v1
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
failureThreshold: 3
readinessProbe vs livenessProbe
| Probe | 목적 | 실패 시 |
|---|---|---|
| readinessProbe | "트래픽 받을 준비 됐나?" | Service에서 제외 |
| livenessProbe | "아직 살아 있나?" | Pod 재시작 |
Pod는 직접 만들지 않는다
실제로는 Pod를 직접 생성하지 않는다. Pod가 죽어도 자동으로 재생성되지 않기 때문이다. 대신 Deployment를 사용한다.
ReplicaSet: Pod를 일정 수 유지하기
ReplicaSet은 "이 Pod를 N개 유지해라"를 담당한다. Pod가 죽으면 자동으로 새로 만든다.
# replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: my-app-rs
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template: # 여기가 Pod 템플릿
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-registry/my-app:v1
근데 ReplicaSet도 직접 사용하지 않는다. 이미지 업데이트 시 롤링 업데이트를 지원하지 않기 때문이다. Deployment가 ReplicaSet을 관리한다.
Deployment: 배포의 실제 주인공
Deployment는 Pod + ReplicaSet + 롤링 업데이트 + 롤백을 모두 처리한다. 실제로 가장 많이 쓰는 리소스다.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: api-service
version: v2
spec:
containers:
- name: api-service
image: my-registry/api-service:v2
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: api-secrets
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "1000m"
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
# 배포
kubectl apply -f deployment.yaml
# 상태 확인
kubectl rollout status deployment/api-service
# 이미지 업데이트
kubectl set image deployment/api-service api-service=my-registry/api-service:v3
# 롤백
kubectl rollout undo deployment/api-service
# 스케일 아웃
kubectl scale deployment api-service --replicas=5
비유: Deployment는 건설 회사 계약서다. "이 스펙의 아파트를 3채 유지하고, 리모델링할 땐 한 채씩 진행해라"를 정의한다.
Service: Pod에 고정 주소 부여하기
Pod는 죽으면 다시 태어나는데, 그때마다 IP가 바뀐다. Service는 Pod들 앞에서 고정 IP/DNS와 로드밸런싱을 제공한다.
비유: Service는 아파트 단지 안내데스크다. 입주자(Pod)가 바뀌어도 데스크 번호(Service IP)는 변하지 않는다.
Service 종류
1. ClusterIP (기본)
클러스터 내부에서만 접근 가능한 IP를 할당한다. 서비스 간 통신에 사용.
# clusterip-service.yaml
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
type: ClusterIP # 기본값, 생략 가능
selector:
app: api-service # 이 라벨을 가진 Pod들로 트래픽 분산
ports:
- port: 80 # Service가 받는 포트
targetPort: 3000 # Pod의 포트
# 클러스터 내부에서 접근
curl http://api-service:80
curl http://api-service.production.svc.cluster.local:80 # 전체 DNS
2. NodePort
Worker Node의 IP와 특정 포트로 외부에서 접근 가능하게 한다. 개발/테스트 목적으로 적합.
apiVersion: v1
kind: Service
metadata:
name: api-service-nodeport
spec:
type: NodePort
selector:
app: api-service
ports:
- port: 80
targetPort: 3000
nodePort: 30080 # 30000-32767 범위. 생략 시 자동 할당
# 노드 IP로 직접 접근
curl http://[NODE_IP]:30080
3. LoadBalancer
클라우드 프로바이더(AWS, GCP, Azure)의 로드밸런서를 자동 생성한다. 프로덕션에서 외부 노출할 때 사용.
apiVersion: v1
kind: Service
metadata:
name: api-service-lb
spec:
type: LoadBalancer
selector:
app: api-service
ports:
- port: 80
targetPort: 3000
kubectl get service api-service-lb
# EXTERNAL-IP에 클라우드 LB의 IP가 할당됨
Service 타입 비교
| 타입 | 접근 범위 | 용도 |
|---|---|---|
| ClusterIP | 클러스터 내부만 | 서비스 간 통신 |
| NodePort | 외부 (Node IP 통해) | 개발/테스트 |
| LoadBalancer | 외부 (LB IP 통해) | 프로덕션 외부 노출 |
| ExternalName | DNS 별칭 | 외부 서비스 참조 |
Ingress: 단일 진입점으로 라우팅하기
LoadBalancer를 서비스마다 만들면 클라우드 LB 비용이 서비스 수만큼 든다. Ingress는 하나의 LB로 여러 서비스에 경로 기반 라우팅을 제공한다.
비유: Ingress는 건물 로비의 안내판 + 엘리베이터다. /api로 오면 백엔드로, /로 오면 프론트엔드로 안내한다.
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: main-ingress
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# 속도 제한
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
tls:
- hosts:
- api.example.com
secretName: tls-secret
rules:
- host: api.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
- host: admin.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin-service
port:
number: 80
Ingress 자체는 라우팅 규칙만 정의한다. 실제로 트래픽을 처리하는 건 Ingress Controller다 (보통 nginx-ingress나 traefik).
# nginx Ingress Controller 설치
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml
ConfigMap: 환경 설정 분리하기
코드와 설정을 분리해야 컨테이너 이미지를 환경별로 재빌드하지 않아도 된다. ConfigMap은 민감하지 않은 설정을 저장한다.
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
NODE_ENV: "production"
LOG_LEVEL: "info"
DB_HOST: "postgres-service"
DB_PORT: "5432"
DB_NAME: "myapp"
# 파일 형태로도 저장 가능
app.config.json: |
{
"featureFlags": {
"newDashboard": true,
"betaSearch": false
}
}
사용 방법
# 환경변수 개별 주입
env:
- name: NODE_ENV
valueFrom:
configMapKeyRef:
name: api-config
key: NODE_ENV
# 전체 ConfigMap을 환경변수로
envFrom:
- configMapRef:
name: api-config
# 파일로 마운트
volumeMounts:
- name: config-volume
mountPath: /app/config
volumes:
- name: config-volume
configMap:
name: api-config
Secret: 민감 데이터 관리
Secret은 ConfigMap과 비슷하지만 Base64로 인코딩되고 접근 제어가 더 엄격하다.
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
data:
# Base64로 인코딩된 값 (echo -n "mypassword" | base64)
DB_PASSWORD: bXlwYXNzd29yZA==
JWT_SECRET: c3VwZXJzZWNyZXRrZXk=
REDIS_PASSWORD: cmVkaXNwYXNz
# Secret 생성 (파일에서)
kubectl create secret generic api-secrets \
--from-literal=DB_PASSWORD=mypassword \
--from-literal=JWT_SECRET=supersecretkey
# 또는 .env 파일에서
kubectl create secret generic api-secrets --from-env-file=.env.production
실제 프로덕션에서는 Secret을 YAML 파일로 관리하지 않는다. Vault, AWS Secrets Manager, External Secrets Operator 같은 도구를 사용해서 동적으로 주입한다.
Namespace: 논리적 격리
클러스터 안에서 리소스를 논리적으로 분리하는 단위다. 팀별, 환경별로 나눌 수 있다.
# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: production
---
apiVersion: v1
kind: Namespace
metadata:
name: staging
# 네임스페이스 목록
kubectl get namespaces
# 특정 네임스페이스의 Pod 보기
kubectl get pods -n production
# 기본 네임스페이스 변경
kubectl config set-context --current --namespace=production
전체 구조 다시 보기
외부 트래픽
↓
[Ingress]
/api → api-service
/ → frontend-service
↓
[Service: api-service] (ClusterIP: 10.0.0.1:80)
- selector: app=api-service
- 라운드 로빈 로드밸런싱
↓
[Pod] [Pod] [Pod]
(app=api-service, v2)
↑
[Deployment: api-service]
- replicas: 3
- RollingUpdate 전략
- ConfigMap + Secret 주입
리소스 간 연결의 핵심은 **라벨(Label)과 셀렉터(Selector)**다. Deployment는 Pod에 라벨을 붙이고, Service는 셀렉터로 그 라벨을 가진 Pod를 찾는다.
로컬 개발: minikube와 kind
minikube
# 설치 (macOS)
brew install minikube
# 클러스터 시작
minikube start --driver=docker --cpus=4 --memory=8192
# 대시보드 열기
minikube dashboard
# Ingress 활성화
minikube addons enable ingress
# 서비스 노출 (NodePort)
minikube service api-service
# 클러스터 삭제
minikube delete
kind (Kubernetes in Docker)
# 설치
brew install kind
# 클러스터 생성
kind create cluster --name dev-cluster
# 멀티 노드 클러스터
cat <<EOF > kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF
kind create cluster --name dev-cluster --config kind-config.yaml
# 로컬 이미지를 kind에 로드
kind load docker-image my-registry/api-service:v1 --name dev-cluster
# 클러스터 삭제
kind delete cluster --name dev-cluster
| 도구 | 특징 | 추천 용도 |
|---|---|---|
| minikube | VM/Docker 기반, 애드온 풍부 | 개인 학습, 빠른 시작 |
| kind | Docker 컨테이너 안에 K8s | CI 파이프라인, 멀티노드 테스트 |
실전 YAML: Node.js API 전체 배포 예시
# complete-deployment.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
namespace: production
data:
NODE_ENV: "production"
DB_HOST: "postgres-service"
DB_PORT: "5432"
LOG_LEVEL: "info"
---
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
namespace: production
type: Opaque
data:
DB_PASSWORD: [base64 encoded]
JWT_SECRET: [base64 encoded]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-service
template:
metadata:
labels:
app: api-service
spec:
containers:
- name: api
image: my-registry/api-service:v1
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: api-secrets
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "1000m"
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: production
spec:
selector:
app: api-service
ports:
- port: 80
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
# 전체 배포
kubectl apply -f complete-deployment.yaml
# 상태 확인
kubectl get all -n production
# Pod 로그 보기
kubectl logs -f deployment/api-service -n production
# Pod 안으로 진입
kubectl exec -it [pod-name] -n production -- /bin/sh
# 리소스 삭제
kubectl delete -f complete-deployment.yaml
자주 쓰는 kubectl 명령어
# 리소스 조회
kubectl get pods,services,deployments -n production
kubectl get all -n production
# 상세 정보 (이벤트 포함)
kubectl describe pod [pod-name] -n production
kubectl describe deployment api-service -n production
# 로그
kubectl logs [pod-name] -n production
kubectl logs -f deployment/api-service -n production # 스트리밍
kubectl logs [pod-name] --previous -n production # 이전 컨테이너 로그
# 포트 포워딩 (로컬에서 Pod에 직접 접근)
kubectl port-forward deployment/api-service 3000:3000 -n production
# 리소스 편집
kubectl edit deployment api-service -n production
# 강제 재시작
kubectl rollout restart deployment/api-service -n production
# 클러스터 전체 리소스 사용량
kubectl top nodes
kubectl top pods -n production
마무리
K8s 개념들의 관계를 다시 정리하면:
Namespace
└── Deployment (배포 정의 + 롤링 업데이트)
└── ReplicaSet (Pod 수 유지)
└── Pod (컨테이너 실행 단위)
├── ConfigMap (일반 설정)
└── Secret (민감 데이터)
Service (Pod 로드밸런싱 + 고정 주소)
└── 셀렉터로 Pod를 찾음
Ingress (외부 → Service 라우팅)
└── Ingress Controller가 실제 처리
처음엔 YAML이 많아서 겁나지만, 실제로 만지다 보면 패턴이 보인다. "Deployment + Service + Ingress" 조합이 90%의 케이스를 커버한다. minikube나 kind로 로컬에서 직접 적용해보는 게 이론만 읽는 것보다 10배 빨리 익힌다.