컨테이너의 헬스체크

livenessProbe

livenessProbe가 실패시 컨테이너를 재시작, 컨테이너에 이상이 있다고 간주

readinessProbe

실패해도 정상동작하지만, 서비스에서 해당 파드로 트래픽을 인가하지 않음

보통 livenessProbe보다 readinessProbe를 많이 사용


  • 기존 상위 리소스까지 전부 삭제
kubectl delete deploy --all
kubectl delete rs --all
kubectl delete pod --all

httpGet을 통한 probe

root@master:~/mani# mkdir probe
root@master:~/mani# cd probe
vi pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    app: nginx
spec:
  containers:
  - name: nginx
    image: 61.254.18.30:5000/nginx
    #일반적인 pod의 컨테이너 정의


    readinessProbe:
      httpGet:
        path: /
        port: 80
      periodSeconds: 5
      initialDelaySeconds: 2

# periodSeconds: 헬스체크 주기 , initialDelaySeconds: 부팅대기시간

# 처음에 2초 대기후 5초에 한번씩 curl localhost/ 와 비슷하다고 생각하면 좋다.

kubectl apply -f pod.yml
kubectl get pod -o wide
kubectl describe pod nginx-pod | grep -i readiness

# -I 반환 코드 조회. 200 성공코드 반환.



보통 내가 만든 앱이 성공코드를 반환하지 않는 앱이라면, /health 같은 경로에 성공코드를 반환하는 간단한 페이지를 만들어두고 해당 경로로 readiness를 통해 헬스체크를 하면 좋다.


실패하는 경우 어떻게 되는지 보자

cp pod.yml rf-pod.yml
vi rf-pod.yml

kubectl apply -f rf-pod.yml

  • rf-nginx-pod는 두고 nginx-pod를 삭제
kubectl delete pod nginx-pod
  • ClusterIP 타입(기본값)으로 svc 생성
vi svc.yml
apiVersion: v1
kind: Service
metadata:
  name: svc-test
spec:
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80
kubectl apply -f svc.yml

 

♨ 서비스의 클러스터 IP로 접속이 안된다면

# 서비스의 클러스터IP로 접속 안되는 것 확인

  • 파드는 정상 동작하므로 파드의 IP로 curl 보냄

# pod는 정상동작하지만, READY가 0/1. 이로인해 결국 svc에서 pod로 트래픽이 인가되지 않는다.

root@master:~/mani/probe# kubectl exec -it rf-nginx-pod -- bash
root@rf-nginx-pod:/# cat run/nginx.pid 

kubectl edit pod rf-nginx-pod

# 현재 동작중인 pod의 status 확인

# 매니페스트의 포트를 80으로 변경하고 다시 apply

kubectl delete -f rf-pod.yml
kubectl apply -f rf-pod.yml
  • readinessProbe 성공

# 정상동작
# svc에서 pod로 트래픽이 잘 인가된것을 확인


실습)

61.254.18.30:5000/nginx 이미지로 httpGet 방식의 readiness를 통해 /test경로에 80번 포트로의 요청이 성공하도록 만들어보세요!
  • svc.yml 
root@master:~/mani/probe# vi svc.yml 
apiVersion: v1
kind: Service
metadata:
  name: svc-test
spec:
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80
root@master:~/mani/probe# kubectl apply -f svc.yml
  • configmap.yml 
root@master:~/mani/probe# vi configmap.yml
# configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  default.conf: |
    server {
        listen 80;
        location /test {
            return 200 'ok';
            add_header Content-Type text/plain;
        }
        location / {
            root /usr/share/nginx/html;
            index index.html;
        }
    }
root@master:~/mani/probe# kubectl apply -f configmap.yml
  • pod.yml
