[k8s] Kubernetes 중급편 정리


Pod의 LifeCycle

1) Pending 단계

  • 제일 먼저 Pod가 Node에 정상적으로 할당되면 PodScheduled가 true가 된다. (사용자가 특정 노드에 직접 할당을 하든지, 쿠버네티스가 자원 상황에 따라 자동으로 하든지)
  • 이후에 Container가 실행되기전에 사전설정해주어야하는 것들이 있다면 실행되는 InitContainer가 Pending 단계 때 실행 된다. (볼륨 / 보안 셋팅 등)
  • pod의 yaml 정의 spec에 initContainers라는 항목을 추가하여 실행 가능 함, 이것이 성공 적으로 수행 되면 Initialized 값이 true가 된다. (InitContainer가 없어도 true)
  • 이후 Container의 이미지를 다운로드하는 동작이 수행 되고, 지금까지 설명한 단계동안 Container의 Status는 Waiting 상태이고, Reason은 ContainerCreating 임.

2) Running

  • 만약 Pod에서 Continaer를 정상적으로 실행하지 못한 경우 Container의 상태는 Waiting이 되고 Reason은 CrashLoopBackOff가 됨.
  • CrashLoopBackOff 상태에서 Container의 상태는 Ready로 판단하지만 내부적으로 ContainerReady 와 Ready는 False가 임.
  • 이후 모든 컨테이너들이 정상적으로 기동이 되어 원할하게 돌아가게 되면 ContainerReady와 Ready는 False가 됨.
  • 그러므로 상황에 따라 Pod의 상태 뿐만 이나리 Container의 상태도 모니터링 할 필요가 있음.
  • 만약 Job이나 CronJob으로 생성된 Pod의 경우, 일을 수행하고 있을 때는 Running이지만 일을 수행하고 나면 Failed / Succeeded가 된다.

3) Failed

  • Failed / Succeded 결과에 상관 없이 ContainerReady와 Ready는 모두 False가 된다.
  • Container가 수행을 완료하지 못한 경우 Terminated / Error가 된다.

4) Succeeded

  • Failed / Succeded 결과에 상관 없이 ContainerReady와 Ready는 모두 False가 된다.
  • Container가 수행을 완료한 경우 Terminated / Completed 된다.

5) Unknown

  • Pending 중에 바로 Failed 혹은 Unkown 상태가 되는 경우도 있음.

Pod - ReadinessProbe / LivenessProbe

  • ReadinessProbe: 앱이 구동 되는 순간(부팅 과정)에 서비스와 연결 되자마자 전달 받은 Traffic에 대해 응답하지 못해 발생하는 문제를 해결하기 위한 기능.
  • LivenessProbe: K8S 서비스와 적접적으로 연결된 Container에는 문제가 없으나 컨테이너 내부의 구동 중인 App에 문제가 생긴 경우 Service가 문제를 감지하여 Traffic 실패를 해결하기 위한 기능.

사용 방법

  • 사용 목적이 다를 뿐 설정할 수 있는 내용은 ReadinessProbe와 LivenessProbe가 같습니다.
  • httpGet / Exec / tcpSocket 이라는 필수 옵션이 있음.
  • initialDealySeconds / periodSeconds / timeoutSeconds / successThreshold / failureThreshold 라는 선택 옵션이 있음. 각각 0초 / 10초 / 1초 / 1회 / 3회 의 default 값을 가짐.
  • Pod가 Running이 되더라도 Probe의 조건을 만족하지 못할 경우 ContainerReady와 Ready가 false에서 true로 변경되지 않음.

ReadinessProbe / LivenessProbe 실습


1) ReadinessProbe 실습

yaml 펼치기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# Service 정의
apiVersion: v1
kind: Service
metadata:
  name: svc-readiness
spec:
  selector:
    app: readiness
  ports:
  - port: 8080
    targetPort: 8080
---
# 일반 Pod 정의
apiVersion: v1
kind: Pod
metadata:
  name: pod1
  labels:
    app: readiness  
spec:
  containers:
  - name: container
    image: kubetm/app
    ports:
    - containerPort: 8080	
  terminationGracePeriodSeconds: 0
---
# rediness Pod 정의
apiVersion: v1
kind: Pod
metadata:
  name: pod-readiness-exec1
  labels:
    app: readiness  
spec:
  containers:
  - name: readiness
    image: kubetm/app
    ports:
    - containerPort: 8080	
    readinessProbe:
      exec:
        command: ["cat", "/readiness/ready.txt"]
      initialDelaySeconds: 5
      periodSeconds: 10
      successThreshold: 3
    volumeMounts:
    - name: host-path
      mountPath: /readiness
  volumes:
  - name : host-path
    hostPath:
      path: /tmp/readiness
      type: DirectoryOrCreate
  terminationGracePeriodSeconds: 0


아래와 같이 일반 pod와 readiness pod를 실행한 후 요청을 보내면 readiness pod로부터는 응답이 없는 것을 확인할 수 있습니다. (ready.txt 파일이 없으므로)

image

몇가지 커맨드를 입력하여 pod의 상태를 확인해보도록 하겠습니다.

1
2
3
$ kubectl get events -w | grep pod-readiness-exec1
$ kubectl describe pod pod-readiness-exec1 | grep -A5 Conditions
$ kubectl describe endpoints svc-readiness

우선 ready.txt 파일을 찾을 수 없구요, Conditions를 보면 ContainersReady와 Ready 값이 false 입니다. 그리고 NotReadyAddresses를 확인해보면 readiness-pod의 IP를 확인할 수도 있네요.

image

이후에 readiness pod가 실행 중인 node에 접속하여 ready.txt 파일을 생성해주니 정상적으로 응답하는 것을 확인할 수 있었습니다.

image


2) LivenessProbe 실습

LivenessProbe 실습을 해보겠습니다. livenessProbe를 가지는 Pod를 생성하고 테스트해보도록 하겠습니다.

yaml 펼치기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
apiVersion: v1
kind: Service
metadata:
  name: svc-liveness
spec:
  selector:
    app: liveness
  ports:
  - port: 8080
    targetPort: 8080
---
apiVersion: v1
kind: Pod
metadata:
  name: pod2
  labels:
    app: liveness
spec:
  containers:
  - name: container
    image: kubetm/app
    ports:
    - containerPort: 8080
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-liveness-httpget1
  labels:
    app: liveness
spec:
  containers:
  - name: liveness
    image: kubetm/app
    ports:
    - containerPort: 8080
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 10
      failureThreshold: 3
  terminationGracePeriodSeconds: 0


현재 LivenessProbe가 정상적으로 동작 중인 것을 확인할 수 있습니다.

image

이번엔 강제로 status 500을 전달하여 pod의 상태가 어떻게 변화하는지 확인해보겠습니다. status500을 3번 전달하니 LivenessProbe가 Container에 이상이 생겼음을 감지하고 Container를 재시작하는 것을 확인할 수 있었습니다.

image

QoS classes (Guaranteed / Burstable / BestEffort)

Node에 균등하게 자원을 사용하는 Pod가 여러개 동작하고 있다고 가정할 떄, 그 중 한개의 Pod가 자원을 추가적으로 사용해야하는 상황에서 추가 자원이 없는 경우 어떻게 동작할지에 대해 정의하는 기능 입니다. 쿠버네티스에서는 앱의 중요도에 따라 이것을 Control할 수 있도록 QoS라는 기능을 제공하고 있습니다. 그러한 Pod들은 중요도 순으로 BestEffort < Burstable < Guranteed로 구분 됩니다.

특이한 점은 따로 QoS 구분을 위한 정의부가 있는 것이 아니라 spec의 resources / requetes / limits이 정의가 어떻게 되어 있냐에 따라 자동으로 설정 됩니다.

1) Guaranteed

  • Pod의 모든 Container에 Request와 Limit가 설정되어 있어야 한다.
  • Request와 Limit에는 Memory와 CPU가 모두 설정되어 있어야 한다.
  • 각 Container 내의 Memory와 CPU의 Reuqest와 Limit의 값이 같아야 한다.

2) Burstable

  • Guaranteed / BestEffort가 아닌 모든 경우
  • 그렇다면 같은 Burstable Pod 간에는 OOM Score가 더 높은 것이 먼저 삭제 된다. (OOM Score는 Request Memory 대비 실제 App의 Memory 사용량)

3) BestEffort

  • 어떤 Container 내에도 Request와 Limit이 설정되지 않은 경우.

Node Scheduling

  • NodeName: 노드의 이름으로 Pod를 할당할 수 있음. 실제 Production 환경에서는 노드가 삭제되고 추가되면서 노드의 이름이 변경될 수 있기 때문에 범용설 떨어짐.
  • NodeSelector: 가장 많이 사용되는 방법으로 key / label을 추가하면 해당 label이 달려있는 Node에 할당할 수 있음. 자원이 없어도 label에 맞는 Node에만 할당하려고 하는 점이 단점.
  • NodeAffinity: Pod에 key만 설정하면 특정 Node에 할당 시킬 수 있음. 만약 자원이 없는 경우에 조건에 따라 할당하도록 하는 옵션도 있음.
  • Pod Affinity: Pod를 Node에 할당할 때 집중하는 것을 지원하는 기능, 특정 2개의 Pod가 hostPath를 공유하는 상황에서 2개의 Pod는 같은 Node에 할당되어야만 하는데 이러한 상황에서 유용.
  • Pod Anti-Affinity: Pod를 Node에 할당할 때 분산하는 것을 지원하는 기능, 특정 2개의 Pod 중 1개의 Pod에 문제가 생겼을 때 나머지 Pod가 문제를 기록해야하는 상황에서 유용. (만약 2개의 Pod가 같은 Node에 있고 Node에 문제가 생겨 2개의 Pod 모두에 문제가 생기면 안되므로)
  • Taint / Toleration: 특정 Node에 아무 Pod나 할당되지 않도록 제한하는 용도, Node가 하나 있고 해당 Node는 고성능 그래픽 자원을 사용하는데에 필요한 Node일 경우 그래픽 자원이 필요한 Pod만 할당되도록 설정할 수 있음. Taint라는 옵션이 붙은 Node에는 Toleration이라고 옵션이 붙은 Pod만 할당 될 수 있음. 참고로 Taint Node에는 Toleration이 아닌 Pod가 자동으로 할당 안될 뿐 아니라 Node를 직접 지정하는 방법으로도 할당 될 수 없음. 여기서 중요한 점은 Pod가 Taint Node를 찾아서 할당하는 것이 아니라 Node에 할당되는 순간에 Toleration인지를 판단하는 것이므로 Taint Node에 할당될 수 있도록 Node Scheduling을 직접해주어야만 함.

