
Kubernetes 핵심 개념: Pod, Service, Ingress 이해하기
Kubernetes는 처음엔 용어만 봐도 압도된다. Pod, ReplicaSet, Deployment, Service, Ingress가 각각 무엇이고 어떻게 연결되는지, ConfigMap과 Secret까지 실전 YAML과 함께 한 번에 정리한다.

Kubernetes는 처음엔 용어만 봐도 압도된다. Pod, ReplicaSet, Deployment, Service, Ingress가 각각 무엇이고 어떻게 연결되는지, ConfigMap과 Secret까지 실전 YAML과 함께 한 번에 정리한다.
AWS 콘솔 클릭질로 만든 서버가 왜 문제인지, Terraform으로 인프라를 코드로 선언하면 무엇이 달라지는지 실전 예제와 함께 정리했다.

새벽엔 낭비하고 점심엔 터지는 서버 문제 해결기. '택시 배차'와 '피자 배달' 비유로 알아보는 오토 스케일링과 서버리스의 차이, 그리고 Spot Instance를 활용한 비용 절감 꿀팁.

서버를 끄지 않고 배포하는 법. 롤링, 카나리, 블루-그린의 차이점과 실제 구축 전략. DB 마이그레이션의 난제(팽창-수축 패턴)와 AWS CodeDeploy 활용법까지 심층 분석합니다.

내 서버가 해킹당하지 않는 이유. 포트와 IP를 검사하는 '패킷 필터링'부터 AWS Security Group까지, 방화벽의 진화 과정.

처음 Kubernetes를 접하면 압도된다. Pod, Node, Cluster, ReplicaSet, Deployment, Service, Ingress, ConfigMap, Secret... 용어가 너무 많다. 게다가 각각이 어떻게 연결되는지 설명해주는 글이 생각보다 없다.
이 글은 Kubernetes의 핵심 개념들을 도시 인프라 비유와 함께 설명한다. 아파트 건물, 도로, 신호등으로 K8s의 구조를 이해하면 YAML 파일이 훨씬 자연스럽게 읽힌다.
Docker로 컨테이너를 만드는 건 쉽다. 근데 프로덕션에서 100개의 컨테이너를 관리하면? 이런 문제들이 생긴다.
Kubernetes(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는 K8s에서 배포의 최소 단위다. 하나 이상의 컨테이너를 묶는 그릇이다.
Pod
├── Container 1: nginx (메인 앱)
└── Container 2: log-shipper (사이드카, 로그 수집)
같은 Pod 안의 컨테이너는:
localhost로 통신비유: Pod는 아파트 한 호실이다. 한 호실에 여러 사람(컨테이너)이 살고, 같은 주소(IP)를 공유한다.
# 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가 죽어도 자동으로 재생성되지 않기 때문이다. 대신 Deployment를 사용한다.
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는 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채 유지하고, 리모델링할 땐 한 채씩 진행해라"를 정의한다.
Pod는 죽으면 다시 태어나는데, 그때마다 IP가 바뀐다. Service는 Pod들 앞에서 고정 IP/DNS와 로드밸런싱을 제공한다.
비유: Service는 아파트 단지 안내데스크다. 입주자(Pod)가 바뀌어도 데스크 번호(Service IP)는 변하지 않는다.
클러스터 내부에서만 접근 가능한 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
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
클라우드 프로바이더(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가 할당됨
| 타입 | 접근 범위 | 용도 |
|---|---|---|
| ClusterIP | 클러스터 내부만 | 서비스 간 통신 |
| NodePort | 외부 (Node IP 통해) | 개발/테스트 |
| LoadBalancer | 외부 (LB IP 통해) | 프로덕션 외부 노출 |
| ExternalName | DNS 별칭 | 외부 서비스 참조 |
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.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은 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.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를 찾는다.
# 설치 (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
# 설치
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 파이프라인, 멀티노드 테스트 |
# 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 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배 빨리 익힌다.