Back to Notes

Notes

K8s 01. Docker와 Kubernetes

Docker와 Kubernetes를 경쟁 관계가 아니라 container packaging과 cluster orchestration의 역할로 구분하고, production 운영에서 Kubernetes가 필요한 이유를 정리한다.

Published
Updated
Area
Cloud Infrastructure
Type
concept
Series
Kubernetes Essentials
Category
Notes
KubernetesDockerContainerOrchestrationDeploymentServiceSelf-Healing

핵심 요약

DockerKubernetes는 같은 문제를 해결하는 경쟁 도구가 아니다.
Docker는 애플리케이션을 container image로 만들고 개별 container를 실행하는 데 강점이 있고, Kubernetes는 그렇게 만들어진 containerized application을 여러 node에 걸쳐 배포, 확장, 복구, 네트워킹하는 orchestration layer에 가깝다.

따라서 질문은 다음처럼 바뀌어야 한다.

잘못된 질문:
Docker를 써야 하나, Kubernetes를 써야 하나?

더 정확한 질문:
Docker 등으로 만든 containerized application을 production 환경에서 어떻게 안정적으로 운영할 것인가?

작은 개발 환경이나 단일 서버에서는 Docker만으로도 충분할 수 있다.
하지만 microservices, 다중 node, traffic 증가, 장애 복구, rolling update, service discovery가 필요해지는 순간 Kubernetes가 필요해진다.


Docker와 Kubernetes의 역할 구분

구분DockerKubernetes
핵심 역할애플리케이션을 container image로 packaging하고 실행여러 containerized workload를 cluster 단위로 배포하고 운영
주요 관심사image build, image tag, registry push/pull, container runscheduling, desired state, replica 유지, self-healing, service discovery
운영 단위개별 containerPod, Deployment, Service, Node, Cluster
사고방식docker run으로 container 실행YAML manifest로 desired state 선언
적합한 상황개발 환경, local test, 단일 서버, 작은 배포production, multi-node, microservices, 자동 복구, scale-out
한계다수 서버와 다수 container 관리가 복잡해짐cluster 운영 자체의 학습 비용과 복잡도가 있음

핵심 문장으로 정리하면 다음과 같다.

Docker는 애플리케이션을 어떻게 포장하고 실행할 것인가에 가깝고, Kubernetes는 포장된 애플리케이션을 여러 서버에서 어떻게 안정적으로 굴릴 것인가에 가깝다.


Cloud-Native Application 예시

단순한 웹 서버 하나가 아니라 여러 component로 구성된 cloud-native application을 생각해보자.

User

React Frontend

Node.js Backend
  ↓             ↓
Java DB Layer   Python Flask External API Layer
  ↓             ↓
Database        External API

이 구조에서는 각 component의 역할과 부하 특성이 다르다.

Component예시 역할운영상 고려할 점
React Frontend사용자 UI 제공정적 파일 제공, frontend traffic 증가 대응
Node.js BackendAPI 요청 처리request 처리량, latency, horizontal scaling
Java DB Layerdatabase access 처리connection pool, transaction, database 부하
Python Flask Layer외부 API 호출 또는 별도 business logictimeout, retry, 외부 dependency 장애

이런 구조에서는 모든 component를 같은 방식으로 확장할 수 없다.

예를 들어 frontend 요청이 많아졌다고 해서 database access layer까지 같은 비율로 늘릴 필요는 없다. 반대로 외부 API 호출이 병목이면 Python Flask layer만 독립적으로 늘려야 할 수도 있다.

Docker는 이 component들을 각각 독립적인 image로 만들 수 있게 해준다.

frontend: react-app:v1
backend: node-api:v1
db-access: java-db-service:v1
external-api: flask-api-worker:v1

이렇게 image를 분리하면 각 component를 독립적으로 build, deploy, scale할 수 있다.
하지만 이 image들을 운영 환경에서 지속적으로 관리하는 문제는 별개의 문제다.


Docker만 사용할 때의 기본 실행 구조

단일 서버 기준 Docker 실행 구조는 대략 다음과 같다.

Physical / Virtual Hardware

Operating System, e.g. Ubuntu

Docker Daemon

Containers

개발자는 보통 다음 흐름으로 containerized application을 만든다.

  1. Dockerfile 작성
  2. image build
  3. image tag 지정
  4. registry push
  5. 운영 서버에서 image pull
  6. container 실행

예시 Dockerfile은 다음과 같다.

FROM node:20

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

CMD ["npm", "start"]

image build와 push는 다음처럼 수행할 수 있다.

