Data science/MLOps

Kubernetes: Pod 이해하기 (Init container, livenessProbe, Infra container, static pod)

연금(Pension)술사 2022. 7. 15. 21:30

Pod(파드): 여러 컨테이너를 묶어 놓은 논리적인작업 단위이다. 파드 내에는 1개의 컨테이너가 존재할 수 있고, 여러 개의 컨테이너가 존재할 수도 있다. 1개만 존재하면 싱글 컨테이너 파드, 2개 이상 존재하면 멀티 컨테이너파드라고 부른다. 그럼 컨테이너는 무엇인가? 하나의 "어플리케이션"이라고 생각하면 된다. 파드에는 여러 개의 컨테이너가 존재할 수도 있기 때문에, 웹 서버 컨테이너, 로그 수집기, 볼륨(=디스크)을 묶어 하나의 파드로 구성할 수도 있다. 

 

파드 사용하는 방법

 


파드를 사용하는 방법은 CLI(커멘드 명령의 인터페이스)로 실행시키는 방법이 있고, 아래의 그림과 같이 YAML포맷의 파일을 이용해서 생성하는 방법이 있다.

  • metdata.name: 값에 해당하는 필드 및 필드명은 이 파드의 이름을 설정하기 위함이다. 
  • spec.containers: 이 필드아래에 container을 적으면 하나씩 컨테이너가 생긴다. 아래의 예시는 컨테이너 1개를 생성하는 예시이다. "-"(하이픈)은 하위필드를 배열로 묶겠다는 것이다. 즉, 파이썬의 딕셔너리로 생각하면 {"spec": {"container": [{"image":"nginx":"latest"}, {"name":"my-nginx"}, {"resources": {}}]와 같이 속성들을 containers의 하위 필드 배열로 취급하겠다는 것이다.
  • spec.containers.image: 컨테이너에 사용될 이미지명을 적는다, ":"앞은 어플리케이션 명,  뒤는 어플리케이션의 버전정보이다. 사용자가 다운로드 받지 않아도, 이 컨테이너를 실행하는 순간에 컨테이너 허브에서 이 이미지를 다운로드(PULL)하여 가져온다.

 

Pod 생명주기


파드 생명주기는 파드 생성부터 파드의 삭제, 실패까지의 과정 중에 각각 어느단계에 있는지에 대한 개념이다. 만일 파드가 러닝중인지? 생성중인지? 실패했는지에 관한 내용을 알 수 있다. 일단 파드가 한번 생성되면 유니크안 식별자(UID, Unique ID)을 갖는다. 그리고, 삭제되기 전까지는 유지된다. 파드 생명주기 중에 어느 상태인지는 status의 필드로 확인할 수 있다.

이름이 mywebserver인 STATUS가 실행중인 파드. 어플리케이션은 1개중에 1개가 구동중

각 단계별로 의미하는 것은 아래와 같다.

상태(STATUS) 의미
Pending 쿠버네티스 클러스에서 일단 실행이 가능한 상태라고 판명된 상태. 하지만, 컨테이너중에 몇개는 아직 구동을 준비중인 상태를 의미한다. (=즉, "만들어지는 중이에요" 라는 상태)
Running 파드 내에 모든 컨테이너가 구동중인 상태
Succeeded 파드 내 모든 컨테이너가 구동에 성공된 상태. 
Failed 파드 내 모든 컨테이너가 종료된 상태. 정상 실행이 아닌 (non-zero status) 컨테이너가 존재하는 상태. 
Unknown 모종의 이유로 컨테이너 상태를 얻을 수 없는 경우

 

LivenessProbe 사용하기: 자동으로 컨테이너가 죽는 경우, 재시작 하기


kubernetes의 livnessProbe의 동작 방식 그림 (참조: https://komodor.com/learn/kubernetes-liveness-probes-a-practical-guide/)

 

  • livenessProbe을 이용해서 컨테이너 sefl-healing 이용하기

livenessProbe가 동작하는 방식은 httpget probe, tcpSocket probe, exec probe라는 것을 이용해서 각각의 목적에 맞는 일부 기능들을 주기적으로 체크를 해서 컨테이너가 정상적으로 동작 중인지를 확인한다. 예를 들어, http 웹서버라면, httpget probe은 주기적으로, 지정한 IP, port, path에 대해서 HTTP get요청을 보내서 응답이 잘 못나오는 경우 컨테이너를 다시 시작한다. tcpSocket probe은 ssh로 접속해보고 접속이 몇 번 실패하면, 해당 컨테이너를 다시 시작하는 것이다. exec probe은 명렁을 전달하고 명령의 종료코드가 0 (정상종료)가 아니면, 컨테이너를 다시 시작하는 것이다. 중요한 것은 Pod을 재시작하는 것이 아니라 Container을 재시작하는 것이다. 따라서, Pod은 그대로 이기 때문에, IP을 동일하게 사용할 수 있다.

livenessProbe을 사용하기 위해서는 몇 가지 전달해야하는 인자가 있는데, 아래와 같다. 자세한 인자들(Configuration)은 여기에서도 확인이 가능하다. initialDelaySeoncds은 컨테이너 실행 후 몇초 뒤에 probe을 실행할 것인지에 대한 속성이다. periodSeoconds은 몇초 주기로 프로브를 실행할 것지에 대한 속성, timeoutSeconds은 프로브를 실행하고나서 몇초 내에 명령들이 종료되어야하는지에 대한 속성, seccessThreshold은 이 프로브가 몇 번 연속 성공하면 성공이라고 볼것인가에 대한 것이다. failureThreshold은 프로브가 몇 번 실패해야 실패로 볼 것인지에 대한 기각역이다.

아래의 예시는 buxybox라는 이미지를 이용해서 컨테이너 1개짜리를 담은 "liveness-exam"이라는 Pod을 생성한다. 추가로, 컨테이너를 실행할 때, 컨테이너 상의 '/tmp/healthy'라는 파일을 만들고 30초 후에 삭제하는 명렁도 같이 넣어놓았다. 즉, 컨테이너가 실행되고나서 30초 동안만 파일이 존재할 것이다.

exec livenessProbe을 사용하기위한 yaml 예시. 컨테이너 실행할 때 초기에 30초동안만 파일을 생성하고, 그 뒤에 지우는 커멘트가 들어가있다. 마찬가지로 이 파일을 cat으로 확인하는 커멘드도 포함되어 있다.

 

아래와 같이 exec livenessProbe가 실행을 몇 번해보다가, failthrehold만큼 실패하면 해당 컨테이너를 재시작한다.

위의 yaml파일에서 failureThrehold가 3번이 지정되었기 때문에, 3번 검사하고 (x3 over 11s) 실패했기 때문에, 해당 컨테이너를 삭제하고 재시작하는 것이다.

 

초기화 컨테이너(Init container): 초기화 컨테이너는 파드에 앱 컨테이너가 실행되기 전에, 실행되는 특수한 컨테이너이다. 이 초기화 컨테이너로, 앱 이미지(도커이미지)에 없는 설정, 유틸리티 스크립트를 실행하여 일부 변형을 할 수 있다.


초기화 컨테이너(init container)은 Pod내에서 어플리케이션이 동작하기전에 실행되는 특별한 컨테이너를 의미한다[ref]. 주로, 만들어진 이미지를 컨테이너로 실행하면서 추가사항을 실행하기 위한 목적으로 쓰인다. 예를 들어, 구동하고자하는 앱은 RestAPI + 딥러닝추론엔진이라면, 컨테이너에는 FastAPI이미지만 있고, 초기화 컨테이너가 실행될 때, 딥러닝추론엔진만 다운로드 받아오는 식이다. 이렇게하면 딥러닝 추론엔진이 사이즈가 엄청 크더라도(>10Gb) 한 이미지에 담을 필요없이, 이미지 + 추가실행사항 정도로 이해하면된다.

일반적인 컨테이너랑 거의 유사(리소스 제한, 볼륨 사용, 보안세팅)한데, 다른 몇 가지가 있다, 대표적으로 lievnessProbe을 지원하지 않는 것이다. 그리고 초기화 컨테이너도 일반 컨테이너(regular container)와 마찬가지로 다수의 초기 컨테이너를 사용할 수 있다. 초기 세팅이 2~3개 이상 필요한 경우, 초기화 컨테이너를 2~3개 이상 명시하는 것이다. 이 때, yaml파일에 적인 순서대로 동작한다. 먼저 선언된 초기화 컨테이너가 동작하지 않으면, 뒤의 초기화 컨테이너가 동작하지 않는다. 초기화 컨테이너의 실행이 실패하면, 쿠버네티스는 실행 될 때까지 초기화 컨테이너를 구동해본다. (=쿠버네티스의 "선언적 특징"). 그리고, 초기화 컨테이너가 다 실행되어야, 메인 컨테이너를 동작한다(=초기화 컨테이너가 실행이 안되면 메인컨테이너가 구동이 안됨)

 

아래는 일반 컨테이너를 1개 구성하고 초기 컨테이너를 2개 구성한 것이다.

# 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"]

 

실제 파드를 쿠버네티스에 띄워보면 아래와 같아. myapp-pod가 초기화되려고할 때, STATUS은 Init:0/2상태로 2개의 초기화 컨테이너가 파드내에서 동작해야함을 의미하고 있다.

 

그리고, kubectl desribe pod myapp-pod을 찍어보면, 첫 번째 초기화 컨테이너(init-myservice)가 실행중임을 알 수 있다.

kubectl desribe pod myapp-pod

# std out
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  13m   default-scheduler  Successfully assigned default/myapp-pod to docker-desktop
  Normal  Pulling    13m   kubelet            Pulling image "busybox:1.28"
  Normal  Pulled     13m   kubelet            Successfully pulled image "busybox:1.28" in 5.871629222s
  Normal  Created    13m   kubelet            Created container init-myservice
  Normal  Started    13m   kubelet            Started container init-myservice

 

조금 더 자세히, 첫 번째 컨테이너의 로그를 보면(kubectl logs myapp-pod -c init-myservice) 계속 실패중이다. 

$kubectl logs myapp-pod -c init-myservice | tail

# std out
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

nslookup: can't resolve 'myservice.default.svc.cluster.local'
waiting for myservice
nslookup: can't resolve 'myservice.default.svc.cluster.local'
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

waiting for myservice

위와 마찬가지로 두 번째 컨테이너의 로그를 보면, 첫 번째 컨테이너가 실패했기 때문에, 구동을 시도조차 하지 않았을 것이다.

 

 

Infra container 사용하기


싱글 컨테이너로 파드를 구동하면 일반 컨테이너(regular)가 1개 돌아가는데, 사실은 아무일도 하지 않는 Infra container (pause) 컨테이너가 숨어있다.파드 생성될 때, 1개씩 생성된다. 파드에 대한 인프라 IP, HOSTNAME 을 관리한다. 이 파드는 식별자(UID)가 1이다. 다른 컨테이너의 부모 컨테이너로 사용(다른 컨테이너를 생성할 때, 상속받듯이 사용)이 된다. 그래서, 파드 안에 새로운 컨테이너 생성되면, Infra contatiner을 참조하여 만드니까, IP, hostname이 동일하게 생성된다. 혹시나, 이 infra conatiner가 재시작이 된다면, 파드 내에 네트웍구성이(IP, hostname)가 동일해야되기때문에, 모든 컨테이너가 다시 재시작이 된다.

 

 

스태틱 파드(Static Pod)


각 워커노드에서는 kublect 이라는 데몬이 돌아이 돌아가는데, kubelet은 파드에서 컨테이너가 확실하게 돌아가게끔 관리해주는 역할을 한다. 여태까지는 컨테이너를 띄울 때, Kubectl을 통해서, 각 노드에게 명령을 전달했다. 반면, 스태틱 파드는 직접 데몬에서 실행하는 (노드에 직접 들어가서 파드를 정의하는 디렉토리(/etc/kubernetes/manifests, kubelete configuration file)에서 직접 설정하게끔 파일을 설정하면, 자동으로 kubelet이 이 파일을 참조해서 컨테이너를 만드는 데, 이것이 스태틱파드이다. 즉, API서버 없이 특정노드에 있는 kubelet에 의해 직접 실행되는 파드를 의미한다. 쉽게 말해, 수동으로 돌리는 파드이다.

# 특정 노드로 접속
$ ssh alchemist@node1

# kubelet 설정 파일을 확인
$ docker exec -it 0a5f48991df2 /bin/bash # minikube을 docker에서 띄운 경우
$ cat /var/lib/kubelet/config.yaml  | grep staticPodPath
staticPodPath: /etc/kubernetes/manifests

# PodPath에 지금 kubelet이 실행하고 있는 자원들을 확인
$ cd /etc/kubernetes/manifests
$ ls /etc/kubernetes/manifests
etcd.yaml  kube-apiserver.yaml  kube-controller-manager.yaml  kube-scheduler.yaml

# nginx을 예시로 실행하기 
$ vi nginx-pod
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: nginx-pod
  name: nginx-pod
spec:
  containers:
  - image: nginx:latest
    name: nginx-pod
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}

# control-plane으로 들어와서
$ kubectl get pods
NAME                 READY   STATUS    RESTARTS   AGE
nginx-pod-minikube   1/1     Running   0          14s

 

Pod 생성시 컨테이너에서의 자원 제한하기(requests, limits)


Pod을 생성할 때, 각 컨테이너가 어느 노드에 실행될지는 컨트롤플래인(control-plane)의 스케쥴러가 확인해준다고 했다. 사용자가 지정으로 "특정 컨테이너를 돌릴 때, 적어도 이정도 자원이 필요해요", 또는 "특정 컨테이너를 돌릴 때, 다른 컨테이너에 영향안받게 최대 여기까지만 자원을 할당해주세요" 와 같이 요구사항/제한을 둘 수 있다. 전자(특정 컨테이너를 돌릴 때, 적어도 이정도 자원이 필요해요)는 requests, 후자(특정 컨테이너를 돌릴 때, 다른 컨테이너에 영향안받게 최대 여기까지만 자원을 할당)은 limits을 의미한다. 혹여나 컨테이너가 limits의 자원을 넘어서 할당되면, 해당 파드가 죽게되고, 다시 스케쥴링이 된다. Requests은 container을 생성할 때, 어느 최소요구사항을 만족하는 node을 찾아달라는 요청이다. 신기하게도? limits을 넣으면 requests도 같은 크기로 함께 자동으로 갖게된다.

쿠버네티스에서는 메모리의 크기는 Mi단위로 표현하고, CPU은 숫자(코어수), 또는 1000mc(밀리코어, 1CPU)로 표현한다.

 

# ngnix-pod.yaml 작성
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
  - image: nginx:latest
    name: nginx-container
    ports:
    - containerPort: 80
      protocol: TCP
    resources:
      requests:
        memory: 500Mi
        cpu: 1
        
# Pod생성
kubectl create -f ngnix-pod.yaml
  
# 파드 확인
kubectl describe pod <파드명>
 Containers:
  nginx-container:
    Container ID:   docker://8cb4710e30e4ac033276b51361791de5ff9b39edfdc84ff6e315ab71f8af8d38
    Image:          nginx:latest
    Image ID:       docker-pullable://nginx@sha256:db345982a2f2a4257c6f699a499feb1d79451a1305e8022f16456ddc3ad6b94c
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Tue, 19 Jul 2022 14:20:39 +0000
    Ready:          True
    Restart Count:  0
    Requests:
      cpu:        1
      memory:     500Mi
    Environment:  <none>
    Mounts:

 

 

환경변수 설정하기


Kubernetes에서 컨테이너를 실행할 때, 컨테이너에  내에서 쓰이는 환경변수를 설정할 수 있다. 아래와 같이 컨테이너(Containers)이하에 env라는 속성명 이하에 담으면 된다.언제 쓰느냐? 도커 컨테이너에서 정의된 환경변수외에 추가로 필요하거나, 변경할 때 필요하다. 

apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
  - image: nginx:latest
    name: nginx-container
    ports:
    - containerPort: 80
      protocol: TCP
    env:  # 환경변수의 설정
    - name: env_name
      value: "test_env_name"

 

Pod 디자인 패턴


파드 디자인패턴은 파드 내에 컨테이너를 어떻게 구성해서 실행할 것인지에 대한 설계도를 의미한다.  파드가 2개 이상의 컨테이너로 이루어져있기 때문에, 여러 파드가 있거나 의존성이 어떤지에 따라서 구성이 각가 다를 수 있다. 크게는 1) 사이드카(Side car), 2) 엠베서더(Ambassador), 3) 어뎁터(Adaptor) 패턴이 있다. 자세한 내용은 여기서도 확인할 수 있다.

