[Kubernetes] 3. Controller로 더 나아간 애플리케이션 배포

우리는 쿠버네티스의 4개 객체들의 역할과 그들의 모임으로 한 서비스를 배포하고 운영하는 데 있어 기본적인 요소라는 것을 알았습니다.

 

쿠버네티스는 서비스의 트래픽에 따라 컨테이너를 자동으로 증가시켜주는 스케일 업/아웃, 애플리케이션에 필요한 batch 작업 등 애플리케이션 설정, 배포에 더욱 빛을 발휘할 수 있는 강력한 기능을 제공합니다.

 

 

 

Controller

쿠버네티스의 컨트롤러는 이전에 설명한 객체를 이용하여 배포한 서비스들에 더 많은 기능을 추가시켜주는 존재입니다. 예를 들어, 우리의 서비스는 사용자가 많아 요청 수가 많을 것이므로 Namespace에 부여된 리소스 허용량에 한하여 트래픽량이 증가함에 따라 자동으로 컨테이너를 생성/삭제하는 오토 스케일링 기능, 내 서비스 앞단에 로그를 수집하거나 데몬을 지속적으로 동작시켜주는 DaemonSet 등 다양한 기능이 존재합니다.

 

  • Replication Controller / Set
  • DaemonSet
  • Job
  • StatefulSet
  • Deployment

쿠버네티스의 기본 객체로도 서비스를 배포하는 데는 큰 무리가 없었습니다. 그러나 Controller를 사용하면 좀 더 쉽게 배포할 수 있고, 시간을 줄일 수 있는데요. 하나씩 살펴보도록 하죠.

 

 

Replication Controller / Set

Replication Controller와 ReplicationSet은 지정된 수 만큼 Pod를 가동시키고, 관리하는 역할을 합니다. 이 컨트롤러가 관리하는 주 대목에는 3가지가 있는데, 그 대목은 아래와 같습니다.

 

  1. Replica의 수
  2. Pod Selector
  3. Pod Template

Replica의 수는 Docker Swarm에서도 제공하는 Replica처럼 컨테이너의 수, 즉 Pod의 수를 말합니다. 내가 지정한 갯수만큼 Pod을 띄우게 되며 하나씩 순차적으로 띄우게 됩니다. 

 

Pod Selector는 label selector와 동일합니다. label을 기반으로하여 Replication Controller가 관리할 Pod을 가져오는 역할을 합니다. Replication Controll가 문자열의 equal 알고리즘에 기반하여 label을 선택한다면, ReplicationSet은 Set을 기반으로 label을 선택하는데, Replication Controller와 Replication Set은 이 점만 제외한다면 거의 동일한 역할을 합니다.

 

Pod Template은 Pod의 정보가 담겨져 있는 곳으로 Pod을 추가로 기동하였을 때 어떻게 Pod을 만들지,(서비스할 포트 주소나 IP 주소의 설정 등) Pod에 대한 정보 (도커 이미지, 포트 주소, 라벨 등)를 담겨놓는 곳입니다. 이는 Replication Controller가 Pod을 재시작 혹은 추가할 때 사용할 용도이므로 매우 중요한 것입니다.

 

 

Replication Controller가 동작하는 형태는 대충 위 그림과 같습니다. Replication Controller가 가지고 있는 정보 3개를 기반으로 이미지의 정보를 가지고 있고, 작동을 한다면 이미지의 주소에서 도커 이미지를 Pull 한 후, Pod 정보에 기반하여 새로운 Pod을 생성합니다.

 

만약, 기존에 이미 label이 app:nk-app으로 되어진 Pod이 이미 존재한 상태로 Replication Controller을 생성하면, label이 app:nk-app인 Pod들은 이 순간부터 Replication Controller의 하부에 속하게 됩니다. 즉, 이 때부턴 Pod들이 Replication Controller에 의해 제어되며, 만약 이 컨트롤러에 설정된 Replica의 수보다 Pod의 수가 적으면 새롭게 Pod을 하나씩 생성합니다. (그러나 Pod의 수가 많다고 하여 기존에 동작된 Pod이 삭제되진 않습니다.)

apiVersion: v1
kind: ReplicationController
metadata:
  name: nk-app-controller
