본문 바로가기

프로그래밍/kubernetes

CKAD 관련 잡다한 정리 (1)

CKAD는 CKA와는 겹치는 범위가 한 70% 정도 되기 때문에 CKAD 관련 잡다한 정리 시리즈 글은 많지는 않을듯 하다. 다만 CKA 잡다한 정리에서 놓친것들은 좀 챙길려고 한다..

 

Docker Image와 Kubernetes Pod의 Command와 Argument의 관계 정리

 

Docker Image를 만들때 CMD 를 설정했으면 Kubernetes에서의 command는 Docker의 CMD를 덮어 씌운다.

 

Docker Image를 만들때 CMD ["sleep", "5"] 로 했을 경우 Kubernetes에서 Pod을 만들때 Docker의 이 이미지를 사용하면서 command: ["sleep", "10"] 으로 했으면 sleep 10을 실행한다.

그러나 위와 동일한 결과를 낸다 하더라도 이에 대한 구현방법을 ENTYPOINT를 사용했을 경우는 다르게 한다.

만약 Docker에서 이미지를 만들때

 

ENTRTPOINT ["sleep"]
CMD ["5"]

이렇게 주었을 경우 kubernetes의 command는 ENTRYPOINT와 매핑되기 때문에 sleep의 시간만 변경하고 싶을려면 kubernetes에서 command를 써야 하는게 아니라 args를 이용해서 줘야 한다. 즉 args: ["10"]으로 설정하면 sleep 10이 실행되는 것이다. Dokcer에서 ENTRYPOINT를 이미지에서 사용하고 있는 상황에서 kubernetes의 command를 사용해버리면 ENTRYPOINT 값을 kubernetes의 command 항목 값이 덮어 씌워버리기 때문에 전혀 엉뚱한 명령을 실행하게 될 수도 있다.

정리하자면 Docker 이미지에서 ENTRYPOINT가 없는 상태에선 kubernetes의 command를 통해서 명령을 실행하도록 kubernetes command 를 작성하면 되지만 ENTRYPOINT가 있는 상태일 경우 Docker의 ENTRYPOINT = kubernetes의 command 항목과 매핑되는 관계가 성립되고 Docker의 CMD = kubernetes의 args 항목과 매핑되는 관계가 성립되기 때문에 이러한 관계를 고려해서 사용하고자 하는 docker의 이미지에 따른 kubernetes의 command 항목과 args 항목값을 적절하게 설정해야 한다.

오해의 소지가 있을것 같아서 한마디 덧붙이면 매핑관계라는건 반드시 값까지 매핑하라는 의미는 아니다. 이렇게 써놓으면 마치 Docker의 ENTRYPOINT 값을 kubernetes의 command 항목 값과 똑같이 맞추라는 의미로 비춰질까바 덧붙여서 쓰도록 하겠다

예를 들어 Docker 이미지에서는 ENTRYPOINT를 sleep으로 설정해서 만들었지만..kubernetes에서는 이를 실행할떼 sleep 명령어가 아닌 좀더 개선된 기능의 updatesleep 을 사용할 수도 있다. 이럴때는 kubernetes의 command 항목에 ["updatesleep"] 이라고 값을 주면 Docker container에서는 sleep이 아닌 updatesleep을 실행하게 된다. 이런것는 구버전 명령어와 신버전명령어를 같이 넣어 만든 이미지에서 아무것도 지정안하면 구버전 명령어로 실행하고 신버전 명령어를 실행할땐 별도로 명렁어를 지정해서 신버전 명령어를 실행하고자 할 때 이렇게 사용하게 되는 것이다. 


readinessProbe와 livenessProbe는 kubernetes 관리자 입장이라기보단 개발자 입장에서 다뤄줘야 할 속성이어서 만약 개발자라면 이 두 속성이 가지고 있는 의미를 잘 알아둘 필요가 있다.

 

예를 들어 Spring Boot로 만든 게시판을 Kubernetes에서 운영한다고 가정해보자. 이 게시판이 최종적으로 사용자에게 자신의 기능이 서비스되어질려면 최소한의 준비시간이 필요하다. 예를 들어 게시판에 필요한 Spring Bean 객체들을 생성하는 시간이나 또는 게시판 데이터를 저장할 DB와의 연동 및 Connection Pool을 구성하기 위한 지정된 갯수만큼의 Connection을 생성한다든가 하는 그런 일련의 시간이 필요하다. 이러한 작업들이 완료되어야 비로서 게시판 서비스를 할 수 있게 되는 것이다. 만약 이러한 작업이 아직 마무리가 되지 않은 상태에서 외부에서의 접속이 이루어진다면 필요 이상의 대기 시간을 갖거나 오류 화면을 보게 될 것이다. 이러한 상황을 막기 위해 readinessProbe를 이용해서 현재 Pod이 서비스 할 준비가 되었는지 체크할 필요가 있는 것이다. 

 

