Chapter 3

파드: 쿠버네티스에서 컨테이너 실행

  • 3.1 Pod 소개
  • 3.2 YAML/JSON으로 Pod 만들기
  • 3.3 라벨로 Pod 조직하기
  • 3.4 라벨 셀렉터로 Pod 부분 집합 다루기
  • 3.5 라벨과 셀렉터로 스케줄링 제약하기
  • 3.6 어노테이션
  • 3.7 네임스페이스로 리소스 그루핑
  • 3.8 Pod 멈추기와 삭제하기

3장은 쿠버네티스에서 가장 기본이 되는 객체인 Pod를 깊게 파는 장이야. 2장에서 kubectl run으로 대충 띄웠던 걸 이번엔 YAML로 직접 쓰고, 라벨과 셀렉터로 조직화하고, 네임스페이스로 격리하는 법까지 봐. Pod를 제대로 이해해두면 이후에 나오는 모든 컨트롤러(RC, RS, Deployment, StatefulSet)가 왜 그렇게 생겼는지가 쉽게 이해되거든.

Pod가 왜 필요하냐면, 컨테이너 안에 프로세스 하나만 넣는 게 원칙이기 때문이야. 그러면 같이 떠야 하는 여러 프로세스는 어떻게 묶지? 그걸 위한 한 단계 위의 구성 요소가 Pod야. Pod는 긴밀하게 관련된 컨테이너들의 묶음이고, 같은 Pod의 컨테이너들은 거의 같은 환경에서 돌지만 그래도 약간은 격리돼 있어. 구체적으로 보면, Network와 UTS 네임스페이스를 공유해서 같은 IP, 같은 호스트명, 같은 네트워크 인터페이스를 보고, IPC 네임스페이스도 공유해서 IPC로 서로 통신할 수 있어. PID 네임스페이스는 기본값에선 분리돼 있고(최신 버전에선 옵션으로 공유 가능), 파일시스템은 격리돼서 기본적으론 각자 자기 이미지의 파일시스템만 봐. 공유하려면 Volume을 써야 하는데 그건 6장에서 다뤄. 그리고 같은 Pod 컨테이너들은 같은 네트워크 네임스페이스를 공유하니까 같은 포트를 두고 충돌할 수 있어. 같은 Pod 안에서는 서로 localhost로 통신 가능하고.

Pod 사이의 네트워크는 더 재밌어. 쿠버네티스는 모든 Pod가 NAT 없이 서로의 IP로 직접 통신할 수 있는 플랫 네트워크를 가정해. Pod가 어느 노드에 있든 상관없이, 모든 Pod가 같은 LAN에 있는 컴퓨터처럼 서로를 봐. 이건 실제 네트워크 토폴로지와 무관하게 그 위에 SDN(software-defined network)을 올려서 구현하는 거야. Calico, Flannel, Cilium 같은 CNI 플러그인이 여기서 일하지.

여기서 자연스럽게 따라오는 질문이 "멀티티어 앱의 프론트와 DB를 한 Pod에 넣을까 두 Pod로 나눌까?" 인데, 답은 나누는 게 맞다야. 이유는 세 가지. 첫째 자원 활용 측면 — 한 Pod는 항상 한 노드에 떠. 둘을 한 Pod에 넣으면 나머지 노드들이 놀게 돼. 둘째 스케일링 단위 — Pod가 스케일링의 단위인데, 프론트는 5개 DB는 1개처럼 따로 스케일해야 하는 경우가 대부분이거든. 같이 있으면 그게 안 돼. 셋째 수명과 역할 — 프론트와 DB는 수명과 업데이트 주기가 완전히 다른데, 한 Pod에 묶으면 한쪽 바꿀 때마다 다른 쪽도 재시작돼. 그럼 언제 여러 컨테이너를 한 Pod에 넣냐면, 메인 컨테이너 + 보조 컨테이너(사이드카) 패턴일 때야. 웹서버 컨테이너랑 그 디렉터리에 외부에서 콘텐츠를 주기적으로 받아오는 컨테이너, 로그 로테이터, 데이터 프로세서, 통신 어댑터 같은 거. 판단 기준 세 가지를 두면 돼 — 꼭 같은 호스트에서 돌아야 하는가, 전체를 하나의 단위로 볼 수 있는가, 같이 스케일되어야 하는가. 이 중 하나라도 "아니오"면 Pod를 나눠.

