Container Orchestration


Container Orchestration 이란 복잡한 컨테이너 환경을 효과적으로 관리하기 위한 도구로 쿠버네티스가 주로 쓰인다.

  • Cluster = Master Node 가 중앙제어, 다수의 Worker Node 가 클러스터를 이루어 서로 통신하며 작동
  • State = Desired State 선언 시 관리자의 개입 없이 자동으로 상태를 유지
  • Scheduling = Container 를 배치할 적합한 Worker Node 를 찾아 배포
  • Rollout Rollback = 배포 버전관리
  • Service Discovery = 서비스 등록 및 조회
  • Volume = NFS, EBS 등 다양한 스토리지 마운팅

Kubernetes Architecture


K8s 는 Master Node 와 Worker Node 의 집합으로 이루어진다.

Master Node 는 Worker Node 들을 Manage, Plan, Schedule, Monitor 하는 역할을 수행하기 위해 아래와 같은 Component 들을 가진다.

Worker Node 는 Containerized Application 이 실행되는 Node 로, 이를 위해 아래와 같은 Component 들을 가진다.

Docker vs containerd


  • 2013년 Docker 출현으로 인한 컨테이너 기술 부상 이후 타 경쟁사 역시 각자 Container Runtime 을 개발하기 시작했고 이로 인해 표준화가 필요해졌다. 2016년 Docker 와 CoreOS 가 주축으로 imagespecruntimespec 등을 포함한 Open Container Initiative, OCI 를 출범하여 Container Runtime 의 표준화를 재정했다. 이후 OCI 스펙에 맞춰 각 기업들이 Container Runtime 을 발전시켰고, Docker 의 경우 OCI 표준에 맞춰 개발한 Container Runtime 이 runc 이다.
  • K8s 의 경우 초창기엔 Docker 만을 유일한 Container Runtime 으로 지원했었는데, OCI 등장 이후 다양한 Container Runtime 들이 등장하면서 각 Container Runtime 에 따라 kubelet 이 관리해야 하는 방법을 따로 유지보수해야하는 어려움이 발생하기 시작했다. 이런 kubelet 유지보수 문제를 해결하면서 동시에 다양한 Container Runtime 들을 지원하기 위해 kubelet 에서 Container Runtime 을 관리할 수 있는 인터페이스인 Container Runtime Interface, CRI 가 등장하게된다.
  • 초창기 Docker 는 monolithic 한 구조로 Docker Daemon 하나에서 Docker Client, Docker API, Container Runtime, Image Build 등을 포함하고 있었고, 이는 kubelet 이 Docker 의 Container Runtime 을 사용할 때 CRI 표준에 부합하는 dockershim 이라는 컴포넌트를 추가적으로 사용해야했다.
  • 이후 K8s 의 dockershim 지원이 중지되고, containerd 가 Docker 에서 분리되면서, CRI 스펙에 맞추기 위한 cri plugin 등을 containerd 내부적으로 추가하여 containerdkubelet 에 의해 관리될 수 있게되었다.
  • runc 가 컨테이너를 실행하는 실질적인 주체라면, containerd 는 한 단계 위에서 이미지 저장, 네트워킹, 스냅샷 및 기타 관리 작업 등의 기능들을 포함한 고수준의 Container Runtime 으로, 내부적으로 runc 를 사용하기에 OCI 와 CRI 표준을 모두 준수할 수 있는 Container Runtime 이 된 것이다.
  • We can use the 2 CLIs below instead of Docker to work with K8s
    • nerdctl = for general purpose from the containerd community
    • crictl = for debugging from the K8s community (works with all CRI-compatible container runtimes)

etcd


  • etcd is a distributed reliable key-value store that is Simple, Secure & Fast
  • K8s 의 모든 상태와 데이터를 저장
  • Key-Value 형태로 데이터를 저장
  • 분산 시스템으로 구성하여 고가용성 확보
  • TTL, watch 등 부가 기능 제공

kube-apiserver


  • 쿠버네티스의 상태를 바꾸거나 조회
  • etcd 와 유일하게 통신하는 모듈
  • REST API 형태로 제공
  • 요청에 대한 권한 체크
  • 수평적 확장 가능

Installing kube-apiserver

wget https://storage.googleapis.com/kubernetes-release/release/v1.xx.0/bin/linux/amd64/kube-apiserver
  • kubeadm 으로 설치할 경우 Pod 의 형태로 실행된다.

