[Kubernetes] 2. Kubernetes 기본 구성과 객체(object)

지난 포스트에서 Kubernetes와 Container Deployment에 대해 알아보며 컨테이너로 서비스 배포하기까지의 과정과 Docker Swarm 등의 다른 오케스트레이션 툴과 어떤 점이 다른지 살펴봤습니다.

 

이러한 점을 확인하고, 우리가 운영하는 서비스가 쿠버네티스에 적합한지, 차후 미래 운영 대책으로 사용할만한지를 정확하게 고려 대상으로 선정하셨다면 이 글을 계속 읽으며 Kubernetes를 직접 공부해보고, 사용해보시는 것이 좋을 것 같네요.

 

 

 

 

Kubernetes의 기본 구조

이제 본격적으로 Kubernetes가 어떻게 이루어졌는지 알아보겠습니다.

 

기존의 Docker에서는 컨테이너를 관리할 때 중앙의 Docker Engine이 자리잡고 있어, 이 엔진이 애플리케이션 이미지를 관리하고, 생성하는 방식으로 운영했습니다.

 

이런 형태는 단일 머신 혹은 노드에서 Docker를 설치하여 운영할 수 있는 형태였고, 모든 애플리케이션의 운영과 배포를 한 머신에서 운영할 수 있도록 하는 아주 간단한 형태였습니다.

 

이런 형태로 운영하기 위해서 Docker 프로세스는 cgroup을 이용하여 리소스를 나눠야 했기 때문에 모든 작업을 관리자 권한으로 실행해야 했고, 자원 분배를 위해 서로가 독립된 프로세스로 운영되었습니다. 그러나 메인 프로세스의 역할이 커지고, 불안정해지기 시작하면 메인 프로세스가 죽게 되고, 메인 프로세스가 죽으면 그 자식 프로세스에 영향을 받는 치명적인 단점이 존재합니다.

 

 

Kubernetes는 기본적으로 클러스터의 구조를 위와 같은 뿌리로 시작합니다. 여기서 말하는 클러스터란, 독립된 Kubernetes 1개를 의미합니다. 이 클러스터부터 하여금 여러 머신과 클러스터링을 시작할 수 있습니다.

 

클러스터 구조는 위 이미지처럼 Master와 Node로 이루어집니다. Master는 클러스터 전체를 제어하며 이는 물리적인 기계 또는 가상적인 기계(가상 머신)로 될 수 있습니다.

 

하위 노드들은 컨테이너가 배포되는 머신으로 이 역시, 물리적인 기계 혹은 가상적인 기계(가상 머신)로 될 수 있습니다. 기본적인 큰 구조를 봤을 때는 복잡하지 않습니다.

 

그렇다면 제어하는 마스터와 실제 컨테이너가 별도의 머신으로써 존재한다는 것인데, 이들은 어떤 방식으로 데이터를 주고 받을까요? 

 

기본적으로 Kubernetes는 REST API로 리소스를 제어하고 서로 데이터를 교환하는 방식으로 서비스를 운영합니다. 즉, 마스터와 각각의 노드가 REST API로 통신하고, 이들간에 컨테이너가 생성되고, 볼륨이 생성되는 등의 형태가 이루어집니다. 이는 클라우드에서도 동일하게 동작합니다.

 

이는 Docker와는 상반적인 관계를 지니고 있습니다. Docker는 단일 머신에서 운영체제의 프로세스와 스레드라는 자원을 가지고컨트롤러와 컨테이너 단위를 구별하였지만 Kubernetes는 서로 다른 머신을 이용하여 제어하는 머신과 운영하는 머신을 분리시킨 것입니다. 따라서 컨트롤러인 마스터 노드의 애플리케이션에서 많은 기능을 부여하여 불안정한 형태를 보여 프로세스가 죽는다 하더라도 운영되고 있는 컨테이너에는 영향을 받지 않는 구조로 되어 있습니다.

 

 

 

 

Kubernetes 객체

Kubernetes는 컨테이너 오케스트레이션 툴 중에서도 엔터프라이즈급 형태에 해당합니다. 소규모가 아닌 대규모 서비스에 적합하여 기본 요구 사항이 단일 머신이 아닌 복합 머신 형태이며, 물론 물리적인 머신이 아니더라도 두 개 이상의 노드로 구성해야 한다는 점이 존재합니다.

 

