본문 바로가기

Kubenetes/Kubernetes Pods

Pod의 Lifecycle

Pod는 단일 애플리케이션만 실행하는 VM과 비교할 수 있다. Pod 내부에서 실행되는 애플리케이션은 VM에서 실행중인 애플리케이션과 다르지 않지만 중요한 차이가 있다. 한 가지 예는 쿠버네티스가 어떤 이유로 Pod를 다른 Node로 재배치해야 할 필요가 있거나 Node의 ScaleDown 요청 등이 있을 때 실행 중인 애플리케이션을 언제든지 종료할 수 있다는 것이다.

 

1. Application 종료/재배치

 

쿠버네티스 밖에서는 VM에서 실행되는 애플리케이션이 한 컴퓨터에서 다른 컴퓨터로 거의 이동하지 않는다. 운영자가 애플리케이션을 이동한다면 애플리케이션을 다시 구성하고 새 위치에서 애플리케이션이 정상적으로 작동하는지 수동으로 확인해야 한다.

쿠버네티스를 사용하면 애플리케이션을 훨씬 자주 그리고 자동으로 이동할 수 있다. 작업자가 재구성하지 않아도 이동 후에도 제대로 실행된다.

 

즉 애플리케이션 개발자는 애플리케이션이 자주 이동이 가능하도록 상대적으로 애플리케이션을 만들어야 한다.

 

ㅇ 변경될 로컬 IP 및 호스트 이름 예상하기

 

포드가 종료되고 다른 곳으로 옮겨지면(실제로는 옮겨지는 것이 아닌 재생성) 새로운 IP 주소뿐만 아니라 새로운 이름과 호스트 이름도 갖게 된다.  대부분의 상태 비저장 애플리케이션은 보통 부작용 없이 이것을 처리할 수 있지만 상태 저장 애플리케이션은 일반적으로 그렇게 할 수 없다. 이런 경우 Statefulset(동일한 Host 이름과 동일한 Volume 연결)을 통해 상태 저장 애플리케이션을 실행할 수 있으나 이 경우에도 Pod의 IP는 변경이된다.  

 

따라서 애플리케이션 개발자는 구성원의 IP 주소에서 클러스터된 애플리케이션의 구성원을 기반으로 해서는 안되며 호스트 이름을 기 반으로 하는 경우 반드시 StatefulSet을 사용해야 한다.

 

ㅇ 디스크에 기록된 데이타가 사리지는 경우 예상하기

 

애프리케이션이 데이터를 디스크에 쓸 때 애플리케이션이 작성중인 위치에 영구 저장 장치를 마운트 하지 않았다면 새로운 Pod 내부에서 애플리케이션을 시작한 후에 디스크에 저장된 데이터를 사용할 수 없다는 것이다.

 

 

 

ㅇ 컨테이너를 다시 시작하더라도 데이터의 보존을 위해 볼륨 사용

컨테이너가 다시 시작되어 데이터가 유실되지 않도록 하려면 적어도 Pod 범위의 볼륨을 사용해야 한다. 볼륨은 Pod와 함께 죽기 때문에 새 컨테이너는 이전 컨테이너가 볼륨에 쓴 데이터를 재사용할 수 있다.

 

볼륨을 사용해 컨테이너 재 시작시 파일을 보존하는 것은 때로는 좋은 아이디어가 아니다. 데이터가 손상돼 새로 생성된 프로세스에서 다시 크래시가 발생하면 반복적으로 발생할 수 있게 된다.

 

볼륨을 사용해 컨테이너 재시작 시 파일을 보존하는 방법은 양날의 검이다. 사용 여부를 신중하게 생각해야 한다.

 

 

2. 죽은 포드 또는 부분적으로 죽은 포드의 재스케줄링

 

포드의 컨테이너에서 계속 크래시가 생기면 Kubelete은 무한정 다시 시작하게 된다. 재시작 시간은 5분에 도달할 때까지 기하 급수적으로 증가한다. 5분 간격 동안 컨테이너의 프로세스가 실행되고 있지 않기 때문에 Pod는 본질적으로 죽었다고 볼 수 있다.

 

실제로는 다중 컨테이너 Pod라면 특정 컨테이너가 정상적으로 작동할 수 있으므로 Pod는 일부만 죽은 것으로 볼 수 있지만 포드에 단일 컨테이너만 들어 있는 경우 프로세스가 더 이상 실행되고 있지 않으므로 사실상 죽은 것이고 쓸모가 없어진다.

 

레플리카셋 또는 유사한 컨트롤러의 일부인 경우에도 이런 포드가 자동으로 제거되고 재스케줄되지 않는다. 원하는 Replica가 3인 레플리카셋을 만든 다음 해당 Pod중 하나에 있는 Container 중 하나에서 Crash가 발생하기 시작하더라도 쿠버네티스는 Pod를 삭제하고 교체하지 않는다.  마지막에는 원하는 세 개의 복제본 대신 두 개의 제대로 실행되는 복제본만 있는 ReplicaSet이 된다.

 

 

