카테고리 없음

[Kubernetes-in-actions]ch17. 애플리케이션 개발을 위한 모범 사례(feat. Pod lifecycle)

minhan2 2022. 6. 17. 14:17
728x90
반응형

파드 라이프사이클 이해

애플리케이션을 종료하고 파드 재배치 예상하기 (어디로 갈까?)

실행중인 파드는 언제나 종료될수 있다.

기존의 환경에서는 실행중인 애플리케이션을 한 시스템에서 다른 시스템으로 이동 하는 경우가 드물지만, 쿠버네티스를 사용하면 더 자주 자동으로 이동, 배치된다.
쿠버네티스에서 기술적으로는 이전 파드를 대체하는 새로운 파드가 생성되는것이다.
스테이트리스(상태가 없는) 파드에 대해서는, 재배치 된것이 아니라, 새로운 IP, 이름, 호스트 이름을 갖는다.
스테이트풀(상태가 있는)인 스테이트풀셋은 새 노드로 재배치 되어도, 이전과 동일한 이름, 호스트 이름을 갖고, IP는 변경된다.

- POD의 IP와 호스트 이름 변경이 되는것 확인
POD을 배치하고 삭제하는것을 확인해보자.

kubectl run nginx-pod-test --image=nginx
kubectl get pods
kubectl delete pod

kubectl create deployment nginx-deploy-test --image=nginx 
kubectl delete nginx-pod-test-
kubectl get deploy
kubectl delete deployment nginx-deploy-test
apiVersion: v1
kind: Service
metadata:
  name: nginx-statefulset-service
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-statefulset
spec:
  serviceName: "nginx-statefulset-service"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: k8s.gcr.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
kubectl delete nginx-statefulset-0
kubectl get statefulset nginx-statefulset
kubectl delete statefulset nginx-statefulset

디스크에 기록된 데이터 사라지는것 확인

경고: gitRepo 볼륨 유형은 사용 중단되었다. git repo가 있는 컨테이너를 프로비전 하려면 초기화 컨테이너(InitContainer)에 EmptyDir을 마운트하고, 여기에 git을 사용해서 repo를 복제하고, EmptyDir을 파드 컨테이너에 마운트 한다.

경고: HostPath 볼륨에는 많은 보안 위험이 있으며, 가능하면 HostPath를 사용하지 않는 것이 좋다. HostPath 볼륨을 사용해야 하는 경우, 필요한 파일 또는 디렉터리로만 범위를 지정하고 ReadOnly로 마운트해야 한다.
AdmissionPolicy를 사용하여 특정 디렉터리로의 HostPath 액세스를 제한하는 경우, readOnly 마운트를 사용하는 정책이 유효하려면 volumeMounts 가 반드시 지정되어야 한다.

참고: secret 볼륨은 암호와 같은 민감한 정보를 파드에 전달하는데 사용된다. 쿠버네티스 API에 시크릿을 저장하고 쿠버네티스에 직접적으로 연결하지 않고도 파드에서 사용할 수 있도록 파일로 마운트 할 수 있다. secret 볼륨은 tmpfs(RAM 기반 파일시스템)로 지원되기 때문에 비 휘발성 스토리지에 절대 기록되지 않는다.

persistent volme을 사용하지 않으면 파드와 함께 데이터가 날라가거나, 다른 노드에 배치되어, 사용하지 못할수가 있다.
파드가 변경되지 않을 Config 파일 또는 Secret파일이 필요하다면, ConfigMap, Secret을 volume으로 사용
파드가 삭제될떄 볼륨이 같이 삭제되도 괜찮고, 파드 내의 컨테이너가 재실행 되어도 데이터를 유지할 필요가 없다면 volume을 쓰지 않음
파드가 삭제될때 볼륨이 같이 삭제되도 괜찮고, 파드 내의 컨테이너가 재실행 되어도 데이터를 유지하고 싶다면 emptyDir
파드가 삭제될때 데이터를 유지하고싶고, 노드가 바뀔일이 없다면 hostVolume
파드가 삭제될때 데이터를 유지하고싶고, 노드가 바뀔일이 있다면 persistentVolume

# configmap volume
apiVersion: v1
kind: Pod
metadata:
  name: configmap-pod
spec:
  containers:
    - name: test
      image: busybox
      volumeMounts:
        - name: config-vol
          mountPath: /etc/config
  volumes:
    - name: config-vol
      configMap:
        name: log-config
        items:
          - key: log_level
            path: log_level
#emptyDir
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}
# hostPath
apiVersion: v1
kind: Pod
metadata:
  name: test-webserver
