SLASH 기술 블로그

Kubernetes 개념 파악하기 본문

인프라/쿠버네티스

Kubernetes 개념 파악하기

SLASH 2021. 4. 30. 16:32
반응형

요즘 백엔드 개발을 한다면 피해갈 수 없는 주제가 바로 도커쿠버네티스일 것이다. 보통 어떤 기술에는 장점과 단점이 혼재하기 마련인데, 도커와 쿠버네티스가 제공하는 강력한 기능들은 그 단점이 무색해질 정도로 좋아서, 서버 개발의 패러다임을 완전히 바꿔버렸다.

 

내가 처음 일을 시작한 2017년만 해도, 회사에서 도커에 대해 아시는 분들이 적었고, 쿠버네티스는 당연히 모르는 분위기였는데, 몇 년 사이 도커를 쓰지 않고 배포하는 경우를 찾아보기 힘들 정도로 널리 퍼지고 대중화가 되었다. (물론 내가 일을 시작한 회사가 새로운 기술에 둔감했던 것도 한 몫 했던 것 같다)

 

이번 글에서는 쿠버네티스가 다루는 컨테이너에 대한 이해를 위해 도커를 간단히 살펴보고, 쿠버네티스의 기본적인 요소들 - Pod, ReplicaSet, Deployment 등에 대해 알아보도록 하자.

 

Docker - 실행 환경의 가상화, 컨테이너

과거에는 클라우드에서 서버를 배포하려면 서버 호스팅을 위한 인스턴스를 띄우고, 해당 인스턴스에 접속해 서버 실행에 필요한 환경을 직접 구축해주어야 했다.

 

서버를 재시동하거나 새로 배포할 때마다 해줘야하는 이러한 설정 작업이 번거로워, 필요한 작업을 스크립트로 작성한 startup script (시작 스크립트) 같은 것들을 사용하기도 했고, 어플리케이션 실행 환경을 마련해주는 Heroku나 Elastic Beanstalk과 같은 PaaS 서비스를 사용하기도 했다.

 

하지만 다양한 배포 환경을 매 번 관리하기는 어렵기 마련이다. 배포 스크립트는 서버가 실행되는 OS를 바꾸거나 버전을 변경하면 잘 동작하지 않는 경우가 종종 있고, Heroku는 파이썬, 자바스크립트 등 여러 기술 스택을 함께 사용했을 때의 설정이나 관리가 어렵다.

 

도커는 이러한 실행 환경을 격리시켜 - 즉, 컨테이너화해서, 어떤 플랫폼에서든 도커만 있다면 일관된 동작을 보장하게 해준다. 이는 하드웨어를 소프트웨어적으로 가상화하는 VM(가상 머신)과는 본질적으로 다른 개념이다.

 

실행 환경을 격리한다는 건 무슨 뜻일까? MySQL과 같은 데이터베이스를 로컬에서 써보기 위해 설치하는 상황을 생각해보자. 우선 Mac인지 Windows인지, 혹은 그 외 운영체제인지에 따라, 또 어떤 아키텍처인지에 따라 설치 방법이 다를 것이다. 만약 실행 환경이 윈도우이고 배포 환경이 리눅스라면 설치 방법도 다르고, 빌드가 운영체제마다 다르다면 서로 다른 동작을 할 가능성도 있다.

 

도커를 사용하면 이미지(Docker image)라고 하는, 컨테이너 실행을 위한 일종의 템플릿을 사용하게 되는데, 로컬 실행 환경과 배포 환경에서 모두 같은 이미지를 사용하기 때문에 로컬에서 테스트하던 그대로 서버에서도 동일하게 다룰 수 있다.

 

도커의 구체적인 사용법과 작동 원리에 대해서는 이후 별도의 포스팅에서 다뤄볼 생각이다. 우선은 이 정도로 컨테이너에 대한 개념을 잡고 넘어가자.

 

Kubernetes - 컨테이너 관리의 자동화, 컨테이너 오케스트레이션

쿠버네티스에 대해 공부하다보면 컨테이너 오케스트레이션이라는 다소 생소한 단어가 등장한다. 이게 도대체 무슨 말일까? 간단한 예시를 통해서 알아보자.

 