image

Node Affinity 실습

1) matchExpressions Exists operator 실습

worker-1 노드에는 kr=az-1, worker-2 노드에는 us=az-1 Label을 붙인 뒤, key가 kr인 Label이 있는 Node에 Pod를 생성해보도록 하겠습니다.

1
2
kubectl label nodes worker-1 kr=az-1
kubectl label nodes worker-2 us=az-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
 name: pod-match-expressions1
spec:
 affinity:
  nodeAffinity:
   requiredDuringSchedulingIgnoredDuringExecution:   
    nodeSelectorTerms:
    - matchExpressions:
      -  {key: kr, operator: Exists}
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

image

2) Required 실습

Required는 반드시 조건을 만족하는 Node에서만 생성 됩니다. key: ch인 label을 가지는 Node가 없으므로 Pending 상태에서 넘어가지 못합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
 name: pod-required
spec:
 affinity:
  nodeAffinity:
   requiredDuringSchedulingIgnoredDuringExecution:
    nodeSelectorTerms:
    - matchExpressions:
      - {key: ch, operator: Exists}
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

image

2) Preferred 실습

현재 ch key를 가지는 Node가 존재하지 않습니다. Preferred의 경우 반드시 조건을 만족해야만 생성하는 것이 아니라 뜻 그대로 ‘선호’정도의 강제성입니다. 그러므로 worker-1에는 pod-match-expressions1 pod가 있으므로 worker-2에 할당이 될 것 입니다. (상황에 따라 아닐 수도 있음.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
 name: pod-preferred
spec:
 affinity:
  nodeAffinity:
   preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 1 # 여러개의 weight 설정할 수 있고, 
                # weight를 설정해서 여러개의 옵션에 대한 중요도 설정이 가능.
      preference:
       matchExpressions:
       - {key: ch, operator: Exists}
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

image

Pod - Affinity / Anti-Affinity 실습

1) Web1 Pod 실습

먼저, worker-1 / worker-2 node에 라벨링 작업을 수행합니다.

1
2
kubectl label nodes worker-1 a-team=1
kubectl label nodes worker-2 a-team=2
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
 name: web1
 labels:
  type: web1
spec:
 nodeSelector:
  a-team: '1'
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

a-team=1인 node는 worker-1이므로 worker-1에 할당 됩니다. 이 Pod에는 type: web1 label이 설정되어 있는데, Pod Affinity 기능 실습에 다시 사용할 예정입니다.

image

2) Web1 Affinity Pod 실습

server1이라는 pod를 web1이라는 pod가 존재하는 node에 생성하는 기능을 실습해보겠습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
 name: server1
spec:
 affinity:
  podAffinity:
   requiredDuringSchedulingIgnoredDuringExecution:   
   - topologyKey: a-team
     labelSelector:
      matchExpressions:
      -  {key: type, operator: In, values: [web1]}
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

server1 pod가 web1 pod가 존재하는 worker-1 node에 생성된 것을 확인할 수 있습니다.

image

3) Web2 Affinity Pod 실습

이번엔 web2 pod가 존재하는 node에 server2 pod를 생성하는 예제를 실행해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
 name: server2
spec:
 affinity:
  podAffinity:
   requiredDuringSchedulingIgnoredDuringExecution:   
   - topologyKey: a-team
     labelSelector:
      matchExpressions:
      -  {key: type, operator: In, values: [web2]}
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

현재 web2 pod는 존재하지 않습니다, 그래서 server2 pod는 생성되지 않고 pending 상태에서 대기하게 됩니다.

image

4) Web2 Pod 실습

그렇다면 server2 pod가 pending된 상태에서 web2 pod를 생성하면 어떻게 될까요?

image

5) Anti-Affinity Pod 실습

Pod Anti-Affinity 기능을 실습해보겠습니다. master pod를 생성하고, slave pod를 생성할 때 podAntiAffinity를 master pod로 설정하여 동일한 Node에 생성되지 않는지 확인해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: v1
kind: Pod
metadata:
  name: master
  labels:
     type: master
spec:
  nodeSelector:
    a-team: '1'
  containers:
  - name: container
    image: kubetm/app
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
 name: slave
spec:
 affinity:
  podAntiAffinity:
   requiredDuringSchedulingIgnoredDuringExecution:   
   - topologyKey: a-team
     labelSelector:
      matchExpressions:
      -  {key: type, operator: In, values: [master]}
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

서로 다른 node에 생성되는 것을 확인 할 수 있습니다.

image

Taint / Toleration 실습

Taint에 대한 설명을 좀 더 보충하였습니다.

  • “Taint”는 노드에 적용되는 제약 조건으로서, 특정 리소스를 사용할 수 없게 하거나 특정 용도로 제한하는 등의 제어를 가능하게 함.
  • 일반적으로는 클러스터 관리자가 특정 노드에 Taint를 설정하여 해당 노드에 특정 유형의 Pod가 스케줄링되지 않도록 하는 등의 용도로 사용됨.
1
2
kubectl label nodes worker-1 gpu=no1
kubectl taint nodes worker-1 hw=gpu:NoSchedule

즉, “NoSchedule” Taint가 설정된 노드에는 해당 Taint와 일치하는 Tolerations(톨러레이션)이 없는 한 Pod가 스케줄링되지 않음.

1) Pod without Toleration 실습

아래 pod는 node selector를 사용하여, worker-1에 pod를 생성하게 합니다. 그런데 worker-1에는 현재 taint가 설정되어 있는데 pod에는 Toleration이 없으므로 pending 상태가 됩니다.

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
 name: pod-without-toleration
spec:
 nodeSelector:
  gpu: no1
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

image

2) Pod with Toleration 실습

이번엔 toleration 옵션을 넣은 pod를 생성해보도록 하겠습니다, 이번에는 정상적으로 생성 되는 것을 확인할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
 name: pod-with-toleration
spec:
 nodeSelector:
  gpu: no1
 tolerations:
 - effect: NoSchedule
   key: hw
   operator: Equal
   value: gpu
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

image

3) Pod1 with NoExecute 실습

Taint / Toleration 에서 지원하는 옵션에는 NoSchedule 외에도 PreferNoSchedule / NoExecute / Custom Taint 등이 있습니다.

그 중 NoExecute를 추가로 실습해보도록 하겠습니다. 참고로 Taint의 NoSchedule 기능은 기존에 Node에서 동작 중인 Pod에는 영향이 가지 않는데 NoExecute는 영향이 갑니다. NoExecute에 대해서 추가로 테스트 해보도록 하겠습니다.

먼저 아래와 같은 Pod를 생성하면, worker-1에는 taint가 있기 때문에 무조건 worker-2에 할당됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
 name: pod1-with-no-execute
spec:
 tolerations:
 - effect: NoExecute
   key: out-of-disk
   operator: Exists
   tolerationSeconds: 30
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

그리고, 바로 이어서 tolerationSeconds가 없는 pod2를 하나 더 생성해보도록 하겠습니다.

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Pod
metadata:
 name: pod2-without-no-execute
spec:
 containers:
 - name: container
   image: kubetm/app
 terminationGracePeriodSeconds: 0

image

그리고 2개의 pod가 생성된 상태에서, 기존에 worker-1에 달았던 taint를 삭제하고, worker2에는 새로운 taint인 out-of-disk=True:NoExecute를 추가한 뒤 결과를 살펴보겠습니다.

1
2
3
4
# worker-1에 NoSchedule를 삭제해주지 않으면, worker-2에도 taint가 있고
# 그 어떤 곳도 taint가 없는 곳이 없어서 문제 발생.
kubectl taint nodes worker-1 hw=gpu:NoSchedule-
kubectl taint nodes worker-2 out-of-disk=True:NoExecute

worker-2에 taint를 부여한 뒤, pod2는 tolerationSeconds가 없으므로 곧바로 삭제 됩니다.

image

이후 pod1은 tolerationSeconds가 30이므로 30초 뒤에 삭제되는 것을 확인할 수 있습니다.

image

Service (Headless / EndPoint / ExternalName).

앞서 배운 Service의 내용이 사용자 입장에서의 Service 였다면 이제 알아볼 내용의 Pod의 입장에서의 Service 입니다. 하나씩 알아보도록 하겠습니다.

Headless Service

아래와 같이 default namespace의 Service에 Pod가 2개 연결되어 있다고 가정해보겠습니다.

이 Service는 ClusterIP로 만든 서비스 입니다. 이름도 service1이라고 지어줬습니다. 당연히 IP도 동적으로 할당이 될 것 입니다. 그리고 쿠버네티스의 DNS Server가 있습니다. 이 DNS Server의 이름은 cluster.local 입니다. 이 DNS Server는 Pod든 Service든 생성되면 자동으로 도메인과 IP가 저장 됩니다.

저장되는 규칙을 잠깐 살펴보면 다음과 같습니다.

1
2
3
4
5
# {service의 이름}.{namespace}.{svc}.cluster.local
service1.default.svc.cluster.local

# {Pod의 IP}.{namespace}.{pod}.cluster.local
20-109-5-11.default.pod.cluster.local