그런 Kubernetes에서 사용할 수 있는 여러 객체들이 있습니다. 이 객체들은 서로 용도가 재각 다르며 하나씩 알아보도록 하겠습니다.

 

 

Object (객체)

Kubernetes의 가장 기본적인 단위인 object는 쿠버네티스의 리소스를 사용하기 위한 기본적인 요소입니다. 그 리소스에는 컨테이너, 스토리지 등이 있고, Controller라는 또 다른 컴포넌트에 의해 관리되어 집니다. 우리는 이 object를 CLI의 커맨드나 json, yaml 등의 파일로 정의할 수 있습니다.

 

  • Pod
  • Service
  • Volume
  • Namespace

Kubernetes를 가장 기본적인 방식으로 사용했을 때 위 4가지 객체가 기본 객체라고 할 수 있습니다. 애플리케이션이 컨테이너화 되어 배포되는 워크로드(Workload)를 기술하는 객체에 해당됩니다.

 

기본적으로 Object의 구조는 아래와 같은 구조를 따릅니다.

 

  • metadata: Object의 Meta 정보
  • spec: Object가 원하는 상태 (Desired State)
  • status: Object의 현재 상태 (Current State)

쿠버네티스의 object는 CRD(Custom Resource Definition)을 이용하여 내가 원하는 object를 만들 수도 있는데, 이 부분은 다른 글에서 다뤄보도록 하겠습니다.

 

 

 

Pod

Pod은 하나의 컨테이너를 포함하는 객체를 말합니다. 일반적인 Docker처럼 컨테이너를 하나씩 배포하지 않고, Pod이라는 단위로 배포하는데, 여기서 Pod은 하나 이상의 컨테이너를 포함합니다.

 

이를 YAML로 살펴보면 아래의 코드와 같습니다.

apiVersion: v1
kind: Pod
metadata:
  name: nk-app
spec: 
  containers: 
    - name: nk-app
      image: neonkid/nk-app:latest
      ports:
      - containerPort: 4001

yaml을 작성시 주의할 점은 간격 단위가 tab이 아닌 space (공백)이어야 합니다. 각 코드를 설명드려보자면..

 

  • apiVersion: 위 스크립트를 실행하기 위한 Kubernetes의 API 버전
  • kind: 리소스의 종류 (LoadBalancer, Service, Deployment 등)
  • metadata: 리소스의 메타데이터 (리소스 이름, label 등)
  • spec: 리소스의 스펙 (사용할 포트 주소, 환경 변수, 컨테이너 이미지 레지스트리 주소 등)

여기까지는 Docker Swarm이랑 크게 다른 부분이 없어보입니다. 그런데, Pod은 하나 이상의 컨테이너를 포함한다고 했는데, 어떻게 이들을 한 그룹으로 묶고, 서비스할 때는 어떻게 서비스 해야 클라이언트가 연결할 때 한 호스트로 연결을 붙일 수 있을까요? Docker Swarm처럼 별개의 로드 밸런서나 게이트웨이를 사용해야할까요?

 

 

Pod은 위의 구조로 이루어져 있습니다. 실제로 우리가 배포하는 하나의 컨테이너와 Pod이라는 기본적인 구조로 가지고 있는 시스템 컨테이너가 별도로 존재합니다. 

 

시스템 컨테이너와 애플리케이션 컨테이너는 서로가 같은 디스크 볼륨을 공유하며, Pod이 소멸되면 볼륨에 저장된 데이터들 또한 같이 소멸하게 됩니다. 여기까지는 컨테이너의 기본 구조와 동일합니다.

 

Pod에는 가상의 IP가 존재합니다. 즉 Pod이라는 그룹 안에 여러분들이 개발한 애플리케이션 컨테이너가 2개, 3개 이상이 될 수 있고, 이들 컨테이너가 하나의 Pod으로 배포되었을 떄 Pod 내부에서는 각기 다른 포트 주소로 통신하며, Pod 외부에서는 Pod이 가지고 있는 가상의 IP 주소로 통신하게 됩니다.

 