웹 서비스를 운영하기 위해서 Node 기반의 서버를 컨테이너화시켜 배포를 했다. 배포를 하기 위한 작업은 무척 간단하다. 아래와 같이 Dockerfile을 작성했다.

FROM node:14.16.0-alpine3.13

# Set working directory
WORKDIR /app

COPY ./src ./src
COPY ./apidoc ./apidoc
COPY ./nodemon.json ./nodemon.json
COPY ./package.json ./package.json
COPY ./tsconfig.json ./tsconfig.json

RUN yarn install

CMD [ "yarn", "run", "serve" ]

 

이후 서비스가 어느 정도 성숙하고, 백그라운드 작업을 처리하기 위해 파이썬 백엔드를 사용하려고 하는데.. 이를 위해서 또 다른 컨테이너를 만들었다.

FROM python:3.9

EXPOSE 5000

WORKDIR /app

COPY requirements.txt ./
RUN pip install -r requirements.txt

ADD . .

ENV FLASK_APP=app.py
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"]

 

이 두 인스턴스간에 통신을 하기 위해서는 어떻게 해야할까? 도커의 네트워크에는 3가지 종류가 있다.

  • bridge - 하나의 호스트 내에서 여러 컨테이너들간의 통신
  • overlay - 여러 호스트에 분산된 컨테이너들간의 통신
  • host - 컨테이너를 호스트와 동일한 네트워크에서 돌리기 위해 사용

 

우리는 하나의 호스트 내에서 두 컨테이너간의 통신을 하려고 하는 것이니 bridge 네트워크를 사용하는게 좋겠다. 결론적으로 배포를 위한 절차는 다음과 같다.

# 네트워크 생성
$ docker network create -d bridge app-server
> cf961f72da4f88a1d137bbc6d34e36a7ac0c5de612127f56b8afa4067af48d73

# 네트워크를 사용해 빌드
$ docker build --tag api --network app-server
$ docker build --tag worker --network app-server

# 빌드한 이미지 실행
$ docker run api
$ docker run worker

 

응..? 무언가 잘못된 것 같다. 도커를 사용하면서 이미지를 통해 배포 스크립트가 더 이상 필요하지 않게 되었는데, 이제는 컨테이너 실행을 위한 스크립트가 필요하게 되었다!

 

지금은 컨테이너 두 개를 하나의 네트워크에서 사용하는 상황인데, 여러 대의 컨테이너를 묶어서 하나의 네트워크를 만들고, 로드 밸런싱을 하려고 한다면..? 여기에 컨테이너마다 포트나 환경 변수, 스토리지를 위해 볼륨을 지정하는 등 다양한 작업들이 필요하면 이러한 과정은 더 복잡해진다.


이렇듯 컨테이너를 사용하면서 배포를 위한 과정은 간단해졌지만 기존과는 다른 다양한 관리 이슈가 발생하게 되는데, 컨테이너의 배포, 네트워킹, 그리고 확장까지, 컨테이너들을 보다 효율적으로 관리하기 위한 기술이 바로 컨테이너 오케스트레이션이다.

 

RedHat에서 소개하는 컨테이너 오케스트레이션의 효용은 대략 이렇다.

  • 프로비저닝 및 배포
  • 설정 및 스케줄링
  • 리소스 할당
  • 컨테이너 스케일링 (고가용성)
  • 로드 밸런싱
  • 모니터링
  • 보안

 

Kubernetes는 이러한 컨테이너 오케스트레이션 도구 중 하나이고, 사실상 표준이라고 봐도 무방하다. (CNCF의 Graduated Project 중 하나)

 

쿠버네티스의 정체성에 대해 알았으니, 이번에는 쿠버네티스가 가지고 있는 구조, 컨셉과 요소들을 파헤쳐보자.

 

쿠버네티스 클러스터 구조

먼저 하나 확실히 짚고 넘어가야 하는 것이 있다. 쿠버네티스 자체는 구현체가 아닌 클러스터 구조에 대한 추상화된 스펙이라는 것. 때문에 "쿠버네티스를 실행한다"고 했을 때 쿠버네티스라는 프로그램이 있어 이를 실행하는 것이 아니고, kops, minikube, kubeadm같은 다양한 쿠버네티스 구현체를 가지고 쿠버네티스 클러스터를 실행하는 것을 의미한다.

 

