Istio와 Spinnaker를 활용한 Blue-Green + Canary 자동 배포 전략 도입기

정종현

데브시스터즈에서는 쿠키런: 오븐브레이크 외에도 많은 내부 개발팀과 외부 개발사가 개발한 많은 게임들을 운영, 관리하고 있습니다. 유저분들께 더 재밌고 풍성한 컨텐츠와 경험을 제공하기 위해서는 잦은 업데이트가 필연적인데요. 이렇듯 빈번하게 일어나는 배포를 조금 더 효율적이고 안전하게 하기 위해서 여러 배포 전략과 기술들을 연구하고 있습니다. 이번 글에서는 그 중 하나인 Kubernetes 환경 하에서의 Blue-Green 전략과 Canary 전략의 장점을 조합해서 실제 서비스에 적용한 사례를 소개합니다.

0. 심장 건강을 지키고 싶어요.

서버 엔지니어, 특히 DevOps 엔지니어라면 실제 서비스 하고 있는 제품을 업데이트 하는 일은 언제나 떨리고 부담스럽습니다. 아무리 많은 테스트와 QA를 거치더라도 실제 배포하려는 순간은 심장이 두근대기 마련이죠. 특히, 여러 게임을 배포해야 하는 팀 특성 상 배포가 몰리거나, 핫픽스를 해야 하는 순간이 닥치면 모두가 힘든 상황이 벌어졌습니다. 이런 상황이 반복되면서, 내부적으로 이런 물음에 다달았습니다.

어떻게 하면 고통 받지 않고 편안하게 게임을 업데이트할 수 있을까?

1. 요구 사항

지금까지는 Kubernetes가 자체적으로 지원하는 하나를 띄우고 하나를 끄는 형식의 Rolling Update 방식을 사용하고 있었습니다. 이 배포 전략으로는 위에 있는 물음을 해결하지 못한다는 결론에 다달았습니다.

그래서 팀 내부적으로 모여 어떤 요구 사항들이 이루어지면 위의 물음을 해결할 수 있을 지 논의하였고, 회의 끝에 모은 요구 사항은 다음과 같았습니다.

  • 구 서버로 Rollback이 쉽게 가능해야 한다.
  • 신 서버가 정상 작동하는지 모니터링 및 디버깅 할 수 있어야 한다.
  • 신 서버의 Warm-up이 가능해야한다.
  • 업데이트에 걸리는 시간이 단축되어야 한다.

회의가 끝난 후 위의 요구 사항을 만족하는 배포 전략을 연구하기 시작했습니다.

2. 무중단 배포

배포 전략은 크게 두 분류로 나눌 수 있습니다. 첫째는 중단 배포 방식입니다. 트래픽이 없는 상황에서 업데이트가 일어나기 때문에 가장 간단한 전략인 구 서버를 모두 종료시킨 후 신 서버를 배포하는 방식인 Recreate 전략이 가능합니다. 하지만 이 방식은 점검을 수반해야 하기에 유저 경험 측면에서 좋지 않습니다. 그래서 데이터베이스의 스키마 변경, 서버 간 프로토콜 변경과 같은 구 서버와 신 서버가 공존할 수 없을 때에만 제한적으로 사용합니다.

둘째는 무중단 배포 방식입니다. 무중단 배포 방식은 말 그대로 서비스의 중단 없이 구 서버에서 신 서버로 업데이트하는 방식입니다. 어떻게 이게 가능할까요? 무중단 배포 방식에는 많은 전략이 있지만 이 중에 3개, Rolling Update, Blue-Green, Canary 전략을 소개합니다.

Rolling Update

Rolling Update Overview
Rolling Update Overview

Rolling Update Overview

Ramped, Incremental 이라고도 불리는 Rolling Update는 위와 같이, 서버가 조금씩 교체되면서 모든 서버가 교체될 때까지 진행하는 배포 전략입니다.

해당 배포 전략은 Kubernetes에서 지원하는 배포 전략으로 maxSurge, maxUnavailable 파라미터 값을 통해 의도한 파드의 수에 대해 생성할 수 있는 최대 파드 수와 업데이트 도중 사용할 수 없는 최대 파드의 수를 지정할 수 있습니다.

무중단 배포를 할 때 저희가 기존에 사용하고 있었던 전략이며, 서비스의 안정성을 위해 maxUnavailable 값을 최소로 하여 최대한 천천히 rolling update 하고 있습니다.

Blue-Green

Blue-Green Overview
Blue-Green Overview

Blue-Green Overview

