본문 바로가기

프로그래밍/kubernetes

CKA 시험 관련 잡다한 정리(2)

쓰다보니 점점 내용이 길어지게 되어서 연재스타일로 가야할듯 싶다(실은..저 내용 이상으로 글씨가 타이핑이 안되어서 나눈 것임..ㅋㅋ..)


NodeAffinity를 사용하면 NodeSelector 보다 좀더 다양한 조건들을 만들어서 Pod Schduling을 할 수 있다.

이전글에서 NodeSelector 설명시 NodeSelector에서는 할 수 없는 size가 Large이거나 Medium인 label을 가지고 있는 Node에 배포한다고 가정해보자. 이때 해당 Pod의 yaml 설정을 다음과 같이 하면 된다

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
spec:
  containers:
  - image: nginx
    name: nginx-container
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions :
          - key: size
            operator: In
            values:
            - Large
            - Medium

spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions 항목에 Pod이 어떤 label을 가진 Node에 배포되는지의 조건을 설정해주면 된다. matchExpressions는 조건을 설정하는 부분이기 때문에 배포에 대한 여러 조건을 가질수 있으므로 배열의 형태를 지니게 된다. label 또한 여러개의 label을 사용할 수 있기 때문에 key 부분에서부터 배열의 형태를 지닐수 있으며 value 또한 하나의 key에 대해 여러개의 값을 조건으로 사용할 수 있기 때문에 (size = Large OR Medium) 배열의 형태를 지닐수 있다. operator에 들어가는 것은 In (value 항목에 설정한 값으로 되어 있는 node label의 value가 있는 node 선택), NotIn(value 항목에서 지정한 값이 아닌 다른 값으로 되어 있는 node label의 value가 있는 node 선택), Exists(설정된 matchExpressions.key가 node의 label을 구성하는 key들 중에 존재하는 Node 선택), NotExists(설정된 matchExpressions.key가 node의 label을 구성하는 key들 중에 존재하지 않는 Node 선택), Gt(value 항목에서 지정한 값보다 큰 값을 가진 node label의 value가 있는 node 선택), Lt(value 항목에서 지정한 값보다 작은 값을 가진 node label의 value가 있는 node 선택) 이렇게 총 6가지를 사용할 수 있다.

 

requiredDuringSchdulingIgnoredDuringExecution 옵션에 대해선 설명이 필요한 부분이다. 이 옵션 위치에 들어갈수 있는 것은 방금 말한거 말고도 preferredDuringSchedulingIgnoredDuringExecution 도 있다. 이 옵션 2개를 볼때는 다음의 부분을 끊어서 볼 필요가 있다. 먼저 requiredDuringSchedulingIgnoredDuringExecution의 경우는 requiredDuringSchedulingIgnoredDuringExecution 이렇게 2개로 나눠서 봐야 하는데 requiredDuringScheduling은 단어 의미 그대로 Scheduling 하는 동안에는 nodeAffinity에 설정한 조건을 반드시 만족해야 한다는 것이다. 즉 requiredDuringSchedulingIgnoredDuringExecution 옵션을 사용한 Pod을 Node에 배포하려 할때는 지정된 조건들을 만족해야 한다는 것이다. IgnoredDuringExecution 또한 단어 의미 그대로 실행중일때는 무시한다는 의미이다. 이 의미는 예를 들어 해당 조건을 만족하여 Node에 Pod이 배포되어 현재 Pod이 실행중인 상태인데 이렇게 운영되던 도중 Node의 Label이 변경되어 해당 조건을 만족하지 못하는 상황이 벌어졌을 경우 이를 무시한다는 뜻이다. 즉 무시했기 때문에 실행에는 아무 영향을 미치지 않는다. 

preferredDuringSchedulingIgnoredDuringExecution 또한 preferredDuringSchduling과 IgnoredDuringExecution 이렇게 2개로 나눠서 볼 수 있다. IgnoredDuringExecution은 앞서 설명했기때문에 생략하고 preferredDuringScheduling에 대해서만 설명하도록 하겠다. preferred란 단어의 뜻인 선호한다는 의미를 내포하고 있는 옵션인데 이 옵션은 조건을 만족하는 Node에 최대한 배포되도록 하겠으나 만약 그런 Node를 찾지 못하면 조건을 만족하지 못하는 Node라 하더라도 해당 Node에 배포하겠다는 의미이다. 반면 앞서 설명했던 requiredDuringSchedulingIgnoredDuringExecution 옵션의 경우는 required(반드시)이기 때문에 조건을 만족하는 Node를 찾지 못하면 해당 Pod은 배포가 되지 않는다(Pending..대기상태에 빠진다)


Taints & Tolerations 와 NodeAffinity 에 대한 Pod Schedule 정리

 

Taints & Tolerations의 경우는 다음과 같이 정리될 수 있다

  Pod에 해당 Taints에 대한 Toleration이 있을 경우 Pod에 해당 Taints에 대한 Toleration이 없을 경우
Node에 Taints가 있을 경우 배포 가능 배포 불가능
Node에 Taints가 없을 경우 배포 가능 배포 가능

NodeAffinity의 경우는 다음과 같이 정리될수 있다(requiredDuringSchedulingIgnoredDuringExecution 을 사용했을 경우에만 해당된다. preferredDuringSchedulingIgnoredDuringExecution 를 사용하면 반드시 조건을 만족할 필요는 없기 때문에 아래의 도표에서 배포 불가능한 경우에서도 배포가 가능하게 된다)

  Pod에 NodeAffinity를 설정했을 경우 Pod에 NodeAffinity를 설정하지 않았을 경우
Node에 label을 설정했을 경우 Pod에 설정된 NodeAffinity의 조건을 만족했을 경우 Node에 배포 가능, 그렇지 않을 경우 Node에 배포 불가능 배포 가능
Node에 label을 설정하지 않았을 경우 배포 불가능(이건 직접 내가 가지고 있는 kubernetes cluster Node에 존재하지 않는 label로 Pod에서 NodeAffinity를 설정한뒤 테스트해서 검증 완료) 배포 가능

그래서 특정 Pod은 반드시 특정 Node에만 배포되게끔 설정할려면 Taints & Toleration과 NodeAffinity를 조합해야만 한다. Node에 Taints를 설정해서 해당 Taints를 만족하지 못하는 Pod은 들어오지 못하게 막고 Taints가 없을 경우 Taints와 매핑되는 Toleration의 유무와는 관계 없이 Pod이 Node에 배포되는 상황을 막기 위해 NodeAffinity로 이를 제어한다.


Pod의 Resource 설정

 

kubernetes에서 node의 pod을 schedule 할때는 해당 node의 resource 여유를 체크해서 생성되는 Pod이 사용하는 resource를 감당할수 있을정도의 resource 여유가 되는 node에 schedule 하게 된다. 만약 pod이 사용하게 되는 resource를 감당할수 있는 node가 없을 경우 pod은 어떤 node에도 배포되지 않고 대기상태(pending)에 머물게 된다

 

pod에서 사용하는 1 cpu는 다음과 같다

1 cpu =1 AWS vCPU = 1 GCP Core = 1 Azure Core = 1 HyperThread 

 

메모리는 다음과 같이 계산되어진다

 

1 K(Kilobyte) = 1,000 bytes

1 M(Megabyte) = 1,000,000 bytes

1 G(Gigabyte) = 1,000,000,000 bytes

 

1 Ki(Kibibyte) = 1,024 bytes

1 Mi(Mebibyte) = 1,048,576 (= 1024 * 1024) bytes 

1 Gi(Gibibyte) = 1,073,741,824 (= 1024 * 1024 * 1024) bytes

 

접미사 i가 붙은 단위는 1 KB = 1024 Byte 단위로 계산해서 그 결과를 표현한다. 즉 1000을 곱해야 표현할 단위를 1000대신 1024를 곱해서 표현하게 된다.

 