전통적인 배포 방식에서는 애플리케이션을 배포하면 이를 서비스 하기 위해 리버스 프록시를 사용하기 위해 HAProxy나 nginx 등을 별도로 구축하여 운영했습니다. 그러나 Kubernetes에서는 컨테이너 배포시 애플리케이션만 올라가는 것이 아닌 리버스 프록시와 로그 수집기가 같이 올라갑니다. 이는 Docker로 배포할 떄도 필수가 아닌 선택이었습니다. 그러나 Kubernetes에서는 이것이 필수적으로 올라갑니다.

 

게다가 Docker Swarm 등을 이용하여 로그 수집기를 탑재할 경우, 서로 볼륨을 공유시키는 스크립트를 삽입하는 번거로운 작업까지 해야했지만 Kubernetes는 이를 자동으로 제공해주는 구조를 지니고 있습니다. 이처럼 애플리케이션과 애플리케이션에서 사용하는 주변 프로그램을 같이 배포하는 패턴을 MSA에서 Sidecar 패턴이라고 이야기합니다.

 

 

Volume

위 이미지에서도 잠깐 언급되었지만 Volume은 컨테이너가 저장한 데이터를 보관하는 곳입니다. 그러나 위의 볼륨과 여기서의 볼륨은 조금 다릅니다.

 

Pod 내에서 움직이는 볼륨은 Pod과 일체형으로 Pod이 소멸함과 동시에 Volume도 소멸되어 없어집니다. 그런데, Database와 같이 Pod이 소멸되어도 영구적으로 저장되어야 하는 서비스의 경우에는 어떻게 해야할까요? 그래서 필요한 것이 바로 Storage Volume 객체입니다.

 

Traditional Intfrastructure로 봤을 때, 이 모습은 스토리지와 서버의 모습으로 연상할 수 있습니다. Pod에 내장된 스토리지는 Pod이 소멸되면 그 즉시 같이 소멸되는데, 위와 같이 Storage Volume 객체를 사용하면 Pod과 독립된 또 다른 Storage를 사용할 수 있습니다.

 

이들은 각각의 Pod 혹은 컨테이너와 공유되는 속성을 가지고 있으며 예를 들어, 위 이미지처럼 DB Pod 안에 애플리케이션 컨테이너가 3개 존재한다면 이들 3개의 인스턴스가 오른쪽의 Storage Volume을 공유하는 방식으로 이루어집니다. 

 

같은 맥락으로 위 이미지에서 운영하는 웹의 로그나, DB 인스턴스의 로그를 영구적으로 보관하여 처리하고자 한다면 이 역시 별도의 볼륨을 생성하여 진행할 수 있습니다. 

 

Kubernetes에서는 이러한 외장 디스크를 추상화하여 제공하는데, 지원하는 프로비저닝은 아래의 Kubernetes 공식 사이트를 참고하면 좋습니다.

 

https://kubernetes.io/ko/docs/concepts/storage/storage-classes/

 

스토리지 클래스

이 문서는 쿠버네티스의 스토리지클래스의 개념을 설명한다. 볼륨과 퍼시스턴트 볼륨에 익숙해지는 것을 권장한다. 소개 스토리지클래스는 관리자가 제공하는 스토리지의 "classes"를 설명할 수

kubernetes.io

 

Service

Pod은 Kubernetes Cluster에서 제공하는 IP를 부여 받아 각 Pod들과 연결할 수 있도록 도와줍니다. 그러나 이들 IP는 어디까지나 내부에서 사용 가능한 IP입니다. 실제로 외부에서 내가 배포한 애플리케이션에 접속할 수 있도록 하려면 Service 객체를 이용해야 합니다.

 

이처럼 Service는 Pod을 생성한 후, 로드 밸런서 등을 이용해 하나의 IP와 포트 주소로 묶어서 사용할 수 있도록 해주는 역할을 합니다. Pod의 장점은 동적 생성이 가능하다는 점입니다. Docker Swarm처럼 정적으로 Replica의 수를 주는 방법도 있지만 일반적으로 오류가 발생하는 등의 장애가 생기면 자동으로 Restart 되는 등의 역할을 수행하는데, 이렇게 되면 Pod의 IP가 바뀌는 등의 상황이 발생합니다.

 

또, Auto scaling 등을 이용하게 되면 Pod이 동적으로 추가/삭제가 빈번히 발생하는데, 이 역시 Pod의 IP가 고정적으로 될 수 없다는 점을 보여주고 있습니다. 로드 밸런서에서 이러한 점까지 Watching하는 기능은 찾아볼 수 없습니다. 그래서 위에서 언급한 label과 label selector를 이용하여 어떤 Pod 이었는지 이름을 명시해주는 방법이 있습니다.

 

 