1) 사이드 카 패턴: 사이드카는 아래의 그림과 같이 오토바이에 옆에 타는 부분을 의미한다. 사이드 카라는 의미는 여기서 가져왔다. 사이드카 패턴은 이미 기능적으로 잘 실행 될 수 있는 컨테이너에 뭔가 기능을 덧댄(Add on) 이미지 패턴을 의미한다. 본디 실행하고자하는 컨테이너 자체가 잘 동작하는 경우를 의미한다. 주로 로그를 쌓거나, 모니터링하는 기능을 더해서 사용하기도한다. 

Side car

2) 엠베서더(Ambassodor): 파드 외부와 내부를 연결해주는 역할을하는 것이다. 엠베서더는 대사를 의미한다. 외교사절단 대표정도로 의미가되는데, 쿠버네티스에서는 외부 TCP-Proxy로 DB을 연결해주는 역할을할수 있다. 주로 로드밸런서 역할을

3) 어뎁터 패턴(Adaptor): 어뎁터패턴은 어뎁터 용도의 컨테이너가 있어서, 각 컨테이너에서 생성하는 정보들을 모아서 관리해주거나, 또는 실제 물리적 DB에 인터페이스해서(또는 외부 데이터를 가져오는 역할), 어플리케이션에 다시 전달해주거나 해주는 역할로 사용된다. 가령 A, B, C 어플리케이션(각 컨테이너)의 로그가 제각각으로 저장되면, 이를 전처리해서 모아주는 역할을 하는 컨테이너가 있으면, 이 컨테이너가 어뎁터가 된다.

반응형