어떤 구현체든 쿠버네티스를 실행하면, 쿠버네티스 클러스터를 실행하게 된다. 공식 문서에서 이 클러스터 내부의 다양한 컴포넌트에 대해 설명하고 있지만, 크게 두 가지로 나누면 컨트롤 플레인, 그리고 워커 노드로 나눌 수 있다.

 

컨트롤 플레인 - 상태 기반의 클러스터 관리

쿠버네티스 클러스터에는 Desired State - 원하는 상태가 있고, 이 상태에는 어떤 어플리케이션을 실행할 것인지, 어떤 컨테이너 이미지를 사용할지, 그리고 어떤 리소스를 사용할지 등 서비스를 위한 다양한 세부 구성들이 포함되어 있다.

 

이러한 상태는 보통 쿠버네티스의 오브젝트에 대한 구성을 정의하는 JSON/YAML 형식의 구성파일을 통해 정의할 수 있는데, 컨트롤 플레인의 역할은 클러스터를 원하는 상태로 유지하는 것이다.

 

가령 어플리케이션을 배포하는데, 로드 분산과 가용성을 위해 이 어플리케이션의 복제본을 3개로 하고 싶다고 하자. 이 때 원하는 목적 상태는 "어플리케이션의 복제본이 3개 존재하는 상태"이다. 클러스터를 실행하면 자동적으로 복제본을 3개 생성하게 된다. (초기 상태에는 복제본이 0개이므로, 원하는 상태에 도달하기 위해 어플리케이션을 3개 추가)

 

이후 서비스를 하다가 장애가 발생해 죽게 되면, 쿠버네티스는 복제본이 2개임을 감지하고 원하는 상태에 도달하기 위해 새로운 어플리케이션을 하나 더 추가하게 된다. 컨트롤 플레인은 이러한 역할을 비롯해 생성한 컨테이너 집합을 후술을 노드에 배치하는 역할도 담당한다.

 

워커 노드 -  추상화된 컨테이너 실행 환경

컨테이너는 결국 어딘가에 위치한 호스트에 올라가야 한다. 컨테이너가 실행될 환경이 바로 워커 노드다. 이 실행 환경은 AWS, Azure같은 범용 클라우드 서비스에서 제공하는 Compute 엔진(EC2)이 될 수도 있고, VM(가상 머신)이 될 수도 있다. (minikube를 통해 쿠버네티스 클러스터를 생성한 경우)

 

노드는 이러한 다양한 실행 환경에 대한 추상화라고 볼 수 있다. 클러스터를 사용하는 입장에서는 노드의 구체적인 형태가 무엇인지 알 필요 없이 사용할 수 있다.

 

어떤 노드를 사용할 것인지는 쿠버네티스 구현체에 따라 결정된다. kops같은 도구를 사용하면, InstanceGroup의 machineType과 같은 속성을 통해 인스턴스의 타입을 결정할 수 있다. (e.g. t3.medium같은 타입들)

 

워크로드

워크로드? 처음 들으면 Workflow와 비슷해 실행 흐름을 생각할 수 있다. 쿠버네티스에서 "워크로드"라는 단어는 간단히 쿠버네티스에서 구동되는 어플리케이션을 지칭한다. 쿠버네티스를 사용하는 우리의 최종적인 목적은 쿠버네티스가 제공하는 여러 가지 리소스들을 활용해 내가 원하는 형태의 워크로드를 구동하는 것이라고 할 수 있겠다.

 

워크로드과 관련된 개념들은 쿠버네티스의 핵심이라고 할 수 있다. 우선 가장 기본이 되는 Pod, 파드부터 시작해보자.

 

Pod - 컨테이너들의 집합, 가장 작은 배포 단위

파드컨테이너들의 그룹이다. 쿠버네티스에서는 가장 작은 배포 단위로서 컨테이너가 아닌 파드를 사용한다.

 

컨테이너는 어디가고 갑자기 웬 파드인가? 여기에는 몇 가지 이유가 있다.

 