docker build -t my-api:v1 .
docker tag my-api:v1 registry.example.com/my-api:v1
docker push registry.example.com/my-api:v1

운영 서버에서 container를 실행하려면 다음처럼 실행할 수 있다.

docker run -d --name my-api -p 3000:3000 registry.example.com/my-api:v1

이 방식은 단일 container 또는 소규모 배포에서는 단순하고 직관적이다.

그러나 production scale에서는 곧 다음 문제가 생긴다.

container를 어느 서버에 띄울 것인가?
CPU와 memory가 남는 node는 어디인가?
container가 죽으면 누가 다시 실행할 것인가?
새 replica를 load balancer에 어떻게 반영할 것인가?
새 버전 배포 중 기존 요청은 어떻게 유지할 것인가?
container IP가 바뀌면 다른 component는 어떻게 찾아갈 것인가?

Docker는 container 실행을 쉽게 해주지만, cluster 전체의 운영 상태를 지속적으로 맞춰주는 시스템은 아니다.


Docker-Only Scaling의 한계

처음에는 Docker만으로도 scale-out이 가능해 보인다.

docker run -d --name frontend-1 frontend:v1
docker run -d --name frontend-2 frontend:v1

docker run -d --name api-1 api:v1
docker run -d --name api-2 api:v1
docker run -d --name api-3 api:v1

하지만 여러 서버를 사용하는 순간 운영자는 다음 작업을 직접 처리해야 한다.

1. node별 CPU/memory 사용량 확인
2. 여유 있는 node 선택
3. 해당 node에서 image pull
4. docker run 실행
5. load balancer 설정 변경
6. health check 등록
7. 장애 발생 시 container 재시작
8. 배포 실패 시 rollback
9. 로그 수집
10. traffic routing 확인

이 정도 작업은 처음에는 shell script로 처리할 수 있어 보인다.
하지만 scale이 커질수록 script는 점점 작은 orchestration system처럼 변한다.

예를 들어 다음 요구사항을 생각해보자.

frontend는 10개 replica가 필요하다.
api는 20개 replica가 필요하다.
worker는 queue 길이에 따라 5~50개 사이에서 변해야 한다.
특정 node가 장애 상태가 되면 해당 node의 container를 다른 node로 옮겨야 한다.
새 버전을 배포할 때 전체 traffic을 한 번에 끊으면 안 된다.
unhealthy container는 자동으로 traffic 대상에서 제외해야 한다.

이 문제는 더 이상 docker run을 몇 번 실행할지의 문제가 아니다.
cluster 전체의 desired state와 actual state를 지속적으로 맞추는 문제다.


Kubernetes의 핵심 관점: Desired State

Kubernetes의 핵심은 desired state를 선언하고, actual state가 그 상태에 가까워지도록 지속적으로 조정하는 것이다.

Docker만 사용할 때는 보통 다음처럼 명령형으로 운영한다.

docker run ...
docker stop ...
docker rm ...
docker run ...

Kubernetes에서는 다음처럼 선언형으로 운영한다.

이 애플리케이션이 항상 3개 replica로 실행되게 하라.

즉, Kubernetes는 단순히 container를 실행하는 도구가 아니라 상태를 조정하는 시스템이다.

Observe current state
Compare with desired state
Act to reduce difference
Repeat

이 반복 과정을 통해 Kubernetes는 cluster가 사용자가 선언한 상태에 가까워지도록 유지한다.


Kubernetes Cluster의 기본 구조

Kubernetes를 도입하면 구조는 다음과 같이 바뀐다.

Kubernetes Cluster
├─ Control Plane
│  ├─ API Server
│  ├─ Scheduler
│  ├─ Controller Manager
│  └─ etcd

├─ Worker Node 1
│  ├─ kubelet
│  ├─ container runtime
│  └─ Pods

├─ Worker Node 2
│  ├─ kubelet
│  ├─ container runtime
│  └─ Pods

└─ Worker Node 3
   ├─ kubelet
   ├─ container runtime
   └─ Pods

각 구성요소의 역할은 다음과 같이 볼 수 있다.

구성요소역할
API ServerKubernetes API 요청을 받는 중심 진입점
Scheduler새 Pod를 어느 node에 배치할지 결정
Controller Managerdesired state와 actual state를 비교하고 조정
etcdcluster state 저장소
kubelet각 worker node에서 Pod 실행 상태 관리
container runtime실제 container 실행 담당
PodKubernetes에서 container를 실행하는 기본 단위

사용자는 개별 node에 직접 docker run을 실행하기보다, Kubernetes API에 원하는 상태를 선언한다.