Replicaset Controller는 포드가 죽었는지 상관 하지 않고 관심 있는 모든 것은 포드 수가 원하는 복제 카운트와 일치하냐는 것이다. 위의 경우 포드는 원하는 복제 수와 일치하기 때문에 아무런 작업도 하지 않는다.

 

3. 원하는 순서로 Pod 시작하기

 

Pod에서 실행되는 애플리케이션과 수동으로 관리되는 애플리케이션의 다른 점 중 하나는 해당 애플리케이션을 배포하는 운영자가 애플리케이션간 의존성을 알고 있다는 것이다. 이것은 운영자가 원하는 때 순서로 애플리케이션을 시작할 수 있다는 의미다.

 

ㅇ 포드의 시작 방식

 

쿠버네티스를 사용해 다수의 Pod 애플리케이션을 실행할 때 쿠버네티스가 먼저 특정 Pod를 실행하고 나머지 Pod는 이미 동작중이며 준비가 완료 됐을 때만 알려주는 기본 제공 방법은 없다. 

쿠버네티스 API 서버는 나열된 순서대로 YAML/JSON의 객체를 처리하지만, 이는 그 순서대로 etcd에 기록된다는 것을 의미한다. Pod가 그 순서대로 시작된다는 보장이 없다.

 

그러나 전제 조건이 충족될 때까지 Pod의 주 컨테이너가 시작되는 것을 방지할 수 있다. 이것은 포드에 초기화 컨테이너를 포함시켜서 수행하게 된다.

 

ㅇ 초기화 컨테이너 소개

 

일반 컨테이너 외에도 포드에는 초기화 컨테이너가 포함될 수 있다. 이름에서 알 수 있듯이 포드를 초기화 하는데 사용할 수 있다. 포드의 주 컨테이너에 마운트된 포드의 볼륨에 데이터를 쓰는 것을 종종 의미한다.

 

포드에는 여러 개의 초기화 컨테이너가 있을 수 있다. 순차적으로 실행되며 마지막 컨테이너가 완료된 후에만 포드의 주 컨테이너가 시작된다. 이는 초기화 컨테이너를 사용해 특정 전제 조건이 충족될 때까지 포드의 주 컨테이너 시작을 지연시킬 수 있음을 의미한다.

 

초기화 컨테이너는 포드의 주 컨테이너에 필요한 서비스가 준비되고 준비될 때까지 기다릴 수 있다. 그럴 때 초기화 컨테이너가 종료되고 주 컨테이너가 시작될 수 있다. 이렇게 하면 주 컨테이너는 서비스가 준비되기 전에 서비스를 사용하지 않게 된다.

 

ㅇ 초기화 컨테이너를 포드에 추가하는 예시

spec:
  initContainers:         # 일반 컨테이너가 아닌 초기화 컨테이너를 정의하고 있다.
  - name: init
    image: busybox
    command:
    - sh
    - -c
    # 초기화 컨테이너는 fortune 서비스가 가동될 때까지 실행하는 루프를 실행한다.
    - 'while true; do echo "Waiting for fortune service to come up ...";      \
      wget http://fortune -q -T 1 -0 /dev/null >/dev/null 2>/dev/null         \ 
      && break; sleep 1; done; echo "Service is up! Starting main container."'

 

이 포드를 배포하면 초기화 컨테이너만 시작된다. 이것은 kubectl get으로 포드를 나열할 때 포드 상태에 표시된다.

$ kubectl get po
NAME           READY  STATUS    RESTARTS   AGE
fortune-client 0/1    Init:0/1  0          1m

STATUS 열은 하나의 초기화 컨테이너 중 0개가 완료됐음을 나타낸다. kubectl 로그를 사용해 초기화 컨테이너의 로그를 볼 수 있다.

$ kubectl logs fortune-client -c init
Waitting for fortune service to come up...

kubectl logs 명령을 실행할 때 -c 스위치를 사용해 초기화 컨테이너의 이름을 지정해야 한다.

 

ㅇ 포드 간 의존도를 다루는 모범 사례

 

전제 조건이 충족될 때까지 초기화 컨테이너를 사용할 수 있으나 애플리케이션을 시작하기 전에 의존하고 있는 모든 서비스의 준비를 요구하지 않는 애플리케이션을 작성하는 편이 훨씬 낫다. 애플리케이션이 지금 시점에는 실행 중이지만 결국 나중에 오프라인으로 전환 될 수도 있기 때문이다.

 