root@master:~/mani/probe# vi pod.yml 
# pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    app: nginx
spec:
  containers:
  - name: nginx
    image: 61.254.18.30:5000/nginx
    ports:
    - containerPort: 80
    readinessProbe:
      httpGet:
        path: /test
        port: 80
      initialDelaySeconds: 2
      periodSeconds: 5
    volumeMounts:
    - name: nginx-config
      mountPath: /etc/nginx/conf.d
  volumes:
  - name: nginx-config
    configMap:
      name: nginx-conf
root@master:~/mani/probe# kubectl apply -f pod.yml
  • 확인
root@master:~/mani/probe# kubectl get pod
root@master:~/mani/probe# kubectl get pod -o wide

root@master:~/mani/probe# kubectl describe pod nginx-pod | grep -i readiness

root@master:~/mani/probe# curl -I 10.244.2.138
root@master:~/mani/probe# curl 10.244.2.138 

root@master:~/mani/probe# kubectl exec -it nginx-pod -- bash


configMap으로 index파일을 만들어서 컨테이너에 /usr/share/nginx/html/test 경로에 넣어줄 예정

vi test.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-index
data:
  index.html: |
    readiness success
---
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
    - name: nginx
      image: 61.254.18.30:5000/nginx
      volumeMounts:
        - name: vol-index
          mountPath: /usr/share/nginx/html/test
      readinessProbe:
        httpGet:
          path: /test
          port: 80
        initialDelaySeconds: 3
        periodSeconds: 5
  volumes:
    - name: vol-index
      configMap:
        name: cm-index


실습2)

실패하는 livenessProbe를 구성해보세요. 이후에 kubectl get pod --watch 같은 옵션을 통해 어떤 현상이 발생하는지 한번 확인해보세요.

 

일부러 실패하게 만들기

vi pod.yml
  • 방법 A. 잘못된 포트 사용 → 실패
 
livenessProbe:
  httpGet:
    path: /test
    port: 800  # nginx는 실제 80포트만 사용 중이므로 실패
  initialDelaySeconds: 2
  periodSeconds: 5

➡️ 실패 → kubectl get pod --watch 시 재시작 반복됨

  • 방법 B. 존재하지 않는 경로 요청 → 실패
livenessProbe:
  httpGet:
    path: /notfound
    port: 80
  initialDelaySeconds: 2
  periodSeconds: 5

➡️ /notfound는 404 반환 → 실패

kubectl apply -f pod.yml
kubectl get pod --watch
kubectl describe pod nginx-pod | grep -A 10 Liveness
kubectl logs nginx-pod


풀이)

vi lf-pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: lf-nginx-pod
  labels:
    app: lf-nginx
spec:
  containers:
  - name: nginx
    image: 61.254.18.30:5000/nginx
    livenessProbe:
      httpGet:
        path: /fail #curl localhost:80/fail
        port: 80
      periodSeconds: 5
      initialDelaySeconds: 5

# livenessProbe가 실패했기때문에 pod안의 컨테이너는 계속 재부팅

  • 삭제
kubectl delete pod lf-nginx-pod
vi lf-pod.yml

# 성공하도록 변경
# 컨테이너는 재시작을 안할것


tcpSocket 을 통한 Probe

MySQL 같은 앱은 httpGet같은 요청으로는 응답을 주지 않는다. 따라서, MySQL의 기본 포트인 3306으로 tcpSocket 이 유효한지를 검증하여 probe하는 방식

vi redis.yml
apiVersion: v1
kind: Pod
metadata:
  name: redis
  labels:
    app: redis
spec:
  containers:
  - name: redis
    image: 61.254.18.30:5000/redis
    readinessProbe:
      tcpSocket:
        port: 6379
      initialDelaySeconds: 5
      periodSeconds: 3
kubectl apply -f redis.yml

command를 통한 Probe

  • $? : 직전에 실행한 명령이 어떻게 종료되었는지 종료코드를 조회
  • 정상종료=0 반환, 그렇지 않은경우 1이나 2를 반환

0을 반환하면 readinessProbe가 성공, 그렇지 않은 경우 실패

  • 이 특성을 갖고 헬스체크를 해보면