Deployment: Replica 수를 유지하는 리소스

Kubernetes에서 application replica를 관리할 때 대표적으로 사용하는 리소스가 Deployment다.

예를 들어 React frontend를 8개 replica로 유지하고 싶다면 다음처럼 선언할 수 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: react-frontend
spec:
  replicas: 8
  selector:
    matchLabels:
      app: react-frontend
  template:
    metadata:
      labels:
        app: react-frontend
    spec:
      containers:
        - name: react-frontend
          image: registry.example.com/react-frontend:v1
          ports:
            - containerPort: 80

이 YAML의 의미는 다음과 같다.

react-frontend라는 애플리케이션을
항상 8개 replica로 유지하고
각 replica는 registry.example.com/react-frontend:v1 image를 사용하며
container 내부 80번 포트를 사용한다.

여기서 중요한 점은 사용자가 container를 8번 직접 실행하지 않는다는 것이다.
사용자는 “8개가 유지되어야 한다”는 상태를 선언하고, Kubernetes가 그 상태를 맞춘다.


Self-Healing: 죽으면 다시 살린다

Production 환경에서는 장애가 예외가 아니라 상수에 가깝다.

container가 OOM으로 죽을 수 있다.
node가 장애 상태가 될 수 있다.
새 버전이 crash할 수 있다.
network partition이 발생할 수 있다.
disk pressure 때문에 Pod가 evict될 수 있다.

Kubernetes는 선언된 replica 수를 유지하기 위해 지속적으로 상태를 확인한다.

예를 들어 desired state가 다음과 같다고 하자.

api replica = 5

실제 상태에서 Pod 하나가 죽으면 actual state는 다음처럼 변한다.

desired: 5
actual: 4
difference: +1

그러면 Kubernetes는 부족한 replica를 채우기 위해 새 Pod를 생성한다.

action: create 1 new Pod

이것이 Kubernetes의 self-healing 개념이다.

다만 주의할 점이 있다.
Kubernetes가 application bug 자체를 고쳐주는 것은 아니다.

예를 들어 container 내부 코드가 잘못되어 계속 crash한다면 Kubernetes는 계속 재시작을 시도할 뿐이다. 이 경우에는 다음 정보를 통해 원인을 분석해야 한다.

kubectl get pods
kubectl describe pod <pod-name>
kubectl logs <pod-name>
kubectl get events

Self-healing은 “항상 정상적인 애플리케이션으로 고쳐준다”는 의미가 아니라, 선언된 운영 상태를 유지하려고 자동으로 조치한다는 의미에 가깝다.


Service: Pod IP가 바뀌어도 안정적으로 접근하기

Kubernetes에서 Pod는 ephemeral하다.
생성되고 삭제될 수 있으며, 새로 만들어진 Pod는 기존과 다른 IP를 가질 수 있다.

예를 들어 backend Pod가 세 개 있다고 하자.

api-pod-1: 10.244.1.10
api-pod-2: 10.244.2.15
api-pod-3: 10.244.3.21

이 중 api-pod-2가 죽고 새 Pod가 생기면 IP가 바뀔 수 있다.

api-pod-2 old: 10.244.2.15
api-pod-4 new: 10.244.4.33

Frontend가 backend Pod IP를 직접 바라보고 있었다면 장애가 발생한다.
따라서 Kubernetes에서는 Service를 사용해 안정적인 endpoint를 제공한다.

apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 3000

이제 frontend는 개별 Pod IP를 몰라도 된다.

http://api-service

Service는 label selector를 기반으로 대상 Pod 집합을 찾는다.

Service selector:
  app: api

Matching Pods:
  api-pod-1
  api-pod-2
  api-pod-3

Pod가 삭제되고 새로 만들어져도 label이 같다면 Service 뒤에 자동으로 연결될 수 있다.
이 구조 덕분에 application code는 개별 Pod IP가 아니라 안정적인 service name을 바라볼 수 있다.


Service Discovery와 Internal DNS

Kubernetes 환경에서는 component 간 통신에서 IP를 직접 박아 넣는 방식은 피해야 한다.

부적절한 방식은 다음과 같다.

http://10.244.2.15:3000

더 적절한 방식은 Service name을 사용하는 것이다.

http://api-service

또는 namespace까지 명시해야 하는 경우 다음처럼 사용할 수 있다.

http://api-service.default.svc.cluster.local

이 방식은 다음 장점이 있다.