여기서 적용되는 Resource는 Pod에서 실행되는 Container 한개한개가 사용하게 되는 Resource를 설정하는 것이다.

request와 limit의 차이는 다음과 같다. 최소 request 에서 설정한 수치만큼 이용하지만 상황에 따라서는 최대 limit 에서 설정한 수치만큼 이용한다.

(예를 들어 limit memory를 256Mi로 설정하고 request memory를 128Mi로 설정했을 경우 Pod에서 실행되는 container 1개가 사용하게 되는 memory의 크기는  최소 128Mi를 사용하지만 그 이상으로 써야 하는 상황이 왔을 경우 유휴자원이 존재하면 최대 256Mi 까지 사용하게 된다는 것을 의미한다.)

Pod 이 실행되는 Node에서 cpu에서 유휴자원이 더는 없는 상황에서 cpu가 추가로 자원을 써야 하는 경우엔 cpu throttling이 발생할뿐 container 실행 자체에는 영향을 주지는 않는다. 그러나 memory의 유휴자원이 없을 경우엔 Pod이 실행종료 된다

 

kubectl get pod 명령을 이용해서 특정 Pod의 상세내용을 볼때 Containers.container 이름.Last State.Reason의 항목에 대한 값이 OOMKilled 이면 가용 메모리가 부족해서 Pod이 실행중지 되었음을 의미한다.

 

kube edit pod 명령을 이용해서 container에 할당된 cpu나 memory 크기를 수정할 수 없다. 비단 resource의 cpu 와 memory 값 뿐만 아니라 아래의 것들 또한 pod이 생성되어 있는 상황에서 값을 수정할 수 없는 항목들이다.

spec.containers[*].image

spec.initContainers[*].image

spec.activeDeadlineSeconds

spec.tolerations

수정하고자 하는 pod의 이름이 mypod일 경우 위에서 언급한 항목을 수정하려 할때는 kubectl get pod mypod -o yaml > mypod.yaml 이런식으로 현재 실행중인 pod의 yaml 파일을 생성한뒤 이 파일을 열어서 값을 수정한 후 파일을 저장한다. 그런 다음에 현재 생성되어 있는 pod을 삭제한 후(kubectl delete pod mypod) 다시 생성해서(kubectl apply -f mypod.yaml) 수정된 값으로 pod이 운영되게 해준다


DaemonSets은 모든 Node에 동일한 Pod을 1개씩 생성하는 kubernetes resource 이다. 예를 들어  logging을 해야 한다거나 모니터링, 그리고 네트워킹 등을 위한 에이전트를 각 Node에 생성해야 할때 DaemonSets를 사용하게 된다.

(calico 네트워크 플러그인을 설치하면 Pod의 이름이 calico-node 라는 문자열로 시작하는 Pod이 각 Node마다 생성되는데 이것은 DaemonSets로 인해 생성된 Pod 이다)

 

각 Node마다 생성해야 하기 때문에 Node에 설정된 Taints에 대응되는 Toleration을 설정할 필요가 있다. Master Node에도 Pod이 설치되어야 한다면 Master Node의 Taints에 대응되는 Tolerations를 설정해주면 Master Node에도 설치가 가능하다. 그러나 Master Node에 설치할 필요가 없다거나 또는 몇몇 특정 노드에는 설치할 필요가 없다면 해당 Node에 Taints를 설정한뒤 그 Taints에 대응하는 Tolerations를 설정하지 않으면 된다.

 

DaemonSets은 kubectl create 명령으로는 생성할 수 없다. 그래서 DaemonSets를 생성할려면 DaemonSets yaml 구조와 거의 같은 구조인 deployment를 이용해서 먼저 골격을 만들어서 작업하면 된다. 세부적인 방법은 다음과 같다. 예를 들어 nginx image를 사용하고 kube-system namespace에 속하는 daemonsets인 nginx-daemonsets를 만든다고 가정하면 

1. kubectl create deployment nginx-daemonsets -n kube-system --image=nginx --dry-run=client -o yaml > nginx-daemonsets.yaml

2. vi nginx-daemonsets.yaml

3. daemonsets 구성에 속하지 않는 항목들 삭제(spec.replicas)

4. 값이 설정되어 있지 않은 항목들 삭제

5. kind 항목의 값을 DaemonSet 으로 수정

6. nginx-daemonsets.yaml 파일을 저장

7. kubectl apply -f nginx-daemonsets.yaml 명령을 실행하여 DaemonSets 생성

하지만 이렇게 생성하면 Master Node를 제외한 나머지 Worker Node들에만 Pod이 생성된다(Worker Node에 Taints가 설정되어 있지 않았다고 가정할 경우). 왜냐먼 Master Node에는 Taints가 설정되어 있기 때문에 Master Node의 Taint에 대응되는 Tolerations를 nginx-daemonsets.yaml 파일에 설정해주지 않으면 Master Node에는 DaemonSets가 생성하는 Pod이 생성되지를 않는다.

 

DaemonSet에서 Master Node에 설정되어 있는 Taint에 대응되는 Tolerations를 설정해서 Master Node에도 DaemonSet을 통해 만들어진 Pod이 배포되게 할 수도 있지만 다음과 같이 Toleration를 설정해도 MasterNode에 배포될 수 있다. 아래의 yaml 파일 또한 DaemonSet을 만들때 MasterNode에도 Pod을 생성해서 배포할 수 있다.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app: master-daemonset
  name: master-daemonset
spec:
  selector:
    matchLabels:
      app: master-daemonset
  template:
    metadata:
      labels:
        app: master-daemonset
    spec:
      tolerations:
      - effect: NoSchedule
        operator: Exists
      containers:
      - image: nginx:alpine
        name: nginx

사실 이 방법이 어떻게 보면 DaemonSet의 개념 입장에서는 더 확실한 방법이다 Kubernetes 공식 문서의 DaemonSet 설명에 있는 Sample yaml 파일을 통해 DaemonSet을 만들려면 Master Node를 제외한 나머지 Worker Node들에게는 Taint가 걸려있지 않아야 한다 라는 전제가 만족되어야한다. 그러나 현실세계에서는 운영상의 이유로 Worker Node에도 다양하게 Taint가 붙어 있을수 있으며 이렇게 붙게되는 Worker Node 들의 Taint를 신경쓰지 않고 배포되게끔 할려면 tolerations에 key와 value를 생략해버림으로써 Worker Node에 어떤 Taint가 설정되든 다 만들어지게끔 해주면 된다(Node에 Taint가 설정되어 있지 않고 Pod에 toleration만 있어도 그 Node에 Pod이 배포된다는건 위에서 한번 언급했으니 아시리라 생각한다) 실제 Calico Network PlugIn이 DaemonSet으로 Calico 관련 Pod을 Master Node를 포함한 각 Node에 배포할때 이 방법을 사용하고 있다. 그러나 CKA 시험볼때는 key와 value를 설정하지 않고 하는 방법을 잊어먹을수도 있으니 문서대로 하는것도 나쁘지는 않다고 생각한다.


Static Pod은 Kubernetes의 Master Node의 통제하에 관리되는 Pod이 아닌 오직 kubelet에 의해서만 생성되고 관리되며 삭제되는 Pod이다. 일반적으로 Pod을 생성하면 kube-apiserver를 통해서 kube-controller 가 생성, 수정, 삭제하고 kube-scheduler 를 통해 관련 Node에 배포되며 이러한 작업에 필요한 각종 정보를 etcd에 저장하는 일련의 과정들을 거치게 되지만 이 Static Pod은 이러한 과정 없이 오직 kubelet에 의해서 생성, 수정, 삭제된다. 그렇기 때문에 kubectl을 통해 관리가 가능한 Pod이 아니다

 