Red-Black 이라고도 불리는 Blue-Green 전략은 구 서버와 똑같은 스펙으로 신 서버를 띄운 후 신 서버로 트래픽을 돌리는 전략입니다. 이 전략은 구 서버와 신 서버가 모두 떠 있어 신 서버에 문제가 있는 경우 트래픽만 구 서버로 전환하면 되기 때문에 Rollback이 쉽고, 업데이트 과정에서 소요되는 시간이 적습니다. 하지만 원래 서버의 스펙의 2배로 띄워야 하고 서버셋(구 서버, 신 서버)이 같이 뜰 수 있도록 환경을 조성해야 합니다.

Canary

Canary Overview
Canary Overview

Canary Overview

Canary의 사전적 의미는 카나리아라는 참새목의 새입니다. 유독가스에 굉장히 민감한 동물이라 광산에서 유독가스의 누출을 확인하는 용도로 사용되었다고 합니다. 이처럼 일부만 신 서버로 교체하여 모니터링과 디버깅을 한 후, 문제가 없는 경우 모든 서버를 교체하는 방식으로 이루어집니다.

3. Blue-Green + Canary 도입

여러 연구 끝에 Blue-Green 전략처럼 구 서버와 같은 스펙으로 신 서버를 미리 프로비저닝하고, Canary 전략처럼 신 서버에 조금의 트래픽을 흘려 검증 과정을 거친 후 모든 트래픽을 옮기는 방식을 도입하기로 결정하였습니다. 파이프라인의 개요는 다음과 같습니다.

  • i. 구 서버와 같은 스펙으로 신 서버를 프로비저닝한다.
  • ii. 약간의 트래픽을 신 서버로 보낸다.
  • iii. 신 서버를 집중 모니터링, 디버깅 후 신 서버로의 트래픽 양을 높힌다.
  • iv. 신 서버가 충분히 안정되면 구 서버를 종료한다.

파이프라인이 개념적으로는 간단하지만, 이를 구현하는 것에는 몇 가지 기술적 어려움이 있으며, 파이프라인을 제공하기 위해서는 여러 툴들이 필요합니다. 하나 하나씩 살펴보겠습니다.

트래픽 제어; Istio

위 파이프라인에서 핵심적인 기술 요소는 트래픽 제어입니다. 구 서버와 신 서버 간의 트래픽 양을 제어할 수 있어야 합니다. 트래픽 양을 제어하는 방식은 Kubernetes의 서비스 네트워킹(kube-proxy)를 이용하거나, Nginx와 같은 Ingress Gateway를 사용하는 방식, Istio와 같은 Service Mesh Provider를 사용하는 방식 등이 있습니다.

저희는 Istio를 이미 도입하여 사용하고 있어, 아래와 같이 Virtual Service를 통하여 특정 host에 가중치를 정의할 수 있었습니다.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
...
spec:
  ...
  http:
  - route:
    - destination:
        host: old-server
      weight: 90
    - destination:
        host: new-server
      weight: 10

그리고 파이프라인의 자동화를 위하여 Virtual Service를 조작하는 이미지를 만들어 Kubernetes의 Job을 통해 쉽게 트래픽을 제어할 수 있도록 했습니다.

var targetService = flag.String("targetService", "", "Target Service Name. (required)")
var percentage = flag.Int("percentage", 0, "Target gets $percentage of traffic.")

...
    vsClient := ic.NetworkingV1alpha3().VirtualServices(*namespace)
    vs, err := vsClient.Get(*virtualService, metav1.GetOptions{})

    for _, http := range (vs.Spec.Http) {
      ...

      // 변동된 weight에 의해 rebalancing
      var sumOfWeight int32 = 0
      for _, route := range http.Route {
        if (previousWeight == 100) {
          route.Weight = int32((100 - *percentage) / len(http.Route))
        } else {
          route.Weight *= (100 - int32(*percentage)) / (100 - previousWeight)
        }
        sumOfWeight += route.Weight
      }
      for _, route := range http.Route {
        if sumOfWeight == 100-int32(*percentage) {
          break
        }
        route.Weight++
        sumOfWeight++
      }

      // target service 생성
      if *percentage > 0 {
        http.Route = append(http.Route, &istiov1alpha3.HTTPRouteDestination{
          Destination: &istiov1alpha3.Destination{
            Host: *targetService,
          },
          Weight: int32(*percentage),
        })
      }

     ...

      _, updateErr := vsClient.Update(vs)
      if updateErr != nil {
        return updateErr
      } else {
        continue
      }
    }
...
# requires rendering of spinnaker
...
apiVersion: batch/v1
kind: Job
...
spec:
  template:
    spec:
      serviceAccountName: istio-traffic-shifter
      containers:
      - name: istio-traffic-shifter
        image: <REDACTED>
        command:
        - "/bin/ash"
        - "-ecx"
        - "/istio-traffic-shifter \
          -virtualService vs \ 
          -targetService new-server \
          -percentage 10"
...