애플리케이션은 의존성이 준비되지 않았을 가능성을 내부적으로 처리할 수 있어야 한다. 그러므로 Readiness Probe를 사용한다.

 

애플리케이션이 의존성 중 하나가 없어서 작업을 수행할 수 없는 경우 준비가 완료됐음을 나타내는 신호가 있어야 하므로 쿠버네티스는 애플리케이션이 준비 상태가 아니라는 것을 알고 있다. 이렇게 하면 앱이 서비스 앤드포인트로 추가되지 않을 분만 아니라 롤링 업데이트를 수행할 때 Deployment 컨트롤러에서 앱 준비 상태를 사용하므로 잘못된 버전의 롤아웃이 방지된다.

 

4. Lifecycle Hook 추가

 

초기화 컨테이너를 사용해 포드 시작에 연결하는 방법을 설명했지만 포드에서는 라이프 사이클에서 두가지 훅을 정의할 수도 있다.

 

 - post-start hook

 - pre-stop hook

 

이런 라이프사이클 훅은 전체 컨테이너에 적용되는 초기화 컨테이너와 달리 컨테이너별로 지정된다. 이름에서 알 수 있듯이, Lifecycle Hook은 전체 컨테이너에 적용되는 초기화 컨테이너와 달리 컨테이너별로 지정된다. 

이름에서 알 수 있듯이, lifecycle hook은 컨테이너가 시작될 때 실행하고 중지되기 전에 실행한다.

 

Lifecycle Hook은 liveness와 Readiness probe와 유사하다.

 

- 컨테이너 내부에서 명령을 실행하기

- URL에 HTTP GET 요청을 수행하기

 

컨테이너 라이프사이클에 어떤 영향을 미치는지 확인하기 위해 두개의 훅을 각각 할펴보자

 

ㅇ post-start 컨테이너 라이프사이클 사용

 

post-start hook은 컨테이너의 주 프로세스가 시작하고 난 다음 바로 실행된다. 애플리케이션이 시작될 때 추가 조작을 수행하기 위해 이를 사용한다. 물론, 컨테이너에서 실행 중인 애플리케이션 개발자라면 애플리케이션 코드 내에서 항상 이런 작업을 수행할 수 있으나 다른 사람이 개발한 애플리케이션을 실행하는 경우 소스 코드 수정이 쉽지는 않다.

 

이런 경우 post-start hook을 사용하면 애플리케이션을 건드리지 않고 추가 명령을 실행하게 만들 수 있다. 이것들은 애플리케이션이 시작되고 있는 외부 리스너에게 시그널을 보내거나 애플리케이션을 초기화하는 작업을 시작할 수 있다.

 

hook은 주 프로세스와 병렬로 실행된다. (주 프로세스가 완전히 시작될 때까지 기다리지 않기 때문에 다소 오해의 소지가 있지만 병렬로 인지하자)

hook이 완료될 때까지 컨테이너는 Containing Creating인 채로 Waiting 상태로 유지되며 포드의 상태는 Running 대신 Pending 상태가 된다.  hook이 실행되지 않거나 0이 아닌 종료 코드를 반환하면 주 컨테이너가 종료된다.

 

post-start hook이 포함된 pod Menifest는 다음 예제와 같다.

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-poststart-hook
spec:
  containers:
  - image: luksa/kubia
    name: kubia
    lifecycle:       # 컨테이너가 시작될 때 hook이 실행된다.
      postStart:     
        exec:        # 컨테이너 안의 bin 디렉터리에서 postStart.sh 스크립트를 실행한다.
          command:
            - sh
            - -c
            - "echo 'hook will fail with exit code 15'; sleep  5; exit 15"

이 예제에서 echo, sleep, exit 명령은 컨테이너가 생성되는 즉시 컨테이너의 주 프로세스와 함께 실행된다. 이와 같은 명령을 실행하는 대신 컨테이너 이미지에 저장된 쉘 스크립트 도는 이진 실행 파일을 일반적으로 실행할 것이다.

 

하지만 hook에 의해 시작된 프로세스가  표준 출력으로 로깅하면 출력을 어디서든 볼수 없다. 따라서 라이프사이클 훅의 디버깅이 어렵게 된다. 훅이 실패하면 포드의 이벤트 중 FailedPostStartHook 경고만 표시된다. 

 

ㅇ pre-stop 컨테이너 라이프사이클 훅 사용

 

pre-stop hook은 컨테이너가 종료되기 직전에 즉시 실행된다. 컨테이너를 종료해야 하는 경우 Kublet은 pre-stop hook을 실행한 다음 프로세스로 SIGTERM을 전송한다. (프로세스가 정상적으로 종료되지 못했다면 강제 종료시킬 것이다.)

 