Static Pod은 기본적으로 /etc/kubernetes/manifests 디렉토리에 Static Pod을 생성하게 되는 yaml 파일(구조는 Pod을 생성하는 yaml 파일 구조와 동일하다)을 넣은뒤 Node가 가동될때 /etc/kubernetes/manifests 디렉토리에 있는 yaml 파일들을 읽어 현재 생성되지 않은 Pod일 경우 이를 Static Pod으로 생성하게 된다. 이 /etc/kubernetes/manifests 디렉토리를 설정하는 위치는 다음의 2군데중 한군데서 설정되어 있다

 

1. kubelet.service 파일을 열어보면 ExecStart 항목에 kubelet을 실행하는 명령어 라인이 있는데 이 라인을 보면 option으로 --pod-manifest-path=/etc/kubernetes/manifests 라고 설정되어 있다

2. 1번에 설정되어 있지 않으면 kubelet.service 파일의 ExecStart 항목의 kubelet을 실행하는 명령어 라인에서 option으로 --config=kubeconfig.yaml 파일이 있는데 이 yaml 파일의 staticPodPath 항목에 /etc/kubernetes/manifests 라고 설정되어 있다 

 

강의 동영상은 그렇게 설명했지만 실제 보면 그렇게 설정되어 있질 않아서 내가 찾은 방법을 정리하고자 한다

 

1. sudo systemctl status kubelet.service 를 실행하면 Drop-In 항목이 /usr/lib/systemd/system/kubelet.service.d 디렉토리의 10-kubeadm.conf 파일로 설정되어 있는 것을 볼수 있는데 이 파일을 vi로 열어본다

2. 10-kubeadm.conf 파일의 내용을 보면 Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml" 항목을 볼 수 있다. 이 파일을 열어본다.

3. config.yaml 파일을 보면 staticPodPath 항목에 /etc/kubernetes/manifests 라고 설정되어 있는것을 볼 수 있다

그러나 나중에 연습문제를 풀면서 이보다 더 쉬운방법을 찾았는데 그 방법은 밑에 설명해놨다

 

Static Pod은 위에서 설명했다시피 kubectl로 관리되는 pod이 아니기 때문에 kubectl get pod 명령으로 해당 pod을 볼 수 없다. 이러한 Static Pod은 docker ps 명령으로 확인이 가능하다(동영상 내용에서는 그렇게 얘기하는데 kubectl로도 확인은 가능했다. kubectl get pod 에서도 조회가 되고 kubectl describe pod 을 통해 상세내용도 조회가 가능했다)

 

Static Pod이 kubectl을 통해 조회가 될때 그 이름은 Static Pod 이름-Static Pod이 생성된 Node 이름 이 된다. 예를 들어 이름이 node01인 Node에 my-test-job 이란 Static Pod을 생성했다면 kubectl을 통해 보여지게되는 Pod의 이름은 my-test-job-node01 이 된다

 

Kubernetes의 Master Node에 생성되는 etcd cluster, kube-scheduler, controller-manager, kube-apiserver 는 Static Pod 으로 생성된다. 그럴수밖에 없는것이 우리가 kubectl을 사용할려면 방금 언급한 이 4가지가 생성되어야 하는데 그럼 이 4가지는 kubectl 없이 어떻게 생성하겠는가? 결국 Static Pod으로 생성한뒤 kubectl이 활성화되는 것이다.

 

Static Pods DaemonSets
kubelet에 의해 생성 kube-apiserver에 의해 생성
Master Node의 Components 들이 Static Pod으로 생성되어 배포 모니터링 Agent, 로깅 Agent 용도로 각 노드에 배포
Kube-Scheduler를 통해 Node에 배포되는 방법으로 동작하지 않는다

 

kubelet 환경 설정 파일의 위치와 그 이름을 찾는 방법은 ps -ef | grep kubelet을 실행하면 kubelet 실행과 관련된 문자열들이 나오는데 이중 --config 옵션에 대한 값을 보면 된다. 그 값이 kubelet 환경 설정 파일의 위치와 이름이다. yaml 파일일텐데 그 파일을 열은뒤 staticPodPath 항목에 대한 값이 StaticPod 생성과 관련된 yaml 파일이 있는 위치이다


동영상 강좌의 Static Pod 연습문제를 풀면서 알아두어야 할 사항이 생겨서 기록을 해둔다. 연습문제는 Worker Node에 생성된 Static Pod을 삭제하는 문제였는데 Static Pod을 삭제할려면 Worker Node에 접속해서 /var/lib/kubelet/config.yaml 파일의 staticPodPath 항목에 설정되어 있는 디렉토리로 이동한뒤 삭제하고자 하는 StaticPod과 연관된 yaml 파일을 삭제하면 된다. 여기서 이제껏 연습문제들을 풀면서 한번도 안해본것이 있었는데 바로 Worker Node로의 접속이었다. 문제에서 Worker Node의 이름은 node01이었는데 이를 접속할려면 ssh를 이용하면 된다. ssh node01 로 명령어를 실행하면 node01 Worker Node에 접속하게 된다 


multiple kube scheduler에 대해서는 사실 잘 모르겠다. 영상만 놓고 보면 기존의 Taints, Toleration, Node Affinity가 했던 역할들(Pod을 어느 Node에 배포할것인가에 대한 규칙에 따라 Pod을 배포)을 아예 별도의 custom kube-scheduler 를 설정해서 하는 것으로 보이는데 예시를 줬으면 이해하기 한결 수월할텐데 그러지를 않아서 잘 모르겠다.

 

Pod을 생성하는 yaml 파일에서 spec.schedulerName 항목에 해당 Pod을 Node에 배포할 Scheduler를 직접 설정할 수 있다

 

multiple kube scheduler를 구성하기 위해 또 다른 kube scheduler 를 만들려면 다음의 방법으로 진행하면 된다. 새로 만들고자 하는 kube scheduler를 my-scheduler라 가정하고 설명하도록 하겠다

 

1. /etc/kubernetes/manifests 디렉토리에 가면 기존의 kube scheduler를 생성해주는 kube-scheduler.yaml 파일이 있다. 이것을 다른 디렉토리에 파일명을 달리해서 복사하자. 만약 파일명만 달리해서 /etc/kubernetes/manifests 디렉토리에 복사하면 Kubernetes는 이를 Static Pod 으로 인식하고 자동으로 재성성하려 들것이다. 그러나 우리는 아직 설정을 고치기 전 상태이기 때문에 기존의 kube scheduler와 같은 내용으로 만들려고 시도하게 되어 꼬이는 상황이 벌어진다.(실제로 동영상의 테스트 환경하에서 이러한 방법으로 진행하다보니 꼬여버려서 다시 테스트 페이지를 접근하여 테스트 환경을 초기화 했다)

 

2. 1번 과정을 통해 복사된 파일을 열어서 다음의 내용을 수정한다

● metadata.name 항목을 my-scheduler로 수정

● spec.containers.command에 다음의 두 줄을 추가

- --leader-elect=false
- --scheduler-name=my-scheduler