그리고, 이러한 규칙을 가지고 만들어진 Domain Name을 FQDN(Fully Qualified Domain Name)이라고 합니다. 참고로 같은 namespace안에서는 service의 경우는 name만 사용하면 접근할 수 있고 Pod의 경우 전체 Domain Name을 입력해서 접근할 수 있습니다. (Pod의 경우 앞 부분은 IP이기 때문에 사실상 쓸수가 없겠지요.)

그러므로 Pod의 경우 Service의 이름만 가지고 해당 Service에 접근할 수 있게 되는 것 입니다. Service의 이름은 사용자가 지정해주는 것이니 Pod에 미리 심어놓을 수도 있겠지요.

그래서 단순히 Pod에서 Service에만 연결하는데에는 ClusterIP만으로 충분합니다.

하지만 동일한 상황에서 Pod가 동일한 Service에 연결된 다른 Pod에 접근하고 싶다면, Service를 Headless Service로 생성해야만 합니다.

만드는 방법은 2가지를 설정해주면 됩니다.

  • ClusterIP 속성에 None이라고 넣습니다. (이 서비스의 IP를 생성하지 않겠다는 의미이고 실제로 만들어지지 않습니다.)
  • Pod의 속성에 hostname이라는 속성에 Domain name을 넣고 subdomain에 service의 이름을 넣어줍니다.

이렇게 headless service를 생성 및 연결하게 될 경우, DNS Server에 Service의 IP가 없기 때문에 Service의 이름을 호출하게 되면 연결된 모든 Pod의 IP를 반환해줍니다. 그리고 하단에 Pod를 보면 pod의 hostname이 앞에 붙어있고 뒤에는 subdomain이 들어있습니다. 그래서 이후로는 pod4.headless1 (hostname.subdomain) 형식으로 접근할 수 있게 됩니다.

EndPoint

사용자 입장에서는 Service와 Pod가 다이렉트로 연결되어 있는 것 같지만, 쿠버네티스 내부적으로는 EndPoint라는 것이 생성되어 연결 됩니다.

설명을 더 하자면, 쿠버네티스는 서비스의 이름과 동일한 이름으로 EndPoint의 이름을 설정하고 EndPoint 내부에는 Pod의 접속 정보를 추가합니다.

그렇기 때문에 이 규칙을 알면, label/selector를 만들지 않더라도 Service와 Pod를 직접 연결을 할 수 있습니다.

먼저 Service와 Pod를 생성할 때 label/selector를 설정하지 않으면 서로 연결이 안될 것 입니다. 이 때 EndPoint를 직접 만들어서 Service의 이름과 Pod의 IP정보를 직접 넣으면 label/selector를 사용하지 않고도 연결할 수 있습니다.

그런데 외부의 IP든 Pod의 IP든 변경 가능성이 있기 때문에 DNS Server를 사용해야하는데요, 이 때 사용할 수 있는 것이 바로 ExternalName 입니다.

ExternalName

Service에 ExternalName이라는 속성을 넣어서 Domain 이름을 추가하면 DNS cache가 내부/외부 DNS Server를 확인하여 IP를 알아냅니다.

그래서 결국, Pod는 Service를 가리키고만 있으면 Service에서 필요시마다 DNS Server를 갱신해주므로 Pod를 재배포하는 일이 없게 됩니다.

Headless Service 실습

1) pod에서 다른 pod로 service를 거쳐서 요청 보내기

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Service
metadata:
  name: clusterip1
spec:
  selector:
    svc: clusterip
  ports:
  - port: 80
    targetPort: 8080
1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
  name: pod1
  labels:
    svc: clusterip
spec:
  containers:
  - name: container
    image: kubetm/app
1
2
3
4
5
6
7
8
apiVersion: v1
kind: Pod
metadata:
  name: request-pod
spec:
  containers:
  - name: container
    image: kubetm/init

request pod에 접속하여 연결된 service domain을 사용하여 다른 pod에 요청을 보낼 수 있습니다.

image

2) DNS domain 확인

image

3) pod에서 다른 pod로 service를 거치지 않고 요청 보내기

이번에는 service의 name을 사용하지 않고 pod 간에 통신을 수행하는 실습을 진행해보도록 하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
  name: headless1
spec:
  selector:
    svc: headless
  ports:
    - port: 80
      targetPort: 8080    
  clusterIP: None
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
  name: pod4
  labels:
    svc: headless
spec:
  hostname: pod-a
  subdomain: headless1
  containers:
  - name: container
    image: kubetm/app
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
  name: pod5
  labels:
    svc: headless
spec:
  hostname: pod-b
  subdomain: headless1
  containers:
  - name: container
    image: kubetm/app

아래와 같이 request pod에 접속한 service에 대해 nslookup 명령어를 실행하면 연결된 모든 pod의 정보를 받는 것을 확인할 수 있습니다.

image

image

그리고 pod를 생성할 때 입력한 hostname과 subdomain을 사용하여 요청을 보낼 수 있습니다.

image

Endpoint 실습

1) Endpoint 자동 생성 및 확인

먼저 아래 service와 pod를 배포하여 줍니다.

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Service
metadata:
  name: endpoint1
spec:
  selector:
    svc: endpoint
  ports:
  - port: 8080
1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
  name: pod7
  labels:
    svc: endpoint
spec:
  containers:
  - name: container
    image: kubetm/app

service의 이름인 endpoint1이라는 이름의 endpoint object가 생성된 것을 확인할 수 있습니다. 결국 label/selector를 지정하면 쿠버네티스가 service의 이름과 동일한 endpoint를 알아서 만든다는 것을 알 수 있습니다.

image

2) Endpoint 직접 만들기

이번에는 쿠버네티스가 자동으로 endpoint를 생성하는 것이 아니라 직접 만들어서 사용해보도록 하겠습니다. service와 pod 생성 시 label/selector를 지정하지 않고, endpoint 생성 시 pod의 ip주소를 확인하여 넣어주어야 한다는 것 입니다.

1
2
3
4
5
6
7
apiVersion: v1
kind: Service
metadata:
  name: endpoint2
spec:
  ports:
  - port: 8080
1
2
3
4
5
6
7
8
apiVersion: v1
kind: Pod
metadata:
  name: pod9
spec:
  containers:
  - name: container
    image: kubetm/app
1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Endpoints
metadata:
  name: endpoint2
subsets:
 - addresses:
   - ip: 192.168.226.109 # pod의 ip를 확인해서 직접 넣어줘야 함.
   ports:
   - port: 8080

이후 request pod에 접속하여 curl 명령을 날리면 정상적으로 pod가 연결된 것을 확인할 수 있습니다.

1
curl endpoint2:8080/hostname

image

3) Endpoint에 외부 domain 연결

이번에는 외부 domain과 연결되는 endpoint를 만들어보겠습니다. 저의 repo에 있는 README.md 파일을 curl 요청으로 다운로드 할텐데 github ip를 endpoint에 연결해서 endpoint를 사용해서 다운로드 해보겠습니다.

1
2
3
4
5
6
7
apiVersion: v1
kind: Service
metadata:
  name: endpoint3
spec:
  ports:
  - port: 80
1
2
nslookup https://www.github.com
curl -O 185.199.110.153:80/ysbaekFox/ysbaekFox/blob/main/README.md
1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Endpoints
metadata:
  name: endpoint3
subsets:
 - addresses:
   - ip: 185.199.110.153
   ports:
   - port: 80
1
curl -O endpoint3/ysbaekFox/ysbaekFox/blob/main/README.md

image

ExternalName 실습

Endpoint 실습 시 외부 github 도메인의 ip주소를 확인 및 등록할 수 있었습니다. ExternalName을 사용하게 되면 굳이 service와 endpoint를 굳이 생성할 필요 없이, service 만으로 동일한 목적을 달성할 수 있습니다. 한번 확인해보도록 하겠습니다.

1
2
3
4
5
6
7
apiVersion: v1
kind: Service
metadata:
 name: externalname1
spec:
 type: ExternalName
 externalName: github.github.io.
1
curl -O externalname1/ysbaekFox/ysbaekFox/blob/main/README.md

image

Volume (Dynamic Provisioning, StorageClass, Status, RecalimPolicy)

1) Dynamic Provisioning

  • Dynamic Provisioning을 사용하기 위해서는 해당 기능을 지원하는 솔루션을 설치해야만 합니다. ex) STORAGEOS, NFS도 가능
  • 설치를 완료하면 Service/Pod/Secret 등의 여러 Object가 생성이 됩니다. 그 중에서 가장 중요한 것은 StorageClass라는 오브젝트 입니다.
  • 이 StorageClass라는 것을 사용하여 동적으로 PV를 생성할 수가 있는데, PVC를 만들 때 StorageClass라는 속성을 사용하면 됩니다.
  • Dynamic Provisioning을 사용하면 PV를 미리 생성하는 것과 달리, 볼륨이 바로 생성 됩니다. PV를 미리 생성하는 방법은 Pod가 연결 될 때 Volume 이 생성 됩니다.
  • STORAGEOS에 StoraceClass name을 설정하고, PVC 생성 시 StorageClassName이 해당 name을 넣어주면 PV를 동적으로 생성할 수 있습니다.
  • 아래와 같이 default를 설정하면 PVC를 생성할 때 StorageClassName을 생략하면 default StorageClass가 적용이되어 동적으로 생성 됩니다.

2) Status & RecalimPolicy

  • Status는 최초 PV가 만들어졌을 때 Available 상태이고 PVC와 연결이 되면 Bound 상태로 변하게 됩니다.
  • PV 직접 만드는 경우엔, PV가 Bound 상태가 되었다고 볼륨이 생성되는 것은 아니고, Pod가 PVC를 사용해서 구동이 될 때 실제 볼륨이 만들어 집니다.
  • 만약 이후에 Pod가 삭제될 경우 PV/PVC에는 아무 변화가 없고 Bound 상태가 유지 됩니다, 이 때 PVC를 삭제해야지만 PV와 연결이 끊어지면서 PV가 Released 상태가 됩니다.
  • PV와 실제 Data간의 연결에 문제가 생기는 경우 Failed 상태가 되기도 합니다.
  • PVC가 삭제가 되는 경우 PV에 설정해 놓은 RecalimPolicy에 따라 PV의 상태가 달라지는데 Retain / Delete / Recycle 3가지가 있습니다.
    • Retain: Default, 데이터 보존, 재사용 불가
    • Delete: StorageClass를 사용하여 만들어진 볼륨의 Default 정책, Volume에 따라 데이터가 삭제되기도 안되기도 함,재사용 불가
    • Recycle: Deprecated 된 옵션, 데이터 삭제, 재사용 가능