vi cmd-mysql.yml
apiVersion: v1
kind: Pod
metadata:
  name: mysql
spec:
  containers:
  - name: mysql
    image: 61.254.18.30:5000/mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: "password"
  • mysql 컨테이너를 띄워보자
kubectl apply -f cmd-mysql.yml
  • 어떤 명령이 수행됐을때 0을 반환하는지 모르기때문에 한번 들어가서 확인
kubectl exec -it mysql -- bash

# mysqladmin ping -ppassword라는 명령을 통해 헬스체크하는 컨테이너를 띄워보자

  • Ctrl+D로 컨테이너에서 빠져나온후 파드를 삭제
kubectl delete pod mysql
vi cmd-mysql.yml
apiVersion: v1
kind: Pod
metadata:
  name: mysql
spec:
  containers:
  - name: mysql
    image: 61.254.18.30:5000/mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: "password"
    readinessProbe:
      exec:
        command: ["mysqladmin","ping","-ppassword"]
      initialDelaySeconds: 10
      periodSeconds: 3
kubectl apply -f cmd-mysql.yml

# 컨테이너가 잘 뜨는걸 확인 가능


실습)

    readinessProbe:
      exec:
        command: ["cat","/test/test.txt"]
      initialDelaySeconds: 10
      periodSeconds: 3
readinessProbe가 성공하는 pod 매니페스트 파일을 작성해보세요.
root@master:~/mani/probe# vi testpod.yml 
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    app: nginx
spec:
  containers:
  - name: nginx
    image: 61.254.18.30:5000/nginx
    command: ["/bin/sh", "-c", "mkdir -p /test && echo hello > /test/test.txt && nginx -g 'daemon off;'"]
    ports:
    - containerPort: 80
    readinessProbe:
      exec:
        command: ["cat", "/test/test.txt"]
      initialDelaySeconds: 10
      periodSeconds: 3
root@master:~/mani/probe# kubectl delete pod nginx-pod
root@master:~/mani/probe# kubectl apply -f testpod.yml


풀이)

  1. pv-pvc로 호스트에 존재하는 파일을 넣어주기 - 별로
  2. configMap - 세련
  3. command - 금지
    command 명령 = Dockerfile CMD, CMD가 무시되고, 컨테이너가 제대로 동작 안함
  4. Dockerfile을 통한 이미지빌드. - 귀찮
cp test.yml cmd-nginx.yml
vi cmd-nginx.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-test
data:
  test.txt: |
    cat success
---
apiVersion: v1
kind: Pod
metadata:
  name: cmd-pod
spec:
  containers:
    - name: nginx
      image: 61.254.18.30:5000/nginx
      volumeMounts:
        - name: vol-index
          mountPath: /test
      readinessProbe:
        exec:
          command: ["cat","/test/test.txt"]
        initialDelaySeconds: 10
        periodSeconds: 3
  volumes:
    - name: vol-index
      configMap:
        name: cm-test
kubectl apply -f cmd-nginx.yml

# 내부에 파일이 잘 만들어진걸 확인 가능, readiness가 성공한것 자체가 이미 /test/test.txt가 잘 주입된걸 의미


StatefulSet

파드의 이름을 규칙적으로 생성하고 싶을 때 사용하는 리소스

kubectl create deploy test --replicas=2 --image=61.254.18.30:5000/ipnginx

# 우리가 deployment를 통해 pod를 여러개 띄우면 pod의 이름이 우리가 예상할 수 없는 형태의 랜덤한 이름이 부여가 된다.

딱히 내가 앱을 배포할때 pod의 이름이 크게 중요하지 않다면, 빠르게 배포하고 싶다면 이렇게 pod를 생성하면 된다. 하지만 때에 따라서는 내가 pod의 이름을 신경써야하는 경우도 있다. 가령, 예를 들자면 mysql을 구성하는데, 하나는 master로 구성하고, 다른 하나는 slave로 구성을 하는경우가 그렇다. 그래서 내가 첫번째 리소스가 항상 mysql-0 으로 생성되고, 두번째 리소스는 항상 mysql-1 이런식으로 구성된다면, mysql-0을 마스터로 구성하고, mysql-1을 slave로 구성하면 될것이다.


