개발/구름

[쿠버네티스 인 액션] 01. 쿠버네티스 소개

소년택이 2021. 6. 28. 04:06
글 쓰기에 앞서...

이 스터디 시리즈는 불과 넉 달 전까지 도커조차 쓸 줄 몰랐던 글쓴이가 엉겁결에 쿠버네티스 인 액션 스터디에 참가하면서 자신이 공부한 것을 정리한 내용이다. 따라서 책 내용뿐만 아니라 자신이 이해한 나름대로의 해석을 같이 포함하고 있다. 전체적인 맥락은 책과 통하지만, 부분 부분 책이 포함하고 있지 않은 내용도 담고 있다. 아울러 글 전개 순서는 책의 순서가 아닌, 글쓴이가 참가한 스터디의 진행 순서를 따름을 원칙으로 한다.

출처 명시가 되어있지 않은 것은 (이미 공개 중인) 쿠버네티스 인 액션 영문판과 직접 제작한 자원임을 밝힌다.

본 챕터에서는 1. 쿠버네티스를 소개하고 왜 쿠버네티스 같은 도구가 필요하게 되었는지 2. 컨테이너 기술이란 무엇이며 가상화와 어떻게 다른지 3. 어떻게 컨테이너 > 도커 > 쿠버네티스로 기술이 이어지는지 4. 쿠버네티스는 대체로 어떻게 동작하는지를 알아본다.

쿠버네티스의 도래

지금처럼 컨테이너 기술이 대세가 대기 이전의 엔지니어들이 먹고살기 위한 최중요 관심사는 어떻게 시스템을 통합하여 하나에 다 쑤셔 넣을지, 이렇게 다 쑤셔 넣은 SW를 돌리는 슈퍼컴퓨터를 어떻게 만들지였다. 글쓴이가 2008년에 입사하여 만난 첫 프로젝트는 어느 글로벌 기업의 모든 데이터를 담고 처리할 수 있는 DB와 SW를 12대의 슈퍼컴퓨터에 욱여넣는 것이었다. 당시는 아직 (전 세계 적으로 가장 신뢰받는 ERP 솔루션인) SAP R3가 DB, AP, GUI Client로 구성한 3-Tier 시스템을 신기술이라며 자랑으로 삼던 시절이었다. 하지만 트렌드에 훨씬 민감한 서비스는 이미 기존의 SW 개발 컨셉과는 다른 길을 가고 있었다.

모놀리스 에플리케이션
모놀리스 애플리케이션은 어플리케이션 하나에 모든 것을 통합하여 구현한 SW를 뜻한다. 프로세스 입장에서는 하나의 프로세스에서 모든 기능이 돌아간다고 볼 수 있다. 구현하는 방법에 따라 멀티 프로세스이거나, 여러 개의 데몬으로 돌아갈 수 도 있겠지만 결국 주문, 입출고, 채무채권 발행, 자금 회수나 대금 지급 등의 기능이 한 프로세스 안에 다 구현되어 있는 게 모놀리스 애플리케이션이다. 모놀리스 애플리케이션은 단일 프로세스를 어떻게 빨리 돌리냐가 관건이므로 수평 확장(scale out)보다는 수직확장(scale up)이 더 중요하고, 수평 확장을 한다고 해도 상당히 제한적이었다. 앞서 소개한 SAP의 경우 코드 한 줄 고쳤다고 전체 시스템을 리빌드 할 수는 없으니, 기능별로 부분 부분을 빌드하고 필요시 동적으로 다시 로딩하는 방법을 썼다. 이러다 보니 배포하는 도중에 기존 작업 중인 사용자들이 우르르 런타임 에러 화면을 보는 일도 다반사였다.

모놀리스 애플리케이션 예시

 

마이크로서비스
이러다 보니 연속적인 서비스 제공을 위해서 "하나가 죽으면 다 죽는 시스템" 대신 서비스 별로 독립적인 프로세스를 수행하고, 프로세스 간 에는 잘 정의한 API로 통신하는 서비스 컨셉을 도입한다. 큰 틀에서 볼 때 세분화 한 서비스 중 하나가 죽더라도 다른 서비스는 이상 없이 수행이 가능하며, 특정 서비스를 업데이트하여도 API 사양에 변경이 없다면 다른 서비스는 신경을 쓸 필요가 없다. 프로세스와 프로세스는 물리적으로 같은 서버에 있든 다른 서버에 있든 상관없이 항상 독립적으로 존재할 수 있어야 하며, 이를 구현하기 위해 프로세스끼리는 REST API, AMQP, gRPC 등의 기술을 이용하여 통신을 한다.

마이크로서비스 예시

 