spec:
  replicas: 3
  selector:
    app: nk-app
  template:
    metadata:
      name: nk-app
      labels:
        app: nk-app
    spec:
      containers:
        - name: nk-app
          image: http://[docker-registry-address]/nk-app:latest
          ports: 
            - containerPort: 80
apiVersion: v1
kind: ReplicationSet
metadata:
  name: nk-app-controller
spec:
  replicas: 3
  selector:
    app: nk-app
  template:
    metadata:
      name: nk-app
      labels:
        app: nk-app
    spec:
      containers:
        - name: nk-app
          image: http://[docker-registry-address]/nk-app:latest
          ports: 
            - containerPort: 80

이를 YAML로 정의해보면 위와 같습니다. kind는 ReplicationController 혹은 ReplicationSet으로 맞추고, 이 컨트롤러의 이름을 nk-app-controller라고 정의하였습니다.

 

마찬가지로 Pod의 갯수를 3개로 고정하고, app selector를 이용하여 label이 app: nk-app인 Pod만을 골랐으며 Pod을 생성할 경우, label을 name: nk-app으로 잡고, 그의 도커 이미지를 spec: containers의 정의하였습니다.

 

Docker 이미지는 기존의 Docker를 사용했던 그대로 Docker engine을 사용하기 때문에 내가 만든 도커 이미지를 사용하려면 내가 사용하는 도커 이미지 레지스트리 주소를 입력한 뒤, 이미지 이름을 입력하면 되고, Dockerhub에 저장되어 있는 유명 도커 이미지(ubuntu, nginx, postgres)를 이용할 때는 도커 레지스트리 주소를 생략하고, 이미지 이름만을 입력하면 됩니다.

 

이런식으로 기존에 우리가 Pod을 하나 생성하기 위해 Pod을 하나씩 만들었던 것과 달리 Replication Controller를 이용해서 좀 더 편하게 Pod을 생성할 수 있습니다.

 

 

 

 

Deployment

그런데, Replication Controller이나 Set은 단순히 Pod을 만들고 관리하는 것에 지나칩니다. 내 애플리케이션을 외부로 서비스하려면 서비스 객체를 여전히 하나 더 만들어야 하는 번거로움이 있죠. 

 

Deployment는 Replica Controller나 Replica Set보다 좀 더 추상화된 개념입니다. 실제 운영 레벨에서는 Deployment를 자주 사용하며 저 또한 현 회사에서 주로 사용합니다. 

 

Replica Controller를 이용하여 자동으로 Pod를 생성하고 관리하는 형태를 만든 다음 Service에 배포하는 방법이 현재로써는 내 애플리케이션을 배포하는 데 있어 가장 쉬운 방법입니다.

 

그렇다면 애플리케이션이 업데이트 되어 새로 배포해야 한다면 어떻게 해야 할까요? 

 

저의 경우, Docker Swarm을 이용하여 Blue-Green 배포 방식을 이용하여 새로 업데이트된 애플리케이션을 배포한 뒤, 차례로 올려져 있던 컨테이너를 소멸시키는 방법을 이용하였습니다. 이 때, 기존에 발생했던 트래픽을 새로운 애플리케이션 컨테이너로 이동해줘야 하는 작업이 필요했는데, docker-compose에서 지원하는 deploy key를 이용하여 이를 쉽게 구현할 수 있었습니다.

 

위 그림은 쿠버네티스에서 Blue-Green 배포 방식을 사용하여 애플리케이션을 업데이트할 떄의 모습입니다. Rancher 도구를 설치하여 쿠버네티스를 운용해보신 분이라면 쉽게 이해가 되겠지만 처음 보시는 분들이라면 생소할 수도 있습니다.

 

새로운 Pod은 새로운 컨테이너가 원활히 활성화 되었을 떄 기존의 Pod이 소멸되며 만약, 새로운 Pod에서 오류가 발생했거나 활성화 하지 못했을 경우 이전의 Pod는 그대로 운용되기 때문에 안전하게 새로운 애플리케이션을 업데이트 할 수 있습니다.

 

이러한 업그레이드 방식을 우리는 롤링 업그레이드(Rolling-upgrade)라 합니다.

 

