kubernetes의 목적은 개발자의 application을 하나의 container로 만들어서 배포하고, 통신하도록 하는 것이 주된 목적이다.
kubernetes는 container들을 컨트롤하는 역할을 해주는데, 다음과 같다.
---------
|kubectl|
---------
|
| -------worker node1--------
-----Master Node----- | kubelet |
| kube-apiserver |-------| container runtime engine|
| etcd | | (docker, rkt, ...) |
| controller manager| ---------------------------
| kube-scheduler | -------worker node2--------
| |-------| kubelet |
--------------------- | container runtime engine|
| (docker, rkt, ...) |
---------------------------
위의 그림과 같은 형태가 만들어지는 것이다. 단, worker node는 n(n >= 1)개 이상일 수 있고, master node 역시도 m(m >= 1)개 이상일 수 있다.
container가 발전하기 전 docker
는 가장 지배적인 container tool이었다. docker의 container orchestration tool로서 kubernetes를 사용하였고, kubernetes는 container를 관리하기만 했지, container solution들을 제공하진 않았다.
시간이 흘러 docker와 kubernetes 사이에는 CRI
(container runtime interface)가 생겼는데, 이는 kubernetes가 container와 상호작용하기위한 tool로 보면된다. 이 CRI
는 OCI
(Open Container Initiative) spec를 따르는데, OCI
는 imagespec
, runtimespec
들에 대한 기준을 정의한다.
imagespec
은 image 빌드 방식에 대한 기준을 정의하고 runtimespec
은 container runtime 환경을 정의하기 위한 표준 spec으로 container 생명 주기, 자원 사용 세부 사항, 초기 설정, 네트워크 설정 등을 정의한다.
-------------
| rkt |
-------------
|
|
----- -----------------------------
|CRI| --------> |OCI(Imagespec, Runtimespec)|
----- -----------------------------
|
|
--------------
| kubernetes |
--------------
이제 CRI interface을 준수만 하면 어떠한 container runtime이든 kubernetes와 상호작용 할 수 있게 되었다.
그렇지만 docker는 CRI 표준이 나오기 이전에 나왔던 도구였기 때문에, CRI를 지키지 않았고, kubernetes는 docker와의 상호작용을 위해 dockershim
을 만들어 CRI를 통한 상호작용이 아니라 dockershim
을 통한 작업을 실행했다.
------------- -------------
| rkt | | docker |
------------- -------------
| |
| |
----- ------------
|CRI| |dockershim|
----- ------------
| |
| |
------------ |
|kubernetes|-----------
------------
따라서, dockershim
은 docker의 CRI 구현체가 없는 동안 docker
를 사용하기 위한 임시방편이었던 것이다.
요즘의 docker는 여러 도구들이 같이 있는 형식이다. CLI, API, VOLUMES, AUTH, docker engine 등과 같은 기능을 지원하였고, docker engine안에는 container runtime으로 containerd
를 지원하였다. 이 containerd
는 docker
내부에서 container를 동작하기 위한 하나의 runtime기로 동작하였지만, 이와 동시에 OCI
를 따르기 때문에 kubernetes와 상호작용이 가능하여 따로 빼내어 사용이 가능했다.
docker 내부에 있던 docker engine이 내부적으로 containerd
를 사용한다하더라도 docker engine자체가 특정 벤더에 종속된 기술이기 때문에 dockershim
을 사용할 수 밖에 없었다. 그러나, dockershim
의 유지보수가 불가능해지고, 어려움이 많아진 탓에 kuberntes는 dockershim
을 버리고 docker
를 공식적으로 v1.24부터 지원하지 않게되었고, containerd
을 사용하기를 권장하기 시작했던 것이다.
다행히도 docker
는 OCI에서 imagespec
을 따르기 때문에 docker에서 build한 container image는 kubernetes에서 사용하는 containerd
나 rkt
, cri-o
같은 container runtime기에 호환이 가능하다. 단지 docker는 OCI의 runtimespec
을 충족시키지 못하여 CRI를 만족시키지 못한 것일 뿐이다.
------------- ------------ --------------
| rkt | |containerd| | docker |
------------- ------------ |CLI, API |
| | |AUTH, BUILD |
| | --------------
| | |
| | |
------------------ ------------
| CRI | |dockershim|
------------------ ------------
| | |
| | X
------------ | (v1.24 >= version)
|kubernetes|----------
------------
이제 containerd
를 보도록 하자. 위에서 언급했듯이 containerd
는 container runtime이기 때문에 docker처럼 image를 빌드하고, 올리고, 인증 처리를 할 수 없다. 오직 container의 image를 가져오고 실행시켜 container를 동작시키는 것이 주된 기능이다.
containerd
는 ctr
이라는 CLI를 가지고 있는데, 이는 굉장히 불편하다. 왜냐하면 디버깅 위주로 만들어졌기 때문이다.
기본적인 사용 방법은 다음과 같다.
ctr image pull docker.io/library/redis:alpine
ctr run docker.io/library/redis:alpine
이러한 이유로 ctr
대신에 운용을 위해서 nerdctl
이라는 것을 대신 사용한다. docker와 비슷한 기능들과 인터페이스를 제공하여 containerd
의 기능을 제공한다.
다음은 docker 명령어에 대한 nerdctl
의 각 맵핑이다.
docker --> nerdctl
docker run --name redis redis:alpine --> nerdctl run --name redis redis:alpine
docker run --name webserver -p 80:80 -d nginx --> nerdctl run --name webserver -p 80:80 -d nginx
아주 쉽고 간단하다.
더 나아가서 이제, crictl
이라는 것을 보도록 하자. container runtime기는 containerd
말고도 rkt
라는 것이 있다고 했다. 사실 이러한 container runtime 기들이 무엇이든 간에 kubernetes 입장에서는 CRI
만 만족하면 되기 때문에 CRI
에 대한 상호작용 역시도 하나의 CLI로 묶어낼 수 있는 것이다.
그것이 바로 crictl
이다. crictl
은 CRI에 호환되는 container runtime에 대한 CLI를 제공하며, container runtime와 분리되어 실행된다. 따라서 container runtime이 containerd, rkt 이든 상관없이 crictl
을 통해서 상호작용이 가능하다.
crictl
은 기본적은 container에 관한 명령어들이 모두 있으면서도 kuberntes와 상호작용하는 CRI
를 만족하기 때문에 kuberntes에 대한 상호작용 명령어들도 있다는 것이다.
------------- ------------
| rkt | |containerd|
------------- ------------
| |
| |
------------------
| crictl(CLI) |
------------------
|
|
------------------
| CRI |
------------------
|
|
------------
|kubernetes|
------------
단, 유의할 것은 crictl
은 오직 디버깅 용으로만 사용하기를 권장한다. crictl
은 CRI
를 통해 kuberntes와 상호작용하기 위해서 만들어진 것이지, ctr
, nerdctr
처럼 container를 실행하기 위한 목적은 아니라고 생각하면 된다. 왜냐하면 crictl
를 통해 container를 만들면 kubernetes에서는 worker node에 동작중인 kubelet
이 예정에 없던 container가 실행되니, 이를 억제하도록 할 것이다. 이는 crictl
이 CRI
를 통해 kuberntes와 상호작용하고 있다는 것을 보여주는 예인 것이다.
crictl
crictl pull busybox
crictl images
crictl ps -a
crictl exec -i t 3e025....60 ls
crictl logs 3e024....f1
crictl pods
기본적인 image 생성, 삭제, pull, 실행 등의 기능이 있다. 재밌는 점은 crictl
을 통해서 kuberntes의 자원인 pod
를 볼 수 있다는 점이다.
etcd는 신뢰성 기반의 분산 key-value store로 아주 간단하고, 안전하며 빠른 특성을 가지고 있다. etcd
는 kuberntes의 database key store로서 동작하고 있지만, 엄연히 분리된 project이고, software이다.
etcd는 분산 system에서 data의 정합성을 맞추기 위해서 raft algorithm을 도입한다. 이러한 consensus알고리즘을 통해 분산된 host에서도 data의 정합성을 맞추는 것이다.
다음은 etcd
의 사용법이다.
./etcd
./etcdctl set key1 value1
./etcdctl
./etcdctl --version
etcdctl version: 3.3.11
API version: 2
주의할 것은 api version이 2.0이라는 것이고 cli버전이 3.3.11이라는 것이다. 최신 버전에서는 api version 3.0으로 나올 것이다.
ETCDCTL_API=3 ./etcdctl version
etcdctl version: 3.3.11
API version: 3.3
api version이 3부터는 set
이 아니라 put
을 사용한다는 점을 유념하도록 하자.
./etcdctl put key1 value1
./etcdctl get key1
kubernetes에서 etcd의 역할은 분산 system에서의 data를 저장하고, 각기 다른 host에 저장된 data의 정합성을 맞추어, kubernetes cluster의 status를 유지한다. kuberntes에서는 다양한 resource들이 있는데, pod, deployment, service, role 등의 자원들에 대한 정보를 etcd가 담당하여 저장하는 것이다.
ETCD를 통한 HA(high availability)환경은 다음과 같이 동작한다고 생각하면된다.
----------------------
| master node |
| etcd(port: 2380) |
| |
----------------------
----------------------
| master node |
| etcd(port: 2380) |
| |
----------------------
etcd
는 master node 당 한 개씩의 instance를 가지고 있고, etcd API server를 열어 서로 통신할 수 있다. 이렇게 분산된 master node끼리의 data 정합성을 맞추어 data를 저장하는 것은 매우 어려운 일이지만, etcd는 raft 알고리즘을 이용하여 이러한 문제를 해결해 고가용성(High Availability)를 제공할 수 있었다.
kube-apiserver
는 master node의 한 part로 유저의 요청에 대해 kuberntes cluster를 제어한다. 가령, pod를 만들거나 config를 변경하거나, 현재 resource의 list를 가져오거나 다양한 기능들을 kube-apiserver
에 요청을 보내어 실행할 수 있다.
그러나, 직접 kube-apiserver에 http요청을 보내는 것은 매우 비효율적인 방법이다. 따라서 kubectl
이라는 CLI를 사용하여 kube-apiserver
에 요청을 보내는 방법이 더 효율적이고 쉽다.
---------
|kubectl|
---------
|
|
|
-----Master Node-----
| kube-apiserver |
| etcd |
| controller manager|
| kube-scheduler |
---------------------
kubectl get pods -A
다음의 명령어는 kubernetes cluster의 모든 pod들을 가져오는 명령어인데, kubectl
명령어를 통해서 kube-apiserver에 대한 인증, 인가를 허가받고, kube-apiserver를 통해서 요청이 전달된다. kube-apiserver는 etcd를 통해서 pod들의 현재 정보를 가져와 응답으로 전송하는 것이다. 이 연결은 HTTP를 통해서 이루어지며 TLS의 보호를 받는다.
kube-apiserver
에는 여러 가지 option들이 설정되어있는데, 현재 어떤 option들이 설정되어있는 지 확인할 수 있다.
kubeadm
를 통해서 kubernetes를 설치하였을 때cat /etc/kubernetes/manifests/kube-apiserver.yaml
...
spec:
containers:
- command:
- kube-apiserver
...
- --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
- --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
- --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
- --requestheader-allowed-names=front-proxy-client
- --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
- --requestheader-extra-headers-prefix=X-Remote-Extra-
- --requestheader-group-headers=X-Remote-Group
- --requestheader-username-headers=X-Remote-User
- --secure-port=6443
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-cluster-ip-range=10.96.0.0/12
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
위의 spec부분의 kube-apiserver
아래가 바로 kube-apiserver
를 실행하면서 설정된 option들인 것이다.
kube-controller
는 여러 controller들로 구성되어있는데, 시스템의 다양한 구성 요소들을 모니터링하고 원하는 기능 상태로 동작하도록 한다. 이를 위해서 kube-apiserver
에게 적절한 action을 요청하는 것이다.
가장 먼저 Node-Controller
가 있는데, Node-Controller
는 연결된 node의 상태를 확인하고 관리해준다.
--------------master---------------- -------
| | |Node1|
| node-controller -> kube-apiserver | ----->|Node2|
| | |Node3|
------------------------------------- -------
node-controller
는 Node들의 상태를 확인하고 node 하나가 만약 unreachable상태, 즉 NotReady
status를 가진다면, 해당 node에 배포된 pod들을 다른 node들에 옮겨주고 실행한다.
Replication-Controller
는 하나의 node 내에 특정 pod의 개수를 일정하도록 유지해준다. 이렇게 controller
들은 굉장히 다양한데, Deployment-Controller
, Namespace-Controller
, Endpoint Controller
, PV protection-controller
, Replica set
, job-controller
, service-account controller
. cron-job
, node-controller
, PV-builder-controller
, Replication-controller
등이 있다.
이 많은 controller는 kube-controller
에 적재되어 관리되고 설정값들을 통해 동작하는 것이다. kube-controller
는 하나의 pod로서 다양한 설정값들로 이루어져 있는데, 특정 controller만 실행되지 않도록 할 수 도 있다.
현재 kube-controller
가 어떤 설정값을 가지고 있는 지 알기위해서는 다음과 같이 확인해볼 수 있다.
ps -aux | grep kube-controller-manager
다양한 설정값들이 나오는 것을 볼 수 있다.
또한, kubeadm
으로 설치했다면 /etc/kubernetes/manifests/kube-controller-manager.yaml
에 있는 설정값을 수정하면 된다.
cat /etc/kubernetes/manifests/kube-controller-manager.yaml
kube-scheduler
는 node안에 있는 pod를 스케줄링하는데, node에 pod를 두는 직접적인 일을 하는 것이 아니라 어디에 배치할 지 결정만 할 뿐이다.
그렇다면 어떤 방식으로 pod를 node에 배치할 지 스케줄링하는 것일까? 여기에는 두 가지 단계가 있다.
cat /etc/kubernetes/manifests/kube-scheduler.yaml
kubelet은 worker node에서 동작하며 master와 연락하여 node에 pod를 배포한다. kubelet은 cluster에 kubelet이 설치된 node를 등록하고, 자신의 node에 배치되도록 스케줄링된 pod를 실행시켜준다.
---------
|kubectl|
---------
|
| -------worker node1--------
-----Master Node----- | kubelet |
| kube-apiserver |-------| |
| etcd | | |
| controller manager| ---------------------------
| kube-scheduler | -------worker node2--------
| |-------| kubelet |
--------------------- | |
| |
---------------------------
먼저 kubelet
은 자신의 node를 master node
kube-apiserver
와 연락하여 cluster에 등록한다. 이 다음 kube-scheduler
가 특정 pod를 특정 node에 스케줄링하였다면, 해당 node의 내부에서 kubelet은 container runtime engine에게 container를 만들 것을 요청한다. 이 container runtime engine은 docker일 수도 있고, rkt일 수도 있다.
그 다음 instance가 실행되면 kubelet
은 kube-apiserver
와 계속해서 소통하여 배포된 instance의 상태를 보고해준다.
한 가지 알아두어야 할 것은 kubelet
은 자동으로 설치되지 않기 때문에, 따로 설치해야한다. 이는 worker node에 따로 동작하는 것이기 때문이다.
설치 후에 다음의 명령어를 치면 kubelet
에 관한 현재 설정들이 나오게 된다.
ps -aux | grep kubelet
재밌는 것은 kubelet
은 다른 컴포넌트들 처럼 pod가 아니라, 하나의 daemon process로 동작하는데, 이는 kubelet
이 특정 node에 스케줄링된 pod를 실행시켜주는 역할을 하기 때문에, kubelet
을 pod로 실행시켜줄 주체가 없기 떼문이다.
kubernetes cluster 내의 서로 다른 node에 있는 pod들이 이라도, 이들은 서로 IP 통신이 가능하다. 이는 kubernetes cluster 내부의 pod network 덕분이고, 이는 virtual한 network 망을 kubernetes cluster내에 구축한 것과 마찬가지이다. 그런데, 한 pod가 다른 pod로 통신하려고 할 때 다른 pod의 ip를 통해서 통신하는 방법은 좋지 않다. 왜냐하면 pod의 특성상 계속 죽었다, 살아났다가 반복할 수 있고, 이 경우 IP는 계속 변하기 때문이다.
따라서, 이들을 연결해주는 service라는 개념이 생겨났는데, service는 고정 IP를 가지고 있어서 service에게 data를 보내면 해당 traffic을 연결된 다른 pod에 전송해주기 때문에 pod간의 통신에서 IP가 사라진다는 문제점을 해결할 수 있게된 것이다. 그런데 어떻게 pod가 service들과 연결될 수 있는 것일까? service는 하나의 pod일까? 아니다. service는 pod도 아니기 때문에 kubernetes cluster의 virtual한 pod network에 접근할 수 없다. 이 service에 접근할 수 있도록 만들주는 것이 바로 kube-proxy
이다.
kube-proxy
는 하나의 process로 각 kubernetes cluster의 worker node에서 동작한다. kube-proxy
의 주요한 일은 kubernetes cluster에서 새로운 service가 생성되면 각 node마다 적절한 rule을 만든다는 것이다. 이 rule에 따라 service로 전달된 traffic들이 연결된 다른 pod들로 전송되는 것이다. 이러한 기능을 할 수 있도록 구현한 방법 중 하나가 바로 iptables
를 사용한 것이다. 즉, service가 생성되면 kube-proxy
는 각 node에 service에 대한 iptables rule을 만들어 service로 전달한 traffic을 연결된 다른 pod에 내보내는 것이다.
-------------
|pod network|
-------------
|
-------Node1--------- | -------Node2---------
| | | | |
| ----pod----- |-----------| ----pod----- |
| |10.32.0.14| | | |10.32.0.15| |
| ------------ | | ------------ |
| | | |
| --kube-porxy-- | | --kube-porxy-- |
| | | | | | | |
| -------------- | | -------------- |
--------------------- ---------------------
(10.96.0.12 -> 10.32.0.15) (10.96.0.12 -> 10.32.0.15)
| |
| |
------------service--------------
| 10.96.0.12 |
---------------------------------
다음의 그림을 보면 service가 10.96.0.12
IP를 가지고 있고, 이 service는 Node2의 pod인 10.32.0.15
에 연결된다. 이 ip rule을 각 node마다의 kube-proxy
가 인식하여 iptable rule
을 업데이트하는 것이다.
따라서, Node1의 pod에서 service인 10.96.0.12
IP로 요청을 보내면 Node2의 pod인 10.32.0.15
로 포워딩되는 것이다.