환경의 변화와 의존성 문제
그런데 여기도 문제가 생겼다. 개별 서비스를 개발하는 개발자들의 필요에 따라 다양한 라이브러리를 사용하다 보니, 자신의 서비스를 개발할 때는 문제가 안되지만 이를 한꺼번에 서버에서 돌리는 데에는 문제가 생긴다. 다음 예제를 살펴보자.

꼬여버린 의존성

App 1과 App 2는 동일한 Library A를 필요로 하지만 버전은 상이하다. 시스템을 관리하는 입장에서는 모든 의존성을 버전별로 관리하는 것도 어렵고 개발자 입장에서도 자신이 배포한 서비스가 다른 버전의 라이브러리를 참조하여 문제를 일으키는 경우 디버깅 하기도 매우 어렵다. 실제 운영 환경과 개발 환경이 상이하기 때문이다. 의존성 문제가 발생하지 않더라도, 시스템 담당자가 업데이트를 하여 라이브러리의 버전이 바뀌었고 이로 인해 문제가 생길 가능성도 존재한다. 지금의 환경이 내년에도 동일할 거라는 보장은 없다. 미래의 어느 시점에 급히 scale out이 필요하여 현재의 어플리케이션을 deploy 했을 때 지금과 똑같이 동작할 것이라고 아무도 보장할 수 없다.

이러한 이유로 어플리케이션과 인프라를 분리하는 요구사항이 대두하였고, 이 요구사항은 namespace와 cgroups, 컨테이너, 도커를 거쳐 쿠버네티스의 도래로 이어진다. 쿠버네티스는 이 문제들에 대한 답을 준다. 개발자는 인프라를 신경 쓸 필요 없이 배포를 할 수 있고, 시스템 담당자는 개발자를 신경 쓸 필요 없이 인프라를 관리할 수 있다. 심지어 많은 부분은 쿠버네티스가 알아서 해결해준다.

 

컨테이너와 가상화

컨테이너
리눅스는 이미 1970년 말부터 namespace;NS를 이용한 격리;isolation 기술을 도입하였다. 최초의 격리 대상은 파일 시스템이었다. 대표적인 예로 (권한 설정에 따라) FTP로 접속하면 사용자의 home 경로를 root로 mount 해버려 더 이상 위로 갈 수 없게 만들어버리는 것을 들 수 있다. 7가지의 NS (Mount, IPC, NW, PID, Time, User, UTS)를 격리하고 cgroup을 이용해 자원(CPU, RAM, 네트워크 대역폭 등)을 격리하는 기술을 컨테이너 기술이라 부른다. 컨테이너는 자신에게 주어진 공간 및 자원 안에서는 배타적 독점권을 행사한다. 컨테이너와 컨테이너는 적어도 각자의 격리 NS 안에서는 다른 컨테이너에게 영향을 주지도, 영향을 받지도 않는다.

PID 격리 예제

위 예시에서 46번 프로세스와 107번 프로세스는 각각 자신의 NS에서 1번 프로세스이며, RED와 BLUE는 완전 별도의 공간으로 격리되어 서로의 프로세스 정보를 알 수가 없다.

참고
PID NS를 격리하더라도 부모 NS는 자식 NS의 정보를 알고 있다. 따라서 RED의 1번이 부모 입장에서는 46번 프로세스인 것처럼 4번, 9번 프로세스도 부모 기준으로 PID를 채번 하고 있다. 

 

가상화
가상화는 하나의 물리 머신 위에서 수행하는 한 개 또는 복수개의 논리 머신이 단지 하드웨어를 공유할 뿐 완전히 별도의 시스템으로 동작하는 기술이다. 가상화를 통해 생성한 논리 머신을 Virtual Machine;VM 또는 가상 머신이라고 부른다. 가상 머신은 그 자체로 하나의 온전한 Physical Machine;PM과 동일하게 구성을 가진다 - 가상화 한 HW(CPU, RAM, DISK)부터 OS 그리고 실행하는 각종 어플리케이션 까지. 

HOST OS가 없는 가상 머신의 예

참고
가상화는 우선 HOST OS의 유무로 구분할 수 있다. 또 HOST OS가 없는 경우는 하이퍼바이저와 GUEST OS의 의존성에 따라 구분할 수 있다. 어떤 방식이든 VM에 GUEST OS가 존재한다는 건 동일하다.

 