파이프라인 만들기; Helm, Spinnaker

Helm

게임 서버와 관련된 Kubernetes 리소스는 모두 Helm Chart로 관리하고 있습니다. 즉 게임을 서비스하기 위한 백엔드 컴포넌트는 차트 하나를 설치함으로써 프로비저닝이 가능합니다. 이를 이용하여 Blue와 Green를 쉽게 동시에 공존하게 만들 수 있습니다.

.
├── Chart.yaml
├── charts
│   ├── couchbase-0.1.2.tgz
│   ├── devplay-0.2.0.tgz
│   ├── mysql-0.13.1.tgz
│   └── redis-4.2.10.tgz
├── requirements.lock
├── requirements.yaml
├── templates
│   ├── game
│   │   ├── deployment.yaml
│   │   ├── ingress.yaml
│   │   ...
│   │   └── service.yaml
│   └── optool
│       ├── deployment.yaml
│       ├── ingress.yaml
│       ...
│       └── service.yaml
├── test_values.yaml
└── values.yaml

Spinnaker

위에서 언급한 일련의 과정을 직접 사람이 한다면 배포의 리스크가 크게 줄지 않을 것입니다. 배포 시간은 오히려 더 오래 걸릴테고, 아무리 체크리스트를 만든다해도 사람이 하는 이상 휴먼 에러가 발생할 수 밖에 없습니다. 또한, 개발사에게 파이프라인을 제공할 수도 없습니다.

저희는 배포 파이프라인을 위해 Netflix에서 만들고 Google이 확장한 오픈소스 프로젝트인 Spinnaker를 도입했습니다. Spinnaker에서는 Amazon Web Services, Azure 등 여러 Cloud Provider를 지원하는데, 저희는 Kubernetes v2를 Provider로 선택했습니다.

Spinnaker Pipeline Overview
Spinnaker Pipeline Overview

Spinnaker Pipeline Overview

또한, Spinnaker에서는 Manual Judgement라는 Stage를 지원합니다. Canary를 띄운 후 집중 모니터링하고 디버깅하는 동안 해당 Stage를 활용하여 파이프라인을 펜딩할 수 있습니다.

Spinnaker Manual Judgement
Spinnaker Manual Judgement

Spinnaker Manual Judgement

이 사이에 Grafana, Jaeger 등을 이용하여 의도한 대로 동작하는 지 확인합니다. 이번 사례의 경우 서버가 JVM 위에서 구동되었기 때문에 JMX를 이용하여 더 자세히 디버깅할 수 있었습니다.

Grafana
Grafana

Grafana

Jaeger
Jaeger

Jaeger

또한, 이 모든 과정을 Email, Slack, SMS를 통해서 알람 받을 수 있습니다. 해당 게임사와 저희 팀은 Slack으로 커뮤니케이션하고 있었기에 아래처럼 실제 배포되고 있는 상황을 전달받을 수 있었습니다.

Spinnaker Slack Notification
Spinnaker Slack Notification

Spinnaker Slack Notification

4. Before-After

이제 배포 전략이 개선 되었으니 앞서 세웠던 요구 사항을 확인해보겠습니다.

구 서버로 Rollback이 쉽게 가능해야 한다.

마지막 구 서버를 Teardown 하기 전에는 언제든지 위에서 구현해놓은 트래픽 제어 툴을 이용해서 트래픽을 구 서버로 원복할 수 있습니다.

신 서버가 정상 작동하는지 모니터링 및 디버깅 할 수 있어야 한다.

Grafana, Jaeger를 통해서 일부 트래픽이 흘러간 신 서버에 대해서 모니터링이 가능합니다. 서버의 기본적인 메트릭 뿐만 아니라, Prometheus의 커스텀 메트릭을 통해 새로 추가된 기능 등이 잘 작동하는 지 확인할 수 있습니다. 또한, Jaeger를 통해 서버에 들어오는 요청들을 Tracing 하고, 병목 지점이 없는지 확인할 수 있습니다. 이번 사례의 경우 JMX를 통해 자세한 디버깅도 가능했습니다.

신 서버의 Warm-up이 가능해야한다.

JVM의 경우 충분한 성능을 보장받기 위해서는 Warm-up이 중요합니다. 사실 JVM 뿐만 아니라 일반적인 상황에서도 Warm-up 할 시간이 있다면 서버의 안정성은 충분히 올라갑니다. 개선된 배포 전략은 일부 트래픽을 새로 프로비저닝 된 서버에 부여함으로서 유저 평균적인 레이턴시와 경험을 위협하지 않고 Warm-up이 가능합니다.

업데이트에 걸리는 시간이 단축되어야 한다.