Volume 실습 (PV Status, ReclaimPolicy)

1) StorageClass 생성 확인 (nfs로 구성)

아래 명령어로 nfs로 생성한 storageClass를 확인할 수 있습니다.

image

2) Dynamic Provisioning

미리 pv 생성 없이 storageClass 입력 후, pvc가 자동으로 Bound 상태가 된 것을 확인할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1G
  storageClassName: nfs-client

image

3) hostpath 실습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
  name: pod-hostpath1
spec:
  nodeSelector:
    kubernetes.io/hostname: worker-1
  terminationGracePeriodSeconds: 0
  containers:
  - name: container
    image: kubetm/init
    volumeMounts:
    - name: hostpath
      mountPath: /mount1
  volumes:
  - name: hostpath
    persistentVolumeClaim:
      claimName: nfs-pvc

이전에 동적으로 생성한 pvc를 사용하여 hostpath를 사용하는 pod를 생성해줍니다. 그리고 임시 file을 생성합니다.

image

nfs 경로에 정상적으로 생성된 것을 확인할 수 있습니다.

image

4) Recycle 실습

pvc가 삭제되면 pv는 Rleased 상태가 됩니다. 이러한 상태의 pv는 재사용할 수가 없는데 재사용하기 위해서는 Recycle이라는 옵션을 사용해야 합니다. 참고로 hostpath를 사용하는 Recycle 정책의 경우 /tmp 경로만 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-recycle1
spec:
  persistentVolumeReclaimPolicy: Recycle
  capacity:
    storage: 3G
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: /tmp/recycle
    type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-recycle1
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 3G
  storageClassName: ""

Accessing API - Overview

  • 마스터 노드에는 쿠버네티스 API Server가 있고, 이 서버를 통해서만 오브젝트를 관리할 수 있습니다.
  • kubectl을 설치하여 CLI를 통해 관리하는 것도 모두 이 API Server를 통해서 관리하는 것 입니다.
  • https를 사용하거나, kubectl에서 proxy를 연 후에 http를 사용하여 API server에 접근하는 방법도 있습니다.
  • 외부 PC에 kubectl을 설치 후 Config를 설정하여 여러 개의 Kubernetes Cluster에 접슨하는 방법도 있습니다.
  • Pod가 API server에 마음대로 접근할 수 있다면 외부에서 Pod를 통해 API Server에 마음대로 접근할 수 있어선 안됩니다. 그래서 쿠버네티스에서는 Pod가 API server를 접근하는 것을 제한하는 Service Account라는 것이 있습니다.
  • 외부에서 접근하는 것을 제한하는 User Account라는 것이 있고, Service Account를 포함합니다.
  • API Server에 대해 외부에서 사용하기 위해서는 인증(Authentication) / 권한 부여(Authroization)가 있음.

image

Authentication

1) X509 Client Certs

인증 시퀀스

  • 쿠버네티스 API Server는 외부에 6443 포트로 열려있고, Client Key와 Client CRT를 사용하여 접근할 수 있습니다.
  • 또한 k8s cluster안의 kubeconfig 내부에는 CA crt / Client crt / Client key가 있고 이 파일들이 인증에 사용 됩니다.
  • 쿠버네티스 설치 시, kubectl에 kubeconfig 파일이 복사되어 kubectl로 쿠버네티스 API에 인증되어 리소스들을 조회할 수 있습니다.
  • kubectl에 accept-hosts proxy를 사용하여 외부에 8001 port를 열어주면 kubectl에 kubeconfig 파일을 가지고 있기 때문에 외부에서 인증서 없이 쿠버네티스 API에 접근할 수 있게 됩니다.

인증서 만드는 방법

  • 발급 기관과 클라이언트 개인키를 이용하여 인증 요청서(csr) 파일을 생성 후 인증서(crt)를 생성할 수 있습니다.
  • 단 Client crt의 경우 CA key와 CA crt를 추가로 사용해야 생성할 수 있습니다.

2) 외부에 kubectl 설치

  • 사전에 각 클러스터의 kubeconfig 파일이 나의(외부) kubectl에도 있어야 멀티 클러스터에 접근해서 리소스를 조회하고 생성할 수 있음.
  • kubeconfig 안에는 clusters와 users라는 항목을 등록 할 수 있음 (clusters에는 이름, 연결 정보, CA 인증서가 있고, users에는 이름, user의 개인키과 인증서가 있음)
  • 그리고 contexts라는 것을 생성하여 cluster와 user를 연결해준 후, kubectl config 명령어를 사용하여 context를 스위칭하는 것으로 여러 Cluster에 접속할 수 있음.

3) Service Account

  • 쿠버네티스 API가 있고, namespace를 생성하면 default라는이름의 ServiceAccount가 생성 됨.
  • 이 ServiceAccount에는 Secret이 연결되어 있는데 CA crt와 토큰 값이 있음.
  • 결국, 사용자는 이 token 값만 알면 API 서버에 접근할 수 있음.

Authentication 실습

1) Kubeconfig 인증서 확인

먼저 /etc/kubernetes/admin.conf 파일 조회

  • cluster.certificate-authority-data : CA.crt (Base64)
  • user.client-certificate-data: Client.crt (Base64)
  • user.client-key-data: Client.key (Base64)

Base64로 인코딩 된 상태이므로 디코딩 하여 로컬로 가져옵니다.

1
2
grep 'client-certificate-data' /etc/kubernetes/admin.conf | head -n 1 | awk '{print $2}' | base64 -d >> client.crt
grep 'client-key-data' /etc/kubernetes/admin.conf | head -n 1 | awk '{print $2}' | base64 -d >> client.key

2) Https API (Client.crt, Client.key)

가져온 crt,key를 사용하여 (저는 마스터 노드의 ip가 192.168.0.100 입니다) kubeconfig 파일을 조회해봅니다.

1
curl -k --key ./client.key --cert ./client.crt https://192.168.0.100:6443/api/v1/nodes

image

참고로 쿠버네티스 설치 시, 아래의 명령어를 입력하는데, admin.conf 인증서를 .kube 경로에 복사하는 명령어 입니다.

1
2
3
4
# admin.conf 인증서 복사
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

그리고 8001 port로 proxy를 열어줍니다.

1
2
# ^*$ 모든 Host
kubectl proxy --port=8001 --address=192.168.0.100 --accept-hosts='^*$' >/dev/null 2>&1

그리고 외부에서 curl 명령어를 사용하여 8001 port에 curl 요청을 보내면 admin.conf 정보를 조회할 수 있습니다.

image

3) Multi-Cluster 접속

저는 Multi Cluster 환경이 준비되지 않아서 실습은 진행하지 못하였습니다.

여러 클러스터의 admin.conf 파일을 가져옵니다.

  • path : /etc/kubernetes/admin.conf

Window에서 kubectl을 설치합니다.

1
https://kubernetes.io/docs/tasks/tools/install-kubectl/

.kube/config 파일을 수정하여 아래와 같이 준비합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: LS0tLS1KVEUtLS0tLQo=(중략)
    server: https://192.168.0.30:6443
  name: cluster-a
- cluster:
    certificate-authority-data: LS0tLS1KVEUtLS0tLQo=(중략)
    server: https://192.168.0.50:6443
  name: cluster-b
contexts:
- context:
    cluster: cluster-a
    user: admin-a
  name: context-a
- context:
    cluster: cluster-b
    user: admin-b
  name: context-b
current-context: context-a
kind: Config
preferences: {}
users:
- name: admin-a
  user:
    client-certificate-data: LS0tLS1KVEUtLS0tLQo=(중략)
    client-key-data: LS0tLS1KVEUtLS0tLQo=(중략)
- name: admin-b
  user:
    client-certificate-data: LS0tLS1KVEUtLS0tLQo=(중략)
    client-key-data: LS0tLS1KVEUtLS0tLQo=(중략)

이후 config 명령어를 사용하여 context를 스위칭 합니다.

1
kubectl config use-context context-a

node를 조회하면 해당 cluster의 node 정보들이 조회 됩니다.

1
kubectl get nodes

3) ServiceAccount & Secret 확인

먼저 namespace를 생성하여 줍니다.

1
kubectl create ns nm-01

namespace를 생성하면 serviceaccount와 secret이 자동으로 생성 됩니다. 단 1.24 버전 이후로는 secret을 수동으로 생성해주어야 합니다.

image

secret을 수동으로 생성해 줍니다.

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
  name: nm-01-secret
  namespace: nm-01
  annotations:
    kubernetes.io/service-account.name: default
type: kubernetes.io/service-account-token

그리고 나서, 아래 명령어로 token을 조회할 수 있습니다.

1
2
kubectl describe -n nm-01 serviceaccounts
kubectl describe -n nm-01 secrets # 토큰 조회

Pod를 하나 생성 해줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
  name: pod-1
  namespace: nm-01
  labels:
     app: pod
spec:
  containers:
  - name: container
    image: kubetm/app
EOF

token을 사용하여 API에 요청을 보낼 수 있습니다.

1
2
curl -k -H "Authorization: Bearer {TOKEN}" https://192.168.0.100:6443/api/v1
curl -k -H "Authorization: Bearer {TOKEN}" https://192.168.0.100:6443/api/v1/namespaces/nm-01/pods/

Authorization