그런데, 쿠버네티스에서 이러한 배포를 하기 위해서 Replica Controller 내지 Set과 더불어 Service 객체를 모두 사용하기에는 많은 시간이 소모됩니다. 게다가 롤립 업그레이드 방식은 kubectl 명령어를 통해서 클라이언트가 수동으로 발생시키는 과정인데, 중간에 네트워크의 연결이 끊어지는 등의 장애가 발생하면 이로 인한 롤백 작업 또한 수동으로 해줘야 하는 크리티컬한 상황이 발생할 수도 있습니다.

 

이러한 작업을 자동화하고 추상화한 개념이 바로 Deployment 입니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nk-deployment
  namespace: nk-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nk-app
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    spec:
      containers:
      - image: nk-app:latest
        imagePullPolicy: Always
        name: nk-app
        ports:
        - containerPort: 80
          name: default
          protocol: TCP
      restartPolicy: Always

Deployment 하나의 객체를 사용하여 Replica와 컨테이너 이미지를 결정하고, 서비스할 애플리케이션의 포트 주소를 입력해주면 끝입니다. 여기서 주의할 점은 Deployment 자체가 Replication Controller 역할의 대체 수단이 아닌 Deployment 객체 안에 Replication Controller를 포함하고 있으며 그 중에서도 ReplicaSet을 사용한다는 점입니다.

apiVersion: v1
kind: Service
metadata:
  name: nk-service

spec:
  selector:
    app: nk-deployment

  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 8080

  type: LoadBalancer

그 뒤, Service 객체를 생성하고, 이를 label selector로 연결하면 Deployment 객체를 이용한 서비스 배포가 이루어지게 됩니다.

 

 

 

Job

Job은 여러분들이 다들 아실 법한 일련의 예약된 작업을 이야기 합니다. 쿠버네티스에서도 서버 관리 목적이나 애플리케이션 관리 등을 목적으로 단 한 번의 반복된 형태의 작업을 실행할 수 있는 방법이 있는데, 이 오브젝트가 바로 Job입니다.

 

정해진 job이 끝나면 Pod은 소멸된다는 것이 Replica Controller와의 차이입니다. 따라서 서비스가 아닌 작업을 위해 존재하는 컨트롤러입니다.

apiVersion: batch/v1
kind: Job
metadata:
  name: nk-job
spec:
  template: 
    spec:
      containers:
      - name: nk-job
        image: repo/nk-job
        command: ["python", "nk-job.py"]
        restartPolicy: Never
  backoffLimit: 4

그런데, Job에도 restartPolicy 옵션이 있는데요. 이건 왜 존재할까요? 만약 작업 진행 중에 오류가 발생했다면 그 로그를 남겨야할텐데 바로 소멸되버리면 오류가 발생했어도 추적이 힘들겠죠? 오류가 발생했을 경우, OnFailure로 설저하면 작업을 다시 시작할 수 있도록 Pod을 재생성합니다.

 

그렇다면 작업을 다시 시작하면 데이터를 ETL하는 큰 작업에 있어서는 꽤 비효율적이겠군요? 맞습니다. 다시 새로운 Pod가 생성되면서 새롭게 다시 시작하기 때문에 기존의 ETL 한 내역들을 Persistence Volume 등을 이용해서 임시로 저장하는 등의 수단을 개별적으로 마련해야 하는 단점이 존재합니다.

 

그러나 아예 해결 방법이 없는 것은 아닙니다. 이런 경우, 작업을 Pod 단위로 쪼개어 순서를 정하는 방법으로 응용할 수 있습니다. 쿠버네티스의 Job은 같은 작업을 반복할 수 있도록 제시할 수 있는 옵션이 있습니다.

apiVersion: batch/v1
kind: Job
metadata:
  name: nk-job
spec:
  completions: 3
  template: 
    spec:
      containers:
      - name: nk-job
        image: repo/nk-job
        command: ["python", "nk-job.py"]
        restartPolicy: Never
  backoffLimit: 4

completions 옵션은 해당 작업을 3번 반복하며 3번이 모두 성공으로 되어야만 Pod이 정상적으로 소멸되고, 그렇지 않으면 오류가 발생합니다. 이처럼 데이터가 큰 ETL을 같은 작업으로 쪼개어 사용할 수도 있습니다.

 

그런데, 어차피 ETL 한 뒤 변화된 데이터에 의존하지 않는다면 병렬로 처리하는 게 더 좋겠죠? 3번 반복해야 하는 작업을 병렬로 처리하고 싶을 경우는 아래와 같이 작성합니다.

