Reference

이 시리즈는 ‘쿠버네티스 교과서’를 보고 정리한 내용

TL;DR

  • 파드: k8s의 컴퓨팅 기본 단위, 가상 IP를 가지며 컨테이너를 감싸는 래퍼
  • 컨트롤러 객체: desired state와 current state의 차이를 감지하고 자동 보정
  • 디플로이먼트: 파드를 관리하는 대표적 컨트롤러, 레이블 셀렉터로 관리 대상 결정
  • YAML 매니페스트로 리소스 정의, kubectl apply로 클러스터에 적용

1. 쿠버네티스란

왜 k8s가 필요한가

Docker 같은 컨테이너 기술로 애플리케이션을 패키징하고 실행할 수 있다. 하지만 컨테이너 수가 늘어나고 여러 서버에 분산 배포해야 하는 상황이 되면 컨테이너만으로는 해결하기 어려운 문제들이 생긴다.

  • 서버 한 대가 죽으면 그 위의 컨테이너도 전부 죽는다 — 누가 다른 서버에서 다시 띄워줄 것인가?
  • 컨테이너가 수백 개가 되면 어떤 서버에 어떤 컨테이너를 배치할지 누가 결정하는가?
  • 새 버전을 배포할 때 기존 컨테이너를 하나씩 교체하면서 무중단으로 운영할 수 있는가?

이런 문제를 해결하기 위해 컨테이너 오케스트레이션 플랫폼이 필요하다. 쿠버네티스(k8s)가 바로 그 역할을 한다.

k8s의 핵심 개념: API + 클러스터

k8s의 핵심은 API클러스터 두 가지다.

클러스터컨테이너 런타임이 동작하는 여러 대의 서버(노드)가 모여 하나의 논리적 단위를 구성한 것이다. 클러스터의 노드는 크게 두 종류로 나뉜다.

  • Control Plane (마스터 노드): k8s API 서버, 스케줄러, 컨트롤러 매니저 등이 동작한다. 클러스터 전체를 관장하는 두뇌 역할이다
  • Worker Node: 실제 애플리케이션 컨테이너가 실행되는 노드다. 각 노드에는 kubelet, kube-proxy, 컨테이너 런타임이 설치되어 있다

더 자세한 구조

Control Plane의 구성요소(kube-api-server, etcd, kube-scheduler, kube-controller-manager)와 Worker Node의 구성요소(kubelet, kube-proxy, CRI)에 대한 상세 설명은 k8s가 필요한 이유 참고.

API는 클러스터와 소통하는 유일한 창구다. 사용자는 YAML로 원하는 상태를 정의하고 API에 전달한다. k8s는 이 정의를 받아 필요한 컨테이너를 추가하거나 제거하면서 운영한다.

# kubectl은 k8s API와 통신하는 CLI 도구다
kubectl get nodes  # 클러스터에 속한 노드 목록 확인
선언적 관리와 Self-healing

k8s의 핵심 철학은 선언적 관리다. “컨테이너 3개를 실행해라”가 아니라 “이 애플리케이션은 항상 3개의 복제본이 실행 중이어야 한다”고 선언한다. k8s는 현재 상태가 이 선언과 다르면 자동으로 바로잡는다.

이것이 self-healing의 기반이다.

  • 노드가 고장나면 해당 노드의 컨테이너는 다른 노드에서 대체 실행된다
  • 컨테이너에 문제가 생기면 자동 재시작된다
  • 부하가 높아지면 해당 컴포넌트의 컨테이너를 추가 실행한다
클러스터가 제공하는 것

클러스터는 애플리케이션 운영에 필요한 인프라를 내장하고 있다.

  • 분산 데이터베이스(etcd): 앱 구성 정보, API 키, DB 접속 비밀번호 등을 저장
  • 스토리지: 컨테이너 외부에 데이터를 저장하고 고가용성을 확보
  • 네트워크/트래픽 관리: 파드 간 통신, 외부 트래픽 라우팅, 로드밸런싱
애플리케이션 매니페스트

YAML 파일로 작성하는 애플리케이션 정의를 매니페스트라고 부른다. 매니페스트는 여러 k8s 리소스로 구성된다.

  • Pod: 컨테이너를 감싸는 최소 실행 단위
  • Deployment: 파드의 생성과 업데이트를 관리
  • ReplicaSet: 파드의 복제본 수를 유지
  • Service: 파드에 고정된 네트워크 접점 제공
  • ConfigMap / Secret: 설정값과 민감 정보 관리
  • Volume: 데이터 영속성 제공