위 그림을 봤을 떄 서비스 객체는 단순히 외부 접속을 위한 껍데기에 불과합니다. 우리는 이 서비스 객체에서 어떤 Pod을 서비스할지 결정해야 하며 이를 위해  label selector를 사용합니다. 이 라벨을 설정함으로써 서비스는 라벨이 nk-app인 Pod만을 골라내 서비스에 넣고 그 Pod에서만 로드 밸런싱을 통하여 외부로 서비스하는 형태입니다.

kind: Service
apiVersion: v1
metadata:
  name: nk-service
spec:
  selector:
    app: nk-app
  ports:
    protocol: TCP
    port: 80
    targetPort: 4001

YAML 파일로 정의하면 위와 같은데, 위에서 Pod을 작성할 때랑 비교해보면 좀 감이 오실 수 있겠지만 kind는 객체의 종류를 정하고, 밑에 부분의 내용은 대부분 비슷하게 동작하며 spec의 경우만 객체에 따라 다르게 설정되는 것을 알 수 있습니다. 

 

Service 객체의 경우 spec에 label selector를 이용해 label이 app:nk-app인 Pod만을 선택해서 서비스할 수 있습니다. 추가로 하단에 적은 ports의 경우 protocol과 port에 서비스에서 사용할 포트 주소와 그 요청을 받을 컨테이너의 포트를 입력해주면 됩니다.

 

 

Namespace

네임스페이스는 하나의 쿠버네티스 클러스터 내에서 잘개 쪼갤 수 있는 논리적 단위입니다. 만약 쿠버네티스 클러스터를 Master와 하위 노드로 구분한다고 가정해보겠습니다. 하위 노드에서 스토리지로 사용할 용도, 컨테이너를 운영할 용도 등 다양한 용도로 만들 수 있지만 이들 노드로 배포를 나눈다면 매번 가상 머신을 생성하고, 쿠버네티스를 설치해야 하는 번거로움이 생길 것입니다.

 

이를 구분할 수 있는 것이 바로 Namespace입니다. 내가 생성한 Pod이나 Volume 등이 어떤 용도인지 그룹화 해주는 역할을 하며 기본적으로 Service, Pod, Volume은 바로 이 네임스페이스 별로 관리가 가능하고, 사용자 또한 네임스페이스 별로 나눠서 부여할 수 있는 큰 장점이 존재합니다. 예를 들면 스토리지 관리자, 시스템 관리자 등을 따로 구분할 수 있는 것이죠.

 

쉽게 설명하자면, 하나의 클러스터에 개발/운영/테스트 환경이 있다고 가정했을 때, 이를 한 클러스터에서 운영한다고 한다면 이를 네임스페이스로 개발, 운영, 테스트 3개의 네임스페이스로 나눠서 운영할 수 있는 이상적인 모습이 나올 수 있습니다. 운영팀과 QA팀이 각 사용자를 만들게 되고, 그들 네임스페이스만을 이용하여 권한과 역할을 철저히 구분지을 수 있기 때문입니다.

 

이 외에도 네임스페이스에서 사용가능한 것은 아래와 같습니다.

 

  • 사용자별 네임스페이스별 접근 권한 설정 기능
  • 네임스페이스별 리소스 할당량 지정 기능
  • 네임스페이스별 객체(object: Service, Pod) 지정 가능

 

한 가지 확인해야할 점은 네임스페이스가 서로 다르기 때문에 그들 영역이 분리 되어 있는 것은 맞지만 그렇다고 네트워크 통신이 되지 않는 것은 아닙니다. 영역이 논리적으로 분리되어 있기 때문에 물리적으로는 같은 머신에 있고, 그들끼리 네트워크 통신도 물리적인 설정 없이 가능합니다.

(만약, 네트워크 분리 마저 원한다면 클러스터를 분리하는 것을 권장합니다.)

 