spec:
  containers:
  - name: test-webserver
    image: k8s.gcr.io/test-webserver:latest
    volumeMounts:
    - mountPath: /var/local/aaa
      name: mydir
    - mountPath: /var/local/aaa/1.txt
      name: myfile
  volumes:
  - name: mydir
    hostPath:
      # 파일 디렉터리가 생성되었는지 확인한다.
      path: /var/local/aaa
      type: DirectoryOrCreate
  - name: myfile
    hostPath:
      path: /var/local/aaa/1.txt
      type: FileOrCreate


원하는 순서로 파드를 실행시킬수 있는가?

첫번째 파드가 서비스할 준비가 되었을때, 나머지 파드를 실행하도록 자동으로 지시할 방법은 기본적으로 없다.
파드의 초기화 컨테이너를 포함시켜 파드의 주 컨테이너의 서비스 사전 준비를 수행시킬수있다.

없다면 어떻게 해야하는가? (initContainer 추가)

파드는 여러개의 초기화 컨테이너를 포함할수 있다.

#myapp.yaml
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
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.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
  - name: init-mydb
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"]

여기서 until은 조건문이 참이면 루프를 끝내는 반복문이다.
nslookup은 네트워크 디버깅을 위하여 사용하는 리눅스 명령어로, DNS서버에 직접 DNS쿼리를 하고 그 결과를 출력해준다.
상단의 첫번째 initContainer의 command에서는
until 루프 조건문을 시작하는데,
myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local
현재 나의 네임스페이스에서 myservice라는 서비스가 있는지 없는지를 2초마다(sleep 2) 찾고있으며
없으면 다시 처음으로 돌아가서 조건문을 반복한다.

kubectl apply -f myapp.yaml
# 출력 결과 확인
kubectl get -f myapp.yaml
kubectl describe -f myapp.yaml

상단의 initContainer들의 조건을 만족시켜주기 위해서 service를 생성시켜준다

#services.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376
---
apiVersion: v1
kind: Service
metadata:
  name: mydb
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9377
kubectl apply -f services.yaml

# 결과 확인
kubectl get -f myapp.yaml

파드 간 의존성 처리를 위한 모범 사례

#의존해야할 서비스를 필요로 하지 않도록 애플리케이션을 만드는것이 좋은방법이다.
#의존성이 준비되지 않을 가능성을 내부적으로 처리할 레디니스 프로브를 사용해야한다.

#레디니스 프로브 사용 예시
# ch5

apiVersion: v1
kind: ReplicationController
metadata:
  name: kubia
spec:
  replicas: 3
  selector:
    app: kubia
  template:
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia
        ports:
        - name: http
          containerPort: 8080
        readinessProbe:
          exec:
            command:
            - ls
            - /var/ready

라이프사이클 훅 추가

초기화 컨테이너를 사용해 파드 시작 시 사용하는 방법이 있지만, 추가적으로 라이플 훅 두가지를 정의하여 사용할 수 있다.

  • 라이프 사이클은 무엇이며, 어떤것이 있을까?

전체 파드에 적용되는 초기화 컨테이너와 달리
post-start 와 pre-stop 라이프사이클 훅은 파드가 아닌 컨테이너와 관련이 있다.

라이브니스 프로브, 레드니스 프로브와 유사하게

  • 컨테이너 내부에서 명령 실행
  • URL로 HTTP GET 요청 수행
    이 가능하다.

컨테이너의 시작 후 라이프사이클 훅 사용(post-start)

출처 : https://thecloudblog.net/lab/kubernetes-container-lifecycle-events-and-hooks/
애플리케이션이 시작될 때 추가 작업을 수행하는데 사용한다.

훅은 주 프로세스와 병렬로 실행된다.
주 프로세스가 완전히 시작될 때까지 기다리지 않기 때문에 이름에 다소 오해의 소지가 있다.

훅이 비동기로 실행되더라도
훅이 완료 될 때까지 컨테이너는 ContainerCreating인 채로 Waiting 상태가 유지된다.
이때문에 파드 상태는 Running중이 아니라 Pending 상태다.
훅이 실행되지 않거나 0이 아닌 종료 코드를 반환하면 주 컨테이너가 종료된다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-poststart-hook
spec:
  containers:
  - image: luksa/kubia
    name: kubia
    lifecycle:
      postStart:
        exec:
          command:
          - sh
          - -c
          - "echo 'hook will fail with exec code 15'; sleep 5; exit 15"

# 이 예제의 echo, sleep, exix 명령은 컨테이너 생성 시 컨테이너의 주 프로세스와 함께 실행된다.
# 이러한 명령어를 실행하는 대신 컨테이너 이미지 내의 쉘 스크립트 또는 바이너리 실행파일을 실행시키는것이 보편적일것이다.

훅에 의해 시작된 프로세스가 표준 출력으로 기록하면 어디에서도 출력을 볼 수 없을 것이다.
따라서 라이프사이클 훅의 디버깅이 어려우며, 훅이 실패하면 파드의 이벤트중 FailedPostStartHook 경고만 표시된다.
(kubectl describe pod으로 확인)

