환경

  • on premise 서버 4대
    • 전부 Ubuntu 18.04
    • RAM 16GB ~ 128GB
      • 마스터노드 용 서버: 16GB
      • 워커 노드 용 각각
        • 64GB / 64GB /128GB
    • 워커 노드 용 서버 전부 GPU 장착

구성방식

온 프레미스(On premise)방식의 서버 4대 구성에서 마스터 노드로 생각 서버는 GPU가 장착되어 있지 않고 램도 16GB만 있다. 나머지 3대의 서버는 각각 머신러닝을 위한 GPU도 어느정도 장착되어 있다. 그리고 각각의 네트워크 구성은 같은 대역을 가진다.

앞으로의 설명을 위해 IP 셋팅은 다음과 같이 가정한다.

  • 마스터 노드: 172.16.10.90
  • 워커 노드1: 172.16.10.100
  • 워커 노드2: 172.16.10.101
  • 워커 노드3: 172.16.10.102

이러한 구성에서 쿠버네티스 클러스터를 쉽게 구축하고 관리할 수 있는 kubeadm이라는 도구를 통해 클러스터 설치를 구현한다. 목표는 클러스터를 구축 후 간단한 웹 서버들을 파드(pod) 형태로 띄워보는 것을 테스트 해보는 것으로 한다.

사전작업

쿠버네티스 클러스터 서버 구축을 하기에 앞서 각 마스터 노드와 워커 노드가 될 서버들에 아래의 작업들을 해줬다.

  1. NTP 서버 동기화
  2. 스왑메모리 해제
  3. 도커 엔진 설치
  4. 도커 데몬 systemd로 변경

NTP 서버 동기화

NTP 서버 동기화는 Network Time Protocol의 약자로서 클러스터를 구축하게 되면 각 노드들이 네트워크 통신을 하게 되는데 이 때 각각의 시간이 맞지 않아 발생할 수 있는 위험을 없애기 위해 해주었다.

$ sudo apt install ntp

마스터 노드(Master Node)

172.16.10.90의 마스터 노드에서는 설치 후 서비스 리로드, 서비스 동작확인 만 해주었다.

$ sudo service ntp reload
$ sudo ntpq -p

pool로 설정되어 있는 ntp 서버들에서 시간을 받아오는 결과를 확인할 수 있다.

워커 노드(Worker Node)

100~102번의 워커 노드들은 설치 후 ntp 설정을 변경하고 마찬가지로 서비스 재시작, 서비스 동작확인을 해준다.

$ sudo vi /etc/ntp.conf

주석이 해제되어 있는 모든 poolserver를 주석처리 하고 다음 마스터 노드의 서버 ip를 추가해주어 ntp 설정을 잡는다.

server 172.16.10.90

이어서 서비스를 재시작하고 ntpq -p명령어를 실행하면 npt 클라이언트가 ntp서버에와 동기화 하는 것을 확인할 수 있다.

$ sudo systemctl restart ntp
$ sudo ntpq -p

스왑메모리 해제(swapoff)

쿠버네티스 클러스터는 메모리 스왑이 활성화 되어있는 것을 허용하지 않는다. 본인이 공부한 저서에는 이렇게 설명되어있다.

메모리 스왑이 활성화 돼 있으면 컨테이너의 성능이 일관되지 않을 수 있기 때문에 대부분의 쿠버네티스 설치 도구는 메모리 스왑을 허용하지 않습니다.

우리가 사용할 kubeadm도 마찬가지로 스왑메모리를 허용하지 않는다. 터미널에서 다음 명령어를 입력하여 스왑메모리를 해제한다.

$ swapoff -a

추가적으로 혹시 노드 들의 재부팅이 잦을 예정이라면 아예 fstab에서 swap에 대한 부분을 주석처리 하는 것도 좋다. 위의 작업을 해주고 fstab파일을 아래와 같이 열고 swap이 적혀있는 부분을 주석처리 해준다.

$ sudo vi /etc/fstab

>
# swap was on /dev/sda2 during installation
# UUID=e09948a3-(중략)...-79d1d6 none            swap    sw              0       0

도커엔진 설치

도커 관련된 내용은 지금까지 작성한 포스팅들로 내용을 대체한다.

도커 데몬 변경

도커 데몬의 컨테이너 런타임을 변경한다. 자세한 내용은 공식 문서를 참고하면 된다. 간략히 설명해 보면 일반적으로 리눅스에서 프로세스를 관리하며 리소스를 컨트롤 하는 애들이 systemdcgroupfs으로 나뉜다. 컨테이너의 경우 cgroupfs을 사용하는데 일반적 프로세스인 systemd와 충돌을 일으켜 효율적인 자원관리가 안될 수 있다는 것이다. 그래서 systemd로 컨타이너의 런타임을 교체해준다. 아래 명령어를 실행한다.