이해를 돕기 위해 좀더 구체적인 예를 들도록 하겠다. Deployment를 이용해 4개의 게시판 Pod을 운영한다고 가정해보자. 이때 필요성에 의해 2개의 게시판 Pod을 추가해서 총 6개의 게시판 Pod을 운영해야 하는 상황이 왔다고 하자. 일단 게시판 Deployment의 replicas 속성을 6으로 수정하게 될 것이다(kubectl scale 명령으로 하든 Deployment 생성하는 yaml 파일에서 replicas 속성의 값을 6으로 설정해서 적용하든 방법에 있어서는 중요하지 않다) 그러면 2개의 신규 게시판 Pod이 생성되는 동안 게시판 Deployment와 연결되어 있는 kubernetes Service Resource 객체를 통해서 외부해서 접근하게 되는 상황이 온다. Service 입장에서는 load balancing 역할도 같이 하고 있기 때문에 기존에 운영중인 4개 뿐만 아니라 지금 만들고 있는 중인 2개의 Pod에게도 접근하게 하려 할 것이다. 만약 지금 신규 생성중인 2개의 Pod중 1개로 분기시키게 되면 해당 Pod은 아직 준비중이기 때문에 사용자는 필요 이상의 대기 시간을 갖거나 오류 화면을 볼 수 있게 된다. 만약 여기에 readinessProbe를 설정해서 현재 생성중인 Pod이 서비스 가능한 Pod인지 판단할 수 있게 해준다면 Deployment와 연결된 Service는 readinessProbe를 통해서 아직 초기화작업이 진행중인 Pod으로는 분기시켜주지 않고 기존의 운영중인 4개의 Pod 중 1개로 분기시켜주게 된다. 이러한 이유로 readinessProbe 설정이 필요하게 된다.

 

또한 Pod이 운영되는 상황에서도 현재 Pod이 정상적으로 서비스가 가능한지 주기적으로 체크할 필요도 있다. 만약 어떤 이유로 인해 현재 서비스중인 Pod이 정상적인 서비스 제공을 할 수 없게 되면 사용자 입장에서는 특정시간 이상을 대기하게 되거나 오류 화면을 볼 수 있기 때문이다. 그래서 주기적으로 이러한 상황을 체크해서 Pod에 문제가 있게 되면 해당 Container를 재시작하게끔 해줘야 하기 때문이다. 바로 이러한 운영중의 체크를 livenessProbe가 하게 된다.


Job은 Pod과 비슷하지만 그 용도는 다르다. Pod이 항상 실행중인 상태로 유지하는것에 비해 Job은 정해진 목적을 완료하면 작업이 종료되는 Resource 이기 때문이다(여기서 종료..라는 단어땜에 혼선이 있을수 있는데 Pod은 항상 실행중인 상태이기 때문에 Status 관점에서 보게 되면 Ready 이지만 Job은 자신의 목적을 이루면 종료되기 때문에 Status 관점에서 보면 Complete 상태가 된다) 그러나 Job이 종료되었다고 해서 Kubernetes Resource 에서 삭제되는 것은 아니다. Complete 상태로 계속 유지되며 사용자가 삭제하기 전까지는 삭제되지 않는다.

 

Job은 Batch 작업이나 Report 작업같은 일회성 작업을 해야 할때 만든다. Pod으로 하지 않는 이유는 Pod으로 하게 될 경우 해당 작업을 Pod의 Container에서 실행하게 되면 Container는 해당 작업을 마친뒤 Container가 종료된다. 그러나 Kubernetes의 Pod은 Container가 항상 실행중이어야 하기 때문에 종료된 Container를 다시 만들면서 Restart를 하게 되며 Restart 된 Pod의 Container는 다시 해당 작업을 실행하게 되기 때문에 무한반복의 현상이 나타나게 된다(이렇게 하지 않게끔 spec.restartPolicy를 Never나 onFailure로 설정할수도 있다. 그러나 이러면 무슨 문제가 있냐면 container가 작업을 완료하고 종료한건지 아니면 내부 문제로 인해 종료한건지를 알 방법이 없다. 작업을 완료하고 종료한거면 다시 실행하지 않아도 되지만 내부 문제로 인해 종료되었다면 다시 실행되어야 하기 때문이다)

 