네임스페이스는 서비스 운영에 대해서는 크게 관여되지 않는 부분이라고 보시면 됩니다. 논리적으로 팀을 나누는 데 있어서 중요한 부분이며 만약 팀을 나누지 않더라도 리소스별, API별, Web별 이런식으로 나눌 수 있기 때문에 어떤식으로 나누는 것은 기술하는 사람들의 자유라고 생각합니다. 위의 그림은 Google, Kubernetes가 공식적으로 제시한 Best Practice입니다.

 

 

Label

Label(라벨)은 위에서 잠깐 다루기도 하였지만 IP처럼 유동적인 부분의 요소들을 고정적인 네임의 수단으로 사용할 수 있습니다. 이 라벨은 각 리소스 모두가 가질 수 있고, 검색 기능이 지원되기 때문에 특정 라벨만 가지고 있는 리소스에 대해 기능을 적용할 수도 있습니다. (위에서 서비스 객체를 만들고 특정 Pod을 서비스한 것이 그 예시입니다.)

 

이 외에도 특정 라벨이 걸려있는 리소스만 배포/업데이트할 수 있는 방법도 있거나 네트워크 접근 권한을 부여하는 등 쿠버네티스에서 정의할 수 있는 기능들에 일부를 접목하기 위한 수단으로 이용할 수 있습니다. 

...
metadata:
  labels:
    app: nk-app
...

Label은 위와 같이 Key와 value형태로 정의할 수 있습니다. 하나의 리소스에는 n개의 라벨을 지정할 수 있으며 이 라벨을 사용하는 리소스는 label selector를 이용하여 사용할 수 있습니다.

 

  • Equality based selector
  • Set based selector

쿠버네티스에서 제공하는 Label selector는 위와 같이 두 가지가 제공됩니다. Equality based selector는 이름 그대로 값의 문자열을 equals(비교)하는 방식이고, Set based selector는 Set(집합)을 사용하여 app이라는 key 안에 nk-app이라는 값이 있는지를 찾는 방식입니다.

kind: Service
apiVersion: v1
metadata:
  name: nk-service
spec:
  selector:
    app: nk-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 4001

서비스 객체를 이야기할 때 잠깐 다룬 것이지만 위에서 정의한 라벨을 label selector를 이용해서 사용하는 방법은 위와 같습니다. 원하는 리소스 객체에서 spec: selector: 를 이용해 key-value 형태로 라벨을 정의해주면 됩니다.

 

 

 

 

 

마치며...

쿠버네티스의 기본 구조와 이를 이루고 있는 객체에 대해서 알아봤습니다. Docker를 다루시다가 오신 분들이라면 아마 많은 것을 배워야한다는 깊은 러닝 커브에 부담을 느끼실 것이라고 생각합니다.

 

저 또한 쿠버네티스가 과연 단일 서비스의 용도로 적합한지 많이 고민했을 정도로 기능이 많은 것을 보고, 아무런 목적 없이 배우려고 시도했을 때는 거부감이 없지 않아 있었습니다. 확싱히 쿠버네티스는 엔터프라이즈에 적합한 솔루션이고, 더욱이 컨테이너를 여러개를 띄워 사용하는 MSA(Micro Service Architecture)에는 그야말로 환상궁합이라 할 수 있습니다.

 

그러나 MSA가 필수적인 것은 아닙니다. MSA는 비즈니스 로직을 서비스 단위로 나누기 때문에 많은 인스턴스를 필요로 하며 단일의 서버로 운영하는 모놀리식 아키텍처와는 정 반대의 개념입니다. 이전에 온프렘이나 클라우드에서 VM 인스턴스 내지 PaaS와 같은 서비스로 단일 운영해서 사용했다면 쿠버네티스는 적합한 구조라고 볼 수 없습니다. 왜냐하면 그들의 서비스는 단일의 컨테이너 내지 인스턴스로 운영하기 때문에 이런 복잡한 과정을 거치면서까지 클러스터를 만들고 관리해야 할 필요는 없다고 생각합니다.

 

다음 포스트에서는 쿠버네티스를 다양하게 사용할 수 있는 Deployment, DaemonSet 등의 컨트롤러에 대한 이야기를 이어서 해보도록 하겠습니다.

 

 

 

 

참고:

(쿠버네티스 공식 문서) https://kubernetes.io/ko/docs/concepts/

(조대협의 블로그) https://bcho.tistory.com/1256

(하나씩 점을 직어가며) https://dailyheumsi.tistory.com/208

comments powered by Disqus

Tistory Comments 0