그렇다면 로그를 한번 찍어볼까요?

post start 에러를 발생시키지만,
로그가 찍히지 않는다.

apiVersion: v1
kind: Pod
metadata:
  name: sidecar-container-demo
spec:
  containers:
    - image: busybox
      command: ["/bin/sh"]
      args:
        [
          "-c",
          "while true; do echo echo $(date -u) 'Written by busybox sidecar container' >> /var/log/index.html; sleep 5;done",
        ]
      name: sidecar-container
      resources: {}
      volumeMounts:
        - name: var-logs
          mountPath: /var/log
      lifecycle:
        postStart:
          httpGet:
            path: /index.html
            port: 80
            # port: 5000
            # 실패하려면 5000 포트 활성화
            host: localhost
            scheme: HTTP
    - image: nginx
      name: main-container
      resources: {}
      ports:
        - containerPort: 80
      volumeMounts:
        - name: var-logs
          mountPath: /usr/share/nginx/html
  dnsPolicy: Default
  volumes:
    - name: var-logs
      emptyDir: {}


#kubectl describe pod/sidecar-container-demo

컨테이너의 종료 전 라이프사이클 훅 사용(pre-stop)

SIGTERM과 SIGKILL이란?

출처 :https://seereal.pw/22

SIGTERM은 signal + terminate를 뜻하며
프로세스를 종료하라는 신호를 보내는 것이다.
종료 권고에 가깝다.
흔히 쓰는 [Ctrl]+[z]는 SIGTERM이다.
kill 명령어의 default 옵션이다.

kill -15 precessid

여담 : [Ctrl]+[c] 는 SIGINT로 SIGTERM과 비슷한 종료 시그널이다.

SIGKILL은 signal + kill을 뜻한다.
프로세스를 강제로 죽이는것이다.

kill -9 precessid

SIGTERM으로 해당 프로세스에게 필요한 종료 절차를 진행하도록 하고
설정한 기간동안 종료가 안되면 SIGKILL로 강제 종료시키는 느낌

특정 시그널을 받았을때 실행하는 함수를 시그널 핸들러라고 하며, 받을수 없는 시그널도 존재한다.(SIGKILL, SIGSTOP)
특정 시그널을 받을 수 있다는 것은, 시그널 핸들러를 제작할 수 있다는 뜻이기도 하지만 동시에 무시할 수 있다는 뜻이기도 하다.
개발자가 애플리케이션을 개발할때
if precess.GET("SIGTERM") 같은 명령어로 시그널을 받으면
해당 애플리케이션이 종료되기 전에 마무리할수 있게 된다.
SIGKILL은 시그널을 받을수가 없으며,유저가 텍스트를 작성하고있던, 저장하고있던 상관없이
강제 종료 시켜버린다.

프로세스들이 강제로 종료되면, 진행하던 일을 마무리 할수 없게되므로,
데이터나 프로세스 관점에서는 비정상적인 상황이 발생할 가능성이 높다.
그래서 정리할 시간을 주기위하여 SIGTERM을 사용하는 것이고
이것이 graceful shutdown 이다.


apiVersion: v1
kind: Pod
metadata:
  name: prestop-demo
spec:
  containers:
    - image: nginx
      name: nginx-container
      resources: {}
      ports:
        - containerPort: 80
      lifecycle:
        preStop:
          exec:
            command:
              - sh
              - -c
              - echo "Stopping container now...">/proc/1/fd/1 && nginx -s stop
  dnsPolicy: Default


# kubectl logs prestop-demo

쉘 스크립트에서 >와 <는 리디렉션(redirection)을 의미한다.
표준 출력과 입력을 리디렉션할 때 사용한다.

?? /proc/1/fd/1 이건 뭐죠?

$ll /dev/std*
lrwxrwxrwx 1 root root 15 Jul 24  2020 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 Jul 24  2020 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Jul 24  2020 /dev/stdout -> /proc/self/fd/1

$ll /proc/self/fd/[0-2]
lrwx------ 1 root root 64 Jul 13 09:34 /proc/self/fd/0 -> /dev/pts/0
lrwx------ 1 root root 64 Jul 13 09:34 /proc/self/fd/1 -> /dev/pts/0
lrwx------ 1 root root 64 Jul 13 09:34 /proc/self/fd/2 -> /dev/pts/0



echo hello > /dev/null
echo hello > /dev/stdout
echo hello > /proc/self/fd/1

ls fasdfadsf 1> stdout.txt 2> stderr.txt
ls fasdfadsf 1>>stdout.txt 2>&1

리눅스의 /proc 디렉터리에는 프로세스에 관련된 정보들이 담겨있다
도커 컨테이너의 프로세스 아이디(PID)는 1번이다.


파드 셧다운 이해하기