$ sudo mkdir /etc/docker
$ cat <<EOF | sudo tee /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2",

  "runtimes": {
        "nvidia": {
            "path": "nvidia-container-runtime",
            "runtimeArgs": []
        }
    },
  "default-runtime": "nvidia"
}
EOF
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

내용 중 runtimesdefault-runtime은 쿠버네티스에서 GPU 자원을 쓰기 위해 추가하는 부분이다. 기본 런타임을 nvidia로 지정함으로써 K8S 환경에서도 GPU 리소스를 사용할 수 있게한다.

저장소 추가 및 Kubeadm 설치

일단은 gpg 저장소를 추가하여 kubeadm을 받을 수 있는 환경을 만들고 업데이트를 실행해준다. root 계정에서 명령어를 사용한다면 sudo는 생략하면 된다. 해당 작업은 마스터와 워커 노드 각각 공통적으로 적용해준다.

$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
$ sudo cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main 
EOF
$ sudo apt update

다음과 같이 쿠버네티스를 포함한 설치 패키지를 내려받는다. 필요하다면 버전을 명시할 수 있다.

$ sudo apt install -y kubelet kubeadm kubectl kubernetes-cni

버전 명시 설치

설치 가능한 버전을 알아보려면 아래의 명령어를 통해 확인할 수 있다.

curl -s https://packages.cloud.google.com/apt/dists/kubernetes-xenial/main/binary-amd64/Packages 

# 버전만 출력
curl -s https://packages.cloud.google.com/apt/dists/kubernetes-xenial/main/binary-amd64/Packages | grep Version | awk '{print $2}'

버전 명시 시에는 다음과 같이 입력한다. 2021년 12월 1일 기준 1.22.x 버전이 최신버전이다.

$ sudo apt install -y kubelet=1.22.2-00 kubeadm=1.22.2-00 kubectl=1.22.2-00 kubernetes-cni

쿠버네티스 버전 고정

시간이 지남에 따라 쿠버네티스도 버전들이 업데이트 되어 간다. 추후 sudo apt update 시에 버전을 명시 하지 않은 패키지들의 제공 버전이 바뀔 수 있다. 그래서 현재 버전으로 계속 삭제 및 설치를 고려한다면 다음과 같이 패키지들의 버전을 고정할 수 있다.

$ sudo apt-mark hold kubelet kubeadm kubectl

클러스터 실행

마스터 노드(Master Node)

마스터노드에선 kubeadminit 명령어를 이용하여 쿠버네티스 클러스터를 시작할 수 있다. 아래의 명령어 중의 pod-network-cidr 옵션에 들어가는 ip는 기억을 해두자 아래의 네트워크 설정 시에 해당 ip를 사용할 일이 있다.

$ sudo kubeadm init --apiserver-advertise-address 172.16.10.90 --pod-network-cidr=192.168.10.0/24

아래에 언급될 CNI를 calico가 아니라 flannel로 사용하려면 --pod-network-cidr를 다르게 해서 만드는 편이 낫다.

$ sudo kubeadm init --apiserver-advertise-address 172.16.10.90 --pod-network-cidr=10.244.10.0/24

위의 명령어를 실행하면 출력되는 결과에 2가지 볼 것이 있다. 하나는 마스터 노드에서 이어서 작업해줘야하는 부분, 다른 하나는 워커노드들에서 실행할 join명령어 이다. 마스터노드는 주로 다음과 같이 나온다.

$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

행여나 이 내용이 조금 다르게 나오더라도 상관없다. 출력되는 부분을 잘 복사해서 그대로 마스터 노드에서 붙여넣기로 실행한다. 그리고 아래에 더 출력된 init 명령어는 워커노드에서 실행한다.

워커 노드(Worker Node)

워커노드에서 터미널을 열고 마스터노드에서 init을 해서 나온 join명령어를 실행해준다. 아래의 형식과 비슷할 것이다.

$ sudo kubeadm join 172.16.10.90:6443 - token caamm9.q00z842...(생략) \ 
      --discovery-token-ca-cert-hash sha256:e12dcc40156...(생략)

위의 해당 명령어를 입력하고 좀 기다리면 각 워커노드의 터미널에서 This node has joined the cluster: .... 내용이 출력되는 것을 확인할 수 있다.

임시 확인

