사내 개발 시스템에는 쿠버네티스가 구축되어 있어서 배포나 개발 프로세스가 현재 쿠버네티스에 맞추어 진행되고 있다. kubectl을 비롯해서, 그냥 제공해 주는 대로 쓰고 있다 보니 요즘은 거의 툴 사용법만 익히고 있는 것이 아닌가 하는 생각이 들기 시작했고, 특히 당연하지만 슈퍼유저 권한을 주지 않기 때문에 정확히 어떻게 동작하는 건지 잘 모르고 넘어가는 경우가 많다. 그래서 한번 쿠버네티스에 대해 이런저런 공부를 해 보려고 한다.

쿠버네티스 시스템이 실제로 어떤 식으로 동작하는지, 조금 더 높은 권한에서는 무엇을 더 할수 있는지에 대해서 한번 조사해 보자. 더 나아가서는 쿠버네티스를 통해 내부에 이런저런 시스템을 구축해 보고 최종적으로는 이 위키도 쿠버네티스 시스템을 통해 외부에 노출해 보려고 한다.

쿠버네티스를 간단히 설치할 수 있는 kubeadm을 사용해서 한번 간단히 설치하고 사용해 보자. 환경은 다음과 같다.

odroid-u3+

  1. odroid-x2: 쿠버네티스 마스터 노드
  2. odroid-u3+ 3대: 쿠버네티스 슬레이브 노드

대략 환경은 다음처럼 구성되어 있다.

architecture

이들은 모두 우분투 18.04가 실행중이고 도커가 설치되어 있다. 쿠버네티스를 설치하기 위해서는 최신 커널이나 도커 버전이 필요한데, 이를 위해 사전에 필요한 조건을 모두 만족시켜 놓았다.

설치 순서

우선 kubeadm을 설치해서 실행해 보자. kubeadm은 우분투에서 기본적으로 제공하는 패키지가 아니라서 단순히 apt-get을 실행하는 것만으로는 설치할 수 없고 kubeadm이 있는 저장소를 따로 추가하여야 한다.

$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add
$ sudo apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"
$ sudo apt install kubeadm

지금 저장소의 주소는 xenial로 되어 있는데, 우분투 18.04, bionic 저장소는 제공하고 있지 않다. xenial의 저장소를 추가해도 설치와 사용에는 큰 문제가 없다…

쿠버네티스 마스터 노드 설치

kubeadm을 사용하여 쿠버네티스 마스터를 설치해 보자. 마스터 노드는 odroid-x2에 설치하기로 했다.

설치는 kubeadm init 명령으로 진행하는데, 여기에 몇 가지 파라미터를 추가할 수 있다. 그 중에서 --pod-network-cidr을 통해 쿠버네티스가 생성할 팟의 네트워크를 지정할 수 있으니 지정해 주자. 후에 다시 이야기하겠지만, 네트워크 플러그인 설치시 어떤 플러그인은 입력했던 이 파라미터의 값을 통해 네트워크를 구성한다. 그러므로 이를 입력하지 않으면 추후 설치할 네트워크 애드온을 제대로 설정할 수 없는 일이 벌어지기 때문에 왠만하면 입력해 주는 것이 좋은 것 같다.

$ kubeadm init --pod-network-cidr=172.28.0.0/14

네트워크 대역은 172 대역 내에서 적당히 선택해서 정한 건데 뭐 알아서 선택하면 될 것 같다. 어쨌든 그 실행 결과는 아래와 같다.

$ sudo kubeadm init --pod-network-cidr=172.28.0.0/14
[init] Using Kubernetes version: v1.13.3
[preflight] Running pre-flight checks
	[WARNING SystemVerification]: this Docker version is not on the list of validated versions: 18.09.1. Latest validated version: 18.06