RBAC (Role, RoleBinding) - Overview

  • 자원에 대한 권한을 역할 기반으로 부여하는 가장 대중화된 Authorization 방법
  • 쿠버네티스에서는 Role과 RoleBinding이라는 오브젝트가 역할 기반으로 권한을 부여하는 기능을 함.
  • 노드 / PV / Namespace와 같이 클러스터 단위로 관리되는 자원과 Pod / SVC 같이 네임스페이스 단위로 관리되는 자원이 존재함.
  • 네임스페이스를 생성하면 자동으로 ServiceAccount가 생성이 되는데(추가적으로 만들어줄 수도 있음.) ServiceAccount에 Role과 RoleBinding을 어떻게 설정하냐에 따라 ServiceAccount는 Namespace 내부의 자원에만 접근 가능하거나 혹은 Cluster 단위의 자원에도 접근할 수 있게 됨.
  • Role은 여러개를 만들 수 있고, 각 Role은 namespace 내에 있는 자원에 대해서 조회/생성에 대한 권한을 부여하는 등의 여러가지 Case로 만들 수 있음.
  • RoleBinding은 ServiceAccount와 Role을 연결하는 역할을 수행하는데 하나의 Role과 다수의 ServiceAccount가 연결되는 1:N 구조로 수행 함.
  • ServiceAccount에서 Cluster 자원에 접근하고 싶을 경우, ClusterRole과 ClusterRoleBinding을 생성해야 함.
  • ClusterRole은 Cluster 단위의 Object들을 설정할 수 있다는 것이 차이점이고 기본적으로 기능은 Role과 동일함.
  • 그래서 ClusterRoleBinding으로 ServiceAccount를 연결하면 ServiceAccount에서 Cluster 자원에 대한 권한도 가지게 됨.
  • ClusterRole을 생성하고, NameSapce 내부의 RoleBinding/ServiceAccount와 연결해주면 ClusterRole은 일반적인 Role처럼 동작함.
  • 그래서, 만약 각각의 Namespace마다 동일한 Role을 부여하여 사용해야하는 경우라면, Role을 사용하지 않고 ClusterRole을 사용하는 것이 더 효율적임. (ClusterRole을 한번 수정하므로써 모든 Namespace의 Role을 수정하는 효과로 관리가 수월해짐.)

Role, RoleBinding - Detail

  • 아래 그림처럼 namespace에 Pod / Service가 있고 namespace 생성 후 자동으로 생성된 ServiceAccount와 수동으로 생성한 Secret이 있다고 가정해봄.
  • Role을 생성를 생성해줄 때 apiGroups / resources / vers를 설정해줄 수 있습니다.
  • ex1) [””] / [pods] / [get, list]
  • ex2) [“batch”] / [jobs]
  • Role을 생성했다면 이후 RoleBinding을 생성하여 ServiceAccount와 Role을 연결해줍니다.
  • 그리고 외부에서는 Scret으 token을 가지고 API Server에 접근할 것이고 Role의 권한에 따라 외부에서 사용할 수 있는 권한이 정해집니다.

Authorization 실습

1) pod 권한만 부여

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: r-01
  namespace: nm-01
rules:
- apiGroups: [""]
  verbs: ["get", "list"]
  resources: ["pods"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: rb-01
  namespace: nm-01
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: r-01
subjects:
- kind: ServiceAccount
  name: default
  namespace: nm-01
---
apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    app: pod
  ports:
  - port: 8080
    targetPort: 8080

pod에 대한 조회시 정상적으로 반환 되는 것 확인할 수 있습니다.

1
curl -k -H "Authorization: Bearer {TOKEN}" https://192.168.0.100:6443/api/v1/namespaces/nm-01/pods/

하지만, Service에 대한 권한은 없으므로 정상적으로 조회가 불가능 합니다.

1
curl -k -H "Authorization: Bearer TOKEN" https://192.168.0.100:6443/api/v1/namespaces/nm-01/service

2) namespace 전체 권한 부여

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
apiVersion: v1
kind: Namespace
metadata:
  name: nm-02
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sa-02
  namespace: nm-02
---
apiVersion: v1
kind: Secret
metadata:
  name: nm-02-secret
  namespace: nm-02
  annotations:
    kubernetes.io/service-account.name: sa-02
type: kubernetes.io/service-account-token
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cr-02
rules:
- apiGroups: ["*"]
  verbs: ["*"]
  resources: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: rb-02
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cr-02
subjects:
- kind: ServiceAccount
  name: sa-02
  namespace: nm-02

nm-01 / nm-02 모든 namespace에 대해 Service 에 대한 조회가 정상적으로 동작하는 것을 확인할 수 있습니다.

1
2
curl -k -H "Authorization: Bearer {nm-01-secret TOKEN}" https://192.168.0.100:6443/api/v1/namespaces/nm-01/service
curl -k -H "Authorization: Bearer {nm-02-secret TOKEN}" https://192.168.0.100:6443/api/v1/namespaces/nm-02/service

StatefulSet

  • Stateless Application: Web Server ex) APACHE HTTP Server, Nginx, IIS
    • app이 여러개 배포되더라도 모두 동일한 기능을 수행 함.
    • 그래서 app이 죽으면 단순히 동일한 app을 하나 더 만들면 됨.
    • 반드시 볼륨이 필요하진 않음. 필요하더라도 단일 볼륨으로 충분한 경우가 많음.
    • 들어오는 트래픽을 여러 앱에 동일하게 분산시켜야 함. (네트워크의 연결은 단순 분산 용도)
    • 즉, 쿠버네티스의 ReplicaSet에 해당함
  • Stateful Application: Database ex) mongoDB, MariaDB, redis
    • app이 여러개 배포되더라도 각각의 역할이 있음.
    • 그래서 app이 죽으면 상황에 맞게 특정한 app을 복구해야만 함.
    • 반드시 볼륨이 필요함. 그리고 각 상황에 맞게 볼륨이 분리되어야 함.
    • 앱들의 특성에 맞게 트래픽이 분산되어야 함. (네트워크의 연결에 의도가 있음.)
    • 즉, 쿠버네티스의 StatefulSet에 해당함.

StatefulSet Controller

  • replicas: n으로 생성할 경우 ReplicaSet은 Pod의 이름이 랜덤으로 생성 되고, StatefulSet에는 Ordinal Index 이름으로 생성 됩니다.
  • Pod가 생성 될 때 ReplicaSet은 동시에 생성 되고, StatefulSet은 순차적으로 생성 됩니다.
  • Pod가 삭제 될 경우, ReplicaSet은 완전히 새로운 이름으로 Pod를 생성하고, StatefulSet의 경우 기존과 동일한 이름으로 Pod를 생성 합니다.
  • replicas를 0으로 변경하면 ReplicaSet은 Pod들이 동시에 삭제가 되고, StatefulSet은 index가 높은 Pod부터 순차적으로 삭제 됩니다.

StatefulSet - PersistentVolumeClaim / Headless Service

  • ReplicaSet을 생성할때 Pod에 PV를 연결하려면 PVC를 직접 생성해주어야 합니다, 반면 StatefulSet은 volumeClaimTemplate이라는 것을 사용하여 template으로 PVC를 생성하여 Pod에 연결할 수 있습니다.
  • 그래서 ReplicaSet은 생성되는 Pod들이 모두 같은 PVC를 연결하게 됩니다, 반면 StatefulSet은 생성되는 Pod마다 PVC를 다르게 설정해줄 수 있습니다.
  • Replicaset의 경우 PVC가 생성된 node와 동일한 node에 pod가 생성되어야 해서 nodeSelector 옵션을 지정해주어야 합니다. 반면 StatefulSet은 동적으로 PVC와 Pod가 동일한 Node에 생성되기 때문에 nodeSelector를 지정해줄 필요가 없습니다.
  • replicas를 0으로 줄이게 되면 StatefulSet은 Pod를 순차적으로 삭제하지만 PVC는 삭제하지 않습니다. 볼륨은 함부로 삭제되어서는 안되기 때문에 PVC를 지우기 위해서는 수동으로 삭제해주어야 합니다.
  • StatefulSet을 생성할 때 ServiceName을 입력한 후 이 이름과 매칭되는 Headless Service를 생성하게 되면 Pod의 예측 가능한 Domain Name이 만들어지게 되기 때문에 Internal Server의 특정 Pod 입장에서 원하는 StatefulSet의 Pod에 연결을 할 수 있게 됩니다.

StatefulSet 실습

1) ReplicaSet / StatefulSet 비교

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: replica-web
spec:
  replicas: 1
  selector:
    matchLabels:
      type: web
  template:
    metadata:
      labels:
        type: web
    spec:
      containers:
      - name: container
        image: kubetm/app
      terminationGracePeriodSeconds: 10
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: stateful-db
spec:
  replicas: 1
  selector:
    matchLabels:
      type: db
  template:
    metadata:
      labels:
        type: db
    spec:
      containers:
      - name: container
        image: kubetm/app
      terminationGracePeriodSeconds: 10

replicas: 1로 설정했을 때, 각각 1개씩 생성된 것을 확인할 수 있습니다.

image

replicas: 3으로 변경했을 때, statefulset의 경우 index별로 순차적으로 생성되는 것을 확인할 수 있습니다.

image

replicas: 0으로 변경했을 때, statefulset의 경우 index가 높은 것부터 삭제되는 것을 확인할 수 있습니다.

image

2) ReplicaSet / StatefulSet - Volume

replicaset을 storageclass를 사용하여 dynamic provisoing으로 claim 생성후 연결하는 예제 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: replica-pvc1
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1G
  storageClassName: "nfs-client"
---
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: replica-pvc
spec:
  replicas: 1
  selector:
    matchLabels:
      type: web2
  template:
    metadata:
      labels:
        type: web2
    spec:
      nodeSelector:
        kubernetes.io/hostname: worker-1
      containers:
      - name: container
        image: kubetm/init
        volumeMounts:
        - name: nfs
          mountPath: /replicasetDir
      volumes:
      - name: nfs
        persistentVolumeClaim:
          claimName: replica-pvc1
      terminationGracePeriodSeconds: 10