kube-controller-manager


  • 다양한 Controller 가 존재
    • A controller is a process that continuously monitors the state of the components within the system and works towards bringing the whole system to the desired functioning state
    • Replication Controller, Node Controller, Endpoint Controller, …
    • 끊임 없이 상태를 체크하고 원하는 상태를 유지
    • 복잡성을 낮추기 위해 하나의 프로세스로 실행

Installing kube-controller-manager

wget https://storage.googleapis.com/kubernetes-release/release/v1.xx.0/bin/linux/amd64/kube-controller-manager
  • kubeadm 으로 설치할 경우 Pod 의 형태로 실행된다.

kube-scheduler


  • kube-scheduler 는 생성 요청된 Pod 가 어느 Node 에 배포되어야 하는지 확인하는 역할을 수행한다. Node 의 현재 상태와 Pod 의 요구사항을 체크하여 적절한 Node 를 찾는 작업만 수행할 뿐 Pod 를 생성하는 역할은 kubelet 이 수행한다.

How does it work?

  • Pod 에 필요한 리소스를 여분으로 가지고 있는 Node 를 확인하고 Pod 가 배치된 후 남은 리소스량을 기준으로 순위를 매겨 스케쥴링을 수행한다.

Installing kube-scheduler

wget https://storage.googleapis.com/kubernetes-release/release/v1.xx.0/bin/linux/amd64/kube-scheduler
  • kubeadm 으로 설치할 경우 Pod 의 형태로 실행된다.

kubelet


kubelet 은 Master Node 의 kube-apiserver 로 부터 Container 생성 요청을 받아 Worker Node 에 설치된 Container Runtime 을 이용해 Container 를 생성하는 역할을 한다. kubelet 은 자신이 위치한 Node 와 생성한 Pod 들을 모니터링하여 주기적으로 kube-apiserver 와 소통한다.

Installing kubelet

wget https://storage.googleapis.com/kubernetes-release/release/v1.xx.0/bin/linux/amd64/kubelet
  • kubeadm 으로 K8s Cluster 를 구축할 때 kubelet 은 설치되지 않으니 수동으로 Worker Node 에 kubelet 을 설치해주어야 한다. 때문에 kubelet 은 다른 Component 들과 다르게 Pod 이 아닌 Node 의 프로세스로서 실행된다.

kube-proxy


  • 네트워크 프록시와 부하 분산 역할을 하며 K8s Cluster 에 배포된 모든 Pod 간의 통신을 담당한다.
  • ServiceEndpointSlice 의 변경을 감시한다.
  • 성능상의 이유로 별도의 프록시 프로그램 대신 iptables / IPVS / eBPF 등을 사용하여 트래픽 라우팅 설정만 관리한다.

Installing kube-proxy

wget https://storage.googleapis.com/kubernetes-release/release/v1.xx.0/bin/linux/amd64/kube-proxy
  • kubeadm 으로 설치할 경우 Pod 의 형태로 실행된다.
  • 각 Node 마다 Pod 형태로 실행하기 위해 DaemonSet 으로 실행한다.

Kubernetes Cluster Networking


# kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
networking:
  podSubnet: "10.244.0.0/16"      # = --pod-network-cidr
  serviceSubnet: "10.96.0.0/12"   # = --service-cidr
  dnsDomain: "cluster.local"
 
# sudo kubeadm init --config kubeadm-config.yaml

파드 네트워크 설정을 위해 --cluster-cidr--service-cluster-ip-range 가 존재한다.

네트워크kubeadm CLI 플래그kubeadm config 필드실제 컴포넌트 플래그
Pod 네트워크--pod-network-cidrnetworking.podSubnetkube-controller-manager --cluster-cidr
Service 네트워크--service-cidrnetworking.serviceSubnetkube-apiserver --service-cluster-ip-range

Pod Network

  1. kubeadm init --pod-network-cidr=10.244.0.0/16 을 주면 kubeadmcontroller-manager 에 --cluster-cidr=10.244.0.0/16과 --allocate-node-cidrs=true를 세팅한다.
  2. controller-manager가 노드마다 그 풀에서 한 조각씩 떼어 node.spec.podCIDR에 박는다. 조각 크기는 --node-cidr-mask-size-ipv4(IPv4 기본 /24).
  • kube-controller-manager가 node IPAM 컨트롤러를 돌린다. 노드가 새로 붙으면 전체 Pod 풀에서 조각을 떼어 node.spec.podCIDR에 박는 게 이 컴포넌트다. 그래서 전체 풀을 알아야 한다(--cluster-cidr + --allocate-node-cidrs=true).
  • kube-apiserver가 ClusterIP를 배정한다. Service를 만들면 apiserver가 Service 풀에서 빈 IP를 골라 spec.clusterIP에 써넣는다. 그래서 --service-cluster-ip-range를 안다. /16에서 /24씩 떼니 최대 256개 노드, 노드당 254개 Pod가 가능하다. (실제로는 kubelet 기본 max-pods=110이 먼저 걸린다. describe node의 pods: 110이 그것이다.)
  • Pod IP는 실재한다. Pod 안 인터페이스(veth)에 실제로 붙어 있고, CNI가 노드 간 라우팅을 깔아준다. ping도 되고 패킷이 진짜 그 주소로 간다.
  • Service ClusterIP는 실재하지 않는다. 앞서 얘기한 대로 iptables 규칙의 매칭 키일 뿐이다.