장점설명
안정적인 이름Pod IP가 바뀌어도 호출 주소를 유지할 수 있음
느슨한 결합frontend가 backend replica의 구체적인 IP를 몰라도 됨
scale-out 대응backend replica가 늘어나도 호출 방식이 바뀌지 않음
장애 대응unhealthy Pod를 service endpoint에서 제외할 수 있음
운영 단순화application code와 runtime topology를 분리할 수 있음

Rolling Update 관점에서의 차이

Docker-only 방식으로 새 버전을 배포하려면 운영자가 순서를 직접 제어해야 한다.

1. 기존 container 중 일부 중지
2. 새 image pull
3. 새 container 실행
4. health check 확인
5. load balancer 대상 전환
6. 나머지 container 반복 교체
7. 문제 발생 시 rollback

Kubernetes에서는 Deployment의 image tag를 변경하거나 manifest를 수정해 rollout을 진행할 수 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: api
          image: registry.example.com/api:v2

운영상 핵심은 “새 버전 container를 실행한다”가 아니라, 서비스 중단 없이 desired state를 새 버전으로 바꾼다는 점이다.

검증에 사용할 수 있는 명령은 다음과 같다.

kubectl rollout status deployment/api
kubectl rollout history deployment/api
kubectl get pods -l app=api

문제가 있을 경우 rollback을 고려할 수 있다.

kubectl rollout undo deployment/api

Monitoring과 Observability 관점

Kubernetes 기본 명령만으로도 Pod와 Node 상태를 확인할 수 있다.

kubectl get pods
kubectl describe pod <pod-name>
kubectl logs <pod-name>
kubectl top pod
kubectl top node

하지만 production 운영에서는 단순히 Pod가 떠 있는지만으로는 충분하지 않다.

확인해야 할 지표는 더 다양하다.

요청 latency가 증가했는가?
어느 service 간 호출에서 병목이 생겼는가?
특정 version 배포 이후 error rate가 증가했는가?
service A에서 service B로 가는 traffic 중 몇 %가 실패하는가?
timeout은 network 문제인가 application 문제인가?
특정 request가 여러 microservice를 거치며 어디에서 느려지는가?

이런 지점에서 observability stack이 필요해진다.

영역예시
MetricsCPU, memory, request rate, error rate, latency
Logsapplication log, container log, event log
Tracesservice 간 호출 경로와 지연 시간
Traffic Policyretry, timeout, circuit breaking, traffic split
Securityservice-to-service mTLS, policy enforcement

Kubernetes는 container orchestration의 기반을 제공하지만, production 운영에서는 monitoring, logging, tracing, service mesh 같은 ecosystem을 함께 고려해야 한다.


최신 Kubernetes Runtime 관점에서 주의할 점

개념적으로는 “Docker로 image를 만들고 Kubernetes로 운영한다”고 설명할 수 있다.
다만 운영 환경에서는 다음 구분이 필요하다.

Docker as developer tool:
  여전히 image build와 local container workflow에서 유용하다.

Docker image / OCI image:
  Kubernetes에서 계속 사용할 수 있다.

Docker Engine as Kubernetes runtime:
  최신 Kubernetes에서는 직접 runtime으로 사용하는 구조가 일반적이지 않다.

Kubernetes runtime:
  containerd, CRI-O 등 CRI-compatible runtime을 사용한다.

즉, Docker로 만든 image를 Kubernetes에서 사용할 수 없다는 뜻이 아니다.
중요한 것은 Kubernetes가 cluster에서 container를 실행할 때 Docker Engine 자체를 직접 전제로 삼지 않는다는 점이다.

운영 관점에서는 다음처럼 구분하면 좋다.

질문
Docker로 image를 build해도 되는가?가능
Docker image를 registry에 push해도 되는가?가능
Kubernetes가 그 image를 pull해서 실행할 수 있는가?가능
Kubernetes worker node에 반드시 Docker Engine이 있어야 하는가?확인 필요. 보통은 containerd 또는 CRI-O를 사용
Docker와 Kubernetes가 경쟁 관계인가?아님. 역할이 다름

전체 배포 흐름 예시

아래는 Docker와 Kubernetes가 각각 어느 단계에서 등장하는지 보여주는 흐름이다.

1. Docker로 image 생성

docker build -t registry.example.com/api:v1 .
docker push registry.example.com/api:v1

이 단계에서 중요한 것은 application code, runtime, dependency, 실행 명령을 image로 묶는 것이다.

Source Code

Dockerfile

Docker Image

Registry

2. Kubernetes에 Deployment 선언

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: registry.example.com/api:v1
          ports:
            - containerPort: 3000

이 단계에서 중요한 것은 “api container를 3개 직접 실행한다”가 아니라 “api workload가 항상 3개 replica로 유지되도록 선언한다”는 점이다.