기존의 Rolling Update 전략의 경우 서비스의 안정성을 위하여 1대씩 천천히 교체해야 했습니다. 그러다보니, 수십대를 교체해야 하는 경우 업데이트하는데만 걸리는 시간이 거의 1시간 이상이 소요되었습니다. 하지만, 이번 배포 전략은 크게 2단계(트래픽 10% 이동, 트래픽 100% 이동)로 업데이트의 걸리는 시간이 거의 1/4로 줄었습니다.

5. 앞으로 더 나아가야 할 방향은

이번 개선된 배포 전략을 도입하면서 배포 관련하여 많은 프로세스의 효율성과 안정성이 올라가는 경험을 했습니다. 하지만 아직도 더 자동화할 수 있는 부분이, 개선할 수 있는 부분이 많다고 생각합니다.

Automated Canary Analysis

신 서버가 정상 작동하는지 확인 하는 부분은 사람이 관여해야만 하는 작업일까요? 위 파이프라인을 도입 한 이후 경험을 돌이켜보면 암묵적으로 체크리스트를 만들고 해당 기준을 넘어설 때 안정성을 확인하는 것을 느낄 수 있습니다.

Spinnaker Automated Canary Analysis
Spinnaker Automated Canary Analysis

Spinnaker Automated Canary Analysis

그래서 이 기준을 여러 메트릭에 걸쳐 명시적으로 정의하고, 해당 조건을 충족하면 다음 과정을 진행하도록 하여 Manual Judgement 부분을 자동화할 수 있습니다. 이렇게 되면 여러 관리자가 해당 파이프라인에 관여할 때 서로 다른 기준들을 일원화 할 수 있고, 실수 또한 줄일 수 있어 안정성을 증대시킬 수 있습니다.

Argo

가장 최근에 개념증명(PoC)를 진행했던 프로젝트는 CNCF Foundation에서 incubating project로 관리되고 있는 Argo입니다. 특히 이 프로젝트에서 Argo-rollouts이 Blue-Green, Canary 배포 전략을 지원합니다.

Argo Rollouts
Argo Rollouts

Argo Rollouts

특히 CRD로 관리되기 때문에 Kubernetes와 궁합이 잘 맞습니다. 잘 적용할 수 있다면 Spinnaker에서의 매우 긴 Pipeline을 정리하고 간단명료하게 매니페스트로 리소스를 관리할 수 있습니다. 그리고 Nginx와 같은 Ingress Controller, Istio와 같은 Service Mesh Provider와의 Integration 역시 기본적으로 지원하는 점도 도입하는 장벽을 낮춥니다. 그리고 Golang으로 구성되어 있어 Kubernetes와 같은 언어 베이스를 갖는 것도 장점으로 볼 수 있습니다. 또한, Analysis 기능을 지원하여 위에서 언급한 Automated Canary Analysis도 달성할 수 있습니다.

Clutch

운영을 하다보면 많은 기술적 요청을 받게 되는데, 이는 회사 혹은 프로젝트마다 구성된 아키텍쳐와 스택마다 상이하기 때문에 비정형적인 업무를 많이 하게 됩니다. 그리고 개발자 뿐만 아니라 PM, QA, 기획 같은 비개발자 분들과도 많이 협업하게 됩니다. 이러한 요청을 매번 인프라 팀에서 매뉴얼하게 처리하면 일의 효율이 떨어지기 때문에, 저희는 백오피스 툴을 개발하고 있습니다.

개발 진행 중에서 연구한 툴이자 프레임워크가 Lyft에서 공개한 Clutch입니다. 분산되어 있는 인프라의 컨트롤 타워 같은 셈입니다. 자주하고 필요로 하는 업무들을 Task 단위로 정규화하고, 이를 UI를 통해서 수행할 수 있도록 자동화 할 수 있습니다.

Clutch
Clutch

Clutch

6. 마치며

이번 도입기를 거치며 다시 한번 우리 팀의 철학이자 존재 이유를 되새길 수 있었습니다.

동료들이 편하고 행복하게 일하도록 만들자!

이 배포 방식이 적용된 이후로 배포 완료 후 핫픽스를 하는 케이스가 많이 줄었고, 배포를 위해 새벽까지 남아 있는 일이 많이 사라졌습니다. 그리고 모두가 배포에 대해 신뢰성이 높아져 공격적인 업데이트의 자신감도 많이 높아졌습니다.

혹시 배포 때문에 잠 못 이루는 분들이 계시다면, 한 번쯤 도입을 고려해보시길 권해드립니다.

데브시스터즈는 최고의 인재를 찾고 있습니다.

데브시스터즈에서는 능력있는 DevOps 엔지니어를 찾고 있습니다.
자세한 내용은 채용 사이트를 확인해주세요!
게임인프라KubernetesDevOps

© 2020 Devsisters Corp. All Rights Reserved.