아래의 네트워크 연결까지 해야 클러스터 구성이 제대로 되었는지 확인할 수 있지만 일단 initjoin이 잘 되었는지는 다음과 같이 확인할 수 있다. 마스터 노드에서 아래의 명령어를 실행해보자.

$ kubectl get nodes

그럼 현재의 마스터 노드를 중심으로 클러스터가 구성된 워커 노드들의 연결이 다음과 같이 출력된다.

Name        STATUS      ROLES                   AGE      VERSION
master      NotReady    control-plane,master    99s      v1.22.0
worker1     NotReady    <none>                  42s      v1.22.0
worker2     NotReady    <none>                  46s      v1.22.0
woeker3     NotReady    <none>                  48s      v1.22.0

마스터 노드는 control-plane 또는 master라고 표시되어 있는 것을 확인할 수 있다. 컨트롤 플레인 이라고 하면 ‘컨테이너의 라이프 사이클을 정의, 배포, 관리하기 위한 API와 인터페이스들을 노출하는 컨테이너 오케스트레이션 레이어’라고 그 뜻을 찾아볼 수 있지만 이에 해당하는 컴포넌트를 실행하는 호스트를 일반적으로 Master Node를 지칭한다고 알고 있으면 될 것 이다.

네트워크 실행

Calico

본인의 클러스터 구축 예제에서는 Calico 오버레이 네트워크를 설정한다. 설정 파일을 내려받아 클러스터가 구축된 상태에서 kubectl apply -f로 실행하여 네트워크를 적용하면 워커노드의 상태를 실시간 네트워크 통신을 통해 알 수 있게 되는 것이다. 아래의 명령어를 통해 적용한다.

여기서 아까 우리가 마스터 노드에 클러스터를 init할 때 입력한 ip가 필요하다. 기본값은 192.168.0.0/16으로 되어있으나 우리가 입력한 192.168.10.0/24으로 yaml파일의 설정을 바꿔줄 것이다.

$ wget https://docs.projectcalico.org/manifests/calico.yaml
$ sed -i -e 's?192.168.0.0/16?192.168.10.0/24?g' calico.yaml
$ kubectl apply -f calico.yaml

네트워크가 잘 생성 및 적용되었는지 확인해 보려면 생성된 파드를 확인한다.

$ kubectl get pods --namespace kube-system

아래 출력 결과에 calico-로 시작하는 노드들의 상태를 보며 Init 단계를 지나 PodInitializing 혹은 ContainerCreating을 거쳐 최종적으로 Running까지 문제 없이 되는 지 확인하면 된다.

  • Ready 의 값을 보면 1/1을 제대로 만족하는지 확인하면 된다.

Flannel

다른 클러스터 구축 시 Flannel 오버레이 네트워크도 설정해보았다. 위의 마스터노드 실행 시 --pod-network-cidr10.244.0.0/24로 생성하였고 위 방식과 동일하게 flannel yaml을 내려받고 ip 항목을 일부 수정하고 실행한다. 버전은 0.20.0버전을 사용하였다.

$ wget https://raw.githubusercontent.com/flannel-io/flannel/v0.20.0/Documentation/kube-flannel.yml
$ sed -i -e 's?10.244.0.0/16?10.244.10.0/24?g' kube-flannel.yml
$ kubectl apply -f kube-flannel.yml

실행 후 sudo kubectl get nodes -o wide 명령어를 실행 해서 노드의 status 가 NotReady 에서 Ready로 바뀌는지 확인하면 된다. 오버레이 네트워크가 완벽히 실행되는데에 시간이 조금 걸릴 수 있음에 유의한다.

또한 sudo kubectl get pods -n kube-flannel 명령어를 통해 해당 네임스페이스에 flannel관련 파드가 제대로 생성되고 실행되었는지 확인할 수 있다.

작동확인

클러스터 구축 확인

클러스터의 구성 자체를 확인하려면 다음 명령어를 치고 status를 확인하면 된다.

$ kubectl get nodes

위 명령어를 실행하였을 때 나오는 출력 결과는 마스터 노드와 워커노드의 현재 상태들이다. 모든 워커노드의 상태가 Ready를 만족하면 현재 클러스터가 문제없이 구성 되어있음을 알 수 있다.

서비스 작동 확인

간단히 일단 nginx 를 작동시키는 POD의 yaml 파일을 작성한다. 해당 파일안에는 nginx 도커 이미지를 기반으로 하는 쿠버네티스 pod를 실행하는 명세가 담겨 있다. 파일이름은 ‘pod-nginx.yaml’로 작성하였다.