statefulset을 사용하여 volume claim template 사용하여 dynamic provisioning 사용하는 예제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: stateful-pvc
spec:
  replicas: 1
  selector:
    matchLabels:
      type: db2
  serviceName: "stateful-headless"
  template:
    metadata:
      labels:
        type: db2
    spec:
      containers:
      - name: container
        image: kubetm/app
        volumeMounts:
        - name: volume
          mountPath: /statefulset
      terminationGracePeriodSeconds: 10
  volumeClaimTemplates:
  - metadata:
      name: volume
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1G
      storageClassName: "nfs-client"

nfs 경로에 정상적으로 생성된 것을 확인할 수 있습니다.

image

2) StatefulSet - Headless Service

headless service 생성하여 statefulset의 pod에 연결한 후 pod에서 pod로 신호를 보내는 실습입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Service
metadata:
  name: stateful-headless
spec:
  selector:
    type: db2
  ports:
    - port: 80
      targetPort: 8080    
  clusterIP: None
---
apiVersion: v1
kind: Pod
metadata:
  name: request-pod
spec:
  containers:
  - name: container
    image: kubetm/init
1
2
nslookup stateful-headless
curl stateful-pvc-0.stateful-headless:8080/hostname

image

Ingress

case1 - Service LoadBalancing

  • 쿠버네티스에서는 Ingress Controller를 사용하여 도메인으로 들어오는 트래픽을 각 서비스에 맞는 Pod들로 분산시킬 수 있습니다.

case2 - Canary Upgrade

  • Ingress Controller를 사용하여 쿠버네티스로 들어오는 트래픽의 일부만 특정 Service로 분산 시키는 방법으로 신규 서비스를 테스트할 수 있습니다.

Ingress Detail

  • Ingress에는 Host로 Domain name을 넣을 수 있고 이 Domain으로 들어오는 트래픽들은 path에 따라 Service들에 분산 시킬 수 있습니다.
  • Ingress를 정의했다고 해서 동작하지는 않고 별도의 Plug-in인 nginx, kong 등을 설치해야 합니다.
  • 예를 들어 nginx를 설치할 경우 deployment와 replicaset이 생성되고 실제 ingress의 구현체인 ingress pod가 생성 됩니다, 그리고 이 ingress pod가 ingress rule이 있는지 확인하고 rule대로 service에 연결해주는 역할을 하게 됩니다.
  • 그래서 이 rule대로 서비스에 트래픽이 전달되려면 외부에서 접근하게 되는 사용자들은 nginx pod를 반드시 거쳐야하기 떄문에 외부에서 접근할 수 있는 서비스를 만들어서 이 ingress pod에 연결해주어야만 합니다.

image

Service Loadbalancing 실습

  • Ingress rule을 정의하여 요청한 path에 알맞은 서비스로 LoadBalancing을 할 수 있습니다.

각 페이지를 생성한 후에 ingress를 생성 합니다. 그리고 나서 master node의 주소 및 path를 추가하여 curl 요청을 보내면 각 path에 알맞게 트래픽이 전달되는 것을 확인할 수 있습니다.

yaml 펼치기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
apiVersion: v1
kind: Pod
metadata:
  name: pod-shopping
  labels:
    category: shopping
spec:
  containers:
  - name: container
    image: kubetm/shopping
---
apiVersion: v1
kind: Service
metadata:
  name: svc-shopping
spec:
  selector:
    category: shopping
  ports:
  - port: 8080
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-customer
  labels:
    category: customer
spec:
  containers:
  - name: container
    image: kubetm/customer
---
apiVersion: v1
kind: Service
metadata:
  name: svc-customer
spec:
  selector:
    category: customer
  ports:
  - port: 8080
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-order
  labels:
    category: order
spec:
  containers:
  - name: container
    image: kubetm/order
---
apiVersion: v1
kind: Service
metadata:
  name: svc-order
spec:
  selector:
    category: order
  ports:
  - port: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: service-loadbalancing
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: svc-shopping
            port:
              number: 8080
      - path: /customer
        pathType: Prefix
        backend:
          service:
            name: svc-customer
            port:
              number: 8080
      - path: /order
        pathType: Prefix
        backend:
          service:
            name: svc-order
            port:
              number: 8080


Canary Upgrade 실습

  • 기존과 동일한 host로 serviceName만 다르게 Ingress를 생성하여 트래픽의 일부만 새로 생성한 Service로 분산시켜 Canary Upgrade를 진행할 수 있습니다.

yaml 펼치기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
apiVersion: v1
kind: Pod
metadata:
  name: pod-v1
  labels:
    app: v1
spec:
  containers:
  - name: container
    image: kubetm/app:v1
---
apiVersion: v1
kind: Service
metadata:
  name: svc-v1
spec:
  selector:
    app: v1
  ports:
  - port: 8080
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-v2
  labels:
    app: v2
spec:
  containers:
  - name: container
    image: kubetm/app:v2
---
apiVersion: v1
kind: Service
metadata:
  name: svc-v2
spec:
  selector:
    app: v2
  ports:
  - port: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app
spec:
  ingressClassName: nginx
  rules:
  - host: www.app.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: svc-v1
            port:
              number: 8080
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app
spec:
  ingressClassName: nginx
  rules:
  - host: www.app.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: svc-v1
            port:
              number: 8080

app1과 app2를 배포하고 www.app.com을 host로 하는 ingress를 생성합니다. 이 때 www.app.com은 /etc/hosts에 master node ip로 등록하여 줍니다.

1
2
3
sudo cat << EOF >> /etc/hosts
192.168.0.100 www.app.com
EOF
1
curl www.app.com:30431/version

그 결과 app1으로 전달되는 것을 확인할 수 있습니다.

image

이번에는 weight를 지정하여 app2에 대해 일부 트래픽을 분산시켜 보도록 하겠습니다. (yaml 파일에서 annotations가 정의된 부분을 확인해주세요.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: canary-v2
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
  - host: www.app.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: svc-v2
            port:
              number: 8080
1
while true; do curl www.app.com:30431/version; sleep 1; done

아래와 같이 app v2에 대해 일부 트래픽만 분산되는 것을 확인할 수 있습니다.

image

이번에는 curl 요청 시 특정 header가 들어있는 경우 항상 app v2로 트래픽이 전달되도록 구현해보도록 하겠습니다. (마찬가지로 annotation 확인), 단 앞전에 생성한 canary weight ingress 오브젝트는 삭제를 해주어야만 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: canary-kr
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "Accept-Language"
    nginx.ingress.kubernetes.io/canary-by-header-value: "kr"
spec:
  ingressClassName: nginx
  rules:
  - host: www.app.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: svc-v2
            port:
              number: 8080
1
curl -H "Accept-Language: kr" www.app.com:30431/version

이번엔 v2로만 항상 트래픽이 전달 되는 것을 확인할 수 있습니다.

image

Https를 사용한 인증서 관리 실습

  • Ingress를 생성할 때, tls라는 옵션으로 secret name으로 실제 secret 오브젝트를 연결하고 이 secret 안에 인증서를 담아서 구성하면 사용자가 도메인 앞에 https://를 붙여야만 접근할 수 있게 됩니다.

yaml 펼치기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
  name: pod-https
  labels:
    app: https
spec:
  containers:
  - name: container
    image: kubetm/app
---
apiVersion: v1
kind: Service
metadata:
  name: svc-https
spec:
  selector:
    app: https
  ports:
  - port: 8080
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: https
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - www.https.com
    secretName: secret-https
  rules:
  - host: www.https.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: svc-https
            port:
              number: 8080


1
2
3
4
5
6
7
8
9
10
11
# 인증서 생성
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=www.https.com/O=www.https.com"

# Secret 생성
kubectl create secret tls secret-https --key tls.key --cert tls.crt

# /etc/hosts에 등록
192.168.0.100 www.https.com

# 브라우저에서 접속
https://www.https.com:30798/hostname

일반적인 http로 접근하면 접근할 수 없고, https로 접근할 경우 아래와 같이 접근이 가능한 것을 확인할 수 있습니다.

Autoscaler

HPA (Horizontal Pod Autoscaler)

  • Pod의 개수를 조절하는 Autoscaler
  • 트래픽이 증가하여 현재 동작중인 Pod의 리소스 자원이 모두 사용되는 상황에 도달하게 될 경우, 사전에 정의한 HPA가 Controller의 replicas를 조절하여 Pod의 개수를 조절합니다.
  • Pod의 개수가 수평적으로 증가하는 것을 Scale Out / 줄어드는 것을 Scale In이라고 합니다.
  • 장애 복구가 신속하게 이루어져야하므로 App 자체의 기동 시간이 오래 걸리는 App에는 권장하지 않음.
  • Pod의 수평적 증가만이 일어나므로 모든 pod의 역할이 같은 stateless app에서만 가능.

VPA (Vertical Pod Autoscaler)

  • Pod 리소스를 조절하는 Autoscaler
  • 트래픽이 증가하여 현재 동작중인 Pod의 리소스 자원이 모두 사용되는 상황에 도달하게 될 경우, 사전에 정의한 VPA가 Controller를 사용하여 Pod를 restart 시키면서 리소스 limit을 조절합니다.
  • Pod의 리소스양이 수직적으로 증가하는 것을 Scale Up / 줄어드는 것을 Scale Down이라고 합니다.
  • 참고로 하나의 Controller에 HPA와 VPA를 동시에 사용할 경우 기능이 동작하지 않습니다.
  • Stateful App에 사용하기 적절합니다.

CA (Cluster Autoscaler)

  • Node의 개수를 조절하는 Autoscaler
  • Pod를 추가적으로 생성하려고 할 때, Pod를 배치할 Node의 자원이 하나도 없을 때 CA를 특정 클라우드 프로바이더와 연결해놓았다면, 해당 프로바이더에 Node를 생성하고, 생성된 Node에 Pod를 배치시킵니다.
  • CA는 로컬 Node의 자원이 여유롭다고 판단하면 클라우드 프로바이더의 노드를 제거하고 Pod를 다시 로컬 노드에 배치시킵니다.

HPA Architecture

  • Master Node에는 쿠버네티스의 중요 기능을 하는 컴포넌트들이 Pod 형태로 띄워져서 구동되고 있음 (Controller Manager, kube-apiserver)
  • 쿠버네티스 설치 시 kubelet이라는 것이 각각의 노드마다 설치되고, 노드 자신에게 있는 파드들을 관리하는 역할을 함.
  • 대략적인 시퀀스를 설명하자면, 사용자가 replicaset을 생성하면 kube-apiserver를 통해 pod 생성 요청이 kubelet에 전달되고 전달된 요청은 컨테이너 런타임에 컨테이너를 생성해달라고 요청합니다.
  • HPA는 그러면 어떻게 Pod의 성능 정보를 알게될까요?, Resource Estimator인 cAdvisor가 kubelet에 메모리 정보를 전달합니다. 그리고 Addon으로 설치한 metrics-server가 kubelet을 통해 각각의 노드들로부터 리소스 정보를 가져와서 다른 컴포넌트들이 사용할 수 있도록 kube-apiserver에 등록합니다. 그러면 HPA가 리소스 정보를 kube-apiserver를 통해 리소스 정보를 받아갈 수 있게 되고 HPA는 리소스 정보를 15초마다 체크를 하고 있다가 리소스 사용량에 따라 파드의 개수를 조절하게 됩니다.
  • 참고로 프로메테우스를 추가로 설치할 경우, 단순 메모리나 CPU 사용량 외에도 다른 metric 정보들을 수집하여 kube-apiserver에 등록할 수 있습니다. (예를 들어 파드로 들어오는 패킷 수, 인그레스로 들어오는 리퀘스트의 양 등), 그래서 HPA는 이러한 정보들을 Pod 개수 조절에 추가적인 트리거로 활용할 수 있습니다.

image

HPA - Detail

  • HPA를 설정할 때, target으로 스케일링을 수행할 deployment의 name을 설정하고 조절할 pod 개수를 maxReplicas / minReplicas로 설정해줍니다.
  • 그리고 metrics의 어떤 조건을 사용하여 replicas의 개수를 조절할지에 대해 설정합니다.
  • Scale In / Scale Out의 경우 아래와 같은 공식으로 replicas 개수가 결정 됩니다.

HPA - 실습

1) Metrics Server 설치