컨테이너와 가상화의 비교
서로를 서로로부터 격리한다는 점에서 컨테이너와 가상화는 비슷한 개념이다. 또 컨테이너와 가상화 모두 공유 디렉터리와 같은 자원을 두고 동시에 접근할 수 있다. 둘의 결정적인 차이는 컨널의 개수(=GUEST OS 존재 여부)이다. 컨테이너는 하나의 커널이 여러 컨테이너로부터 system call을 직접 받아 처리하는 방식이므로 가상화에 비해 상대적으로 가볍고 빠르다. 가상화는 HOST OS든 하이퍼바이저든 간에 GUEST OS의 커널이 가상화 한 HW에 접근하고, 이를 하이퍼바이저가 처리하므로 상대적으로 느리다. 하지만 이것이 항상 단점인 건 아니다. 컨테이너는 컨테이너 기능을 제공하는 해당 커널에 종속적이므로 컨테이너를 이용해 실행하려는 어플리케이션이 해당 커널 버전을 지원하지 않는다면 실행할 수가 없다. 반면 가상 머신은 윈도우에서 리눅스를 돌리거나, 맥에서 윈도우를 돌리는 것과 같은 이종 실행이 가능하다.  경우에 따라 아키텍쳐 - x86 & ARM - 를 넘나드는 것도 가능하다.

따라서 컨테이너와 가상 머신은 어느 것이 더 우월하다고 말할 수 없으며 각각의 용도가 다르다. 상황마다 최적화하고자 하는 대상이나 목적이 상이하므로 어느 것이 딱 좋다고 집어 말할 수는 없다. 글쓴이가 들은 것 중에는, 최적화한 가상 머신이 독점적으로 모든 자원을 활용한 경우 가상 머신 없이 실행하는 PM 대비 93%의 성능을 보인다는 사례가 있는데, 이 경우에는 백업 등의 유지보수를 원활히 하기 위해 모든 자원을 단독 가상 머신화 한 다음 꾸준히 스냅샷을 찍어두는 전략을 취하였다.

 

컨테이너, 도커, 쿠버네티스

컨테이너 기술을 소개하면 결론은 도커로 수렴한다. 현재까지 컨테이너를 기술적으로나 상업적으로 가장 잘 활용하고 대중적으로도 널리 알린 것이 도커이기 때문이다. 심지어 컨테이너를 두고 벌어진 표준 경쟁에서도 도커가 상당한 지분을 가지고 있다. 쿠버네티스도 컨테이너 표준을 준수하는 runtime이라면 무엇이든지 사용할 수 있지만 많은 예시들이 도커를 컨테이너 인터페이스 런타임으로 채택하고 있다. 

도커의 필요성
컨테이너는 리눅스 표준 기술이므로 도커 없이도 얼마든지 사용할 수 있다. 하지만 도커 없이 컨테이너 기술을 사용하려면 많은 삽질과 시행착오를 요구한다. 한두 개의 어플리케이션만 돌린다면 문제가 없겠지만 복수개의 서버 풀에 이 작업을 반복해서 하는 건 상당한 인내력을 요한다. 도커는 일련의 과정을 자동화하였다.

도커의 deployment 흐름

도커를 구성하는 요소는 다음 세 가지이다.

  • Image - 컨테이너에서 수행할 어플리케이션과 메타정보
  • Registry - Image 저장소, 일명 도커 판 github 
  • Container - 도커 Image를 이용해 실행한 리눅스 컨테이너

특히 도커의 성공은 Registry와 오버레이 파일 시스템의 적절한 조합에 있다고 볼 수 있다.

Overlay FS (Docker docs)

Image의 메타 정보는 image 간의 의존 정보를 포함하고 있다. 그리고 이를 Overlay FS에 적절하게 배치함으로써 중복되는 Image 사본으로 불필요하게 공간을 차지하는 것을 배제하였다. 이는 단순히 하드디스크 공간만을 적게 차지하는 것이 아니라, deployment 시 필요한 네트워크 트래픽도 줄일 수 있다. 

Overlay FS를 이용한 Image 중복 제거

두 컨테이너에서 각각 NGINX를 커스터 마이즈 한 NGINX 1과 NGINX 2가 필요로 할 때, Overlay FS를 고려하지 않고 컨테이너를 구성한다면 각각의 NGINX 1, NGINX 2를 컨테이너에 설치해야 한다. 하지만 도커를 이용하면 NGINX 원본 위에 커스터 마이즈 한 부분만 각각 마운트를 하여 불필요한 공간 소모를 줄일 수 있다. 

쿠버네티스의 필요성 : 오케스트레이션
컨테이너로부터 도커가 필요한 이유처럼, 도커로 부터 쿠버네티스가 필요한 이유도 같은 맥락이다. 

컨테이너, 도커 그리고 쿠버네티스