앞서 노드와 파이썬 두 개의 컨테이너로 이루어진 서비스를 기억하는가? 하나의 컨테이너로 되어 있는 서비스가 있는가 하면, 이렇게 여러 컨테이너로 구성되어 있는 서비스도 있다. 이럴 때는 배포할 때 두 컨테이너를 함께 배포하게 되니, 컨테이너가 아닌 컨테이너의 집합을 배포 단위로 하는 것이 자연스럽다.

 

그렇다면 파드는 단순히 컨테이너들을 모아놓은 것인가? 그렇지 않다! 파드 내의 컨테이너들은 스토리지와 네트워크를 공유하고, 함께 스케줄링되고 함께 배포된다. 이전에 컨테이너들을 묶어서 사용했던 것처럼, 파드를 통해 컨테이너들을 논리적인 단위로 묶어 쉽게 다룰 수 있다.

 

ReplicaSet - 복제본 관리

파드를 직접 생성할 수도 있지만, 워크로드 리소스라고 하는 다양한 컴포넌트들이 파드의 생성 및 관리를 보조하기 때문에 쿠버네티스를 사용하면서 우리가 직접 관리할 필요는 없다.

 

ReplicaSet도 그 중 하나인데, 이 컴포넌트가 하는 일은 조건에 만족하는 파드의 복제본을 원하는 숫자로 유지시키는 것이다. 실제로는 ReplicaSet Controller가 우리가 정의한 ReplicaSet을 가지고 파드의 변화를 감지한 후 파드를 생성/제거하는 방식이다.

 

파드와 마찬가지로 ReplicaSet도 단독으로 쓰이기보다는 보통 Deployment를 통해서 사용한다고 한다.

 

Deployment - 버전 관리

ReplicaSet이 단순히 파드의 복제본을 관리했다면, Deployment는 배포하는 워크로드의 버전을 관리한다. Deployment는 어떤 방식으로 버전을 관리할까? 간단하게 정리하면 다음과 같다.

 

  1. 최초 배포를 할 때, Deployment에 정의된 스펙과 동일하게 ReplicaSet을 생성한다. ReplicaSet이 원하는 만큼의 파드를 생성한다.
  2. 파드의 구성을 변경하면(이미지 버전을 올리거나, 컨테이너 개수가 달라지거나) 변경된 스펙에 맞게 새로운 ReplicaSet을 생성한다. 여기서 기존 ReplicaSet의 복제본 개수를 0으로 조정한다.
  3. ReplicaSet의 복제본 관리 매커니즘에 따라서 변경한 구성의 파드만 남을 때까지 파드의 생성/삭제를 실행한다.
  4. 새로이 배포가 완료되면 기존 ReplicaSet을 삭제한다.

어떠한가? ReplicaSet을 활용하면 아주 우아하게 우리가 배포할 때 사용하는 Rolling Update를 구현할 수 있다. 여기서 Deployment가 한 일은 1) 새로운 ReplicaSet을 생성하고, 2) 기존 ReplicaSet의 복제본을 0으로 변경한 다음 삭제한 것이고, 실질적으로 파드를 다룬 것은 ReplicaSet이다.

 

Deployment는 배포 이외에도 배포한 버전의 히스토리를 가지고 있어, 나중에 배포했던 기록을 보거나 원하는 버전으로 롤백하는 것이 가능하다.

 

서비스

쿠버네티스에서 파드는 Deployment에 의해서 얼마든지 생성되고 삭제될 수 있다. 각각의 파드는 고유한 IP 주소를 갖지만, 현재 시점에 접근한 파드가 조금 뒤에 접근할 파드와 동일할 것임을 보장할 수 없기 때문에, 파드의 집합에 접근할 수 있는 일관된 방법이 필요하다.

 

이럴 때 필요한 게 서비스이다. 서비스를 사용하면 원하는 파드들에 접근할 수 있는 단일 엔드포인트를 부여하고, 파드 간에 로드 밸런싱을 할 수 있다.

 

서비스에는 총 4가지 종류가 있다. ClusterIP, NodePort, LodeBalancer, 그리고 ExternalName이 있는데, 자주 사용되는 앞의 3개만 알아보자.

 