Job에 대한 공부를 하면서 Job의 용도를 Batch 작업할때 사용한다는 식의 설명을 했는데 그게 얼핏 납득하기가 어려웠다. 예를 들어 Kubernetes Cluster 에서 DB 작업을 하더라도 그 Batch 작업을 굳이 Pod 형태의 Job(Job의 내부 구성도 Pod과 비슷한 형태이기에 이런 표현을 썼다)으로 만들어서 해야하나..란 생각이 들었기 때문이다. Job으로 할려면 Batch 작업을 하는 Docker 이미지를 별도로 만든뒤 이를 이용해 Job을 만들어야 하는데..흔히들 shell script 파일 1개 실행하는 형태로 만드는 batch 작업 땜에 그 shell script 파일을 담은 이미지를 만들어가면서까지 해야 하나..란 생각이 들었기 때문이다. 근데 고쳐서 생각해보니 납득할만한 상황도 있었다. 예를 들어 kubernetes cluster의 외부 접근을 Web Service를 제외한 나머지 접근을 막았다고 가정할 경우 외부에서 kubernetes cluster 안에 동작중인 DB Pod을 접근할 방법이 없다. 물론 몇가지 설정을 통해 예외를 만들수도 있지만 예외라는건 바꿔말하면 개구멍 같은 존재이기 때문에 보안관점에서는 문제의 소지가 있다. 이런 상황에서 Batch 작업을 할려면 결국 Batch 작업을 하는 Shell Script 파일을 넣은 이미지를 만든뒤 이를 사용하는 Job Resource를 등록해서 하는 방법외엔 없게 된다. 

 

위에서 Job을 설명할때 일회성 작업을 할때 사용한다고 얘기했지만 이러한 일회성 작업을 반복적으로 실행해야 할 수도 있다. 이 반복적인 작업을 어떻게 구성하느냐에 따라 이것을 그냥 Job으로 가야할지 아니면 CronJob으로 가야 할지가 결정이 된다. 특정 스케줄을 걸지 않고 그냥 작업 자체를 반복적으로 하고자 할때는 Job으로 하면 되지만 이것을 정해진 스케줄(예를 들어 1분에 한번)에 따라 반복적으로 실행해야 한다면 CronJob으로 만들어서 CronTab 문법을 이용해 스케쥴을 정의하면 된다.

 

CronJob은 조금 이따가 언급하기로 하고 Job에서 반복적으로 작업을 하고자 할때는 2가지의 속성을 별도로 더 정의해줘야 한다. spec.completions 항목과 spec.parallelism 항목을 정의해야 한다. 이 두 항목은 정의하지 않았을 경우 모두 1로 작동하도록 되어 있다. spec.completions 항목은 완료된 작업을 총 몇번을 해야 하는지를 설정한다. 여기서 완료된 작업..이란 단어를 주의깊게 볼 필요가 있다. 예를 들어 spec.completions 항목을 10으로 설정해서 10번 실행하도록 했다고 가정하자. 근데 이 중 7번은 성공하고 3번은 실행하는 프로그램의 내부 버그 또는 kubernetes 자체 문제 등의 다른 요인으로 인해 실패했다고 가정하자. 그러면 전체적으로 10번을 실행했다 하더라도 성공한 횟수가 7번이기 때문에 성공한 횟수가 10번이 되기 위해 적어도 3번 이상(3번 이상이 되는 이유는 3번을 실행한다해도 그게 모두 성공한다는 보장이 없기 때문이다)을 실행하게 된다. spec.parallelism 항목은 병렬로 실행하게 되는 잡의 갯수이다. 프로그래머 입장에서 이해하기 쉬운 표현을 하자면 여러개의 스레드로 만들어서 빠르게 처리하는 것으로 이해하면 쉬울듯 하다. 이 둘의 조합을 어떻게 하느냐에 따라 작업의 방향이 반복적 또는 병렬로 동시에 작업이 실행되게 되는 것이다.

 

Kubernetes에서는 이러한 Job의 종류를 크게 3가지로 본다

  • 비-병렬(Non-Parallel) Job
  • 고정적(fixed)인 완료 횟수를 가진 병렬 Job
  • 작업 큐(queue)가 있는 병렬 Job

비-병렬 Job은 Job에 설정해놓은 Pod가 실패하지 않는다면 Job은 한번만 실행되며 Pod에서 실행한 작업이 성공한뒤 Pod이 종료되면 Job도 같이 종료된다. 이러한 성격이기 때문에 위에서 언급했던 spec.completions 항목과 spec.parallelism 항목을 모두 설정하지 않는다(설정하지 않을 경우 위에서 언급했듯이 두 항목 모두 1로 설정되기 때문에 비-병렬 Job 성격에 맞게 되는 것이다)

 