podCIDR 는 기본값이 없다. CNI는 이 podCIDR 을 읽어 Pod에 IP를 준다. 그래서 CNI가 기대하는 대역과 어긋나면 앞서 겪은 것처럼 깨진다. flannel 기본 매니페스트가 10.244.0.0/16 을 전제하는 이유이고, 네가 그 값을 준 게 맞았던 이유다.

Service Network

ClusterIP를 배정할 가상 IP 풀이다. 기본값은 10.96.0.0/12 이고, kubeadm init에 아무것도 안 주면 이게 쓰인다. 관례상 배치도 정해져 있다:

  • 10.96.0.1 = kubernetes 서비스 (apiserver)
  • 10.96.0.10 = CoreDNS (kube-dns 서비스)

앞서 얘기했듯 이 IP들은 어디에도 실재하지 않는다. iptables DNAT 규칙의 매칭 키일 뿐이다.

Kubernetes Cluster Upgrade


Kubernetes Software Versions

K8s 는 일반적인 Software 처럼 Release Version 을 가지고 있으며, 기본적으로 최근 3개의 마이너 버전을 지원한다. kube-apiserver, controller-manager, kube-scheduler, kubelet, kube-proxy, kubectl 은 모두 동일한 버전으로 출시되며, etcd 와 core-dns 는 각각 다른 프로젝트이기에 독립적인 버전을 가지고 있다.

kube-apiserver 를 기준으로 다른 Component 들의 버전이 호환될 수 있는데 이는 아래와 같다.

  • 예를 들어 kube-apiserver 가 v1.10 일 경우,
  • controller-manager 와 kube-scheduler 는 v1.9 와 v1.10 이 호환되고,
  • kubelet 과 kube-proxy 는 v1.8 과 v1.9 와 v1.10 이 호환되고,
  • kubectl 은 v1.9 와 v1.10 과 v1.11 이 호환된다.

kubectl drain, cordon, uncordon

Security Patch 나 Software Upgrade 등의 유지보수 사유로 Node 를 제거해야 하는 경우, 해당 Node 에서 실행되고 있는 Pod 을 다른 Node 에 옮겨두는 방법을 택할 수 있다.

kube-controller-manager --pod-eviction-timeout=5m0s

ReplicaSet 으로 배포된 Pod 의 경우, 기본적으로 5분 뒤 다른 Node 에 재배포된다.

kubectl drain node-1

Node 내 Pod 를 다른 Node 로 옮기고 싶은 경우 drain 명령어를 통해 Pod 를 옮길 수 있다. 만약 ReplicaSet 으로 배포되지 않은 Pod 가 존재한다면 Error 가 발생한다.

kubectl uncordon node-1

Node 가 재시작 된 이후 Pod 가 해당 Node 에 Scheduling 될 수 있도록 uncordon 을 사용할 수 있고,

kubectl cordon node-2

반대로 cordon 명령어를 통해 Node 에 Pod 가 Scheduling 되는 것을 제한할 수 있다.

Kubernetes Cluster Upgrade Process

K8s Cluster 를 업그레이드할 때 EKS 나 AKS 같은 Managed-Service 를 사용한다면 간단하게 몇 번의 클릭으로 업그레이드가 가능하지만, 그렇지 않은 경우엔 kubeadm 툴을 활용해 Cluster 를 업그레이드할 수 있다.

# k8s apt repository 리스트 파일에서
vim /etc/apt/sources.list.d/kubernetes.list
 
# 아래로 변경
echo -e "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
 
# apt 업데이트 후 kubeadm 최신 버전 확인
apt update
apt-cache madison kubeadm
 
apt-get install kubeadm=1.32.0-1.1
 
