쿠버네티스는 현재 대규모 워크로드를 운영하는 데에 있어서 빠지지 않는, 현대 인프라의 가장 핵심인 오픈소스 컨테이너 오케스트레이션 플랫폼입니다. 많은 분들이 쿠버네티스의 장점에 대해서는 알고 계시지만, 어떤 설계 원칙을 가지고 태어났는지는 잘 모르는 경우가 있습니다. (제 이야기입니다ㅎ)
이 4가지 디자인 철학에 대해 알고 나면, 쿠버네티스는 대체 "왜?" 그랬는지를 이해할 수 있는데, 예를 들면 이런 질문들입니다.
- 쿠버네티스는 왜, 스토리지 시스템에서 PV와 PVC를 나누어 관리할까?
- 쿠버네티스는 왜, 컨트롤 플레인과 노드를 분리시켰을까?
- 쿠버네티스 API는 왜, 명령형이기보다 선언형일까?
그 외에도 흔히 쿠버네티스의 특징이라고 알고 있는, 이식성이 있고 1. 확장가능한 플랫폼이면서 2. 선언적 구성과 자동화를 모두 용이하게 해 주고 3. 크고 빠르게 성장하는 생테계를 가지게 되었는지 이유를 알 수 있습니다. 그럼 들어가 볼까요?
이전 베어메탈, VM 시절에서 도커의 등장 이후 인프라 생태계에서는 Containerization이 핵심 키였습니다. 일관성, 반복성, 신뢰성 있는 시스템으로 복잡한 의존성의 문제에서 해방될 수 있었죠. 도커는 훌륭하지만 그 자체로는 운용될 수는 없었습니다. 배포는? 스케일링은? SSH에 직접 다 접속해서 docker 컨테이너를 실행할 것인가? 문제를 해결하기 위해서는 오케스트레이션 플랫폼이 요구되었죠.
직관적으로, 비즈니스 로직이 담긴 한 도커 컨테이너를 배포하고, 실행시키기 위해 사용한 방법은 다음과 같습니다.
기본적으로 master-slave 관계와 동일하죠. 명령형으로 어떤 노드 A에 x라는 컨테이너를 실행시키기라는 구조입니다.
하지만 노드 A가 크래시 나거나 의도하지 않게 죽는다면 어떻게 될까요? 혹은 일시적으로 blip 상태에 빠진다면? 컨테이너가 다양한 이유로 크래시나거나 죽는 경우도 배재할 수는 없을 것입니다.
그럼 엔지니어는 어떻게 해야 할까요? 매번 모든 컨테이너/노드마다 상태를 확인하고 모니터링하고 저장해야 할 것입니다. 뿐만 아니라 다운타임동안 실패한 상태들을 Catch up 해야 할 것입니다.
복잡하고, 커스텀 로직들이 필수로 들어가야 하겠죠. 여기서 쿠버네티스의 첫 번째 설계 원칙이 등장합니다.
Kubernetes APIs are declarative rather then imperative.
쿠버네티스 API는 명령형이기보다 선언적이어야 합니다.
Before:
- 엔지니어: 원하는 상태의 구조를 정확한 set으로 제공 (ex. 컨테이너 x를 노드 A에 배치)
- 시스템: 구조를 그대로 실행
- 엔지니어: 시스템을 지속적으로 모니터링하고 어긋난다면 추가적인 조치를 실행
After:
- 엔지니어: 원하는 상태를 정의
- 시스템: 해당 상태를 유지하도록 최선의 노력
물론 선언적 형식이라도 선언한 형식에 에러가 포함될 수 있으니 비상상황에는 직접 개입해 조치가 필요해지겠죠. 그래서 Declarative API를 비행기를 직접 몰고 계기판으로 상태를 확인하는 조종사와 autoPilot 컴퓨터를 두고 비상 알람이 울리면 그 상황에서만 직접 컨트롤하는 조종사로 비교하기도 합니다.
그래서 엔지니어는 API 오브젝트를 생성하면 kube API 서버는 삭제될 때까지 해당 상태를 유지하게 됩니다. 그리고 시스템의 모든 컴포넌트들은 해당 상태를 유지하기 위해 최선을 다하게 되죠. 이 1 원칙의 가장 중요한 이점이 무엇일까요?
바로 Automatic recovery입니다.
한 노드가 실패상태라도 시스템은 자동적으로 파드를 healthy 노드로 이동하게 되죠. 명령형 구성과 달리 매번 모든 컨테이너/노드마다 상태를 확인하고 모니터링하고 저장해야 하고 추가 조치를 취할 필요가 없어졌습니다. 시스템을 더욱 견고하게 만들었죠.
하지만 이렇게 단순히 파드를 노드에 배치한다고 끝난 건 아닙니다.
컨테이너의 상태를 주기적으로 확인해야 하고, 마스터 노드는 계속해서 요청을 주고받으면서 상태를 확인해야 합니다. 이 구조는 복잡하기도 하고, 노드가 하나하나 추가될수록 확장하기 어렵고 계속해서 요청량은 커지고 커지고 마스터 노드가 처리해야 하는 양은 더 커지고 더 커지는 문제가 있었죠. 이렇게 끝났다면 절대 쿠버네티스는 쓰이지 못했을 것입니다.
여기서 쿠버네티스의 두 번째 원칙이 등장합니다.
The kubernetes control plane is transparent. There are no hidden internal APIs.
쿠버네티스의 컨트롤 플레인은 투명해야 합니다. 숨겨진 internal API가 없어야 합니다.
Before:
- 마스터 노드: 노드에게 원하는 상태의 구조를 정확한 set으로 제공 (ex. 컨테이너 x를 노드 A에 배치)
- 노드: 구조를 그대로 실행
- 마스터 노드: 노드를 지속적으로 모니터링하고 어긋난다면 추가적인 조치를 실행
After:
- 마스터 노드: 원하는 노드의 상태를 정의
- 노드: 해당 상태를 유지하도록 최선의 노력
어딘가 익숙하지 않나요? 맞습니다. 앞서 첫 번째 원칙과 흡사합니다. 마스터 노드가 다른 컴포넌트들에게 명령을 내리는 것이 아니라, API server는 투명하게 모든 정보를 공개하고 다른 컴포넌트들이 이 정보를 watch 하는 형태의 구조가 되었고, 그렇게 각 컴포넌트들은 가져온 정보들을 토대로 그들이 해야 할 일들을 알아챕니다.
이전과 비교해서 달라진 점은 알겠는데, 그래서 이 원칙이 복잡한 구조와 커지는 요청량, 확장성의 한계를 어떻게 극복하는지 궁금한 분도 계실 것 같습니다.
성적 공개날 선생님과 학생의 관계로 비유해 볼까요? hidden internal API 구조를 비유하자면, 선생님이 30명의 학생들을 각 한 명씩 교무실로 불러 성적을 알려줍니다. 덤으로 식사당번 날짜는 언제 언제이고 청소구역은 화장실이라고 디테일하게 각자에게 할당된 개인적인 정보들을 알려주는 방식입니다.
transparent 구조를 비유하자면, 선생님이 반 게시판에 모두의 성적이 담긴 시트지와 식사당번이 적힌 달력과 각자의 청소구역을 담은 그림판을 게시하는 방식입니다. 모두가 투명하게 정보를 공유하되, 숨겨진 개인정보는 없습니다.
첫 번째 방식은 어떤가요? 먼저 선생님은 30명 각각의 학생을 일일이 불러, 개인화된 정보를 선생님이 직접 필터링해서 뽑아, 알려줘야 합니다. 더불어 30명의 학생들을 각각 부르는 사이 민수가 은정이를 때렸다는 신고가 들어오면 하던 일을 모두 멈춰야 하는데, 훈계를 끝내고 교무실로 돌아오면 어느 학생들까지 교무실에서 만났는지 아이들에게 물어물어 찾아봐야겠죠. 심지어 그날 식사당번을 못한다는 학생이 찾아오면 또 문제입니다. 다시 그전에 불렀던 학생을 하나하나 불러 바뀐 식사당번 날짜를 고지해 줘야겠죠.
두 번째 방식은 그에 비하면 간단합니다. 엑셀 시트를 뽑아, 게시판에 붙이기만 하면 됩니다. 식사당번을 못한다는 학생이 와도 엑셀을 변경하고, 시트만 다시 뽑으면 그만이죠. 담당 학생이 200명이 되어도, 2000명이 되어도 동일합니다. 확장성의 한계를 어떻게 극복하는지 이해가 가시나요?
다시 쿠버네티스로 돌아와서, 워크로드를 생성, 실행하는 시나리오를 정리하면 다음과 같습니다.
- kube api server로 pod 객체 생성 명령을 내립니다.
- kubernetes 스케줄러는 새 pod 개체에 대한 Kubernetes API 서버를 모니터링하여 용 가능한 리소스를 기반으로 알고리즘을 실행해 이 pod를 실행할 최적의 노드를 결정합니다.
- Pod가 스케줄링된 후(Pod에 가장 적합한 노드가 선택됨) 스케줄러는 이동하거나 선택한 노드에 Pod를 시작하라고 지시하지 않습니다. Kubernetes API는 선언적이고 내부 구성 요소는 동일한 API를 사용하기 때문이죠. 따라서 스케줄러는 pod 개체의 NodeName 필드를 업데이트하여 pod가 예약되었음을 나타냅니다.
- kubelet(노드에서 실행되는 Kubernetes 에이전트)은 Kubernetes API를 모니터링합니다(다른 Kubernetes 구성 요소와 마찬가지로). kubelet이 자신에 해당하는 NodeName 필드가 있는 pod를 보면 pod가 예약되었음을 알고 이를 수행해야 합니다.
- kubelet이 pod를 시작하면 pod 컨테이너 상태를 계속 모니터링하고 해당 pod 개체가 API 서버에 계속 존재하는 한 컨테이너를 계속 실행합니다.
그렇게 쿠버네티스는 더 나은 확장성과 "missing events"에 대한 이슈를 해결하게 됩니다. 이뿐만이 아닌데요.
또 다른 강점은 바로 add on 툴을 쉽게 구성할 수 있다는 점입니다. 쿠버네티스의 컴포넌트들이 같은 API를 사용함으로써 쉽게 내가 개발한 오브젝트로 컴포넌트를 대체할 수 있게 되었다는 것입니다. 이를 CRD(Custom Resource Definition)이라고 합니다. 더 많은 사람들이 기여하고 확장할 수 있는 생태계로 변모하는 것이죠. 쿠버네티스 API를 기반으로 확장할 수 있는 익스텐션이 되었습니다.
Kubernetes API는 또 워크로드에 활용될 여러 가지 많은 데이터를 포함하게 되는데,
- secrets : KubeAPI에 저장되는 민감한 정보 (e.g. passwords, certificates..)
- ConfigMap : KubeAPI에 저장되는 구성 정보 (e.g. applicaition startup parameters..)
- DownwardAPI : KubeAPI에 저장되는 pod 정보 (e.g. name, namespace, uid..)
그렇다면 애플리케이션이 어떻게 secret,config map 등 정보들을 가져올까요? 모든 정보는 API server에서 투명하게 공개되니, No hidden internal API 철학에 따라 어플리케이션이 직접 디렉트로 API 서버에서 정보를 가져오게 하면 되는 것 아닐까요?
여기서 3번째 원칙이 등장합니다.
Meet the user where they are.
사용자가 있는 곳에서 사용자를 만나야 합니다. (해석: 사용자를 만족시켜야 합니다.)
Before:
- 애플리케이션은 쿠버네티스 환경에 맞게 직접 로직,설정 등을 수정해야 합니다.
After:
- 어플리케이션은 단순히 config나 secret data를 파일이나 환경변수에서 불러오기만 하면 추가로 변경할 필요가 없어졌습니다.
사용자를 만족시켜야 한다는 단순한 원칙은 말 그대로, 쿠버네티스를 더 사용자가 사용하기 쉽고 편하게 만들기 위함입니다. 이전에선 쿠버네티스(마스터 노드)가 알 수 있도록 APP을 수정해야 했지만, config나 secret data를 파일이나 환경변수에서 불러오면 애플리케이션이 추가로 변경해야 할 필요가 없어졌습니다. 파드 스펙에서 특정한 경로를 입력하면 kubernetes가 자동으로 모든 설정을 주입시켜 줍니다.
이는 쿠버네티스가 성공적으로 생태계에 보급된 이유기도 합니다. 굳이 kubernetes 용도를 위해서 워크로드들을 수정할 필요가 없었기 때문이죠.
또 Kubernetes에서 stateful 워크로드를 실행할 땐 어떨까요? Kubernetes는 Kubernetes 워크로드와 함께 다양한 유형의 영구 스토리지 시스템을 사용할 수 있는 강력한 볼륨 플러그인 시스템을 제공합니다.
예를 들어 사용자는 Pod에 특정 path 경로로 Google Cloud Persistent Disk를 설치하도록 쉽게 요청할 수 있습니다.
이 pod가 생성되면 Kubernetes는 지정된 GCE PD를 pod가 예약된 노드에 마운트 하는 작업을 자동으로 담당합니다. 그런 다음 컨테이너는 GCE PD가 컨테이너 또는 pod에 pod 수명주기에 영향을 받지 않는 영구적인 데이터를 마운트 되는 경로에 쓸 수 있습니다.
하지만 이 접근 방식의 문제는 pod 정의(pod YAML)가 Google Cloud Persistent Disk를 직접 참조한다는 것입니다. 볼륨을 직접 참조한다는 것은 PV를 직접적으로 Pod의 컨테이너나 볼륨 마운트에 연결하는 것을 의미합니다. 이렇게 하면 Pod와 PV가 강하게 결합되어, 다른 PV를 사용하거나 스토리지 자원을 동적으로 관리하는 유연성이 제한됩니다. 뿐만 아니라 이 pod가 Google Cloud Kubernetes 클러스터가 아닌 클러스터에 배포된 경우 시작되지 않습니다(GCE PD를 사용할 수 없기 때문).
여기에서 마지막 Kubernetes 원칙이 적용됩니다.
Workload Portability
워크로드의 이식성.
정의된 워크로드는 다양한 클러스터 간에 이식 가능해야 합니다. 사용자는 동일한 워크로드 정의 파일(예: 동일한 pod yaml )을 사용하면 여러 클러스터에 워크로드를 배포할 수 있어야 합니다. 즉, 클라우드나 클러스터에 비종속적이어야 한다는 것입니다.
따라서 원칙상으로, 위의 예제에서 지정된 pod는 GCE PD가 없는 클러스터에서도 실행되어야 합니다. 이를 달성하기 위해 Kubernetes는 PersistentVolumeClaim(PVC) 및 PersistentVolume(PV) API 개체를 도입했습니다. 이러한 개체는 스토리지 사용에서 스토리지 구현을 분리합니다.
PersistentVolumeClaim 객체는 사용자의 구현 독립적인데, 스토리지를 요청하는 방법으로 사용됩니다. 예를 들어 특정 GCE PD를 요청하는 대신 PVC 객체를 생성하여 100GB의 ReadWrite 스토리지를 요청하는 방식입니다.
Kubernetes 시스템은 이 요청을 PersistentVolume 객체를 포함하는 사용 가능한 디스크 풀의 볼륨과 일치시키거나 새 볼륨을 생성하도록 요청합니다. 어느 쪽이든 Kubernetes 클러스터에서 워크로드를 배포하는 데 사용되는 객체는 클러스터 간에 이식 가능하게 됩니다.
이 워크로드 이식성 원칙은 Kubernetes의 핵심 이점과도 연결되어 있습니다. OS가 애플리케이션 개발자를 기본 하드웨어의 세부 사항에 대해 신경 쓰지 않게 하는 것처럼 Kubernetes는 분산 시스템 애플리케이션 개발자를 기본 클러스터의 각종 제약에서 신경쓰지 않게 합니다. Kubernetes를 사용하면 분산 시스템 애플리케이션 개발자가 특정 클러스터 환경에 얽매이지 않습니다. Kubernetes에 대해 배포된 애플리케이션은 애플리케이션 또는 배포 스크립트에 대한 환경 별 변경 없이 온프레미스 및 클라우드 환경의 다양한 클러스터에 쉽게 배포할 수 있게 되죠.
특이 이 원칙은 시대의 배경을 알고 나면 더 흥미로운데요. 2013년, DevOps 운동의 시작점에 서 있던 실리콘밸리에서 Amazon의 급부상으로 클라우드가 대다수 기업의 주류 기술이 될 것임이 확실시되었습니다.
시장의 변화를 느낀 Google은 내부 인프라 전문 지식을 클라우드에 적용할 방법을 찾고 있었습니다. 해당 기술에 대한 전문 지식을 활용한다면 클라우드 공간으로의 진입이 가속화될 것으로 판단했을 것입니다. 쉽게 말해서 Google은 클라우드 분야에서 AWS를 따라잡아야 했기 때문입니다.
그 과정에서 쿠버네티스가 등장한 배경, 쿠버네티스를 오픈소스로 배포하게 된 배경이 굉장히 흥미로운데요. 역사를 설명하기엔 글이 너무 길어질 것 같아 이 내용을 담은 다큐멘터리를 추천드립니다.
https://www.youtube.com/watch?v=BE77h7dmoQU
여기까지 쿠버네티스의 4가지 설계 원칙들이었습니다. 각 원칙이 생겨난 배경을 알고 나니 조금 더 쿠버네티스와 가까워진 것 같은 느낌도 듭니다. 이 내용은 모두 2020 kubecon, Kubernetes Design Principles: Understand the Why라는 제목으로 Google의 엔지니어인 Saad Ali의 발표를 요약하고 정리한 것입니다. 이 내용에 관심이 생기신 분들은 꼭 원본 발표도 챙겨보시길 권해드립니다. 감사합니다.
출처
https://www.youtube.com/watch?v=ZuIQurh_kDk
https://www.alibabacloud.com/ko/knowledge/developer/four-basic-principles-of-kubernetes
https://kubernetes.io/ko/docs/concepts/overview/kubernetes-api/
'🏋️♀️ DevOps, SRE' 카테고리의 다른 글
짧은 생각 모음집 - crossplane, eBPF 편 (2) | 2023.07.06 |
---|---|
Kaniko : 도커 없이 kubernetes에서 이미지 빌드하기 (0) | 2023.06.08 |
신입 데브옵스 (DevOps) 엔지니어 되기 - (2) Drone CI 마이그레이션 하기 (2) | 2023.04.16 |
[k8s] 당신의 pod에서 nslookup이 실패하는 이유 (0) | 2023.02.18 |
신입 데브옵스 (DevOps) 엔지니어 되기 - (1) (25) | 2023.02.06 |