metrics를 설치하여 줍니다.

1
wget https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.4/components.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ vim components.yaml
....
....
....
spec:
  containers:
  - args:
    ....
    ....
    ....
    - --metric-resolution=15s
    - --kubelet-insecure-tls  # 추가
....
....
....
    hostNetwork: true # 추가
    nodeSelector:
      kubernetes.io/os: linux
1
kubectl apply -f ./components.yaml

이후 아래 커맨드 입력 시 설치가 완료된 것을 확인할 수 있습니다.

image

image

2) HPA cpu scaling 실습

HPA를 생성한 후, CPU 사용량에 따라 replicas가 어떻게 변화하는지 확인해보도록 하겠습니다.

yaml 펼치기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: apps/v1
kind: Deployment
metadata:
 name: stateless-cpu1
spec:
 selector:
   matchLabels:
      resource: cpu
 replicas: 2
 template:
   metadata:
     labels:
       resource: cpu
   spec:
     containers:
     - name: container
       image: kubetm/app:v1
       resources:
         requests:
           cpu: 10m
         limits:
           cpu: 20m
---
apiVersion: v1
kind: Service
metadata:
 name: stateless-svc1
spec:
 selector:
    resource: cpu
 ports:
   - port: 8080
     targetPort: 8080
     nodePort: 30001
 type: NodePort
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-resource-cpu
spec:
  maxReplicas: 10
  minReplicas: 2
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: stateless-cpu1
  metrics:
  - type: Resource 
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50


아래 shell 커맨드를 사용하여 트래픽을 증가시켜 cpu에 사용량을 증가시켜보겠습니다.

1
while true;do curl 192.168.0.100:30001/hostname; sleep 0.01; done

cpu 사용량이 증가함에 따라, replicas의 개수가 7개까지 증가하는 것을 확인하실 수 있습니다.

image

이후, 트래픽 전달을 중지시키면 replicas가 다시 줄어들게 됩니다. 단, 즉시 줄어드는 것이 아니라 트래픽 전달이 중단된지 5분이 지나고나면 replicas가 줄어드는 것을 확인할 수 있습니다.

image

실습이 완료되었다면, hpa와 deployment를 삭제하여 줍니다.

1
kubectl delete horizontalpodautoscalers.autoscaling hpa-resource-cpu

3) HPA memory scaling 실습

HPA memory 스케일링도 유사한 방법으로 실습이 가능합니다.

yaml 펼치기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apiVersion: apps/v1
kind: Deployment
metadata:
 name: stateless-memory1
spec:
 selector:
   matchLabels:
      resource: memory
 replicas: 2
 template:
   metadata:
     labels:
       resource: memory
   spec:
     containers:
     - name: container
       image: kubetm/app:v1
       resources:
         requests:
           memory: 10Mi
         limits:
           memory: 20Mi
---
apiVersion: v1
kind: Service
metadata:
 name: stateless-svc2
spec:
 selector:
    resource: memory
 ports:
   - port: 8080
     targetPort: 8080
     nodePort: 30002
 type: NodePort
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: hpa-resource-memory
spec:
  maxReplicas: 10
  minReplicas: 2
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: stateless-memory1
  metrics:
  - type: Resource 
    resource:
      name: memory
      target:
        type: AverageValue
        averageValue: 5Mi


1
while true;do curl 192.168.0.100:30002/hostname; sleep 0.01; done

마찬가지로 테스트가 완료되면 deployment와 hpa는 삭제하여 줍니다.

1
kubectl delete horizontalpodautoscalers.autoscaling hpa-resource-memory

Kubernets Architecture - Overview

Components

1) Pod 생성 과정

  • 쿠버네티스를 설치하면 Master Node에는 Etcd / kube-scheduler / kube-apiserver는 pod형태로 생성되어 구동되고, Worker Node에는 kubelet systemd service와 container runtime이 구동됩니다.
  • 참고로 /etc/kubernetes/maifests 경로를 확인해보면 Etcd / kube-scheduler / kube-apiserver 의 yaml 파일을 확인할 수 있습니다.
  • 이러한 상태에서 사용자가 kubectl을 사용하여 pod 생성을 요청하면, 생성 요청은 kube-apiserver로 전달이 되고, kube-apiserver는 etcd로 다시 pod에 대한 정보를 전달하고 저장합니다. etcd는 pod에 대한 정보를 저장하는 일종의 db 역할을 하게 됩니다.
  • kube-scheduler는 노드의 자원들을 항상 주시하고 있습니다. 그래서 노드의 자원 상태를 항상 알고 있습니다. 그리고 kube-apiserver를 통해 etcd에 pod 생성 요청이 들어온 것이 있는지 항상 감시하고 있습니다.
  • 그래서 만약 pod 생성 요청이 들어온 것을 kube-schedulelr가 감지하면 Node의 자원 상태를 고려하여 etcd에 저장된 pod 정보에 할당될 Node 정보를 추가하는 업무를 수행합니다.
  • Worker Node의 kubelet들은 kube-apiserver를 감시하고 있습니다. 그래서 pod에 자신의 node 정보가 등록되어 있는것이 감지되면 Pod 정보를 가져와서 Pod를 생성하기 시작합니다.
  • 이후 kubelet은 container runtime에 container를 생성해달라고 요청을 보내고, kube-proxy에 network 생성을 요청하고 kube-proxy가 새로 생성된 container의 통신이 가능해지도록 설정해줍니다.

image

2) Deployment 생성 과정

  • Pod의 생성과정과 거의 유사한데, Controller Manager의 동작 부분에 차이가 있음.
  • Pod 생성과는 달리 Controller 리소스 생성의 경우 kube-apiserver가 Controller Manager를 통해 감지 및 생성 요청을 받음.

image

Networking

  • 쿠버네티스의 네트워킹은 크게 Pod간의 통신 / Service간의 통신으로 나뉨.
  • Pod의 경우 내부 컨테이너 간의 통신이 있고 Pod와 Pod간의 통신이 있음. Pod간의 통신의 경우 CNI(Container Network Interface) Plug-in이 필요함(참고로 Calico를 사용하고 있음)
  • 참고로 Network Plug-in은 노드 내부 간의 Pod 통신 뿐 아니라 타 노드 간의 Pod와의 통신을 담당합니다.
  • pod에 service를 연결하게 되면 service도 고유 ip가 만들어지고, 이 service가 생성됨가 동시에 kube-dns에 이 service의 이름과 ip에 대한 domain이 등록 됩니다.
  • 그리고 api server가 worker node들 마다 pod 형태로 실행 중인 kube-proxy에, 생성된 service의 ip가 어떤 pod와 연결이 되어있는지에 대한 정보를 보냅니다
  • 이후에 service ip를 pod ip로 변경하는 NAT 기능이 동작하는데, kube-proxy가 iptables 혹은 IPVS 를 어떻게 이용하냐에 따라서 Proxy Mode라는 작동 방식이 3가지로 나뉩니다. (user space, iptables, ipvs)
  • 이 상태에서 pod가 service의 이름을 호출할 경우, kube-dns를 통하여 ip를 알게 되고, 그렇게 얻은 service ip를 NAT 영역으로 호출하게 되는데 여기에 해당 서비스에 대한 Pod 맵핑 정보가 존재하기 때문에 트래픽을 네트워크 플러그인을 통해 해당 파드로 전달되게 됩니다.

image