apiVersion: batch/v1
kind: Job
metadata:
  name: nk-job
spec:
  completions: 3
  parallelism: 3
  template: 
    spec:
      containers:
      - name: nk-job
        image: repo/nk-job
        command: ["python", "nk-job.py"]
        restartPolicy: Never
  backoffLimit: 4

parallelism 옵션으 사용하여 한 번에 생성할 Pod 갯수를 정해주면 동시에 병렬 처리할 수 있습니다.

 

 

 

Cron Job

Cron을 아시는 분들이라면 벌써 알아챘겠지만, 우리가 원하는 작업을 예약된 스케줄링으로 동작시키고 싶을 떄 사용하는 컨트롤러 입니다.

 

데이터를 ETL 할 때, 실시간으로 들어오는 데이터에 대해서 몇 분에 한 번, 몇 시간에 한 번 작업을 돌리고 싶을 때가 있을 것입니다. 설정을 하는 방법은 Job + Unix Cron을 합친 형태로 쉽게 설정할 수 있습니다.

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: nk-cron-jobs
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: nk-cron-job
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo hello kubernetes
          restartPolicy: OnFailure

Job 설정에서 spec 항목에 schedule 설정값에 Unix Cron에서 사용한 것과 똑같이 반복 횟수를 지정하면 해당 횟수만큼 계속 반복합니다.

 

 

 

StatefulSet

StatefulSet은 쿠버네티스 1.9에서 새로이 등장한 컨트롤러입니다. 쿠버네티스의 Pod는 Docker 컨테이너처럼 애플리케이션이 종료되거나 비정상적인 오류가 발생하면 내부에 저장된 모든 스토리지와 상태까지 소멸되는 형태입니다.

 

그런데, SQL Server와 같은 데이터베이스 서버의 경우 데이터의 저장이 영구적이어야 하고, 애플리케이션의 상태가 있는 서버 애플리케이션입니다. 물론 Persistence Storage 객체를 이용해 데이터의 영구적인 저장을 유도할 수는 있지만 데이터베이스를 이중화 하는 등의 작업은 조금 어렵습니다.

 

이처럼 상태를 유지해야 하는 애플리케이션을 위해 만들어진 것이 바로 StatefulSet 컨트롤러입니다. 기존의 웹 애플리케이션 등은 수시로 재시작이 되도 영구적으로 저장되는 내용들이 없고, 서로 물려 있는 서비스가 없기 때문에 사실상 Stateless에 가까웠고, ReplicaSet 등은 이런 용도에 적합했습니다. 그러나 데이터베이스와 같이 이중화가 필요하고, 마스터 노드부터 순차적으로 생성해야 하는 Stateful Application에는 이 용도가 적합하지 않습니다. 좀 더 구체적으로 알아보기 위해 ReplicaSet의 아래 3가지 특징을 정리해봤습니다.

 

  • Pod 이름의 불규칙한 이름 생성 알고리즘
  • 순서 없는 Pod 생성
  • Persistence Volume과 Pod 1:1 한계

Pod이 재생성될 때마다 불규칙한 이름을 생성하는 것은 기존의 Pod이 마스터 노드였는지, 슬레이브 노드였는지 구분할 수 있는 매체가 무의미해지는 조건이 됩니다. 순서 없는 Pod 생성으로 인하여 마스터 노드보다 슬레이브 노드가 먼저 생성되는 문제가 발생하고, 영구적 볼륨 스토리지가 Pod 하나에 연결되면 다른 Pod에는 연결할 수 없는 것은 각 노드가 같은 데이터로 동기화 할 수 없는 문제점이 존재하게 되는 것이죠.

 

만약 ReplicaSet으로 이들을 구성한다고 가정하면, 위와 같이 구성해야 합니다. 각 Pod별로 마스터/슬레이브를 구성하고 마스터, 슬레이브 마다 ReplicaSet을 그 갯수만큼 생성해야하지요.

 

StatefulSet을 사용하게 되면, 이들의 Pod과 Volume을 전체적으로 관리해주기 때문에 여러 개의 ReplicaSet을 사용해서 개별적으로 관리해야 하는 불편함을 덜어줄 수 있습니다.

apiVersion: apps/v1
kind: StatefulSet
metadata:
 name: postgres