● spec.containers.name 항목을 my-scheduler로 수정(원래 이 부분은 kube scheduler pod에서 실행되는 container의 이름을 지정하는 부분이라 반드시 이 작업을 해야 할 필요가 있는건 아니지만 그대로 두면 혼선이 생길것도 같아서 container 이름을 kube scheduler 이름으로 수정하여 일치시켜줬다

 

3. kubectl apply -f 파일명 을 통해 새로운 kube controller를 실행해준다

 

이렇게 새롭게 생성된 scheduler를 통해서 Pod을 배포하려 할 경우 위에서 언급했다시피 Pod을 생성하는 yaml 파일에서 spec.schedulerName 항목에 my-scheduler로 설정해주면 이 yaml을 통해서 Pod을 배포할 때는 my-scheduler가 Pod을 배포하게 된다.


kubernetes monitoring tool로 metrics server 를 설치할 경우 kubectl top node 명령과 kubectl top pod 명령을 통해 node와 pod이 사용하는 CPU, 메모시 사용율을 알 수 있다. 

metrics server의 설치 방법이 교육동영상에서 알려주는 방법과 공식 홈페이지가 다른 부분이 있기 때문에 설치하려 할 경우 공식 홈페이지를 참조한다


Pod 안에서 동작중인 Container의 Log를 보려 할 경우 다음과 같이 사용한다(ex: Pod의 이름이 event-simulator-pod 이고 이 Pod 안에서 실행중인 Container의 이름이 event-simulator 라고 가정한다)

kubectl logs -f event-simulator-pod

-f 옵션을 주면 현재 기록되는 log을 실시간으로 계속 보여준다(tail -f 명령어와 같은 효과) 그러나 Pod은 1개 이상의 Container가 실행이 가능하다. 만약 Pod에 2개의 Container가 실행중이면 이때는 어떻게 해야 할까? 이 경우 보고자 하는 log를 만들어주는 Container의 이름을 명시해주면 된다. 예를 들어 event-silumator-pod 에서 Container로 event-simulator 와 image-processor 이렇게 2개의 Container가 실행중인 상태에서 image-processor Container의 log를 보고자 할 경우엔 다음과 같이 하면 된다 

kubectl logs -f event-simulator-pod image-processor

정리하자면 Pod 에서 실행중인 Container가 1개일 경우 보고자 하는 log를 만들어주는 Container의 이름을 생략해도 되지만 만약 2개 이상이면 보고자 하는 log를 만들어주는 Container의 이름을 명시해주면된다


rollout : deployment 로 배포되는 Pod을 실제 전개하는 것을 rollout 이라 보면 될듯

 

기존에 전개된 것을 업그레이드 하는 rollout 전략은 크게 2가지가 있다.

- 기존에 배포된 Pod을 모두 죽이고 다시 새 버전으로 구성되는 Pod을 생성해 올리는 방법(Recreate)

- 기존에 배포된 Pod을 1개 죽이고 새 버전으로 구성된 Pod을 1개 생성해 올리는 식의 순차적인 방법(Rolling Update, 이 전략이 Default 전략임)

 

첫번째 방법은 모두 죽인후 모두 살리는 과정에서 그 중간 지점(모두 죽인다 -> 이 중간지점(Pod이 모두 죽었고 아직 새로이 생성된 Pod이 없는 상황) -> 모두 살린다)에서 서비스를 제공할 수 없는 부분이 존재하기 때문에 무중단 서비스가 이루어지지 않아 추천하는 방법이 아니다. 그러나 두번째는 그러한 지점이 없기 때문에 이 전략으로 deploy된 기존 Pod을 업그레이드 한다

 

Recreate 전략을 실제로 구현하는 방법은 내부적으로 replicas 항목을 0으로 하는 scale down 작업을 하여 모든 Pod을 제거한뒤 다시 replicas 항목의 값(현재는 제거하다 보니 0으로 되어 있음)을 원래 설정되어 있던 값으로 수정하여 다시 Pod을 재생성한다

Rolling Update는 구버전과 신버전에 대한 ReplicaSet을 각각 만든뒤 구버전 ReplicaSet을 단계적으로 scale down 하고 신버전 ReplicaSet을 구버전 scale down 한 수치만큼 scale up 하여 이를 구현한다( 2개를 scale down 하면 2개를 scale up 한다는 의미)

 

Upgrade 된것을 원래대로 복구할때는 다음과 같이 한다

kubectl rollout undo deployment/myapp-deployment

정리하면

create kubectl create -f deployment-definition.yaml
get kubectl get deployments
update kubectl apply -f deployment-definition.yaml
kubectl set image deployment/myapp-deployment nginx=nginx:1.9.1
status kubectl rollout status deployment/myapp-deployment
kubectl rollout history deployment/myapp-deployment
rollback kubectl rollout undo deployment/myapp-deployment

Docker Image를 만들때 명령어 와 옵션을 분리해서 설정해주자. sleep 명령어를 container에서 실행하는 Image인ubuntu-sleeper 란 이름의 Image를 만들때

EntryPoint ["sleep"]
CMD ["5"]

이렇게 만들면 docker run ubuntu-sleeper 10 로 run 명령을 실행할 경우 EntryPoint인 sleep 와 CMD가 override 되는 10의 조합으로 인해 실제로는 sleep 10 이 실행된다

만약 명령어를 sleep 에서 sleep2.0으로 바꿔서 최종적으로 실행되는 명령은 sleep2.0 10 으로 실행되도록 하고 싶으면 docker run --entrypoint=sleep2.0 ubuntu-sleeper 10 이렇게 실행하여 EntryPoint를 override 하는 방법으로 접근한다


위의 docker run을 Pod의 Container로 확장해서 적용하면 다음과 같이 된다

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-sleeper-pod
spec:
  containers:
  - image: ubuntu-sleeper
    name: ubuntu-sleeper
    command: ["sleep2.0"]
    args: ["10"]

docker run 명령의 --entrypoint가 spec.containers.command 항목으로, 실행되는 명령의 옵션이 spec.containers.args 항목으로 매핑된다. 


Pod에 환경변수를 설정할때 환경변수에 대한 값을 ConfigMap을 통해 다루게 된다. ConfigMap은 key:value 조합으로 이루어져 있으며 Pod을 생성할때 ConfigMap을 같이 주입시켜 환경변수 설정시 사용할 수 있다. ConfigMap 생성은 kubectl create configmap configmap이름 ... 이런 식으로 만들수 있고(Imperative 방법) 또는 kubectl create -f configmap.yaml 이런 식으로 configmap을 정의한 yaml 파일을 이용해서도 만들수 있다(Declarative 방법)

ConfigMap을 Imperative 한 방법으로 생성할때는 다음과 같은 방법으로 한다

kubectl create configmap app-config --from-literal=APP_COLOR=blue --from-literal=APP_MOD=prod

파일을 기반으로 만들때는 다음과 같이 만든다

kubectl create configmap app-config --from-file=configmap.properties

그러나 여기서혼동하지 말아야 할 내용이 있다. 예를 들어 configmap.properties가 다음과 같이 있다고 가정해보자

 

fruit:apple

animal:lion

 

이렇게 되어 있는 상황에서 --from-file을 이용해서 configmap을 생성할 경우 key는 파일 이름(configmap.properties)이 되고 value는 파일의 내용(fruit:apple\nanimal:lion)이 된다. 환경변수 2개(fruit:apple, animal:lion) 을 생성하는게 아니란 것이다. 특정 파일 내용을 아예 환경변수 개념으로 설정해서 다뤄야 할때 그럴때 이용하면 된다

 

ConfigMap을 Declarative 한 방법으로 만들때는 다음과 같이 한다

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  APP_COLOR: blue
  APP_MOD: prod

이렇게 만든 ConfigMap을 Pod에서 사용할때는 다음과 같이 사용한다

apiVersion: v1
kind: Pod
metadata:
  name: simple-web-color
  labels:
    name: simple-web-color
spec:
  containers:
  - name: simple-web-color
    image: simple-web-color
    ports:
      - containerPort: 8080
    envFrom:
      - configMapRef:
          name: app-config

이렇게 하면 app-config ConfigMap에 있는 모든 key:value 값을 전부 사용하게 된다. 만약 내가 전부를 다 사용하지 않고 일부만 사용하고 싶다면 envFrom 대신 환경변수를 설정하는 항목인 env를 사용해서 다음과 같이 사용하면 된다

    env:
      - name: APP_COLOR
        valueFrom:
          configMapKeyRef:
            name: app-config
            key: APP_COLOR

Pod에서 사용할 APP_COLOR 환경변수를 정의한뒤 env.valueFrom.configMapKeyRef 에서 name 항목에 ConfigMap 이름을 설정하고, key에 현재 설정하고자 하는 환경변수의 값이 저장되어 있는 ConfigMap의 key를 지정해주면 된다


Secrets는 ConfigMap과 같이 key:value 형태로 값을 저장하는 것은 동일하다. 다만 ConfigMap은 value를 평문(Plain Text)로 저장하는것과는 달리 Secrets는 Value를 암호화하지 않은 Base64 Encoding을 거친다(암호화가 이루어져 있지 않기 때문에 Base64 Decoding을 거치면 평문으로 전환이 가능)

Secrets를 Imperative 한 방법으로 생성할때는 다음과 같은 방법으로 한다

kubectl create secrets generic app-secret --from-literal=DB_HOST=mysql --from-literal=DB_USER=root --from-literal=DB_PASSWORD=paswrd

파일을 기반으로 만들때는 다음과 같이 만든다

kubectl create secrets generic app-secret --from-file=./username.txt --from-file=./password.txt

그러나 ConfigMap 때와 마찬가지로 혼동하지 말아야 할 내용이 있다. ConfigMap 때와 마찬가지로 파일로 할 경우엔 파일에 대한 내용이 key에 대한 값 그대로가 되기 때문에 파일에 대한 내용을 key:value 형태로 구성하면 안된다. 즉 username.txt가 key가 되고 username.txt 파일의 내용이 value가 되는 것이다. username.txt 파일의 내용을 Base64로 encode한 값이 value가 된다.

 

Secrets를 Declarative 한 방법으로 만들때는 다음과 같이 한다

apiVersion: v1
kind: Secrets
metadata:
  name: app-secret
data:
  DB_HOST: bXlzcWw=
  DB_USER: cm9vdA==
  DB_PASSWORD: cGFzd3Jk

Secret도 ConfigMap과 동일한 구조를 가지고 있다. 다만 주의할 것이 있는데 Secrets를 명령어로 만드는 Imperative 한 방법으로 만들때는 key에 대한 value를 평문으로 넣었으나 yaml 파일로 만드는 Declarative 한 방식을 사용할 때는 base64로 encode한 값을 넣어야 한다는 것이다. 명령어로 선언하게 되면 해당 value 값을 자동으로 base64로 encode 해서 적용시키기 때문에 이를 kubectl get secrets app-config -o yaml 명령어로 출력시키면 base64로 encode 된 값이 yaml로 출력되기 때문이다. 그래서 Declarative 한 방식을 사용할때는 해당 값을 base64로 encode 한 뒤 그 값을 넣어줘야 한다. 그러면 base64 encode 된 값을 어떻게 구할까? linux에서 echo 명령어를 이용해서 다음과 같이 실행하면 된다

echo -n 'mysql' | base64

이렇게 실행하면 mysql 문자열을 base64로 encode한 값으로 출력해준다. 이때 -n 옵션은 문자열 끝에 개행문자(\n)를 붙이지 않는다는 뜻이다.

 

이렇게 encode한 값들은 kubectl get 이나 describe를 통해서는 decode 된 값을 보질 못한다. 그래서 decode 된 값을 알려면 linux에서 echo 명령어를 이용해서 다음과 같이 실행하면 된다

echo 'bXlzcWw=' | base64 --decode

이렇게 Secrets를 만들면 Pod에서는 다음과 같이 사용하면 된다

apiVersion: v1
kind: Pod
metadata:
  name: simple-web-color
  labels:
    name: simple-web-color
spec:
  containers:
  - name: simple-web-color
    image: simple-web-color
    ports:
      - containerPort: 8080
    envFrom:
      - secretRef:
          name: app-secret

이렇게 하면 app-secret Secrets에 있는 모든 key:value 값을 전부 사용하게 된다. 만약 내가 전부를 다 사용하지 않고 일부만 사용하고 싶다면 envFrom 대신 환경변수를 설정하는 항목인 env를 사용해서 다음과 같이 사용하면 된다

    env:
      - name: DB_Password
        valueFrom:
          secretKeyRef:
            name: app-secret
            key: DB_PASSWORD

Pod 안에 2개 이상의 Container를 만들면 그들간에는 서로가 localhost로 통신이 이루어진다(물론 Port는 다르게..) 그리고 container들이 같은 volume을 사용할 수 있다. 이를 구성하는 방법은 spec.containers 항목이 배열값으로 들어가지기 때문에 yaml 배열 표현 방법으로 이를 구현하면 된다.

multi container가 실행되는 pod을 만드는 방법은 kubectl run pod이름 --image=이미지이름 --dry-run=client -o yaml > app.yaml 이런식으로 일단은 단일 container가 실행되는 pod 을 만드는 yaml 파일을 제작한뒤 spec.containers 항목을 보면 yaml 배열 형태로 kubectl run 실행시 입력했던 image를 사용하는 container 1개 정의되어 있을것이다. 이를 토대로 container를 더 만들면 된다.


Pod에 여러개의 container가 실행되게끔 설계해야 하는 과정에서 이런 상황을 부딪힐수 있다. 예를 들어 프로그램 소스를 github에서 땡겨오는 역할을 하는 container가 있고(부르기 쉽게 1번 container라 하겠다), github에서 땡겨진 소스를 빌드해서 내부에 설정된 WAS에 배포해서 실행시키는 container 가 있다(2번 container)고 가정하자. 1번 container는 소스를 github에서 땡겨오는 작업만 하면 되기 때문에 Pod이 만들어지고 난뒤 실행되는 시점에서 한번만 실행되면 된다. 즉 1번 컨테이너가 1번 실행된뒤 2번 컨테이너가 실행되면 되는 것이다. 이렇게 1번 컨테이너가 하는 역할을 하는 컨테이너를 InitContainer라 하며 Pod을 만드는 yaml에서 spec.initContainers에 별도로 다음과 같이 사용한다.

spec:
  containers:
  - name: myapp-container
    image: busybox:1.28
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init-myservice
    image: busybox
    command: ['sh', '-c', 'git clone <some-repository-that-will-be-used-by-application> ; done;']

spec.initContainers 항목을 보면 배열 형태로 되어 있다. 즉 1개 이상을 initContainer로 설정할 수 있다.

spec:
  containers:
  - name: myapp-container
    image: busybox:1.28
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  initContainers:
  - name: init-myservice
    image: busybox:1.28
    command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
  - name: init-mydb
    image: busybox:1.28
    command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;']

 

initContainer가 Pod에 있는지를 확인하는 방법은 initContainer가 Pod에 있을 경우 kubectl describe pod 명령을 이용해서 pod의 상세내용을 보면 Init Containers: 란 항목이 있게 된다. 그리고 그 밑으로 1개 이상의 container들이 있게 된다. container의 상세내용을 보여주는 구조는 기존의 Pod의 container 상세내용을 보여주는 구조와 동일하다


Node의 핵심적인 Software 업그레이드 작업을 하거나 또는 OS 업그레이드 작업 등으로 인해 Node가 일시적으로 내려갈 수 있다. 만약 Node가 바로 올라온다면(Online) kubectl process가 시작되고 Pod도 자동으로 올라오지만(online) Node가 5분 이상 복구가 되지 않는다면 Node는 죽었다고 간주하고 Pod은 해당 Node 에서 종료된다(5분이 넘어서 Node가 올라오면 Pod은 이미 종료된 상태로 간주되어서 복구되지 않는다는 의미로 해석했음) Master Node 입장에서는 Worker Node가 죽었다고 간주했기 때문에 Pod이 종료되었다고 보게된다.

 

이렇게 죽었다고 판단한 Pod이 replicaset이나 deployment로 인해 생성된 Pod이라면 다른 Node에 해당 Pod을 재생성한다. 이렇게 판단하게 되는 그 5분 이란 시간 설정은 kube-control-manager에서 설정되어진다

kube-control-manager --pod-eviction-timeout=5m0s ...

replicaset이나 deployment에 속한 pod이라면 Node가 5분 넘은 뒤에 복구가 되더라도 이미 다른 Node에 생성되어 있기 때문에 문제가 없지만 replicaset이나 deployment에 속한 pod이 아니면 Node가 5분 넘은뒤 복구 되더라도 자동으로 복구가 되질 않는다. 그렇다고 우리가 Node가 몇분 뒤에 복구가 될지는 알 수는 없는 입장이기 때문에 이러한 경우에 대비해서 우리는 다음의 작업을 해야 한다(kubectl drain kubernetes-worker1, kubectl uncordon kubernetes-worker1)

 

kubectl drain kubernetes-worker1

대상이 되는 Worker Node(kubernetes-worker1)에 drain 명령을 실행한다. drain 명령을 실행하면 해당 Node에서 실행중인 Pod을 다른 Worker Node로 이동시킨다. 여기서 이동시킨다의 정확한 의미는 drain 대상이 되는 Node에서는 Pod을 종료시키고 drain 대상이 아닌 다른 Worker Node(ex : kubernetes-worker2)에 해당 Pod을 재생성한다(외형적으로는 이동된것처럼 보이나 실제 동작은 종료후 다른 Worker Node에 재생성 한것이다) 또한 이 명령을 수행하면 대상이 되는 Node에 Pod이 배포되지 않도록 해주는 기능도 수행한다

이 작업을 할때 주의할점이 있는데 대상이 되는 Node에 DaemonSets가 있을 경우 동작하지 않게 된다. 그도 그럴것이 DaemonSets로 생성된 Pod은 제거해도 다시 자동으로 Pod이 만들어지기 때문이다. 그래서 이럴 경우엔 --ignore-daemonsets=true 옵션을 주면 DaemonSets 로 된 Pod은 무시해버리고 DaemonSets가 아닌 Pod을 대상으로 작업하기 때문에 정상동작하게된다.

또한 대상이 되는 Node에 있는 Pod이 ReplicationController, ReplicaSet, Job, DaemonSet, StatefulSet에 속하지 않는 Pod(우리가 단순히 kubectl run Pod 이름..이런 식으로 단독으로 Pod을 만든 경우가 이러한 경우에 속한다)이 있는 경우에도 drain 명령이 실행되지 않는다. 이럴땐 --force=true를 주어서 해당 Pod을 강제로 삭제시켜주어야 한다

 

kubectl uncordon kubernetes-worker1

작업 대상이 되는 Node가 업그레이드 작업을 마치고 올라오게 되면 이제 이 Worker Node는 다시 본연의 기능을 수행해야 하기 때문에 uncordon  명령을 실행하여 대상이 되는 Node에 Pod이 배포되도록 하게 해준다. uncordon 명령을 실행하여 Pod이 배포되도록 했다 해도 drain 명령을 통해 다른 Worker Node로 이동했던 Pod이 다시 돌아오는 것은 아니다. 기존의 설정된 replicas 수치를 맞추기 위해 다시 생성되거나 또는 기존에 여기서 실행되었던 Pod이 아닌 아예 새로이 만들어져 실행해야 하는 Pod이 작업 대상이 되는 Node에 배포가 된다.

 

kubectl cordon kubernetes-worker2

drain 명령이 Pod을 다른 Worker Node에 재생성하고 현재의 Node에 Pod이 배포되지 않도록 하는 기능이라면 cordon은 대상이 되는 Node에 Pod이 배포되지 않게끔 하는 기능만 동작한다. uncordon의 반대 기능을 한다고 보면 된다


ETCD Cluster와 CoreDNS의 경우 Kubernetes 와는 별개의 Project 이기 때문에 Kubernetes 버전과 동일하지 않다.


Kubernetes 구성요소가 반드시 같은 버전일 필요는 없다. 그러나 kube-apiserver 버전보다 같거나 작아야 한다.

좀더 구체적으로 표현하면 kubernetes 를 구성하는 구성요소는 kube-apiserver, kube-controller-manager, kube-scheduler, cloud-controller-manager, kubelet, kube-proxy, kubectl 이렇게 구성되는데 kube-apiserver를 기준으로 나머지 구성요소들의 지원 가능 버전이 결정된다.

 

kube-controller-manager, kube-scheduler, cloud-controller-manager : kube-apiserver와 같은 버전이거나 최대 1단계 낮은 마이너버전까지 가능하다(예를 들어 kube-apiserver가 1.20 이면 이 3개의 구성요소들은 1.20이거나 1.19까지만 가능하다는 의미)

 

kubelet, kube-proxy : kube-apiserver와 같은 버전이거나 최대 2단계 낮은 마이너버전까지 가능하다(예를 들어 kube-apiserver가 1.20이면 이 2개의 구성요소들은 1.20, 1.19, 1.18까지만 가능하다는 뜻)

 

kubectl : kube-apiserver와 같은 버전이거나 최대 1단계 높은 마이너버전이거나 최대 1단계 낮은 마이너버전까지 가능하다(예를 들어 kube-apiserver가 1.20이면 kubectl은 1.21, 1.20, 1.19까지만 가능하다는 뜻)

 

그러나 kube-apiserver가 서로 다른 버전으로 cluster 구성이 이루어지면 서로 다른 버전이 지원가능한 버전에서 공통적으로 지원 가능한 버전만 지원되므로 범위가 좁아진다. 예를 들어 kube-apiserver 1.20과 kube-apiserver 1.19 이렇게 2개의 버전으로 cluster로 구성하게 될 경우 kube-controller-manager, kube-scheduler, cloud-controller-manager는 다음의 계산법에 따라 지원버전이 결정된다.

① 버전이 1.20일 경우 이 3개의 구성 요소는 1.20, 1.19 버전이 사용가능하다

② 버전이 1.19일 경우 이 3개의 구성 요소는 1.19, 1.18 버전이 사용가능하다

①, 를 모두 만족하는 버전은 1.19 이므로 1.19 버전을 사용해야 한다

이 글에서 별도로 언급은 안하겠지만 나머지 구성요소인 kubelet, kube-proxy, kubectl 도 이러한 계산법에 따라 지원버전을 결정하게 된다

 

이러한 관계로 인해 최신 버전이 지금 사용중인 버전보다 3단계 높은 마이너 버전이면 업그레이드 하기 좋은 타이밍이 된다(현재 사용하는 버전이 1.20이면 최신 버전이 1.23일때가 업그레이드 시기라는거)

 

업그레이드할때는 바로 최신버전으로 점프해서 하는게 아니라 한단계 높은 마이너버전으로 순차적으로 접근해야 한다(현재 사용중인 버전이 1.20이고 최신 버전이 1.23이면 1.20->1.21->1.22->1.23) 이렇게 업그레이드를 해야 각 구성요소의 지원가능 버전 차이에 적합해지면서 무정지 업그레이드가 가능해진다.

 

kubeadm upgrade plan 명령을 실행하면 내용중에 어떤 버전으로 업그레이드하라고 안내를 해준다. 이때 관련 명령까지도 같이 알려준다(kubeadm upgrade apply v1.20.4)

 

업그레이드를 할때는 Master Node를 먼저 업그레이드 하고 Master Node의 업그레이드가 끝나면 Worker Node의 업그레이드를 진행한다. 만약 Master Node를 2대 이상 구성했으면 모든 Master Node의 업그레이드를 끝낸 후에 Worker Node 업그레이드를 진행한다.

 

Master Node를 업그레이드 하는 동안 kube-apiserver, kube-controller-manager, kube-scheduler 같은 Master Node에서 동작하는 구성요소들이 내려가게 되는데 이 구성요소들이 내려가더라도 외부에서 Worker Node를 접근하는 식의 서비스에는 아무 지장이 없다. 그도 그럴것이 외부에서 Worker Node의 Pod을 접근할때는 Master Node를 거쳐서 접근하는것이 아니라 이미 Kubernetes의 Service 를 통해서 Worker Node에서 서비스되는 Pod을 접근하는 것이기 때문에 Master Node가 내려가도 서비스에 지장을 받지 않는다. 그러나 Pod을 Scheduling 하는 작업이나 Deployment나 ReplicaSet 구성을 수정한다거나 하는 작업은 하질 못하게 된다. kubernetes cluster 관리와 관련된 기능은 하지 못한다.

 

Worker Node를 업그레이드 하는 정책은

① 모든 Worker Node를 한꺼번에 동시에 업그레이드 한다

② 모든 Worker Node를 하나하나 차례대로 업그레이드 한다

③ 업그레이드된 새 Worker Node를 Kubernetes cluster 에 참여시킨후 기존 Worker Node를 제거한다

 

①번의 경우는 한꺼번에 내려가고 한꺼번에 올라오기 때문에 그 과정에서 서비스를 할 수 없는 공백이 생긴다.

②번의 경우는 Worker Node 1대가 내려가도 내려간 Worker Node에 서비스 되던 Pod이 다른 Worker Node에서 이미 서비스되고 있거나 다른 Worker Node에 아예 서비스 되고 있지 않는 상황이면 다른 Worker Node에 Pod이 생성되기 때문에 무정지 서비스가 가능해진다.

③번의 경우는 이렇게 보면 된다. 기존 Worker Node의 kubernetes 버전이 1.20이어서 1.21로 업그레이드 한다고 가정하자. 그러면 아예 새로운 Worker Node 가상머신을 1대 만든뒤 여기에  kubelet과 kubeadm을 1.21 버전을 설치한 후 kubernetes cluster에 참여시킨다. 그리고 기존에 서비스 중이던 Worker Node 1대를 kubernetes cluster 구성에서 제외시킨다. 즉 새 버전의 기계를 cluster에 1대 참여시키고 구버전의 기계를 cluster에서 1대 제거 시키는 방식이다. 이것 또한 무정지 서비스가 가능하며 Cloud 기반하에서 kubernetes 서비스를 이용할때 사용하기 좋은 방법이다

 

구체적인 명령을 나열하면서 설명하면 Master Node는

apt-get upgrade -y kubeadm=1.21.0-00
kubeadm upgrade apply v1.21.0
kubectl drain masternode
apt-get upgrade -y kubelet=1.21.0-00
sudo systemctl daemon-reload
sudo systemctl restart kubelet
kubectl uncordon masternode

이 순서로 명령을 입력하여 업그레이드를 진행한다. 

 

Worker Node는 먼저 Master Node에서 kubectl drain kubernetes-worker1 이런식으로 업그레이드 대상 Worker Node에서 운영중인 Pod을 다른 Worker Node로 이동시킨후 아래의 명령들을 업그레이드 대상이 되는 Worker Node에서 입력한다

apt-get upgrade -y kubeadm=1.21.0-00
apt-get upgrade -y kubelet=1.21.0-00
kubeadm upgrade node config -- kubelet-version v1.21.0
sudo systemctl daemon-reload
sudo systemctl restart kubelet

그러나 앞에서 Master Node에 kubectl drain 명령을 실행했기 때문에 업그레이드 작업을 마친 Node에 Pod이 Scheduling 되지를 않는다. 업그레이드 작업을 마쳤으니 이젠 Pod이 Scheduling 되도록 Master node에서 kubectl uncordon kubernetes-worker1 이런 식으로 kubectl uncordon 명령을 실행한다.

 

근데 강의 동영상 보다는 Upgrading kubeadm clusters 문서가 더 정확하다. 이 내용은 강의 동영상에서 설명한 내용을 정리한 것일뿐 구체적인 내용은 링크한 문서에 그대로 있으니 그걸 보고 하자.

 

업그레이드 과정에 있어서 문서에는 없으나 필요한 명령어를 정리했다

cat /etc/*release* 명령을 실행해서 OS가 Ubuntu인지 CentOS인지 확인한다.


kubernetes backup 대상은 크게 3가지가 있다.

Resource를 생성하는 yaml 파일, ETCD 에 보관되어 있는 Cluster 관련 정보, Volume에 저장되어 있는 각종 파일

 

Resource를 생성하는 yaml 파일 백업은 어렵진 않다. 그냥 파일을 복사해서 딴 곳에다가 옮겨두면 된다. 그나마 그것도 평소 git이나 svn 같은 Source 저장소 같은 곳에서 보관하며 작업했다면 아예 할것조차 없다.

아래의 명령을 실행하면 현재 kubernetes에서 실행중인 모든 Resource(Pod, Service, Replicaset, Deployment, ConfigMap, Secrets 등)를 생성하는 yaml 파일을 만들 수 있다

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

 

ETCD Cluster는 kubernetes cluster 의 상태 정보를 저장한다. Node 를 비롯하여 kubernetes cluster에 생성된 모든 Resource에 대한 정보가 여기에 저장된다. 그래서 앞의 Resource를 생성하는 yaml 파일들을 별도로 보관하는 식으로 백업하는 방법도 있고 ETCD Cluster를 백업함으로써 현재의 서버 그 자체를 백업하는 방법도 있는 것이다.

 

참고 : 지금부터 설명하는 ETCD Backup & Restore는 동영상 강좌에서 나온 내용을 정리한것이지 이걸 기반으로 해서 시험을 보려 할 경우 잘 외워지지 않을수도 있다. Kubernetes 공식 문서에서도 이 ETCD Backup & Restore에 대해 정리된 내용이 없어서 시험을 볼 경우 곤란할 수도 있다. 그래서 이런 경우를 대비해 ETCD Backup과 Restore 정리(CKA 시험 대비용) 문서를 별도로 만들어서 좀더 외우기 쉽게끔 정리한 내용이 있다. 관심있는 사람은 보길 바란다

 

ETCD를 restore할땐 sudo systemctl stop kube-apiserver 를 실행하여 먼저 kube-apiserver service를 중지시킨후에 ETCD를 restore 하고 마지막으로 sudo systemctl daemon-reload를 실행하고 sudo systemctl etcd restart를 실행하여 etcd를 재가동한뒤 sudo systemctl start kube-apiserver 를 실행하여 kube-apiserver service 를 실행시킨다.

 

ETCD를 backup하고 restore하는 script는 강의 동영상에 나오는 script나 kubernetes 공식 문서에서 나오는게 간략한 버전이어서 인증서 같은 것들을 사용해야 하는 script에 대해서는 여기를 보고 알아두자. 이런 옵션들이 있다는 것만 알아두고 이 옵션 자체도 etcdctl --help 하면 나오기 때문에 외울것까지는 없고 개념적으로 이런 옵션을 써야겠구나 하는 정도로만 알아두자. 동영상 강의에서 알려준 간략버전보다 조금 디테일한 script는 아래와 같다.

export ETCDCTL_API=3
etcdctl \
snapshot save snapshot.db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/etcd/ca.crt \
--cert=/etc/etcd/etcd-server.crt \
--key=/etc/etcd/etcd-server.key

위의 script 를 만드는 법에 대해 설명하겠다. etcd cluster는 master node의 pod으로 운영되기 때문에 (etcd를 별도로 구축해서 할 수도 있는데 그럴 경우에 대해서는 이따가 설명하겠다) kubectl get pod -n kube-service를 실행하여 etcd pod의 이름을 알아야 한다(이름에 etcd가 들어가기 때문에 이름을 알아보는데는 어렵지 않다) etcd pod의 이름을 알았으면 다음의 명령어를 실행하여 etcd pod의 상세내용을 알아보자

kubectl describe pod etcd-controlplane -n kube-system

위의 명령을 실행하면 etcd-controlplane pod의 상세 내용이 나오는데 여기서 Containers.etcd.Command 항목을 보면 etcd를 실행시킬때 어떤 옵션을 주어서 실행시켰는지 알 수 있다(etcd를 kubernetes pod이 아닌 별도로 구축된 상황에서 이를 연동해서 사용하는 구조로 되어 있으면 etcd가 설치되어 있는 서버에서 ps -ef | grep etcd 를 실행하면 etcd 명령어를 실행한 라인을 보여줄텐데 그 내용이 아래에 보여주게 되는 명령어 및 옵션 라인과 비슷한 구조일것이다)

etcd
--advertise-client-urls=https://172.17.0.36:2379
--cert-file=/etc/kubernetes/pki/etcd/server.crt
--client-cert-auth=true
--data-dir=/var/lib/etcd
--initial-advertise-peer-urls=https://172.17.0.36:2380
--initial-cluster=controlplane=https://172.17.0.36:2380
--key-file=/etc/kubernetes/pki/etcd/server.key
--listen-client-urls=https://127.0.0.1:2379,https://172.17.0.36:2379
--listen-metrics-urls=http://127.0.0.1:2381
--listen-peer-urls=https://172.17.0.36:2380
--name=controlplane
--peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
--peer-client-cert-auth=true
--peer-key-file=/etc/kubernetes/pki/etcd/peer.key
--peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
--snapshot-count=10000
--trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt

위에서 etcdctl을 이용하는 좀더 디테일한 옵션을 사용한 backup script를 쓴게 있는데 etcd 명령어를 실행시킬때 사용한 옵션과 비교해보면 다음의 값을 사용하는 것을 알 수 있다(설명을 보고 위의 etcd 명령어 실행 스크립트와 대조해보면 알 수 있다)

ETCDCTL_API=3 etcdctl \
snapshot save 백업파일경로(ex: /opt/snapshot.db) \
--endpoints=https://127.0.0.1:2379 \ # 이 옵션의 default 값이 https://127.0.0.1:2379 이어서 이 옵션은 사용하지 않아도 됨
--cacert=/etc/etcd/ca.crt \ # 이 옵션에서 사용하는 값은 etcd 명령어 실행 스크립트의 --trusted-ca-file 옵션에서 사용한 값을 넣어주면 됨
--cert=/etc/etcd/etcd-server.crt \ # 이 옵션에서 사용하는 값은 etcd 명령어 실행 스크립트의 --cert-file 옵션에서 사용한 값을 넣어주면 됨
--key=/etc/etcd/etcd-server.key # 이 옵션에서 사용하는 값은 etcd 명령어 실행 스크립트의 --key-file 옵션에서 사용한 값을 넣어주면 됨 

restore를 할때는 다음의 절차를 거친다. 백업할때 사용했던 파일을 이용해서 restore를 실행한다

ETCDCTL_API=3 etcdctl \
snapshot restore 백업된 파일경로(ex: /opt/snapshot.db) \
--data-dir=/var/lib/etcd-from-backup # 백업된 파일을 이용해서 etcd가 이용할 데이터 디렉토리 및 데이터 파일을 만들텐데 그것들의 위치를 정해준다

위의 명령어에서도 언급했지만 --data-dir 옵션은 backup 된 파일을 이용해서 etcd가 이용할 데이터 파일 및 디렉토리를 만들게 될텐데 그 결과물이 위치할 디렉토리를 --data-dir 옵션값으로 설정해준다. 이 명령어를 실행하기 전에는 /var/lib/etcd-from-backup 이란 디렉토리가 없었지만 이 명령어를 실행하게 되면 디렉토리가 생기면서 그 안에 데이터 파일이 있는 디렉토리가 또 생성된다. 그러나 이걸로 끝이 아니다. 왜냐면 현재 실행중인 etcd가 바라보는 데이터 파일은 새로 생긴 데이터 파일이 아니라 기존에 사용하던 데이터 파일이기 때문이다. 그래서 etcd가 바라보는 데이터 파일을 현재 restore 하면서 새로 생긴 데이터 파일로 바라보게끔 수정해야 할 필요가 있다. 

 

그럼 이 수정작업을 진행해보자. 먼저 살펴봐야 할 것은 etcd를 실행시킬때 사용한 option중  --data-dir이란 옵션이있다. 이 옵션에 사용한 값이 무엇인지 알아야 한다(이것은 위에서 언급했던 kubectl describe pod etcd-controlplane -n kube-system 명령어를 실행시켰을때 Containers.etcd.Command 항목을 보면 알 수 있다) 위에서 한번 썼으니 위에 언급한 etcd 명령어 라인 내용을 살펴보자. 그러면 --data-dir 옵션에 사용된 값이 /var/lib/etcd 임을 알 수 있다. 이 /var/lib/etcd 를 일단 기억해두자.

 

그러면 그 다음으로 살펴봐야 할 것은 etcd pod을 생성시키는 yaml 파일 내용을 봐야 한다. etcd pod은 Static Pod 이기 때문에 관련 yaml 파일은 /etc/kubernetes/manifests/ 디렉토리에 있다. 이 디렉토리에 가보면 etcd.yaml 파일이 있는데 이 파일을 열어서 그 안의 내용을 살펴보자. 아래의 yaml 파일은 그 내용중 작업에 필요한 부분만 일부 발췌해놓은 것이다

spec:
  containers:
  - command:
    - etcd
    - --advertise-client-urls=https://172.17.0.78:2379
    - --cert-file=/etc/kubernetes/pki/etcd/server.crt
    - --client-cert-auth=true
    - --data-dir=/var/lib/etcd
    - --initial-advertise-peer-urls=https://172.17.0.78:2380
    - --initial-cluster=controlplane=https://172.17.0.78:2380
    - --key-file=/etc/kubernetes/pki/etcd/server.key
    - --listen-client-urls=https://127.0.0.1:2379,https://172.17.0.78:2379
    - --listen-metrics-urls=http://127.0.0.1:2381
    - --listen-peer-urls=https://172.17.0.78:2380
    - --name=controlplane
    - --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
    - --peer-client-cert-auth=true
    - --peer-key-file=/etc/kubernetes/pki/etcd/peer.key
    - --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
    - --snapshot-count=10000
    - --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
    image: k8s.gcr.io/etcd:3.4.9-1
    
    ...
    
    volumeMounts:
    - mountPath: /var/lib/etcd
      name: etcd-data
    - mountPath: /etc/kubernetes/pki/etcd
      name: etcd-certs
  volumes:
  - hostPath:
      path: /etc/kubernetes/pki/etcd
      type: DirectoryOrCreate
    name: etcd-certs
  - hostPath:
      path: /var/lib/etcd
      type: DirectoryOrCreate
    name: etcd-data
    

여기서 눈여겨 봐야할 부분이 2군데가 있다.

- spec.containers.volumeMounts.mountPath 항목에서 사용된 값중에 위에서 언급한 --data-dir 옵션에서 사용된 값(/var/lib/etcd)을 사용하는 값이 있다. 그 값의 spec.containers.volumeMounts.name 항목을 알아둬야 한다. 여기서는 etcd-data란 녀석이 mountPath로 /var/lib/etcd를 사용하고 있다. 이 etcd-data 란 값을 또 기억해둔다.

- spec.volumes.name 항목에서 etcd-data 를 사용하는 spec.volumes.hostPath 항목을 찾는다. 찾아보면 spec.volumes.hostPath.path 항목에 사용된 값이 /var/lib/etcd인 녀석이 name을 etcd-data로 사용하고 있는데 이 path항목 값으로 사용된 /var/lib/etcd 값을 우리가 restore 명령을 사용할때 --data-dir 옵션에서 사용한 /var/lib/etcd-from-backup을 넣어주자. 이렇게 수정된 yaml 파일은 다음과 같다(필요한 부분만 넣었다)

spec:
  containers:
    
    ...
    
    volumeMounts:
    - mountPath: /var/lib/etcd
      name: etcd-data
    - mountPath: /etc/kubernetes/pki/etcd
      name: etcd-certs
  volumes:
  - hostPath:
      path: /etc/kubernetes/pki/etcd
      type: DirectoryOrCreate
    name: etcd-certs
  - hostPath:
      path: /var/lib/etcd-from-backup
      type: DirectoryOrCreate
    name: etcd-data
    

이러면 restore는 끝났다. kubectl get pod,svc,deployment 를 실행시켜서 원래대로 복구가 되었는지 확인해보면 된다