2. 파드(Pod)

파드는 k8s에서 컴퓨팅의 기본 단위다. 클러스터를 이루는 노드 중 하나에서 실행된다.

파드란 무엇인가

파드는 하나 이상의 컨테이너를 감싸는 래퍼다.

  • 파드는 k8s가 부여한 자신만의 가상 IP 주소를 가진다
  • 이 IP로 가상 네트워크에 존재하는 다른 파드, 심지어 다른 노드의 파드와도 통신할 수 있다
  • 하나의 파드에 여러 개의 컨테이너를 포함할 수 있고, 같은 파드 내 컨테이너는 네트워크를 공유하여 localhost로 통신한다
  • 고급 옵션을 건드리지 않으면 보통 파드 하나에 컨테이너 하나가 실행된다
kubectl run으로 파드 실행해보기

간단한 파드는 YAML 없이 kubectl 명령어로 바로 실행할 수 있다.

# 파드 하나 실행
kubectl run hello-kiamol --image=kiamol/ch02-hello-kiamol
 
# 파드 목록 확인
kubectl get pods
 
# 파드의 상세 정보 확인 — 어느 노드에 배정됐는지, 현재 상태 등
kubectl describe pod hello-kiamol
 
# 파드가 어느 노드에서 실행 중인지 확인
kubectl get pods -o wide
파드와 컨테이너의 관계

k8s는 직접 컨테이너를 실행하지 않는다.

  • 해당 노드에 설치된 컨테이너 런타임(Docker, containerd 등)에 실행을 맡긴다
  • k8s는 파드를 관리하고, 컨테이너의 실제 실행은 k8s 외부의 런타임이 담당한다

파드는 생성 시 한 노드에 배정되고, 해당 노드가 파드를 관리한다. 이때 **CRI(Container Runtime Interface)**라는 공통 API를 통해 컨테이너 생성, 삭제, 정보 확인 등을 표준화한다.

CRI가 왜 필요한가

k8s 초기에는 Docker만 지원했지만, 이후 containerd, CRI-O 등 다양한 런타임이 등장했다. CRI는 k8s가 어떤 런타임이든 동일한 방식으로 컨테이너를 관리할 수 있게 해주는 표준 인터페이스다. 자세한 내용은 k8s가 필요한 이유 참고.

Docker 명령어로 파드 내 컨테이너를 삭제하면 곧바로 다시 생성된다. 이것이 self-healing의 첫 단계다.

파드만으로는 부족한 이유

파드는 너무 단순한 객체다. 직접 파드를 실행하면 다음 문제가 있다.

  • 각 파드는 서로 다른 노드에 배정되는데, 노드가 고장나면 파드가 유실된다
  • k8s는 단독 파드를 새로 대체하지 않는다 — 파드 자체에는 복구 메커니즘이 없다
  • 여러 파드를 실행해 고가용성을 확보하려 해도 노드 배치를 직접 관리해야 한다

그래서 일반적으로 파드를 직접 실행할 일은 없다. 파드를 관리할 컨트롤러 객체를 따로 만든다.


3. 디플로이먼트(Deployment)와 컨트롤러 객체

컨트롤러 객체: desired state vs current state

컨트롤러 객체는 다른 리소스를 관리하는 k8s 리소스다. 컨트롤러의 핵심 동작 원리는 바람직한 상태(desired state)와 현재 상태(current state)를 비교하고, 차이가 있으면 바로잡는 것이다.

이 패턴은 k8s 전체를 관통하는 핵심 개념이다.

  • “파드 3개가 실행 중이어야 한다” (desired state)
  • “현재 파드가 2개뿐이다” (current state)
  • → 컨트롤러가 파드 1개를 새로 생성한다 (reconciliation)

컨트롤러는 k8s API와 연동하여 이 제어 루프를 끊임없이 반복한다.

디플로이먼트의 역할

디플로이먼트는 파드를 주로 관리하는 대표적인 컨트롤러 객체다. 디플로이먼트만 생성해도 정의한 정보대로 파드를 자동으로 만들어준다.

# 디플로이먼트 생성 — 정의한 정보대로 파드가 자동 생성된다
kubectl create deployment hello-kiamol-2 --image=kiamol/ch02-hello-kiamol
 
# 생성된 디플로이먼트 확인
kubectl get deploy
 
# 디플로이먼트가 만든 파드 확인
kubectl get pods
레이블 셀렉터