[preflight] Pulling images required for setting up a Kubernetes cluster
[preflight] This might take a minute or two, depending on the speed of your internet connection
[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Activating the kubelet service
[certs] Using certificateDir folder "/etc/kubernetes/pki"
...
...
[kubeconfig] Using kubeconfig folder "/etc/kubernetes"
[kubeconfig] Writing "admin.conf" kubeconfig file
[kubeconfig] Writing "kubelet.conf" kubeconfig file
[kubeconfig] Writing "controller-manager.conf" kubeconfig file
[kubeconfig] Writing "scheduler.conf" kubeconfig file
[control-plane] Using manifest folder "/etc/kubernetes/manifests"
...
...
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy

Your Kubernetes master has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

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

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of machines by running the following on each node
as root:

  kubeadm join 192.168.16.2555:6443 --token SOMETHING --discovery-token-ca-cert-hash SHA256:SOMETHING

이제 kubectl이 설치되었으므로 이를 통해 쿠버네티스를 컨트롤할 수 있게 되었다. 그 전에, kubectl은 $HOME/.kube/config 파일을 참고하여 억세스 토큰을 사용하는데, 위 결과로 그 파일을 어디서 가져올 수 있는지를 안내하고 있다. /etc/kubernetes/admin.conf가 관리자 권한을 가진 토큰을 가지고 있기 때문에 이를 복사해서 해당 경로에 넣어 두자.

또한 위 결과 출력 중 맨 아래 내용을 보면 방금 초기화가 완료된 쿠버네티스 마스터 노드에 슬레이브 노드를 추가하는 가이드가 나와 있다. 명령어는 kubeadm join이며 어떤 파라미터를 사용하는지도 나와 있으므로 잘 메모해 두자.

이렇게 하면 마스터 노드의 구성은 완료되었다. 당연히 설치할 때 에러가 발생할 수도 있는데, 겪었던 에러의 내용은 아래에 다시 정리해 보았다. 참고로 당연히 각 노드에는 도커가 설치되어 있어야 한다.

error execution phase preflight: [preflight] Some fatal errors occurred:
	[ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]: /proc/sys/net/bridge/bridge-nf-call-iptables does not exist
	[ERROR FileContent--proc-sys-net-ipv4-ip_forward]: /proc/sys/net/ipv4/ip_forward contents are not set to 1
	[ERROR Swap]: running with swap on is not supported. Please disable swap
	[ERROR SystemVerification]: failed to parse kernel config: unable to load kernel module: "configs", output: "modprobe: FATAL: Module configs not found in directory /lib/modules/4.15.18-dirty\n", err: exit status 1
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`

에러 대응: ERROR FileContent–proc-sys-net-bridge-bridge-nf-call-iptables

커널 모듈 br_netfilter를 활성화 시키는 것으로 해결할 수 있다.

$ sudo modprobe br_netfilter

에러 대응: ERROR FileContent–proc-sys-net-ipv4-ip_forward

ip%%_%%forward를 활성화시켜 해결한다. /proc/sys/net/ipv4/ip_forward의 값에 1을 주거나 /etc/sysctl.conf 파일을 열어 ipv4.ip_forward를 0에서 1로 고쳐 해결할 수 있다.

$ echo 1 > /proc/sys/net/ipv4/ip_forward

or

$ vim /etc/sysctl.conf
...(값 수정 후)
$ sysctl -p

에러 대응: ERROR Swap

왜 그런지는 모르겠지만 스왑을 사용할 수 없다. 이유는 우선 천천히 알아보기로 하고, 스왑을 비활성화하기 위해 다음과 같은 커맨드를 사용할 수 있다.

$ sudo swapoff -a

에러 대응: ERROR SystemVerification - failed to parse kernel config

kubeadm에서 커널 플래그들의 값을 읽어 필요한 환경을 구성하므로 커널의 상태를 확인하기 위해 해당 config파일이 필요한 것 같다. 현재 사용중인 오드로이드는 커널을 직접 컴파일하였기 때문에 기본 설정 파일이 존재하지 않았던 것 같다. 커널 컴파일시 사용했던 config 파일을 찾아서 /boot 디렉토리 밑에 커널 버전 이름으로 저장해 놓았다.

$ ls -la /boot
total 144
drwxr-xr-x  2 root root   4096 Feb  3 18:41 .
drwxr-xr-x 21 root root   4096 Feb  3 18:15 ..
-rw-r--r--  1 root root 136218 Feb  3 18:36 config-4.15.18-dirty

kubeadm 설치 후 살펴보기

kubectl 설정이 완료되었으면 기본적인 명령을 통해 이런저런 상황을 살펴볼 수 있다.

기본 네임스페이스는 아래와 같다.

$ kubectl get namespaces
NAME          STATUS   AGE
default       Active   2m57s
kube-public   Active   2m57s
kube-system   Active   2m57s

최초 실행된 팟을 한번 확인해 보자.

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                     READY   STATUS              RESTARTS   AGE
kube-system   coredns-86c58d9df4-cpvkk                 0/1     ContainerCreating   0          5m24s
kube-system   coredns-86c58d9df4-hbpzg                 0/1     ContainerCreating   0          5m24s
kube-system   etcd-cublr-odroidx2                      1/1     Running             0          5m24s
kube-system   kube-apiserver-cublr-odroidx2            1/1     Running             0          5m13s
kube-system   kube-controller-manager-cublr-odroidx2   1/1     Running             0          5m24s
kube-system   kube-proxy-ptcfd                         1/1     Running             0          5m24s
kube-system   kube-scheduler-cublr-odroidx2            1/1     Running             0          5m38s

coredns 팟은 실행한지 5분이 지났음에도 아직 pending중으로 표시되어 있다. 이는 네트워크 애드온이 설치되면 활성화될 것이다. 우선 그것보다도 위 팟들의 리스트를 보면 꽤 여러가지가 실행된 것을 알 수 있는데, 해당 팟들은 당연히 도커 컨테이너로 실행된 것이므로 docker ps -a등의 컨테이너 리스트를 확인하는 명령어를 통해서도 똑같이 볼 수 있다.

네트워크 애드온 flannel 설치

네트워크 애드온을 설치해 보자. 사용 가능한 네트워크 애드온은 공식 설치 가이드에서 직접 확인해볼 수 있다. 확인해 보면 알겠지만 arm에서는 선택지가 별로 없어서 다음의 네트워크 드라이버 두 개만 고려해보면 될 것 같다.

상세한 스펙이나 기능을 본다면 속도만 느릴 뿐 weavenet이 더 좋아보이는데 일단은 flannel로 선택해도 큰 문제가 없을 것 같다. 사용 빈도는 flannel이 더 좋아보이니 여기서는 flannel을 선택해서 설치를 진행한다. 참고로 flannel은 kubectl init시 입력했던 파라미터 --pod-network-cidr 를 기반으로 라우팅을 구성한다.

flannel의 설치에는 다른 쿠버네티스 리소스와 마찬가지로 yaml파일을 사용한다. 이 yaml 파일은 flannel의 저장소에서 kube-flannel.yml의 이름으로 쉽게 찾을 수 있는데, 우리는 위에서 kubeadm init에 대한 파라미터로 --pod-network-cidr을 입력했으므로, 입력한 파라미터에 맞게 이 파일도 약간 수정해야 한다. 변경할 부분은 ConfigMap->data->net-conf.json의 Network 부분이다.

...
kind: ConfigMap
apiVersion: v1
metadata:
  name: kube-flannel-cfg
  namespace: kube-system
  labels:
    tier: node
    app: flannel
data:
  cni-conf.json: |
    {
      "name": "cbr0",
      "plugins": [
        {
          "type": "flannel",
          "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }
      ]
    }
  net-conf.json: |
    {
      "Network": "172.28.0.0/14",
      "Backend": {
        "Type": "vxlan"
      }
    }
...

원래의 내용은 10.244.0.0/16으로 되어 있었는데 이를 우리가 위에서 입력한 --pod-network-cidr파라미터의 값인 172.28.0.0/14로 변경하였다. 백엔드 부분이 vxlan으로 되어있는 부분이 흥미로운데 이는 나중에 flannel을 따로 분석해보는 자리에서 확인해 보면 재밌을 것 같다.

그러면 변경한 kube-flannel.yaml을 사용하여 flannel 네트워크 애드온을 설치한다.

$ kubectl create -f kube-flannel.yml
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.extensions/kube-flannel-ds-amd64 created
daemonset.extensions/kube-flannel-ds-arm64 created
daemonset.extensions/kube-flannel-ds-arm created
daemonset.extensions/kube-flannel-ds-ppc64le created
daemonset.extensions/kube-flannel-ds-s390x created

커맨드의 결과로 쿠버네티스의 리소스 daemonset이 여러개 실행된 것을 볼 수 있다. 뒤에는 플랫폼명이 붙어 있으므로 실제로 실행되는 것은 ds-arm접미사가 붙은 것만 실행될 것이다.

이 외에도 이제 여러 쿠버네티스 리소스가 생성되었는데 간단히 보면 아래와 같다.

  • clusterrole
  • clusterrolebinding
  • configmap

이 내용들은 앞으로 알아갈 것이기 때문에 키워드 정도만 우선 눈여겨 보자.

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                     READY   STATUS    RESTARTS   AGE
kube-system   coredns-86c58d9df4-cpvkk                 1/1     Running   0          6m57s
kube-system   coredns-86c58d9df4-hbpzg                 1/1     Running   0          6m57s
kube-system   etcd-cublr-odroidx2                      1/1     Running   0          6m57s
kube-system   kube-apiserver-cublr-odroidx2            1/1     Running   0          6m46s
kube-system   kube-controller-manager-cublr-odroidx2   1/1     Running   0          6m57s
kube-system   kube-flannel-ds-arm-ghvb2                1/1     Running   0          59s
kube-system   kube-proxy-ptcfd                         1/1     Running   0          6m57s
kube-system   kube-scheduler-cublr-odroidx2            1/1     Running   0          7m11s

coredns 팟이 Running 상태로 변경되었다. 그리고 flannel 컨테이너인 kube-flannel-ds-arm 팟도 생성된 것을 확인할 수 있다. daemonset 하위 리소스로 생성되었을 것이다.

그러면 여기까지 해서 쿠버네티스 마스터 노드의 설치와 설정이 완료되었다. 이대로 사용하면 싱글 노드 설치가 되서, 여기서도 이런저런 테스트를 해볼 수는 있다.

슬레이브 노드의 설치와 join

이번에는 슬레이브 노드를 설치해 보자. 이는 odroid-u3에 설치하려 한다.

설치 방법은 쿠버네티스 마스터와 크게 다를 것이 없어서, kubeadm을 설치하는 것, 그리고 사전에 필요한 조건들도 모두 동일하다. 우리는 위에서 마스터 노드를 이미 설치하였으므로, 이번에 설치할 슬레이브 노드는 이 마스터 노드에 속하도록 하는 것이 목표다.

마스터 노드 설치시 나왔던 kubectl join명령어를 통해 슬레이브 노드에 쿠버네티스를 설치하고 마스터 노드에 포함시킬 수 있다. 명령어에 대한 파라미터는 마스터 노드 결과에서 메모해 두었던 그것을 그대로 사용하면 된다.

$ kubeadm join 192.168.16.165:6443 --token j0dcpb... --discovery-token-ca-cert-hash sha256:ca8a0ca...
[sudo] password for cublr:
[preflight] Running pre-flight checks
	[WARNING SystemVerification]: this Docker version is not on the list of validated versions: 18.09.1. Latest validated version: 18.06
...
...
[discovery] Successfully established connection with API Server "192.168.16.165:6443"
[join] Reading configuration from the cluster...
[join] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
...
...
[tlsbootstrap] Waiting for the kubelet to perform the TLS Bootstrap...
[patchnode] Uploading the CRI Socket information "/var/run/dockershim.sock" to the Node API object "cublr-odroidu3-00001" as an annotation

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the master to see this node join the cluster.

매우 간단히 설치가 완료되었다. 마스터 노드에서 다음과 같은 노드 확인 명령을 통해 정상적으로 등록되었는지를 확인할 수 있다.

$ kubectl get nodes
NAME                   STATUS   ROLES    AGE   VERSION
cublr-odroidu3-00001   Ready    <none>   88s   v1.13.3
cublr-odroidx2         Ready    master   32m   v1.13.3

설치 검증

그러면은 이제 여기까지 진행되었으면 마스터와 슬레이브의 설치와 초기화가 모두 완료되었다. 이제부터는 간단하게 쿠버네티스의 여러 기본적인 동작을 확인해서 제대로 설치되었는지를 확인해 보도록 한다.

간단한 팟 실행

간단히 nginx가 실행되는 팟 하나를 실행해 보자. nginx는 다음의 yaml파일로 정의해 보았다. 아래와 같이 쿠버네티스 리소스를 정의한 simple_pod.yaml 파일을 만들었고, 이를 사용해서 리소스를 만들면 된다.

$ cat simple_pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: new-pod
  labels:
     app: web
     env: test
spec:
   containers:
    - name: test-cont
      image: nginx

$ kubectl create -f simple_pod.yaml
pod/testpod created

그러면 팟 리스트에서 다음과 같이 확인할 수 있고,

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                     READY   STATUS    RESTARTS   AGE
default       testpod                                  1/1     Running   0          3m17s
kube-system   coredns-86c58d9df4-cpvkk                 1/1     Running   0          13m
kube-system   coredns-86c58d9df4-hbpzg                 1/1     Running   0          13m
kube-system   etcd-cublr-odroidx2                      1/1     Running   0          13m
kube-system   kube-apiserver-cublr-odroidx2            1/1     Running   0          12m
kube-system   kube-controller-manager-cublr-odroidx2   1/1     Running   0          13m
kube-system   kube-flannel-ds-arm-ghvb2                1/1     Running   0          7m12s
kube-system   kube-flannel-ds-arm-pgpl5                1/1     Running   0          4m27s
kube-system   kube-proxy-54m5x                         1/1     Running   0          4m27s
kube-system   kube-proxy-ptcfd                         1/1     Running   0          13m
kube-system   kube-scheduler-cublr-odroidx2            1/1     Running   0          13m

슬레이브 노드에서 이 컨테이너를 확인할 수 있다.

$ sudo docker ps
CONTAINER ID   IMAGE      COMMAND                  CREATED         STATUS         PORTS    NAMES
b94f6f08f2ca   nginx      "nginx -g 'daemon of…"   3 minutes ago   Up 3 minutes            k8s_nginx-web-test-container_testpod_default_98979838-2970-11e9-a6da-96683186b805_0
...
...

이 컨테이너는 슬레이브 노드에 만들어졌지만 네트워크는 슬레이브 노드를 따르지 않는다. 쿠버네티스 슬레이브에도 마스터와 마찬가지로 flannel 컨테이너가 떠 있으며 이를 통해 구축된 네트워크를 사용한다. 따라서 docker inspect로는 컨테이너의 네트워크를 확인할 수 없다. 네트워크는 최초에 입력했던 --pod-network-cidr대역을 사용한다.

$ kubectl describe pod testpod
Name:               testpod
Namespace:          default
Priority:           0
PriorityClassName:  <none>
Node:               cublr-odroidu3-00001/192.168.16.224
Start Time:         Tue, 05 Feb 2019 18:05:18 +0000
Labels:             app=nginx-web-test
                    env=test
Annotations:        <none>
Status:             Running
IP:                 172.28.1.2
Containers:
  nginx-web-test-container:
    Container ID:   docker://b94f6f08f2ca621baac7e450802f71e9c1755cc9e60dd7d774107d1d1e86bf2a
    Image:          nginx
    Image ID:       docker-pullable://nginx@sha256:56bcd35e8433343dbae0484ed5b740843dd8bff9479400990f251c13bbb94763
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Tue, 05 Feb 2019 18:05:27 +0000
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-64h45 (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  default-token-64h45:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-64h45
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age   From                           Message
  ----    ------     ----  ----                           -------
  Normal  Scheduled  38s   default-scheduler              Successfully assigned default/testpod to cublr-odroidu3-00001
  Normal  Pulling    35s   kubelet, cublr-odroidu3-00001  pulling image "nginx"
  Normal  Pulled     30s   kubelet, cublr-odroidu3-00001  Successfully pulled image "nginx"
  Normal  Created    30s   kubelet, cublr-odroidu3-00001  Created container
  Normal  Started    29s   kubelet, cublr-odroidu3-00001  Started container

할당받은 ip는 172.28.1.2이다. 해당 컨테이너의 nginx에 접근할 수 있는지를 확인해 보자.

master$ curl 172.28.1.2
<!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>

물론 슬레이브 노드에서도 직접 접근해볼 수 있다.

slave$ curl 172.28.1.2
<!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>

DNS 확인

쿠버네티스에서는 컨테이너가 빠르게 추가되고 삭제되므로 ip를 통해 이를 사용하려고 하면 매우 곤란해진다. 내가 알고 있던 그 컨테이너가 항상 그 ip를 가지고 있을지를 확신할 수 없는 것이다. 따라서 쿠버네티스에서 컨테이너를 찾기 위해서는 컨테이너에 연결된 도메인을 사용하여야 편리한데, 이를 위해서 도메인을 통한 서비스 디스커버리를 제공한다.

서비스 디스커버리 관련 내용은 따로 자세히 정리해 보기로 하고, 우선 쿠버네티스 API서버의 DNS 주소인 kubernetes.default.svc.cluster.local에 요청을 보내 보자.

$ kubectl exec --namespace kube-system -t -i testpod /bin/bash
(testpod 컨테이너 안으로 들어옴)
$ nslookup kubernetes.default.svc.cluster.local
Server:		10.96.0.10
Address:	10.96.0.10#53

Name:	kubernetes.default.svc.cluster.local
Address: 10.96.0.1

DNS를 통해 정상적으로 컨테이너를 찾을 수 있음을 확인하였다.

Deployment 실행해 보기

이번에는 쿠버네티스의 리소스인 Deployment를 통해 어플리케이션을 실행해 보자. 팟과 마찬가지로 yaml파일을 미리 준비해 두었고 이를 실행해 보았다.

$ cat dep_nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 8
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80

$ kubectl create -f dep_nginx.yaml
deployment.apps/nginx-deployment created

Deployment에서 replicas를 8로 지정하였다. 따라서 이 deployment는 nginx는 팟이 8개 실행된다.

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                     READY   STATUS    RESTARTS   AGE
default       nginx-deployment-7db75b8b78-48gbl        1/1     Running   0          3m32s
default       nginx-deployment-7db75b8b78-6ctqc        1/1     Running   0          3m32s
default       nginx-deployment-7db75b8b78-6lzsl        1/1     Running   0          3m32s
default       nginx-deployment-7db75b8b78-fx6qd        1/1     Running   0          3m32s
default       nginx-deployment-7db75b8b78-gpjnf        1/1     Running   0          3m32s
default       nginx-deployment-7db75b8b78-m7h88        1/1     Running   0          3m32s
default       nginx-deployment-7db75b8b78-pmbdj        1/1     Running   0          3m32s
default       nginx-deployment-7db75b8b78-z54d9        1/1     Running   0          3m32s
default       testpod                                  1/1     Running   0          16m
kube-system   coredns-86c58d9df4-cpvkk                 1/1     Running   0          26m
kube-system   coredns-86c58d9df4-hbpzg                 1/1     Running   0          26m
kube-system   etcd-cublr-odroidx2                      1/1     Running   0          26m
kube-system   kube-apiserver-cublr-odroidx2            1/1     Running   0          26m
kube-system   kube-controller-manager-cublr-odroidx2   1/1     Running   0          26m
kube-system   kube-flannel-ds-arm-ghvb2                1/1     Running   0          20m
kube-system   kube-flannel-ds-arm-pgpl5                1/1     Running   0          17m
kube-system   kube-proxy-54m5x                         1/1     Running   0          17m
kube-system   kube-proxy-ptcfd                         1/1     Running   0          26m
kube-system   kube-scheduler-cublr-odroidx2            1/1     Running   0          26m

문제없이 실행되는 것을 확인하였다.

설치 완료?

이제 나머지 두 개의 노드도 추가해 보자. 명령어는 첫 번째 슬레이브를 추가했을 떄와 같다.

kubectl get nodes
NAME                   STATUS   ROLES    AGE   VERSION
cublr-odroidu3-00001   Ready    <none>   21m   v1.13.3
cublr-odroidu3-00002   Ready    <none>   32s   v1.13.3
cublr-odroidu3-00003   Ready    <none>   34s   v1.13.3
cublr-odroidx2         Ready    master   30m   v1.13.3

네 개의 노드가 모두 정상 연결된 것을 확인할 수 있다.

라우터에서 네트워크 라우팅 설정

이제 문제는 오드로이드가 아니라 내가 사용하는 머신에서 팟 네트워크, 정확히는 pod-network-cidr에 접근할 수 있어야 한다. 이를 위해서 공유기의 라우팅 테이블에 해당 네트워크 대역을 추가하자. 추가하는 법은 공유기 종류나 설정마다 다를 것이다..

routing-table

Namespace, ServiceAccount를 사용한 사용자간 자원 격리

위의 nginx 관련 배포된 정보를 보면 알겠지만, 현재 컨테이너는 모두 네임스페이스 “default"에 만들어지고 있다. 같은 네임스페이스에 있다는 것은 서로 통신이 가능하다는 뜻으로, 이래서는 여러 유저가 쿠버네티스를 공유해서 사용하게 된다. 이를 유저별로 격리하기 위해 쿠버네티스에서는 Namespace를 사용할 수 있다.

Namespace를 사용하면 쿠버네티스의 자원, 네트워크나 심지어는 권한도 격리할 수 있다. 따라서 하나의 쿠버네티스 클러스터를 여러 유저에게 안전하게 격리하여 제공할 수 있게 된다. 그러면 이번에는 그 네임스페이스를 사용할 수 있는 유저에 대해서 생각해 보아야 한다.

조금 찾아본 결과 유저는 쿠버네티스에서 따로 추가할 수는 없다. 다만 유저가 정상 유저라는 것을 판단하기 위해 특정 서비스, 이를테면 OAuth2를 통해 인증에 성공했는지에 대한 여부를 확인하는 것으로 동작하는 것 같다. 따라서 특정 회원을 만들고 거기에 권한을 부여하는, 우리가 생각하는 일반적인 방법으로 동작하게 하기 위해서는 ServiceAccount를 추가하여야 한다. 말하자면… 개발자는 일반 인증을 통해 사용하고, 배포용 아이디를 ServiceAccount를 통해 사용하는 느낌으로? 뭐 정확하진 않지만 앞으로 알아가면 된다.

여기서는 서비스어카운트를 추가하고 그 어카운트가 사용할 네임스페이스와 권한 등을 설정해 보자.

네임스페이스 생성

네임스페이스는 쿠버네티스 설치 시 기본적으로 다음과 같은 상태를 가진다.

$ kubectl get namespaces
NAME          STATUS   AGE
default       Active   14h
kube-public   Active   14h
kube-system   Active   14h

kube-public 혹은 kube-system은 쿠버네티스 시스템에서 사용하는 네임스페이스라고 생각할 수 있고, 일반 유저가 기본적으로 사용할 수 있는 네임스페이스는 default가 되겠다. 따라서 위에서 nginx를 생성했을 때 만들어진 결과는 default 네임스페이스에 생성된 것이다.

ojdspace라는 네임스페이스를 생성해 보자. 아래의 yaml 파일로 생성해볼 수 있다.

$ cat 00001.namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: ojdspace

$ kubectl create -f 00001.namespace.yaml
namespace/ojdspace created

$ kubectl get namespace
sNAME          STATUS   AGE
default       Active   14h
kube-public   Active   14h
kube-system   Active   14h
ojdspace      Active   11s

네임스페이스 ojdspace가 생성되었다. 앞으로 일반적인 목적으로 생성되는 리소스는 default와 ojdspace 두 네임스페이스에 나뉘어 만들어지게 될 것이다.

Role, ServiceAccount, 그리고 RoleBinding

롤과 서비스어카운트, 롤바인딩은 다음과 같은 관계를 가진다.

  1. 서비스어카운트는 네임스페이스를 가질 수 있다. 그리고 Token 정보를 포함하고 있다.
  2. 리소스 Role에는 쿠버네티스 API에 대한 권한을 지정할 수 있다.
  3. 리소스 RoleBinding에는 어떤 Role이 어떤 유저 혹은 서비스어카운트에 부여될 지 정할 수 있다.
  4. 롤바인딩에 유저와 롤이 명시되면 서비스어카운트의 토큰을 통해 쿠버네티스 API를 호출할 수 있다.

그러면 대충 순서가 보인다. 제일 먼저 네임스페이스 ojdspace에 리소스 CRUD를 할 수 있는 특정 유저 ojd-caller를 생성해 보자.

$ cat 00005.serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ojd-caller
  namespace: ojdspace

$ kubectl create -f 00001.serviceaccount.yaml
serviceaccount/ojd-caller created

$ kubectl describe serviceaccount ojd-caller --namespace ojdspace
Name:                ojd-caller
Namespace:           ojdspace
Labels:              <none>
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   ojd-caller-token-wtmhj
Tokens:              ojd-caller-token-wtmhj
Events:              <none>

서비스어카운트 ojd-caller는 현재 권한이 아무것도 없는 빈 유저이다. 단지 소속만 metadata.namespace를 통해 ojdspace안에 있도록 만들어 놓았는데, 여러 원할한 작업을 위해 이 유저에게 권한을 부여해야 한다. 토큰 정보는 쿠버네티스 리소스 secret을 통해 확인할 수 있는데, 그건 잠시 후에 확인해 보도록 한다.

이번에는 Role을 생성해 보자.

$ cat 00010.role.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ojd-caller
  namespace: ojdspace
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: ojd-caller-role
rules:
- apiGroups: [""]
  resources: ["configmaps", "endpoints", "persistentvolumeclaims", "pods", "secrets", "services", "serviceaccounts"]
  verbs: ["create", "delete", "deletecollection", "patch", "update", "get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets", "statefulsets"]
  verbs: ["create", "delete", "deletecollection", "patch", "update", "get", "list", "watch"]
- apiGroups: ["autoscaling"]
  resources: ["horizontalpodautoscalers"]
  verbs: ["create", "delete", "deletecollection", "patch", "update", "get", "list", "watch"]
- apiGroups: ["batch"]
  resources: ["cronjobs", "jobs"]
  verbs: ["create", "delete", "deletecollection", "patch", "update", "get", "list", "watch"]
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["roles", "rolebindings"]
  verbs: ["create", "delete", "deletecollection", "patch", "update", "get", "list", "watch"]
- apiGroups: ["extensions"]
  resources: ["ingresses", "deployments", "replicasets"]
  verbs: ["create", "delete", "deletecollection", "patch", "update", "get", "list", "watch"]
- apiGroups: [""]
  resources: ["resourcequotas"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["namespaces"]
  verbs: ["get", "watch"]

$ kubectl create -f 00010.role.yaml
role.rbac.authorization.k8s.io/example created

$ kubectl get role --all-namespaces
NAMESPACE     NAME                                             AGE
kube-public   kubeadm:bootstrap-signer-clusterinfo             15h
kube-public   system:controller:bootstrap-signer               15h
kube-system   extension-apiserver-authentication-reader        15h
kube-system   kube-proxy                                       15h
kube-system   kubeadm:kubelet-config-1.13                      15h
kube-system   kubeadm:nodes-kubeadm-config                     15h
kube-system   system::leader-locking-kube-controller-manager   15h
kube-system   system::leader-locking-kube-scheduler            15h
kube-system   system:controller:bootstrap-signer               15h
kube-system   system:controller:cloud-provider                 15h
kube-system   system:controller:token-cleaner                  15h
ojdspace      ojd-caller-role                                  32s

롤이 다음과 같이 생성되었다. yaml 안에 정의된 rule을 보면 여러 API에 대해 허용한 것을 확인할 수 있는데, 따라서 필요한 권한과 필요하지 않은 권한을 액션별로 잘 제어해서 조금 더 안전하게 권한을 부여할 수도 있으니 참고해 보자.

이번에는 롤바인딩을 통해 유저와 롤을 연결해 보자.

$ cat 00020.rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: ojd-caller-rbinding
  namespace: ojdspace
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: ojd-caller-role
subjects:
- kind: ServiceAccount
  name: ojd-caller
  namespace: ojdspace

$ kubectl create -f 00020.rolebinding.yaml
rolebinding.rbac.authorization.k8s.io/ojd-caller-rbinding created

롤바인딩을 보면 ojd-caller-rbinding을 정의하고 여기에 roleRef와 subject를 연결해 놓은것을 확인할 수 있다. 이제 서비스어카운트 ojd-caller는 role ojd-caller-role의 권한을 부릴 수 있게 된다.

sa/role/rolebinding

여러 유저가 사용 가능한 kubectl 설정

이번에는 실제로 잘 적용되었는지를 확인하기 위해 kubectl에 추가 설정을 확인해 보자.

kubectl 을 통해 pod/deployment를 생성하고 삭제할 수 있는 이유는 마스터 노드 초기화시 나왔던 메시지처럼, $HOME/.kube/config 파일이 /etc/kubernetes/admin.conf로부터 복사되었기 때문이다. 이 파일은 관리자의 권한을 가지기 때문에 여러 API를 문제없이 호출할 수 있었던 것이다.

이 파일에는 하나의 유저 뿐 아니라 여러 유저를 추가할 수 있다. 물론 호출하기 전에 어느 유저를 사용할지 정할 수도 있다. 그러면 이 부분에 대해서 정리해 보자.

기본 설정

$HOME/.kube/config파일을 직접 열어서 확인해봐도 되지만 단순히 확인만 하는 용도라면 아래의 명령으로도 충분하다.

$ kubectl config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: DATA+OMITTED
    server: https://192.168.16.165:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubernetes-admin
  name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
  user:
    client-certificate-data: REDACTED
    client-key-data: REDACTED

위 결과로부터 다음의 정보를 확인할 수 있다.

  1. cluster: 클러스터는 쿠버네티스 API 서버의 정보가 kubernetes의 이름으로 정의되어 있다.
  2. users: 유저는 유저의 이름 kubernetes-admin과 그 유저의 인증 정보, 여기서는 certificate/key로 정의되어 있다.
  3. context: 컨텍스트는 클러스터와 유저의 쌍이 kubernetes-admin@kubernetes의 이름으로 정의되어 있다.
  4. current-context: 현재 kubernetes-admin@kubernetes 컨텍스트를 사용한다고 지정되어 있다. 유저이름@클러스터이름으로 지정된 것 같다.

이걸 보면 이제 해야 할 일이 보인다. 우리가 할 것은 클러스터를 추가하고 유저를 추가한 후 컨텍스트를 새로 만들고, current-context를 새로 만든 컨텍스트로 바꾸는 것이다.

유저는 우리가 추가한 ojd-caller를 사용할 것이다. 유저의 정보를 확인해 보자.

$ kubectl describe secrets ojd-caller-token-wtmhj
Name:         ojd-caller-token-wtmhj
Namespace:    default
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: ojd-caller
              kubernetes.io/service-account.uid: e8a649cf-29e6-11e9-a6da-96683186b805

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1025 bytes
namespace:  7 bytes
token:      eyJhbGciOiJSUzI1NiIsI...

위에서 확인한 kubectl describe seviceaccount의 명령 결과로 Tokens를 확인하였다. 이 토큰 이름을 kubectl describe secrets 를 통해 확인하면 위와 같은 결과를 확인할 수 있다. 이 서비스어카운트 ojd-caller는 token eyJhbGci…를 가지는데, 이것을 인증 정보로 사용할 수 있다.

클러스터는 새로운 정보를 입력하는 것이 아니라 우리가 위에서 구축한 것을 그대로 쓰는 것이므로, 따로 추가할 필요는 없이 그대로 kubernetes를 사용하면 된다.

그러면 컨텍스트는 이제 ojd-caller@kubernetes를 새로 만들면 된다. 따라서 결과는 다음과 같을 것이다. 너무 길어서 많이 생략했는데 알아보는 것은 어렵지 않다. 이런 내용으로 $HOME/.kube/config를 수정하면 아래와 같은 내용을 확인할 수 있다.

$ kubectl config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: DATA+OMITTED
    server: https://192.168.16.165:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubernetes-admin
  name: kubernetes-admin@kubernetes
- context:
    cluster: ojdspace
    namespace: ojdspace
    user: ojd-caller
  name: ojd-caller@kubernetes
current-context: ojd-caller@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
  user:
    client-certificate-data: REDACTED
    client-key-data: REDACTED
- name: ojd-caller
  user:
    token: eyJhbGciOiJSUzI1NiIsI...

설정을 이렇게 만들게 되면 이제부터 kubectl은 ojd-caller@kubernetes 컨텍스트에 따라 쿠버네티스 API를 호출하게 된다. ojd-caller@kubernetes에 대한 내용을 따로 빼서 파일로 만들어 두면 다른 곳에서도 kubectl을 사용할 수 있으니 참고하자.

네임스페이스 격리 테스트

이제 새로운 네임스페이스 ojdspace에 리소스를 생성해 보자.

$ kubectl get pods
No resources found.

현재 ojdspace에 생성한 리소스가 아무것도 없으므로 당연히 pod이 아무것도 보이지 않는다.

$ kubectl create -f simple_pod.yaml
pod/testpod created
$ kubectl get pods
NAME      READY   STATUS              RESTARTS   AGE
testpod   0/1     ContainerCreating   0          8s
$ kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
testpod   1/1     Running   0          12s

참고로 testpod은 네임스페이스 default에 이미 생성한 팟의 이름이지만 네임스페이스가 다르므로 문제없이 생성되었다.

이번에는 Deployment를 생성해 보자.

$ kubectl create -f dep_nginx.yaml
deployment.apps/nginx-deployment created
$ kubectl get pods
NAME                                READY   STATUS              RESTARTS   AGE
nginx-deployment-7db75b8b78-c85hd   0/1     ContainerCreating   0          4s
nginx-deployment-7db75b8b78-cn8t9   0/1     ContainerCreating   0          4s
nginx-deployment-7db75b8b78-dhqtk   0/1     ContainerCreating   0          5s
nginx-deployment-7db75b8b78-gmsxl   0/1     ContainerCreating   0          4s
nginx-deployment-7db75b8b78-gxd2j   0/1     ContainerCreating   0          4s
nginx-deployment-7db75b8b78-pdpt8   0/1     ContainerCreating   0          5s
nginx-deployment-7db75b8b78-pzbkb   0/1     ContainerCreating   0          5s
nginx-deployment-7db75b8b78-vc9tf   0/1     ContainerCreating   0          4s
testpod                             1/1     Running             0          37s

지금은 ContainerCreating상태이지만 잠시 후에 Running상태로 정상 동작하는 것을 확인하였다. 특히나 이 Deployment또한 네임스페이스 default에 이미 생성되어 있는 것으로, 이름이 같이도 네임스페이스가 다르므로 정상적으로 생성되었다.

이번에는 네임스페이스 상관없이 모든 팟의 리스트를 확인해 보자. –all-namespaces 키워드를 사용 가능하다.

$ kubectl get pods --all-namespaces
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:ojdspace:ojd-caller" cannot list resource "pods" in API group "" at the cluster scope

에러가 발생한 것을 확인하였다. ojd-caller는 모든 네임스페이스를 확인할 수 있는 권한이 없으므로, kubectl의 권한을 기존에 사용한 어드민 권한의 컨텍스트 kubernetes-admin@kubernetes로 다시 전환하여야 한다. 유저간의 전환은 $HOME/.kube/config의 current-context를 직접 수정해도 되지만 다음의 명령으로도 전환이 가능하니 참고해 보자.

$ kubectl config use-context kubernetes-admin@kubernetes
Switched to context "kubernetes-admin@kubernetes".

$ kubectl get pods --all-namespaces
NAMESPACE     NAME                                     READY   STATUS    RESTARTS   AGE
default       nginx-deployment-7db75b8b78-48gbl        1/1     Running   0          15h
default       nginx-deployment-7db75b8b78-6ctqc        1/1     Running   0          15h
default       nginx-deployment-7db75b8b78-6lzsl        1/1     Running   0          15h
default       nginx-deployment-7db75b8b78-fx6qd        1/1     Running   0          15h
default       nginx-deployment-7db75b8b78-gpjnf        1/1     Running   0          15h
default       nginx-deployment-7db75b8b78-m7h88        1/1     Running   0          15h
default       nginx-deployment-7db75b8b78-pmbdj        1/1     Running   0          15h
default       nginx-deployment-7db75b8b78-z54d9        1/1     Running   0          15h
default       testpod                                  1/1     Running   0          15h
kube-system   coredns-86c58d9df4-cpvkk                 1/1     Running   0          15h
kube-system   coredns-86c58d9df4-hbpzg                 1/1     Running   0          15h
kube-system   etcd-cublr-odroidx2                      1/1     Running   0          15h
kube-system   kube-apiserver-cublr-odroidx2            1/1     Running   0          15h
kube-system   kube-controller-manager-cublr-odroidx2   1/1     Running   0          15h
kube-system   kube-flannel-ds-arm-5gd8k                1/1     Running   0          15h
kube-system   kube-flannel-ds-arm-fc5z4                1/1     Running   0          15h
kube-system   kube-flannel-ds-arm-ghvb2                1/1     Running   0          15h
kube-system   kube-flannel-ds-arm-pgpl5                1/1     Running   0          15h
kube-system   kube-proxy-54m5x                         1/1     Running   0          15h
kube-system   kube-proxy-84fzl                         1/1     Running   0          15h
kube-system   kube-proxy-ptcfd                         1/1     Running   0          15h
kube-system   kube-proxy-vvpvs                         1/1     Running   0          15h
kube-system   kube-scheduler-cublr-odroidx2            1/1     Running   0          15h
ojdspace      nginx-deployment-7db75b8b78-c85hd        1/1     Running   0          62m
ojdspace      nginx-deployment-7db75b8b78-cn8t9        1/1     Running   0          62m
ojdspace      nginx-deployment-7db75b8b78-dhqtk        1/1     Running   0          62m
ojdspace      nginx-deployment-7db75b8b78-gmsxl        1/1     Running   0          62m
ojdspace      nginx-deployment-7db75b8b78-gxd2j        1/1     Running   0          62m
ojdspace      nginx-deployment-7db75b8b78-pdpt8        1/1     Running   0          62m
ojdspace      nginx-deployment-7db75b8b78-pzbkb        1/1     Running   0          62m
ojdspace      nginx-deployment-7db75b8b78-vc9tf        1/1     Running   0          62m
ojdspace      testpod                                  1/1     Running   0          62m

네임스페이스 default와 ojdspace에 같은 이름의 팟들이 생성되어 있는 것을 확인할 수 있다.

스토리지 클래스 생성

쿠버네티스니 뭐니 해도 결국은 컨테이너라는게 결국 특정 컨테이너 호스트에 생성되는 것이므로 그 컨테이너가 사라지면 데이터도 함께 사라지는게 문제다. 그래서 영속성의 스토리지가 필요한데, 이 때 필요한 것이 바로 스토리지 클래스가 되겠다. 스토리지 클래스를 통해 (PV/PVC)PersistentVolume/PersistentVolumeClaim 을 생성하여 이런 문제를 해결해 보자. 이 부분은 내용이 길기 다른 페이지를 할애해서 따로 정리해 보도록 하는 것이 좋겠다.

사내에서 사용하는 쿠버네티스 클러스터는 Ceph나 Cinder로 잘 구성되어 있어서 그렇게 구성할까 하다가 심플하게 설치하는데 뭐 그런것까지 해야되나 싶기도 해서 조금 찾아보니 nfs로도 구성할 수 있는 것을 확인했다. 특히나 nfs-client의 경우 이미 서버가 존재한다는 가정하에 helm을 통해 매우 간단한 설치를 제공해서 이것으로 하기로 결정했다.

wd my book

마침 집에 WD MY BOOK 8테라바이트짜리 기기가 남는 것이 있어서 이걸로 사용하기로 했다. 항상 켜져있는 컴퓨터에 연결해서 NFS를 구성했는데, USB3가 지원되면서 기가비트랜인데 2A가 되는 싱글보드가 있으면 그걸로 교체해서 다시 구성해보고 싶다.

helm을 통한 스토리지 클래스 생성

우선 현재 설치되어 있는 스토리지 클래스는

$ kubectl get storageclass
No resources found.

없다. 그래서 직접 스토리지 클래스를 생성해도 되는데, 어떤 스토리지 프로바이더가 있는지 여기에서 확인해 보자. 문서를 읽어 보면 Storage Provisionerinternalexternal 두 가지 타입이 있는데, 일단 NFS는 internal이 아니므로 external에서 찾아보아야 한다.

그것은 kubernetes-incubator/external-storage에서 찾아볼 수 있고, 여기에서 보면 nfs-client가 있는 것을 확인할 수 있다.

nfs-client는 상세한 설치 가이드를 제공하지만, Helm으로 설치하는 것은 설명조차도 몇 줄로 치울 정도로 간단하므로 Helm을 통해 설치해 보자. 아래와 같이 설치하였다.

$ helm install --set nfs.server=192.168.16.164 --set nfs.path=/mnt/nfs1 stable/nfs-client-provisioner --set image.repository=quay.io/external_storage/nfs-client-provisioner-arm
NAME:   wrinkled-moose
LAST DEPLOYED: Sat Feb  9 16:23:14 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Pod(related)
NAME                                                    READY  STATUS             RESTARTS  AGE
wrinkled-moose-nfs-client-provisioner-6c9564c94d-9wvkd  0/1    ContainerCreating  0         0s

==> v1/StorageClass

NAME        PROVISIONER                                          AGE
nfs-client  cluster.local/wrinkled-moose-nfs-client-provisioner  1s

==> v1/ServiceAccount

NAME                                   SECRETS  AGE
wrinkled-moose-nfs-client-provisioner  1        1s

==> v1/ClusterRole

NAME                                          AGE
wrinkled-moose-nfs-client-provisioner-runner  1s

==> v1/ClusterRoleBinding

NAME                                       AGE
run-wrinkled-moose-nfs-client-provisioner  1s

==> v1/Role

NAME                                                  AGE
leader-locking-wrinkled-moose-nfs-client-provisioner  1s

==> v1/RoleBinding

NAME                                                  AGE
leader-locking-wrinkled-moose-nfs-client-provisioner  1s

==> v1/Deployment

NAME                                   DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
wrinkled-moose-nfs-client-provisioner  1        1        1           0          1s

차트의 설명에서는 arm에서 설치할 경우 다른 이미지를 사용하도록 안내를 주고 있으므로 이미지 주소도 –set으로 오버라이드하여 실행하였다. 잠시 후에 실행 결과를 확인했더니

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                                     READY   STATUS    RESTARTS   AGE
default       wrinkled-moose-nfs-client-provisioner-6c9564c94d-9wvkd   1/1     Running   0          30s
...

$ kubectl get storageclass
NAME         PROVISIONER                                           AGE
nfs-client   cluster.local/wrinkled-moose-nfs-client-provisioner   29m

$ kubectl describe storageclass nfs-client
Name:                  nfs-client
IsDefaultClass:        No
Annotations:           <none>
Provisioner:           cluster.local/wrinkled-moose-nfs-client-provisioner
Parameters:            archiveOnDelete=true
AllowVolumeExpansion:  True
MountOptions:          <none>
ReclaimPolicy:         Delete
VolumeBindingMode:     Immediate
Events:                <none>

정상적으로 스토리지 클래스와 팟이 실행된 것을 확인했다. 팟이 하는 일은 automatic provisioner인데 스토리지를 감시하고 PVC/PV를 생성해 주는 일을 대행해 주는 서버라고 생각하면 크게 무리없는 것 같다.

PVC의 생성

그러면 PVC를 생성해 보자.

$ cat pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: demo-claim
  annotations:
    volume.beta.kubernetes.io/storage-class: nfs-client
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi

그냥 인터넷에서 구한 PVC의 기본 형태인데, 볼륨의 이름과 스토리지 클래스의 이름, 그리고 사이즈를 수정하니 이런 모양이 됐다.

$ kubectl create -f pvc.yaml
persistentvolumeclaim/demo-claim created

$ kubectl get pvc
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
demo-claim   Bound    pvc-5ad0fe2e-2c87-11e9-a6da-96683186b805   3Gi        RWO            nfs-client     10s

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS   REASON   AGE
pvc-5ad0fe2e-2c87-11e9-a6da-96683186b805   3Gi        RWO            Delete           Bound    default/demo-claim   nfs-client

이 리소스 PersistentVolumeClaim을 생성했더니 10초 정도 지나서 완전히 생성된 것을 확인했다. NFS 서버에는 무슨 일이 일어났나 가 봤더니 다음과 같았다.

$ ls -la
합계 28
drwxr-xr-x 4 root root  4096  2월 10 01:25 .
drwxr-xr-x 3 root root  4096  2월 10 00:02 ..
drwxrwxrwx 2 root root  4096  2월 10 01:25 default-demo-claim-pvc-5ad0fe2e-2c87-11e9-a6da-96683186b805

nfs-client에서 설명하기를,

Persistent volumes are provisioned as ${namespace}-${pvcName}-${pvName}

이라고 되어 있는데 딱 그런 식으로 생성된 것 같다. 잘 동작하는 것 같다.

마지막으로 이 스토리지 클래스를 디폴트로 변경하면 앞으로 PVC를 생성할 때 자동으로 이 스토리지 클래스를 선택할 것이다.

$ kubectl patch storageclass nfs-client -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

테스트: gogs 배포

gogs라고, 깃허브와 매우 유사한 사용성을 제공하는 깃 호스팅 어플리케이션이 있는데 이것을 한번 배포해 보자. 챠트는 인큐베이터 저장소에서 가져왔다.

$ helm install incubator/gogs --set image.repository="gogs/gogs-rpi" --set postgresql.imageTag="11.1" --set serviceType="LoadBalancer"
NAME:   quieting-mite
LAST DEPLOYED: Sat Feb  9 17:51:42 2019
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1/Secret
NAME                      TYPE    DATA  AGE
quieting-mite-postgresql  Opaque  1     2s
quieting-mite-gogs        Opaque  1     2s

==> v1/ConfigMap

NAME                        DATA  AGE
tcp-quieting-mite-gogs-ssh  1     2s
quieting-mite-gogs-config   1     2s

==> v1/PersistentVolumeClaim

NAME                      STATUS   VOLUME                                    CAPACITY  ACCESS MODES  STORAGECLASS  AGE
quieting-mite-postgresql  Bound    pvc-5c2fa8b2-2c93-11e9-a6da-96683186b805  8Gi       RWO           nfs-client    2s
quieting-mite-gogs        Pending  nfs-client                                1s

==> v1/Service

NAME                      TYPE          CLUSTER-IP     EXTERNAL-IP  PORT(S)                    AGE
quieting-mite-postgresql  ClusterIP     10.105.97.166  <none>       5432/TCP                   1s
quieting-mite-gogs        LoadBalancer  10.110.83.253  <pending>    80:30283/TCP,22:30661/TCP  1s

==> v1beta1/Deployment

NAME                      DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
quieting-mite-postgresql  1        1        1           0          1s
quieting-mite-gogs        1        1        1           0          1s

==> v1/Pod(related)

NAME                                      READY  STATUS             RESTARTS  AGE
quieting-mite-postgresql-975d75f54-jcs8c  0/1    ContainerCreating  0         1s
quieting-mite-gogs-744bcfdb76-q8p6k       0/1    Pending            0         1s


NOTES:
1. Get the Gogs URL by running:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace default -w quieting-mite-gogs'

  export SERVICE_IP=$(kubectl get svc --namespace default quieting-mite-gogs -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
  echo http://$SERVICE_IP/

2. Register a user.  The first user registered will be the administrator.

플랫폼이 arm이라서 values를 조금 수정했는데, gogs는 아직 멀티 아키텍쳐용 이미지가 아니고, postgresql은 이 차트에서 9버전대를 사용하는데 이것 또한 멀티 아키텍쳐를 지원하지 않는거 같아 최신 버전을 사용하였다. 또한 이 차트의 설명을 보면, 서비스타입은 minicube를 사용할 때 NodePort를 쓰고 다른 타입은 “LoadBalancer"를 사용하라고 가이드가 되어 있어서 그렇게 했지만 우선은 로드밸런서는 안된다고 생각해 두자.

위 실행 결과에 팟 정보가 있으므로 해당 팟의 정보를 확인해 보자.

$ kubectl describe pod quieting-mite-gogs-744bcfdb76-q8p6k
Name:               quieting-mite-gogs-744bcfdb76-q8p6k
Namespace:          default
...
...
Status:             Running
IP:                 172.28.1.20
Controlled By:      ReplicaSet/quieting-mite-gogs-744bcfdb76
Containers:
  gogs:
    Container ID:   docker://eb20684ee37b50c3fb048dd3ffb6dc9037521b78504a395da5e7b9c705005608
    Image:          gogs/gogs-rpi:0.11.79
    Image ID:       docker-pullable://gogs/gogs-rpi@sha256:d1b1ccee6e94207eaf0ebe565ca173d147af29ce3c3ab0531f12f3e49fb6f3eb
    Ports:          3000/TCP, 22/TCP
...
...
Events:
  Type     Reason            Age                    From                               Message
  ----     ------            ----                   ----                               -------
  Warning  FailedScheduling  2m15s (x6 over 2m27s)  default-scheduler                  pod has unbound immediate PersistentVolumeClaims (repeated 3 times)
...
...

ip와 포트가 각각 172.28.1.20, 3000인 것을 확인할 수 있는데, 우리는 위에서 172 대역에 대한 라우팅을 라우터에 추가하였으므로 웹 브라우저에서 바로 이 팟으로 접근할 수가 있다.

gogs-result

바로 이렇게!!