kubectl run은 편하지만 설정의 일부만 건드릴 수 있어. 실전에선 YAML 매니페스트로 정의해서 버전 관리해. 이게 GitOps의 출발점이야. Pod YAML의 뼈대는 apiVersion, kind: Pod, metadata(이름, 네임스페이스, 라벨, 어노테이션), spec(원하는 상태 — 컨테이너, 볼륨 등) 이렇게 구성되고, 거기에 런타임에 쿠버네티스가 채우는 status 필드가 더해져. 사용자가 status를 쓸 일은 없어. 이 metadata/spec/status 세 부분 구조는 거의 모든 쿠버네티스 리소스에 공통이라서 한 번 익혀두면 다른 리소스도 금방 읽혀. 필드가 뭐가 있는지 모를 땐 kubectl explain podskubectl explain pod.spec 같이 드릴다운으로 볼 수 있어. 문서 안 뒤져봐도 돼. kubectl create -f kubia-manual.yaml로 생성하고, kubectl get po kubia-manual -o yaml로 실제 등록된 전체 정의를 볼 수 있어. 참고로 컨테이너 포트 필드는 정보 제공용일 뿐이야 — 안 적어도 접속은 되지만 써두면 누가 이 Pod를 쓸 때 명시적으로 보여. 로그는 kubectl logs <pod>로 보고 멀티 컨테이너면 -c <container>로 지정. SSH로 노드 들어가서 docker logs 할 필요 없어. 디버깅 목적으로 특정 Pod에 직접 붙고 싶으면 kubectl port-forward kubia-manual 8888:8080. 로컬 8888로 접속하면 Pod의 8080으로 포워딩돼. Service 거치지 않고 개별 Pod를 찔러볼 때 유용해.

마이크로서비스 환경에선 Pod가 금방 수십, 수백 개로 늘어. 이걸 조직화할 방법이 없으면 kubectl get pods가 의미 없는 목록 폭탄이 되거든. 쿠버네티스의 답은 라벨이야. 라벨은 그냥 key-value 쌍이야. 리소스에 여러 개 붙일 수 있고, 라벨 셀렉터로 필터링할 수 있어. 예를 들어 app=ui, app=product-catalog 같은 식으로 어떤 마이크로서비스에 속하는지 표시하고, rel=stable, rel=beta, rel=canary 같이 어떤 릴리스인지 표시해. 이 두 라벨만 있어도 "UI의 stable 버전" 같은 2차원 그리드로 Pod를 볼 수 있어. YAML에서는 metadata.labels 아래에 적고, 이미 떠 있는 Pod에 추가하려면 kubectl label po kubia-manual creation_method=manual. 기존 값 바꿀 땐 --overwrite. 보기는 kubectl get po --show-labels-L creation_method,env로 특정 라벨을 컬럼으로 뽑을 수 있어.

라벨이 진짜 힘을 내는 건 셀렉터와 조합될 때야. 필터 조건은 네 종류 — creation_method=manual처럼 키·값 일치, env처럼 키가 있기만 하면, !env처럼 키가 없음, env in (prod,devel)이나 env notin (...) 같은 집합 연산. 콤마로 연결하면 AND 조건이라 app=pc,rel=beta는 두 조건 모두 만족해야 해. 중요한 건 라벨 셀렉터는 kubectl만 쓰는 게 아니라는 것이야. 뒤에 나올 ReplicationController, Service 같은 게 "내가 관리할 Pod"를 고를 때 전부 라벨 셀렉터를 써. 그래서 라벨을 일관되게 붙이는 규칙이 시스템 전체 설계의 축이 돼.

기본적으로는 스케줄러한테 "아무 데나 띄워" 하고 두는 게 맞아. 그게 쿠버네티스가 인프라를 추상화해주는 이유니까. 근데 하드웨어가 이질적이면 얘기가 달라져 — "이 Pod는 SSD 노드에만", "이 Pod는 GPU 노드에만" 같은 거. 이때 방법은 노드에 라벨을 달고 Pod에 nodeSelector를 거는 것이야. 노드에 직접 노드 이름으로 박지 않는 게 포인트인데, 이름으로 박으면 그 노드가 죽으면 Pod가 영영 못 떠. 라벨로 조건을 기술해야 탄력적이지. kubectl label node gke-kubia-... gpu=true로 라벨 달고, Pod spec에 nodeSelector: { gpu: "true" }를 두면 그 라벨이 붙은 노드 중에서만 스케줄러가 골라. 더 정교한 제약(taint, affinity 등)은 16장에서 다뤄.