디플로이먼트는 파드와 직접적으로 관계를 갖지 않는다. 대신 레이블 셀렉터와 일치하는 파드가 있으면 된다.

모든 k8s 리소스는 key-value 형태의 레이블을 가지고, 이를 통해 리소스 간 관계를 연결한다.

# 디플로이먼트가 파드에 자동으로 부여한 레이블 확인
kubectl get deploy hello-kiamol-2 -o jsonpath='{.spec.template.metadata.labels}'
# 출력: {"app": "hello-kiamol-2"}
 
# 해당 레이블로 파드 조회
kubectl get pods -l app=hello-kiamol-2
레이블 조작을 활용한 디버깅

레이블을 강제로 수정하면 디플로이먼트가 해당 파드를 더 이상 인지하지 못한다. 이 특성을 디버깅에 활용할 수 있다.

# 1. 레이블 변경 — 디플로이먼트 관리 대상에서 이탈
kubectl label pods -l app=hello-kiamol-2 --overwrite app=hello-kiamol-x
 
# 2. 파드 목록 확인 — 기존 파드(이탈) + 새로 생성된 파드
kubectl get pods -l app -o custom-columns=NAME:metadata.name,LABELS:metadata.labels

무슨 일이 벌어지는지 정리하면:

  1. 레이블을 바꾸면 디플로이먼트는 “관리 대상 파드가 0개”라고 판단한다
  2. desired state는 “파드 1개”이므로 새 파드를 생성한다
  3. 기존 파드는 레이블이 바뀌었을 뿐 여전히 실행 중이다 — 이 파드에 직접 접속해서 문제를 확인할 수 있다
  4. 다시 원래 레이블로 복원하면 파드가 2개가 되어 디플로이먼트가 1개를 삭제한다
# 3. 이탈된 파드에 직접 접속해서 디버깅
kubectl exec -it <이탈된-파드-이> -- sh
 
# 4. 원래 레이블로 복원 — 파드가 2개가 되어 하나 삭제됨
kubectl label pods -l app=hello-kiamol-x --overwrite app=hello-kiamol-2

4. YAML 매니페스트로 배포 정의하기

kubectl run, kubectl create deployment 같은 명령어만으로는 간단한 앱밖에 배포할 수 없다. 본격적인 배포에는 YAML 매니페스트가 필요하다.

Pod YAML 구조
# 정의하려는 k8s API 버전과 리소스 유형
apiVersion: v1
kind: Pod
 
# 리소스의 메타데이터 — 이름(필수)과 레이블
metadata:
  name: hello-kiamol-3
 
# 스펙 — 리소스의 실제 정의 내용
# 파드의 경우 실행할 컨테이너를 정의한다
spec:
  containers:
    - name: web
      image: kiamol/ch02-hello-kiamol

YAML은 크게 4개 블록으로 구성된다.

  • apiVersion: 이 리소스가 속한 k8s API 버전
  • kind: 리소스 유형 (Pod, Deployment, Service 등)
  • metadata: 리소스의 이름(필수)과 레이블
  • spec: 리소스의 실제 정의 내용
Deployment YAML 구조
apiVersion: apps/v1
kind: Deployment
 
metadata:
  name: hello-kiamol-4
 
spec:
  # 디플로이먼트가 관리 대상을 결정하는 레이블 셀렉터
  selector:
    matchLabels:
      app: hello-kiamol-4
 
  # 파드를 만들 때 사용하는 템플릿
  template:
    # 디플로이먼트 속 파드는 이름이 없고, 레이블 셀렉터와 일치하는 레이블을 지정
    metadata:
      labels:
        app: hello-kiamol-4
    spec:
      containers:
        - name: web
          image: kiamol/ch02-hello-kiamol
selector와 template.labels를 분리하는 이유
  • selector: 디플로이먼트가 관리할 파드를 고르는 조건. 안정적이고 잘 바뀌지 않는 최소 레이블만 지정한다
  • template.metadata.labels: 새로 생성할 파드에 실제로 붙일 레이블 집합. selector 조건을 만족하면서 추가 레이블도 붙일 수 있다
selector 조건 ⊆ template.labels

추가 레이블의 예시:

  • 고정적: team=platform, app.kubernetes.io/part-of=kiamol
  • 가변적: version=v2, release=canary

실무에서는 selector에는 안정적인 최소 레이블만, template.labels에는 그 레이블 + 운영/분류용 추가 레이블을 넣는다.

kubectl apply로 적용하기