참고 : deletionTimestamp
책의 번역이 잘못되어 있는것 같다. deleteTimestamp 또는 deletedTimestamp 라고 되어있는데 deletionTimestamp이 맞는듯 하다.

파드의 종료는 API서버로 파드의 오브젝트를 삭제하면 시작된다.
사용자가 kubectl delete pod test-pod 라는 delete 명령어를 실행하면, API서버로 HTTP DELETE 요청을 보내게 되고,
API서버가 요청을 수신하면, 삭제하고자 하는 해당 오브젝트의 metadata.deletionTimestamp 필드를 오브젝트가 삭제 표시된 시간(요청을 보낸 현재 시간)으로 설정한다.

metadata.deletionTimestamp 이 설정된 순간, terminateGracePeriodSeconds(종료 유예 기간)에 설정된 시간이 흐른다.
이후의 종료 과정은

  1. prestop hook 을 실행하고 완료될때까지 기다린다.
  2. SIGTERM 신호를 컨테이너의 주 프로세스로 보낸다.
  3. 컨테이너가 완전히 종료될 때까지 또는 종료 유예 기간이 끝날 때까지 기다린다.
  4. 아직 정상적으로 종료되지 않은경우는 SIGKILL로 프로세스를 강제 종료한다.

image

참고

  1. prestop이 지정 되어있지 않다면, 곧장 SIGTERM이 전달됩니다.
  2. terminateGracePeriodSeconds가 지정 되어있지않다면 기본값은 30초입니다.


쿠버네티스에서 애플리케이션을 쉽게 실행하고 관리할 수 있게 만들기

관리 가능한 컨테이너 이미지 만들기

  1. 많은 라이브러리를 갖고있는 이미지는 바람직하지 않다. 대부분의 파일은 사용되지 않는다.
  2. 최소한의 이미지 만으로는 디버깅하기가 매우 어려우므로, 자신에게 필요한 최소한의 도구 세트를 포함시켜서 사용한다.

이미지 예시 : scratch, alpine, slim,

이미지에 적절한 태그를 지정하고 imagePullPolicy를 현명하게 사용

기본적으로 container이미지 명을 작성할때, 태그명을 적지 않으면 lastet 태그 이미지를 사용하게 된다.
latest 태그는 최신의 이미지 버전을 뜻한다.
하지만, 특정한 버전 명을 작성하지 않고, 실행 시간이 다른 container들의 latest태그가 여러가지 버전이 혼용되는 문제가 생길수가 있다.
예를들면 2020년 1월1일에 latest태그로 Deploy 오브젝트를 배포하였을때는, node가 10버전으로 배포되어있었고, 현재까지 운용중이였다가.
2022년 1월1일 node가 16버전이 최신버전이 나왔을때, 운영자가 POD이 부족한듯하여, replicas만 늘리게 된다면, image:node:latest는 바뀌지 않았기때문에, 이전에 생성된 POD들에게는 node가 10버전으로 배포되어있고, 현재의 새로 늘린 POD에는 image:node:latest로 배포되어 node 버전이 16이 되어버릴수있다. (그전에 POD이 죽고 재배치 될때마다 그때의 최신 node버전으로 배포되어있으면 더 끔찍하다.)

그러므로 개발을 제외하고 latest버전 대신 버전 지정자를 포함하는 태그를 사용하는것이 필수이다.
그리고, 상용서버 배포시에는 imagePullPolicy를 IfNotPresent으로하고, tag를 버전별로 다르게하여 배포하는것이 좋다.

만약
imagePullPolicy가 always로 설정되어 있으면, 이미지가 있더라도, 새 파드가 배포될 때 마다 컨테이너 런타임이 이미지 레지스트리에 접속해서 가져온다.
다만, 동일한 name과 tag를 가지는 이미지라도, name과 tag와 digest가 같은 이미지라면 가져오지 않는다.
노드는 이미지가 수정되었는지 확인해야 하기 떄문에 파드 시작 속도가 약간 느려진다.
게다가 더 나쁜 점은 이 정책은 레지스트리에 연결할 수 없는 경우 파드가 시작되지 않게 된다.

imagePullPolicy의 기본값은 IfNotPresent으로, 이미지가 없을시에만 다운로드 받는다.

일차원 레이블 대신 다차원 레이블 사용

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: wordpress
    app.kubernetes.io/instance: wordpress-abcxzy
    app.kubernetes.io/version: "4.9.4"
    app.kubernetes.io/managed-by: helm
    app.kubernetes.io/component: server
    app.kubernetes.io/part-of: wordpress
...
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: wordpress
    app.kubernetes.io/instance: wordpress-abcxzy
    app.kubernetes.io/version: "4.9.4"
    app.kubernetes.io/managed-by: helm
    app.kubernetes.io/component: server
    app.kubernetes.io/part-of: wordpress
...
728x90
반응형