이렇게 일정한 이름을 갖도록 구성되는 리소스가 StatefulSet이다.

vi sts.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sts-mysql
spec:
  serviceName: "mysql" #반드시 svc를 이 이름으로 만들어줘야한다
  replicas: 3
  selector:
    matchLabels:
      app: sts-mysql
  template:
    metadata:
      labels:
        app: sts-mysql
    spec:
      containers:
      - name: sts-mysql
        image: 61.254.18.30:5000/mysql
        ports:
        - containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "password"


DaemonSet

각 노드마다 무조건 한개씩만 파드를 띄우고 싶을때 사용하는 리소스 (도커스웜에서 deploy.mode.global: 옵션과 비슷)

kube-system의 kube-proxy, metallb-system의 speaker 같은 리소스가 이에 해당

모니터링 시스템처럼 모든 노드에 자원을 수입할 exporter들을 배치하고 싶은 경우

vi ds.yml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: ds-nginx
spec:
  selector:
    matchLabels:
      app: ds-nginx
  template:
    metadata:
      labels:
        app: ds-nginx
    spec:
      containers:
      - name: ds-nginx
        image: 61.254.18.30:5000/nginx
kubectl apply -f ds.yml

Quiz)

k8s 클러스터에 600gram이라는 이름의 deployment를 생성해서, replicas=7로 nodePort는 30500으로 배포하였다. 이 deployment를 통해 'k8s joa'라는 내용의 인덱스 파일을 배포했다. 몇 분 후 'k8s siro'라는 내용을 갖는 인덱스 파일을 포함하는 mypod라는 이름의 Pod를 nodePort 31000으로 배포했는데, 이상하게 계속 'k8s joa'라는 내용만 뜨는것이었다.

이 현상을 재현해보세요.
kubectl delete deploy --all
kubectl delete rs --all
kubectl delete svc --all
kubectl delete pod --all
  • 조아
root@master:~/mani/600g# vi joa-deploy.yml
# joa-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: 600gram
spec:
  replicas: 7
  selector:
    matchLabels:
      app: k8s-joa
  template:
    metadata:
      labels:
        app: k8s-joa
    spec:
      containers:
      - name: web
        image: nginx
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html
        configMap:
          name: joa-index

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: joa-index
data:
  index.html: |
    k8s joa

---
apiVersion: v1
kind: Service
metadata:
  name: svc-joa
spec:
  type: NodePort
  selector:
    app: k8s-joa
  ports:
  - name: joa-main
    port: 80
    targetPort: 80
    nodePort: 30500
root@master:~/mani/600g# kubectl apply -f joa-deploy.yml
  • 시로
root@master:~/mani/600g# vi siro-pod.yml
# siro-pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: mypod
  labels:
    app: k8s-siro
spec:
  containers:
  - name: web
    image: nginx
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
    ports:
    - containerPort: 80
  volumes:
  - name: html
    configMap:
      name: siro-index

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: siro-index
data:
  index.html: |
    k8s siro
---
apiVersion: v1
kind: Service
metadata:
  name: svc-siro
spec:
  type: NodePort
  selector:
    app: k8s-joa
  ports:
  - port: 80
    targetPort: 80
    nodePort: 31000
root@master:~/mani/600g# kubectl apply -f siro-pod.yml


풀이)

pod에 동일한 라벨링을 하면 어떻게 되는지?

서비스는 라벨을 기준으로 트래픽을 인가하기때문에 같은 라벨을 달고있으면 해당 라벨을 달고 있는 모든 pod에 트래픽을 인가시킬것. 따라서 31000 포트로 접속해도 siro에 갈 확률이 1/8 밖에 안된다.