SIGTERM 신호를 받고 정상적으로 종료되지 않는 경우 pre-stop 훅을 사용해 컨테이너를 정상적으로 종료할 수 있다. 또한 애플리케이션 자체에서 이런 작업을 구현하지 않고도 종료하기 전에 임의이 작업을 수행하는데 사용할 수 있다. (이것은 소스코드의 액세스 권한이나 수정권한이 없는 서드파티 애플리케이션을 실행할 때 유용하다.)

 

Pod Menifest에서 pre-stop hook을 구성하는 것은 post-start hook을 추가하는 것과 크게 다르지 않다.

 

아래에서는 HTTP GET 요청을 수행하는 pre-stop hook을 살펴본다. Pod에서 pre-stop HTTP GET Hook을 정의하는 방법은 다음 예제와 같다.

lifecycle:
  preStop:       # 이것은 HTTP GET 요청을 수행하는 pre-stop hook 이다.
    httpGet:
      port: 8080 # 이 요청은 http://POD_IP:8080/shutdown으로 보내진다.
      path: shutdown
     

pre-stop hook은 kubelet이 컨테이너 종료를 시작하자마자 http://POD_IP:8080/shutdown의 HTTP GET을 요청한다. 목록에 표시된 port 및 경로(path)외에도 필드 스키마(HTTP 또는 HTTPS) 및 호스트뿐만 아니라 요청에서 보내야 하는 http 헤더도 설정할 수 있다.

 

호스트 필드의 기본 값은 포드 IP이다. localhost는 Pod가 아닌 Node를 참조하므로 localhost로 설정하면 안된다.

 

post-start hook과는 달리 hook의 결과에 관계없이 컨테이너는 종료된다. 명령 기반 훅을 사용할 때 HTTP 응답코드가 오류나 종료코드가 0이 아니더라도 컨테이너는 종료되지 않는다. pre-stop hook이 실패하면 포드의 이벤트 중 FailedPreStopHook 경고 이벤트가 표시되지만 이후에 포드는 곧 삭제되므로 pre-stop hook이 제대로 작동하지 않는 것을 알지도 못할 수도 있다.

 

pre-stop hook의 성공적인 완료가 시스템의 올바른 작동에 중요할 경우, 시스템이 완전히 실행 중인지 확인해야 한다. 

 

5. 포드 셧다운

 

포드의 종료는 API 서버의 포드 객체 삭제에 의해 트리거된다. HTTP DELETE 요청을 받으면 API 서버는 객체를 아직 삭제하지 않고 삭제 전용 타임스탬프 필드만 설정한다. deletionTimestamp 필드가 설정된 포드가 종료된다.

 

Kubelet이 포드를 종료해야 한다는 사실을 알게 되면 각 포드의 컨테이너를 종료하기 시작한다. 각 컨테이너에 정상적으로 종료할 시간이 주어지지만 시간이 제한된다. 그 시간을 종료 유예 기간(the termination grace period)이라고 하며 포드당 구성할 수 있다.

 

종료 프로세스가 시작되자마자 타이머가 시작된다. 그런 후 다음과 같은 일련의 이벤트가 수행된다.

 

1. pre-stop hook이 구성된 경우 이를 실행하고 완료될 때까지 기다린다.

2. SIGTERM 신호를 컨테이너의 메인 프로세스로 보낸다.

3. 컨테이너가 완전히 셧다운 될 때까지 또는 종료 유예 기간이 만료될 때까지 기다린다.

4. 정상적으로 종료되지 않은 경우 SIGKILL로 프로세스를 강제 종료한다.

 

 

ㅇ 종료 유예기간 설정

 

종료 유예기간은 포드 스펙에서 terminationGracePeriodSeconds 필드에서 설정할 수 있다. 기본 값은 30이고 포드의 컨테이너가 강제 종료되기 전에 정상 종료할 수 있도록 30초가 주어진다.

 

--> 프로세스가 그 시간 내에 정리를 끝마칠 수 있도록 유예 기간을 충분히 길게 설정해야 한다.

 

다음과 같이 포드를 삭제하면서 포드 스펙에 지정된 유예 기간을 재설정할 수도 있다.

$ kubectl delete po mypod --grace-period=5

위의 내용은 kubelet이 포드를 말끔히 정리할 때까지 5초 동안 기다린다. 모든 포드의 컨테이너가 멈추면 Kubelete이 API 서버에 알리고 포드 리소스가 최종적으로 삭제된다.

 

확인을 기다리지 않고 리소스를 즉시 삭제하도록 유예 기간을 0으로 설정하고 --force 옵션을 추가해 API 서버가 강제로 삭제하게 할 수 있다.

$ kubectl delete po mypod --grace-period=0 --force

하지만 StatefulSet의 포드와 함께 할 때 이 옵션의 사용을 주의해야 한다.