Pause Container

  • Pod를 생성하게 되면, 내가 직접 생성한 Container 외에 네트워크를 담당하는 Pause Container라는 것이 자동으로 함께 생성 됩니다.
  • 이 Puase Container는 생성될 때 고유한 IP를 가지고 있고 (20.111.156.66), 접근할 수 있는 Interface도 가지고 있습니다, 그리고 쿠버네티스가 Pause Container의 Network namespace를 이 Pod내의 다른 Conainer와 공유하도록 설정합니다.
  • 그렇게 되면, Puase Container의 IP에 대한 Network를 Pod 내의 모든 Container가 공유하게 되고 각 Container들은 Port를 통해 구분할 수 있게 됩니다.
  • 한편 Work Node에는 Host Network Namespace가 별도로 존재하고, Host IP 와 Interface가 존재합니다. 만약 Pause Container가 생성 될 경우, Host Network Namespace에 가상의 Interface가 생성 되면서 Pause Container의 Interface와 연결이 됩니다. (calid12 <-> eth0)
  • Work Node에서 IP와 port를 사용하여 트래픽을 전달할 경우 앞서 연결된 Interface를 통해 트래픽이 전달 됩니다.

Network Plugin(kubernet, cni)

1) kubenet

  • pod가 생성 되어 host 자체의 network와 pod의 network가 연결이 될 때, kubenet이 host의 가상 network들을 birdge로 연결시킵니다.
  • 그리고, 이 birdge 대역은 pod의 network 대역 (아래 그림에서는 20.96.0.0/12, 즉 20.96.0.0 ~ 20.111.255.255)을 참고해서 이보다 낮은 단계의 대역 (20.96.1.0)으로 설정 됩니다.
  • 그리고, 이 bridge 내에서 생성되는 pod의 ip들은 이 birdge의 cidr 대역 내에서 만들어집니다. 그래서 kubenet을 사용할 경우 아래와 같이 IP 대역의 제한이 있다는 단점이 있음
  • network plugin에는 라우터 역할도 포함하는데 NAT가 그 역할을 합니다. NAT는 들어온 트래픽의 IP가 pod 영역 내이면 Bridge 쪽으로 보내고, 아닐 경우 Bridge가 아닌 위로 보냅니다.

2) cni with calico

  • kubenet 대신 calico cni를 사용하는 경우, host network에 생성된 가상 network가 별도의 NAT와의 연결 없이 가상의 Route와 바로 연결되는 구조 입니다.
  • 그렇기 때문에 두 Pod간의 통신은 이 Route가 담당을 해주고 IP 대역은 kubenet보다 더 많은 범위를 가지고 있어서 더 많은 Pod IP를 생성할 수 있습니다.
  • 그 외에 Host Network와 Pod Network 간의 보안 기능도 제공하여 줍니다 (아래 그림에서 firewall)
  • 그리고 Route 윗 단에는 Overaly Network를 제공해주는데 IPIP / VXLAN 이라는 2가지 방식으로 제공하고 있습니다. 이것의 역할을 Node 외부에 있는 Pod와 통신을 할 수 있게 해주는 것 입니다.

3) calico node간 통신 구조

  • pod D에서 pod B로 트래픽을 보내고자 할 때, Pod D는 Pod B의 IP인 20.11.156.7로 트래픽을 보낼 것 입니다.
  • 이 것은 곧장 Route로 전달 되는데, 해당 IP는 Node 내부에 없기 때문에 Route를 지나 Overlay 계층까지 전달 됩니다.
  • 그리고 calico는 이 Pod의 IP 대역이 어느 node에 존재하는지 알고 있기 때문에 이 overaly network 층에서 packet을 해당 Node의 ip로 변경되어 외부 Node(192.168.59.22)로 전달 됩니다.(이 과정에서 Pod의 IP는 인캡슐레이션 되어 패킷에 숨겨짐)
  • 그리고 전달 받은 Node에서는 패킷에서 Pod IP를 디캡슐레이션하여 Pod B로 트래픽이 전달되게 됩니다.

Service Network (with calico)

Proxy Mode

  • Pod 생성 후, Service를 연결했을 때 Pod가 정상적으로 기동된 상태라면 EndPoint라는 Object가 생성 되어 실제 연결을 담당합니다.
  • 그리고 Service IP는 service-network-cidr 대역 내에서 생성이 되는 것 입니다. 이 때 api-server는 EndPoint를 감시하고 있다가 모든 Node 위에 데몬셋으로 실행되고 있는 kube-proxy에 이 service의 ip와 포워딩 되는 pod의 ip를 알려줍니다.

1) Userspace Mode

  • 리눅스의 iptables로 service CIDR IP 대역으로 들어오는 모든 트래픽은 kube-proxy로 전달하도록 설정 되어집니다.
  • kube-proxy로 모든 트래픽이 전달 되고 kube-proxy가 가지고 있는 맵핑 정보를 사용하여, 이 트래픽을 Pod IP로 변경하여 Pod Network 영역으로 통신이 됩니다.
  • 단점은 모든 트래픽이 kube-proxy를 지나야하는 구조인데, 이 모든 트래픽을 감당하기엔 kube-proxy 자체의 성능이나 안전성이 좋지 않음.

2) iptables / IPVS Mode

  • kube-proxy가 맵핑 정보를 직접 사용하지 않고 맵핑 정보를 iptables로 알려주어 트래픽을 전달하는 방식 입니다.
  • 쿠버네티스를 설치했을 때, 기본 설정이고 userspace보다 성능이나 안전성이 더 뛰어납니다.
  • 그리고 리눅스에서는 IPVS라고 해서 iptables와 유사한 기능일 제공합니다. 참고로 iptables와 동일한 낮은 부하 상태에서는 iptables와 성능이 비슷하고 부하가 클 때는 IPVS가 더 성능이 좋다고 합니다.

Service Type

1) ClusterIP

  • ClusterIP Type의 Servce를 생성 및 연결 했을 때, Calico의 Pod Network 구조의 Route에서 ServiceIP를 PodIP로 변환해주는 NAT를 이용하여 Pod D에서 Pod B로 트래픽을 보내준다고 가정해봅시다.
  • 먼저, Pod B의 ClusterIP type의 service를 연결합니다 (10.103.9.116), 그리고 Pod D에서 Service IP(10.103.9.116)로 트래픽을 보내면 NAT에서 ServiceIP와 매칭되는 Pod IP로 변환이 됩니다.
  • 그 다음부터는 Overaly Network에서 IP가 인캡슐레이션 되고, 이전에 학습한 외부 Node의 Pod와 통신하는 것과 동일한 작업을 수행합니다.

2) NodePort

  • NodePort Type의 Servce를 생성 및 연결 했을 때, 모든 Node에서 각각 동작 중인 kube-proxy가 해당 Node에 3만번대의 Port를 열어줍니다.
  • 이후, 외부에서 열어준 Port로 트래픽이 들어오게 되면 iptables에서 이 트래픽을 calico network로 보내주게 됩니다.
  • 그리고 나서 이전에 학습한 ClusterIP로 생성된 서비스의 트래픽 전달 동작과 동일하게 NAT 기능을 통해 Pod IP로 변환이 되면서 Pod Network로 전달 됩니다.

Networking 실습

1) pod / service network 대역 확인

1
kubectl  --namespace kube-system get configmap kubeadm-config -o yaml

2) Calico Interface 확인

1
route | grep cal

3) Pod Network - Calico

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# source pod
apiVersion: v1
kind: Pod
metadata:
  name: pod-src
  labels:
    type: src  
spec:
  nodeSelector:
    kubernetes.io/hostname: worker-1
  containers:
  - name: container
    image: kubetm/init
    ports:
    - containerPort: 8080
---
# destination pod
apiVersion: v1
kind: Pod
metadata:
  name: pod-dest
  labels:
    type: dest
spec:
  nodeSelector:
    kubernetes.io/hostname: wroker-2
  containers:
  - name: container
    image: kubetm/app
    ports:
    - containerPort: 80

Calico Overlay Network 확인합니다.

1
kubectl describe IPPool

트래픽을 확인해봅니다.

1
2
3
4
route | grep cal
tcpdump -i <interface-name>
# tcpdump -i 명령어는 네트워크 트래픽을 캡처하고 분석하는 명령어입니다. 
# 주어진 네트워크 인터페이스(interface)에서 들어오고 나가는 패킷을 모니터링하고 캡처합니다. 

tcpdump로 트래픽을 기다립니다.

아래와 같이 트래픽을 전달하면 (그런데 왜 8080 port로 보내는지 모르겠음.)

다음과 같이 수신 된 것을 확인할 수 있습니다.

4) Service Network[clusterIP] - Calico

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  selector:
    type: dest
  ports:
  - port: 8080
    targetPort: 8080
  type: ClusterIP
1
2
route | grep cal
tcpdump -i <interface-name>

tcpdump로 트래픽을 기다립니다.

아래와 같이 트래픽을 전달하면 (그런데 왜 8080 port로 보내는지 모르겠음.)

다음과 같이 수신 된 것을 확인할 수 있습니다.

5) Service Network[NodePort] - Calico

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
  name: svc-nodeport
spec:
  selector:
    type: dest
  ports:
  - port: 8080
    targetPort: 8080
    nodePort: 31085
  type: NodePort
1
2
3
route | grep cal
tcpdump -i <interface-name>
curl 192.168.100:31085/hostname

image

image

Storage

  • Pod에서 Data를 저장하는 방법에 대한 내용
  • hostPath / Cloude Service의 Storage / 3rd Party Storage Solution 등을 사용하는 방법 등이 있음.
  • Volume의 Type (FileStorage / BlockStorage / ObjectStorage)의 특징

Logging

  • 쿠버네티스 내에서 동작하는 앱에서 출력되는 로그들을 어떻게 관리해야하는지에 대한 부분
  • 크게 Core pipline과 Service pipeline이 존재함.
  • Core pipeline은 pod에서 생성된 로그들이 어떻게 쌓이는지, 그리고 이 쌓여진 로그들을 어떻게 보는지에 대해 설명
  • Service pipeline은 별도의 plug-in을 설치하게 되면 모니터링에 관련된 pod들이 생성되고, 이 pod들이 각각의 노드에서 로그들을 가져다가 수집 서버에 모은다음 사용자에게 UI로 보여줌.

image

Updated:

Leave a comment