ClusterIP - 클러스터 내부 접근

기본적으로 서비스는 1) 셀렉터를 통해서 원하는 파드들을 선택하고, 2) 접근할 수 있는 정책을 정의하는 두 가지 기능을 한다.

 

ClusterIP는 이 두 기능을 통해 클러스터 내부에서 해당 파드들에 접근할 수 있는 단일 엔드포인트를 제공한다. 일반적으로 로드 밸런싱을 생각하면 웹 사이트처럼 외부로 노출되어 있는 서비스만 생각하기 쉽지만, 마이크로 서비스 관점에서 바라봤을 때 클러스터 내부에서도 이러한 로드 밸런싱과 디커플링은 필요하다.

 

구체적으로는 서비스를 생성했을 때, 이 서비스를 통해 연결된 파드들의 IP가 담겨 있는 Endpoint 오브젝트를 만들고, 서비스에 대한 트래픽을 각 파드로 분산하는 kube-proxy가 이것을 사용한다. 세부적인 프록시 방법은 다양하다.

 

NodePort - 클러스터 외부 접근

ClusterIP를 사용하더라도 외부에서는 파드에 접근할 수 없다. NodePort 타입의 서비스는 클러스터의 모든 노드에 원하는 포트를 개방하고, 이렇게 개방된 포트를 파드의 포트로 연결한다.

 

클러스터 외부에서 접근할 수 있게 하려면 기본적으로 파드들을 선택하고 접근할 수 있는 정책이 존재해야 하기 때문에, NodePort를 생성하면 내부 접근을 위한 ClusterIP 서비스가 자동으로 생성된다.

 

LoadBalancer - 부하 분산

NodePort는 들어오는 트래픽을 각 노드로 나눠주기는 하지만, 어떤 노드가 살아 있는지는 알 수 없기 때문에 한 노드가 죽었을 때 다른 노드로 접근하도록 불가능하다.

 

LoadBalancer 타입의 서비스는 AWS와 같은 프로바이더가 제공하는 로드밸런서를 사용해 각 노드의 상태를 체크하고 적절하게 트래픽을 분산하는 것이 가능하다.

 

LoadBalancer 서비스를 사용하면 그에 해당하는 NodePort와 ClusterIP 서비스가 자동으로 생성된다.

 

정리

지금까지 이야기한 내용을 정리하면 다음과 같다.

  • 쿠버네티스 컨테이너를 효율적으로 관리하기 위한 컨테이너 오케스트레이션 도구이다.
  • 쿠버네티스의 클러스터는 클러스터의 상태를 유지하는 컨트롤 플레인과 워크로드가 실행되는 워커 노드로 나누어진다.
  • 파드는 컨테이너들의 집합으로 쿠버네티스에서 가장 작은 배포 단위이다.
  • 워크로드 리소스는 파드의 생성과 관리를 보조하는 컴포넌트로, ReplicaSet, Deployment 등이 있다.
  • 서비스를 통해 원하는 파드들에 접근하는 방법을 정의할 수 있고, 서비스의 종류에는 그 용도에 따라 ClusterIP, NodePort, LoadBalancer 등이 있다.

 

마치며

아래는 내가 쿠버네티스를 공부하면서 많은 도움을 얻었던 문서들이다. 역시 공식 문서만큼 근본적인 설명을 담고 있는 곳은 없다.

 

 

개념을 어느 정도 파악하고 실제로 쿠버네티스를 사용해보려고 하면 어디서부터 시작해야할지 막막할 수 있다. 실전에 뛰어들려고 할 때는 쿠버네티스 안내서가 최고다. 공식 문서보다 훨씬 따라가기 쉽다. VuePress로 만들어져서 보기도 편한듯.

 

 

쿠버네티스는 워낙 방대한 주제인 만큼, 여러 관점에서 다양한 문서를 읽으면서 이해하는 게 좋다고 생각한다. 쿠버네티스를 배우는 분들이 기본 개념을 잡을 때 이 글이 조금이나마 도움이 됬기를 바라면서 글을 마친다.

 

@turastory

반응형
Comments