YAML 파일을 작성했으면 kubectl apply 명령어로 클러스터에 적용한다.

# YAML 파일로 리소스 생성/업데이트
kubectl apply -f pod.yaml
kubectl apply -f deployment.yaml
 
# 디렉터리 내 모든 YAML 적용
kubectl apply -f ./manifests/

kubectl create vs kubectl apply

  • kubectl create: 리소스를 새로 생성만 한다. 이미 존재하면 에러가 난다
  • kubectl apply: 리소스가 없으면 생성하고, 이미 있으면 변경된 부분만 업데이트한다
  • 실무에서는 **kubectl apply**를 주로 사용한다. 선언적 관리에 적합하기 때문이다

5. 파드 접근과 리소스 관리

파드에서 실행 중인 앱에 접근하기
# 컨테이너에 원격 접속
kubectl exec -it hello-kiamol -- sh
 
# 로그 확인
kubectl logs hello-kiamol
 
# 레이블 셀렉터로 디플로이먼트가 관리하는 파드의 로그 확인
kubectl logs -l app=hello-kiamol-4
 
# 파드의 상세 정보 확인 — 이벤트, 상태, 조건 등 디버깅에 필수
kubectl describe pod hello-kiamol
 
# 파드의 파일을 로컬로 복사
kubectl cp hello-kiamol:/usr/share/nginx/html/index.html /tmp/kiamol/ch02/index.html
Port-forwarding

파드는 클러스터 내부의 가상 네트워크에 존재하기 때문에 호스트에서 직접 접근할 수 없다. kubectl port-forward로 로컬 포트와 파드의 포트를 연결해야 한다.

# 로컬 8080 포트를 파드의 80 포트로 연결
kubectl port-forward pod/hello-kiamol 8080:80
 
# 디플로이먼트 대상으로도 가능 — 관리 중인 파드 중 하나를 자동 선택
kubectl port-forward deployment/hello-kiamol-2 8080:80

이후 브라우저에서 http://localhost:8080으로 접근하면 파드 내 애플리케이션을 확인할 수 있다.

대상별 동작 차이
  • pod/…: 해당 파드 하나에 고정
  • deployment/…: 디플로이먼트의 파드 중 하나를 골라서 연결
  • service/…: 서비스가 고른 파드 하나에 연결

어떤 대상을 지정하든 최종적으로 파드 하나에 연결되는 것이지, 로드밸런서처럼 분산되지 않는다. 선택된 파드가 종료되면 port-forward 세션도 끝난다.

멀티 컨테이너 파드에서의 port-forward

파드 안의 컨테이너들은 IP와 포트 공간을 공유한다. port-forward는 컨테이너를 고르는 게 아니라 파드의 네트워크 포트로 연결되고, 해당 포트를 리슨하는 프로세스가 응답한다.

# 앱 컨테이너가 80, 사이드카가 15000을 리슨하는 경우
kubectl port-forward pod/mypod 8080:80      # 앱 쪽
kubectl port-forward pod/mypod 15000:15000  # 사이드카 쪽

kubectl port-forward에 컨테이너 이름을 지정하는 옵션은 없다. 보통 한 파드 안에서 같은 포트를 여러 컨테이너가 동시에 쓸 수 없으므로 포트 번호로 구분한다.

리소스 삭제 시 동작

컨트롤러 객체가 관리하는 리소스를 직접 삭제하면 이를 대체하는 새로운 리소스가 자동 생성된다. 이것이 desired state를 유지하는 컨트롤러의 동작이다.

# 파드 목록 확인
kubectl get pods
 
# 파드 전체 삭제
kubectl delete pods --all
# → 디플로이먼트로 생성한 파드(이름 뒤에 hash가 있는)는 바로 재생성된다
# → kubectl run으로 직접 만든 파드는 재생성되지 않는다
 
# 재생성 확인
kubectl get pods

파드를 완전히 제거하려면 해당 파드를 관리하는 컨트롤러 객체를 삭제해야 한다.

# 디플로이먼트 확인
kubectl get deploy
 
# 디플로이먼트 삭제 — 관리하던 파드도 함께 삭제된다
kubectl delete deploy --all

정리

  • kubectl run으로 만든 파드: 삭제하면 영구 삭제. 아무도 재생성하지 않는다
  • 디플로이먼트가 만든 파드: 삭제해도 디플로이먼트가 즉시 재생성한다. 파드를 없애려면 디플로이먼트를 삭제해야 한다