# pod-nginx.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-nginx
spec:
  containers:
  - name: my-nginx-pod
    image: nginx:latest
    ports:
    - containerPort: 80
      protocol: TCP

해당 파일 작성이 완료 되었다면 kubectl apply -f 명령어를 이용하여 yaml파일 기반으로 파드를 생성할 수 있다. 파일이 존재하는 디렉토리에서 아래와 같은 명령어를 이용하여 실제로 파드를 생성해본다.

$ kubectl apply -f pod-nginx.yaml

생성 확인 및 IP 확인

쿠버네티스 클러스터에서 생성한 파드나 노드의 목록을 보기 위해선 주로 kubectl get {오브젝트} 를 사용한다. 여기서 {오브젝트}에는 pods, nodes, services 등등 여러가지 오브젝트가 있다. 아래의 명령어를 사용하여 위에서 우리가 생성한 파드가 제대로 실행되었는지 확인한다. 결과는 아래와 같이 출력된다.

$ kubectl get pods

>>>
NAME        READY   STATUS    RESTARTS   AGE
pod-nginx   1/1     Running   0          94s

생성된 파드에 대해 보다 더 많은 정보를 얻기 위해 kubectl describe {오브젝트} 명령어를 사용할 수 있다. describe는 더 디테일한 정보들을 출력해준다. 아래와 같이 명령어를 입력하여 생성된 파드가 현재 가지는 IP 정보를 얻어오자. 해당 IP는 우리가 클러스터 구성을 해줄 때 입력한 네트워크 인터페이스의 IP대역으로 설정 되어 있을 것이다.

% kubectl describe pods pod-nginx

>>>
Name:         pod-nginx
Namespace:    default
Priority:     0
Node:         worker3/172.16.10.102
...
(생략)
...
Annotations:  cni.projectcalico.org/containerID: 67b3c2...(중략)...c4f955467e
              cni.projectcalico.org/podIP: 192.168.100.141/32
              cni.projectcalico.org/podIPs: 192.168.100.141/32
Status:       Running
IP:           192.168.100.141
IPs:
  IP:  192.168.100.141
Containers:
  my-nginx-pod:
...
(생략)

위의 결과로 보아 3번째 워커에서 파드가 생성되어있음을 알 수 있고 해당 파드가 가지는 컨테이너의 IP는 192.168.100.141 임을 알 수 있다. 해당 IP에 curl 명령어를 날려서 nginx 서버 구성이 제대로 되었는지 확인한다.

$ curl 192.168.100.141

>>>
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

위 내용과 같이 nginx 서버의 html 스크립트 내용이 응답이 오는 것을 확인할 수 있다.

문제해결

증상

내가 겪은 문제는 네트워크까지 다 설정을 했을 때 워커 중 하나의 status가 NotReady에서 Ready로 바뀌지않는 문제를 겪었다. kubectl get nodes를 입력했을 때 아래와 같은 상태였다.

Name        STATUS      ROLES                   AGE      VERSION
master      Ready       control-plane,master    3m       v1.22.0
worker1     Ready       <none>                  4m       v1.22.0
worker2     NotReady    <none>                  4m       v1.22.0
woeker3     Ready       <none>                  4m       v1.22.0

원인을 찾기 위해 kubectl describe nodes를 마스터 노드 터미널에 입력해서 노드들의 상태를 자세히 보면 worker2 노드의 Conditions 탭에 다음과 같은 오류 메시지가 있었다.

container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized

해결

문제 해결을 위한 구글 탐색 중 DNS 값을 확인해 보라고 해서 /etc/resolv.conf를 열어보려했는데 애초에 파일이 존재하지 않았다. 여기서 왜 애초에 해당 파일이 존재하지 않는가를 조사해보았고 systemd-resolved 서비스가 올라와 있지 않은 것을 확인. 나머지 2대의 워커노드는 해당 서비스가 문제 없이 실행되고 있는 상태였다.

클러스터를 리셋 후 아래의 명령어로 해당 워커노드의 서비스를 재실행하였다.

$ sudo systemctl start systemd-resolved.service
$ sudo systemctl enable systemd-resolved.service

후에 다시 위의 클러스터 구성을 하니 모든 워커노드의 스테이터스가 문제 없이 잘 작동 되었다.

Name        STATUS      ROLES                   AGE      VERSION
master      Ready       control-plane,master    3m       v1.22.0
worker1     Ready       <none>                  4m       v1.22.0
worker2     Ready       <none>                  4m       v1.22.0
woeker3     Ready       <none>                  4m       v1.22.0

Leave a comment