kubeadm upgrade plan v1.32.0
kubeadm upgrade apply v1.32.0
kubectl get nodes
 
apt-get install kubelet=1.32.0-1.1
systemctl restart kubelet
kubectl get nodes

먼저 Master Node 를 위 명령어들을 통해 업그레이드하자. kubeadm 툴 역시 업그레이드하고자 하는 버전으로 새로 설치해주어야한다. 또한, kubectl get nodes 는 기본적으로 kubelet 의 버전을 보여주기 때문에 kubelet 역시 새로운 버전으로 설치해준 뒤 확인해보자.

# 먼저 node01 에 실행중인 Pod 을 drain 하고
kubectl drain node01
 
# node01 으로 접속한 뒤
ssh node01
 
# k8s apt repository 리스트 파일에서
vim /etc/apt/sources.list.d/kubernetes.list
 
# 아래로 변경
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.32/deb/ /
 
# apt 업데이트 후 kubeadm 최신 버전 확인
apt update
apt-cache madison kubeadm
 
apt-get install kubeadm=1.32.0-1.1
kubeadm upgrade node config --kubelet-version v1.32.0
 
apt-get install kubelet=1.32.0-1.1
systemctl restart kubelet
 
exit
 
kubectl uncordon node-1

이제 Worker Node 로 넘어간 뒤 마찬가지로 업그레이드해주면 된다.

Kubernetes Backup and Restore Methods


Kubernetes Object Backup

kubectl get all --all-namespaces -o yaml > all-deploy-services.yaml

Resource Configuration 자체를 YAML 파일로 저장하여 백업하는 방법이 있고,

ETCD Backup & Restore

# etcd 스냅샷 생성
export ETCDCTL_API=3
etcdctl snapshot save /tmp/snapshot.db \
  --endpoints=https://127.0.0.1:2379 \  # 로컬은 127.0.0.1:2379, 외부 etcd 는 kubeadm-config.yaml 내 etcd.external.endpoints
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/etcd-server.crt \
  --key=/etc/kubernetes/pki/etcd/etcd-server.key
etcdctl snapshot status snapshot.db
 
# etcd 복원
etcdctl snapshot restore /opt/snapshot-pre-boot.db \
  --data-dir /var/lib/etcd-from-backup
 
# kube-apiserver 정지
service kube-apiserver stop
 
# etcd 재시작
systemctl daemon-reload
service etcd restart
 
# kube-apiserver 재시작
service kube-apiserver start

또는 etcd 스냅샷을 이용해 백업하는 방법이 있다.

vi /etc/kubernetes/manifests/etcd.yaml
 
# 복원 이후 volume path 수정
  volumes:
  - hostPath:
      path: /var/lib/etcd-from-backup
      type: DirectoryOrCreate
    name: etcd-data

복원 이후 etcd volume path 를 수정해주자.

CRD, Custom Resource Definition


apiVersion: flights.com/v1
kind: FlightTicket
metadata:
  name: my-flight-ticket
spec:
  from: Mumbai
  to: London
  number: 2

K8s 에선 ReplicaSet, Deployment 등 기본적으로 제공되는 Resource 외에 사용자가 커스텀으로 Resource 를 만들 수 있다. 위와 같은 Resource 를 만들고 싶을 때 Custom Resource Definition 을 생성해 Object 를 생성하고 관리할 수 있다.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: flighttickets.flights.com
spec:
  scope: Namespaced
  group: flights.com
  names:
    kind: FlightTicket
    singular: flightticket
    plural: flighttickets
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              from:
                type: string
              to:
                type: string
              number:
                type: integer
                minimum: 1
                maximum: 10

위와 같이 CustomResourceDefinition 을 생성하면 정의된 Custom Resource 를 생성하고 관리할 수 있다. 다만 ETCD 에 저장만 될 뿐 어떤 기능이 있는 것은 아니다. 기능을 추가하기 위해 Custom Controller 를 작성할 수 있다.

Custom Controllers

Custom Resource 를 제어하려면 Custom Controller 를 작성해야 하는데, 파이썬 등 다양한 언어로 작성할 수 있지만 API 콜을 기반으로 통신하기 때문에 자체적으로 큐 및 캐싱 시스템을 포함해야 하는 수고가 있을 수 있다. 때문에 K8s Go 클라이언트로 Custom Controller 를 작성하면 좀 더 쉽게 Controller 를 구축할 수 있다.

Operator Framework

Operator Framework 는 CustomResourceDefinitionCustomController 를 패키지화하여 K8s 에 배포할 수 있도록 도와준다.

References