어노테이션은 라벨과 비슷한 key-value 쌍이지만 식별·그루핑 용도가 아니야. 어노테이션 셀렉터 같은 건 없어. 대신 라벨보다 훨씬 크고 자유로운 정보를 담을 수 있어. 총 256KB까지. 용도는 도구가 읽는 메타데이터(kubernetes.io/created-by 같은 시스템 어노테이션), 새 기능의 알파/베타 단계에서 필드 대신 임시로 쓰는 용도, "이 객체 만든 사람 누구", "무슨 이슈 때문에 만든 건지" 같은 설명. 추가는 kubectl annotate pod kubia-manual mycompany.com/someannotation="foo bar"로 하고, 키에 도메인 프리픽스를 붙이는 게 관례야. 여러 도구가 서로 어노테이션 키를 덮어쓰지 않게.

라벨은 겹칠 수 있고 같은 kubectl 명령에 항상 모든 Pod가 잡혀. 진짜로 격리된 그룹을 만들고 싶으면 네임스페이스를 써. 네임스페이스는 리소스 이름의 유니크 스코프이기도 해서, 같은 이름의 Pod를 다른 네임스페이스에 둘 수 있어. 용도는 멀티테넌시(테넌트마다 네임스페이스 분리), 환경 분리(dev/staging/prod), RBAC과 ResourceQuota 적용 단위("이 네임스페이스에선 최대 10코어까지") 같은 거야. kubectl get ns로 목록 보고, 생성은 kind: Namespace로 YAML 만들거나 kubectl create namespace custom-namespace. 해당 네임스페이스에 리소스 만들 땐 -n custom-namespace를 붙이거나 Pod metadata에 namespace를 박아. 한 가지 주의할 점이 있어 — 네임스페이스는 네트워크 격리를 주지 않아. 기본값에선 다른 네임스페이스의 Pod와도 서로 통신 가능해. 진짜 네트워크 격리는 NetworkPolicy로 따로 걸어야 해. 이건 13장이야.

마지막으로 Pod 삭제. kubectl delete po kubia-manual로 단건 삭제하고, 라벨 셀렉터로 kubectl delete po -l creation_method=manual처럼 여러 개를 한 번에 삭제할 수 있어. 네임스페이스 통째로 밀고 싶으면 kubectl delete ns custom-namespace. "이 네임스페이스의 모든 Pod"는 kubectl delete po --all이고, "네임스페이스는 두고 리소스만 통째로 밀기"는 kubectl delete all --all. 다만 all이 진짜 전부는 아니라서, 어떤 리소스는 명시적으로 지정해야 지워져 (Secret 같은 건 따로). 그리고 Pod를 수동으로 만들었으면 지우면 끝이지만, ReplicationController가 관리 중이면 지워도 바로 다시 생겨. desired state가 "N개 있어야 해"로 박혀있기 때문이야. 이건 4장에서 본격적으로 다뤄.


정리

3장 읽고 기억할 거 세 가지:

  1. Pod는 컨테이너의 묶음이고, 같은 Pod 안 컨테이너들은 Network/UTS/IPC 네임스페이스를 공유하지만 파일시스템은 격리된다. Pod를 설계할 때는 "꼭 같은 호스트여야 하는가, 같이 스케일되어야 하는가, 하나의 단위인가"를 기준으로 쪼갠다. 웹서버와 DB는 나눠야 하고, 웹서버와 로그 수집 사이드카는 같이 둔다.
  2. 라벨과 셀렉터는 쿠버네티스 조직화의 중심축이다. 단순한 key-value지만 Pod뿐 아니라 노드, 그리고 나중에 배울 RC/Service가 전부 라벨 셀렉터로 대상을 고른다. nodeSelector로 스케줄링 제약을 걸 때도 "노드 이름"이 아니라 "노드 라벨"로 기술해야 인프라 변화에 탄력적이다.
  3. 네임스페이스는 리소스 이름의 스코프이자 RBAC/쿼터 적용 단위지만 네트워크 격리는 기본 제공되지 않는다. 멀티테넌트 환경을 설계할 땐 네임스페이스만으로 격리됐다고 착각하지 말고 NetworkPolicy까지 같이 고려해야 한다.