3. Kubernetes Service로 안정적인 접근점 제공

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 3000

이제 다른 component는 다음 이름으로 접근할 수 있다.

http://api

4. 장애 발생 시 Kubernetes가 상태 조정

desired: 3
actual: 2
action: create 1 new Pod

Kubernetes의 핵심 가치는 바로 이 상태 조정에 있다.


Docker만으로 충분한 경우와 Kubernetes가 필요한 경우

상황Docker만으로 충분할 가능성Kubernetes가 유리한 경우
local development높음낮음
단일 container test높음낮음
단일 서버에 작은 서비스 배포중간낮음~중간
여러 microservice 운영낮음높음
여러 node에 workload 분산낮음높음
replica 자동 복구낮음높음
rolling update / rollback낮음~중간높음
service discovery낮음높음
autoscaling낮음높음
production observability 연계낮음~중간높음

Kubernetes를 도입해야 하는 기준은 “container를 쓰는가?”가 아니다.
기준은 “운영 복잡도가 container 단위 명령으로 감당 가능한 수준을 넘었는가?”에 가깝다.


자칫 실수하기 쉬운 부분

1. Docker와 Kubernetes를 경쟁 관계로 이해하는 것

Docker와 Kubernetes는 계층이 다르다.

Docker:
  image build와 개별 container 실행

Kubernetes:
  containerized workload의 cluster orchestration

따라서 “Docker vs Kubernetes”가 아니라 “Docker + Kubernetes workflow”로 보는 편이 더 정확하다.

2. Kubernetes가 애플리케이션 문제를 자동으로 해결한다고 생각하는 것

Kubernetes는 container를 재시작하고 replica를 맞출 수 있지만, application bug를 수정하지는 않는다.

가능한 것:
  죽은 Pod 재생성
  replica 수 유지
  unhealthy endpoint 제외
  rollout 관리

불가능한 것:
  잘못된 business logic 수정
  memory leak 원인 제거
  database query 최적화
  외부 API 장애 자체 해결

3. Pod IP에 직접 의존하는 것

Pod IP는 고정된 식별자로 사용하면 안 된다.
Kubernetes에서는 Service name을 기준으로 통신해야 한다.

피해야 할 방식:
  http://10.244.2.15:3000

권장 방식:
  http://api-service

4. YAML을 단순 설정 파일로만 보는 것

Kubernetes YAML은 단순 실행 옵션이 아니라 desired state 선언이다.

명령형 사고:
  이 container를 지금 실행해라.

선언형 사고:
  이 workload가 항상 이런 상태가 되게 유지해라.

이 차이를 이해해야 Deployment, ReplicaSet, Service, rollout, self-healing이 자연스럽게 연결된다.


검증 포인트

Docker와 Kubernetes의 역할을 실제 환경에서 확인할 때는 다음 관점으로 검증할 수 있다.

검증 항목확인 방법
image가 정상적으로 build되는가docker build
image가 registry에 push되는가docker push
Kubernetes가 image를 pull할 수 있는가kubectl describe pod <pod-name>
replica 수가 유지되는가kubectl get deployment
Pod 장애 시 재생성되는가Pod 삭제 후 kubectl get pods -w
Service가 Pod 집합을 바라보는가kubectl get endpoints
rollout 상태가 정상인가kubectl rollout status deployment/<name>
application log가 정상인가kubectl logs <pod-name>

예시 명령은 다음과 같다.

kubectl get deployments
kubectl get pods -o wide
kubectl get svc
kubectl get endpoints
kubectl describe pod <pod-name>
kubectl logs <pod-name>
kubectl rollout status deployment/<deployment-name>

정리

Docker와 Kubernetes를 구분하는 가장 좋은 기준은 다음이다.

Docker:
  애플리케이션을 container image로 만들고 실행한다.

Kubernetes:
  그 containerized application을 cluster에서 원하는 상태로 유지한다.

Docker는 개발과 packaging 단계에서 매우 강력하고, Kubernetes는 production 운영 단계에서 강력하다.

따라서 둘 중 하나를 선택하는 문제가 아니라, 다음 흐름으로 이해하는 것이 좋다.

Source Code

Dockerfile

Docker Image

Registry

Kubernetes Deployment

Pod Replica

Service

Self-Healing / Scaling / Rolling Update

운영 규모가 작을 때는 Docker만으로 충분할 수 있다.
그러나 여러 microservice, 여러 node, 자동 복구, 안정적인 service discovery, 무중단 배포가 필요해지면 Kubernetes의 orchestration model이 필요해진다.