spec:
 selector:
   matchLabels:
     app: postgres

 serviceName: "postgres"
 replicas: 3
 template:
   metadata:
     labels:
       app: postgres

   spec:
     terminationGracePeriodSeconds: 10
     containers:
     - name: postgres
       image: postgres:10.1
       ports:
       - containerPort: 5432
         name: postgres-data

       volumeMounts:
       - name: postgrs-data
         mountPath: /var/lib/postgresql/data

 volumeClaimTemplates:
 - metadata:
     name: postgres-data

   spec:
     accessModes: [ "ReadWriteOnce" ]
     storageClassName: "standard"
     resources:
       requests:
         storage: 4Gi

volumeClaimTeplates를 이용해서 각 Pod마다 Volume Claim과 Volume을 생성하도록 하면 됩니다. StatefulSet은 기본적으로 Pod을 순차적으로 기동하며 삭제도 순차적으로 삭제합니다.

 

이 옵션은 podManagementPolicy라는 옵션을 통해 설정할 수 있는데, 위 파일에서는 별도로 설정이 되어 있지 않지만 기본적으로 OrderedReady로 설정되어 동작합니다. 만약 순차적이지 않고 동시에 가동되도록 설정하고 싶다면, 아래와 같이 pararell로 변경하면 한꺼번에 가동과 삭제가 이루어집니다.

apiVersion: apps/v1
kind: StatefulSet
metadata:
 name: postgres
spec:
 selector:
   matchLabels:
     app: postgres

 serviceName: "postgres"
 podManagementPolicy: Parallel
 replicas: 3
 template:
   metadata:
     labels:
       app: postgres

   spec:
     terminationGracePeriodSeconds: 10
     containers:
     - name: postgres
       image: postgres:10.1
       ports:
       - containerPort: 5432
         name: postgres-data

       volumeMounts:
       - name: postgrs-data
         mountPath: /var/lib/postgresql/data

 volumeClaimTemplates:
 - metadata:
     name: postgres-data

   spec:
     accessModes: [ "ReadWriteOnce" ]
     storageClassName: "standard"
     resources:
       requests:
         storage: 4Gi

StatefulSet은 Pod 이름을 생성하는 알고리즘이 규칙적이기 떄문에 중간에 다른 Pod이 삭제되더라도 Volume Claim과 Volume은 그대로 유지됩니다. 그리고 다시 해당 Pod을 생성할 경우, 기존에 사용했던 이름 그대로 다시 생성되기 떄문에 살아있는 Volume Claim과 Volume으로 연결되어 재사용할 수 있게 됩니다.

 

 

 

마치며...

여기까지 쿠버네티스의 컨트롤러에 대해서 설명해봤습니다. Controller를 한 줄로 요약하면 쿠버네티스에서 리소스(컨테이너, 볼륨, 볼륨 설정, 사용자, 사용자 설정)를 제어하기 위한 수단입니다. 기본적으로 쿠버네티스는 이러한 리소스를 코드로 사용할 수 있도록 object를 제공하며 커스텀 할 수도 있습니다.

 

더 나아가서 이 Controller도 여러분들이 원하는 Controller를 개발할 수 있습니다. 기본적으로 쿠버네티스는 Go 언어로 되어 있고, 예를 들어 우리 회사의 서비스가 쿠버네티스에서 제공하는 Replication Controller만으로는 뭔가 부족하다 생각한다면 이러한 컨트롤러를 만들 수도 있고, 네트워크를 위한 오브젝트도 만들 수 있습니다.

 

실제 이러한 비슷한 사례를 가진 회사 중에 카카오의 DKOS가 있습니다. 카카오의 DKOS는 자사의 쿠버네티스 서비스를 외부로 배포하기 위해 개발된 컨트롤러로 알려져 있고, 현재도 사용 중에 있는 것으로 알고 있습니다.

 

이처럼 여러분들이 운영하는 쿠버네티스 클러스터에도 여러분만의 쿠버네티스 클러스터 운영 환경을 만들 수 있도록 컨트롤러 개발 도구를 제공하는데, 대표적으로 Operator SDK와 KubeBuilder가 있습니다. 차후 기회가 된다면 이 부분도 별도로 글을 작성해보고 싶네요.

 

 

 

comments powered by Disqus

Tistory Comments 0