이제 도커를 이용하여 컨테이너를 쉽게 구성하고 배포하고 배치할 수 있다. 하지만 도커를 설치해야 할 머신이 기하급수적으로 늘어난다면 도커 컨테이너 하나하나를 사람이 직접 배치하는 것은 엄청난 노력을 필요로 한다. 사람의 노력이 필요하다는 것은 곧 휴먼에러가 늘어난다는 뜻이다.

이로써 쿠버네티스의 필요성은 명확해진다. 컨테이너를 배포하고, 스케일을 조정하며, 오류를 탐지하고, 복구하는 일련의 과정을 자동화하는 것을 "컨테이너 오케스트레이션"이라 하며 쿠버네티스는 이를 위한 도구이다.

 

쿠버네티스의 동작

인프라의 추상화
쿠버네티스는 인프라를 추상화한다. 인프라를 추상화한다는 건 데이터센터를 구성하는 각각의 서버들을 개별적으로 인식할 필요 없이, 데이터센터 전체를 하나의 배포 플랫폼으로 취급한다는 뜻이다.

인프라의 추상화 개념도

애플리케이션 개발자는 특정 인프라 관련 서비스를 직접 구현할 필요가 없다는 걸 의미하며 애플리케이션 기능 개발에만 집중할 수 있게 한다. 운영팀 입장에서도 적절한 리소스 안배가 자동으로 이루어지므로 적절한 배치를 위해 서버 하나하나를 뒤지는 일을 할 필요가 없다.

쿠버네티스 클러스터 아키택쳐
쿠버네티스 클러스터는 크게 컨트롤 플레인과 워커 노드로 구성하며 세부 사항은 다음과 같다.

쿠버네티스 클러스터 구성 요소

  • 컨트롤 플레인 (마스터) - 전체 시스템을 제어 관리
    • 스케줄러 - 어플리케이션을 배포
    • 컨트롤 매니저 - 장애 처리 등을 포함하는 실질적인 클러스터 관리를 수행
    • etcd - 작업 요청, 진행 상황, 결과 등 트랜잭션 정보를 저장
  • 워커 노드 - 실제로 컨테이너를 배치하고 실행
    • 컨테이너 런타임 - 실제 컨테이너를 실행하는 프로그램
    • 큐블렛 - 마스터와 통신하고 워커 노드를 관리
    • 큐브 프록시 - 구성 요소 간 네트워크 트래픽 로드 밸런싱

쿠버네티스 파드
쿠버네티스의 기본 단위는 파드이다. 파드는 단일 또는 복수의 컨테이너로 구성하며, 특정 서비스를 구성하는 최소 단위를 말한다. 하나의 파드에 속한 컨테이너들은 서로 격리해서는 안된다.

디스크립션
쿠버네티스는 디스크립션 즉 명세로 작업을 지시하는 선언적 구성을 수행한다. 

디스크립션에 따른 동작 예시

위의 예제를 보면 디스크립션은 어떤 파드를 몇 개 배치해야 할지만 선언하고 있다. 컨트롤 플레인은 디스크립션을 참고하여 적절하게 각 워커 노드에게 배치를 지시한다. 지시를 받은 워커 노드는 이미지 레지스트리로부터 이미지를 가져와 파드를 배치한다. 각 파드 끼리는 격리한다. 선언적 구성에서는 절차 자체를 명시하지는 않는다. 컨트롤 플레인은 명세를 분석하여 적절한 워커 노드에 적절한 파드를 배치하기 위해 지시하고 또 지시를 하며 목표하는 상태가 될 때까지 작업을 반복해서 지시한다.

유지보수 자동화도 결국 현재의 상태와 디스크립션의 상태가 일치하는지를 확인하는 과정을 반복하는 과정이다. 만약 상태가 상이하다면 재기동하든가, 새로이 스케쥴링을 하든가, 컨테이너를 이동하든가, 새로 복제본을 실행하든가, 초과한 복제본을 정지한다.

네티워크 구성
유지보수가 자동화되어있다면 내부적으로 컨테이너의 재배치가 계속해서 발생할 가능성이 있는 거고 그때마다 네트워크 정보가 바뀔 것이다. 사람이 직접 네트워크 정보를 유지 보수하는 것은 사실상 불가능하다. 대신 쿠버네티스에서 NAT로 관리하므로, 모든 노드는 대외적으로는 고정 아피를 유지하고 있다.

모니터링과 복구
위에서 말한 것처럼 쿠버네티스는 디스크립션과 현재 상태를 계속 비교하다고 노드 장애 발생 시 자동으로 어플리케이션을 다른 노드로 스케줄링한다. 이 과정에서 운영팀 담당자가 직접 마이그레이션이나 배치를 수행할 필요가 없다. 따라서 장애 발생 시 인프라 담당자는 장애가 발생한 노드를 복구하는 것 자체에만 집중하면 된다.