고정적인 완료 횟수를 가진 병렬 Job은 spec.completions 항목에 1 이상의 값을 설정하여 1에서 spec.completions 항목에 정해진 숫자만큼 반복적으로 Pod을 만들면서 작업을 실행하여 각 실행 결과가 모두 성공적으로 완료되어야 Job이 종료된다(상공적으로 완료..라는 말에 주목해야 한다. 위에서 언급했듯이 실패하면 다시 Pod을 만들어서 실행하게 된다)

spec.parallelism 항목을 설정할 수 있으며 설정하지 않으면 위에서 언급했듯 기본값인 1이 설정된다. 이 잡에서 spec.completions 항목과 spec.parallelism 항목의 관계는 이렇게 보면 된다. 예를 들어 10번을 작업완료해야 하는데 작업 속도를 올리기 위해 한번에 실행되는 Pod을 2개로 하고 싶을수 있다. 만약 실패만 하지 않으면 Pod이 동시에 2개씩 실행되기 때문에 Pod을 2개씩 실행하는 것을 5번 실행하여 작업 완료 시간을 단축시킬수 있다. 그러나 만약 spec.parallelism을 생략하거나 1로 설정하게 되면 동시에 실행되는 Pod이 1개이기 때문에 결국 10번의 작업이 순차적으로 각각 1번씩만 실행하게 된다

 

작업 큐(queue)가 있는 병렬 Job은 spec.completions 항목을 정하지 않고 spec.parallelism 만 설정한다. 그도 그럴것이 이것은 내가 Pod을 몇개를 반복적으로 실행하여 최종 작업을 완료하는 개념이 아니라 작업의 대상을 가지고 있는 자료구조에서 내가 일정갯수를 가지고 와서 이를 처리하는 방식이기 때문이다. 그래서 이 작업에 모두 참여한 Pod이 성공해야 작업이 완료되는 것이기 때문에 작업의 반복횟수는 아무 의미가 없게 된다. 프로그래밍 관점에서 보면 메시지큐 같은 자료구조에서 데이터를 읽어와 처리하는 Job을 만들고자 할 때 이러한 병렬 Job으로 설계하면 된다.

 

Job의 실패처리와 관련되어서는 spec.backoffLimit와 spec.activeDeadlineSeconds 이 2개의 속성을 이용해서 처리하게 된다. 위에서 예를 들었던 10번 작업해서 7번 성공했고 3번 실패한 경우를 예로 들겠다. 이 둘에 대한 별도의 설정을 하지 않으면 Job은 내부 오류로 인해 작업이 실패하게 되면 Pod을 다시 재생성해서 작업을 처리하려 하게 된다. 그러나 이러한 과정을 마냥 하게 둘 수도 없는 노릇이다. 내부적인 코드 버그로 인해 무한루프에 빠져 계속 Pod 생성하고 작업시작하고 실패하고 또 Pod 생성하고 작업시작하고 실패하고..이러한 반복이 벌어질수도 있기 때문이다. 이런 상황을 막기 위해 위의 2가지 속성을 지정하게 되는 것이다. 만약 spec.backoffLimit 속성을 2로 주게 되면 2번 실패한 상황에서까지는 Pod을 다시 생성해서 Job을 다시 진행하려 하겠지만 3번 실패하게 되면 더는 이러한 작업을 하지 않고 Job을 종료한다. 또 버그로 인해 내부적으로 쓸데없는 대기시간이 생겨서 오래 기다리게 되는 경우를 방지하기 위해 spec.activeDeadlineSeconds 속성에 전체적인 Job의 실행시간을 설정하게 두어서 만약 지정된 시간 이상으로 Job이 실행하게 되면 Job을 종료하게 된다. 이 둘에 있어서는 둘 중 어느 하나를 먼저 우선순위를 두어야 하는데  spec.backoffLimit에 설정한 수 만큼 재시도를 하지 않았다 하더라도 spec.activeDeadlineSeconds 속성에 지정한 시간을 초과하게 되면 Job을 종료하게 된다.

 

Job 의 yaml 구조는 Deployment와 거의 같다. 차이가 나는 부분을 설명한다면

  • apiVersion 항목이 batch/v1 이다
  • kind 항목이 Job이다(Job이니까 당연한거..)
  • spec 항목의 하위 항목인 template 항목과 같은 레벨의 항목으로 위에서 언급했던 속성들(completions, parallelism, backoffLimit, activeDeadlineSeconds)이 있다