배포를 안전하게 - 카나리 배포, 롤백

Image not Found

배포에서 속도와 안전성 두 마리 토끼를 잡기란 쉽지 않습니다. 이 글에선 버즈빌에선 어떻게 안전성 문제를 해결했는지 소개합니다.

배포를 안전하게 - 카나리 배포 전략, 롤백

이전 글 - 배포를 쉽게 - 에서 버즈빌의 배포 시스템 구조를 설명했습니다. 이번 글에선 버즈빌의 주요 런타임 환경인 AWS EKS 기반의 쿠버네티스(Kubernetes)에 배포 과정과 구현 방법을 자세하게 설명합니다. 또한 고급 배포 전략인 카나리 배포와 롤백에 대한 구현도 설명하도록 하겠습니다.

깃헙에서 쿠버네티스까지

Architecture

쿠버네티스 배포는 다음 두 가지 단계로 이루어져 있습니다.

  1. 도커 이미지 빌드한다.
  2. 쿠버네티스 메니페스트(manifest) 생성 후 반영한다.

커밋 SHA는 유니크(unique) 하지만 길고 읽기 어려우며 빌드 환경을 표현하지 못한다는 단점이 있습니다. 버즈빌의 경우 UI 상에서 SHA 값을 지정할 수 있고 빌드 환경에 영향을 받지 않기 때문에 SHA 값을 선택했습니다 (참고).

버즈빌은 도커 이미지를 모든 커밋 별로 빌드하고 있습니다. 푸쉬 이벤트가 발생하면 CI에서 도커 이미지를 빌드하고 이미지를 저장소에 저장합니다. 이미지 태그는 커밋 SHA 값을 사용해 유니크(unique)함을 보장합니다. 또한 각 커밋별 이미지 존재 여부는 커밋 상태에 표기해서 배포 시 확인할 수 있습니다 (i.e, docker-image 컨택스트).

깃헙 상태

Commit Status


이미지 빌드 후 쿠버네티스 메니페스트 생성이 필요합니다. 저희는 쿠버네티스 메니페스트 생성 도구로 헬름(Helm, 문서 참고)을 선택해서 사용하고 있습니다. 그리고 모든 마이크로서비스는 동일한 헬름 차트로 쿠버네티스 메니페스트를 생성하며, 각 서비스별로 다른 메니페스트를 생성하기 위해 values.yaml 파일과 helm--set옵션으로 오버라이드를 합니다. 특히, --set 옵션은 위에서 빌드한 이미지 태그를 동적으로 지정할 수 있게 합니다. 헬름 커맨드 예시는 아래와 같습니다.

헬름 커맨드 예시
helm upgrade \
	buzzvil-microservice \
	buzzvil-chartmuseum/buzzvil-chart \  # buzzvil-chartmusuem: 차트 저장소, buzzvil-chart: 단일 헬름 차트
	--install \
	-f release/values.yaml \
    --set global.version=COMMIT_SHA  # 커밋 SHA 값을 이미지 태그로 지정

내부적으로 버즈빌은 직접 헬름 명령어를 실행하지 않고 배포 도구인 스피네이커(Spinnaker, 공식 문서)를 통해 간접적으로 실행하고 있습니다. 스피네이커 파이프라인은 헬름 차트는 버즈빌 내부 차트 저장소에서, values.yaml 파일은 각 마이크로서비스의 깃헙 레포지토리에서 가져오고 있습니다. 마지막으로 도커 이미지 태그 - 커밋 SHA - 는 깃헙 배포 이벤트를 통해서 받습니다. 그렇다면 스피네이커 파이프라인은 어떻게 배포 이벤트를 수신할까요?

스피네이커(Spinnaker) 파이프라인은 여러 종류의 트리거를 제공하며, 그 중 웹훅 트리거를 제공하고 있습니다 (문서 참고). 버즈빌은 웹훅 트리거 설정을 통해 깃헙 배포 이벤트로 파이프라인이 트리거 되도록 하였습니다. 배포 이벤트는 레포지토리 정보, 배포 정보 등을 가지고 있어 웹훅 트리거의 제약 조건(constraint)으로 사용하기 용이합니다. 버즈빌은 레포지토리 이름( repository.name)과 배포 환경(deployment.environment)을 트리거 제약 조건으로 사용하여 특정 마이크로서비스가 배포되면 파이프라인이 트리거되도록 합니다.. 참고로, 배포 테스크(deployment.task)도 사용하는데 다른 런타임 환경에 대한 배포와 구분하기 위한 용도로 사용하고 있습니다.

스피네이커 트리거

Spinnaker Trigger


또한, 스피네이커는 파이프라인 표현식을 통해 전달 받은 페이로드에 접근이 가능합니다(문서 참고). 버즈빌은 파이프라인 표현식을 통해 배포 된 커밋 SHA을 가져올 수 있으며, 동적으로 이미지 태그 설정이 가능해 집니다(i.e., ${trigger['payload']['deployment']['sha']}).

스피네이커 템플릿

Spinnaker template

# 템플릿과 동일한 명령어
helm upgrade \
	buzzvil-microservice \
	buzzvil-chartmuseum/buzzvil-chart \
	--install \
	-f release/values.yaml \
    --set global.version=${trigger['payload']['deployment']['sha']}

마지막으로 스피네이커는 동적으로 생성된 쿠버네티스 메니페스트를 배포하게 됩니다.

카나리 배포

Canary Demo

버즈빌은 두 가지 배포 전략을 - 롤링(rolling) 업데이트와 카나리 배포 - 선택할 수 있습니다. 롤링 업데이트란 점진적으로 서버를 새로운 버전으로 업데이트하는 전략이며 쿠버네티스트 Deployment 리소스가 기본으로 제공하고 있습니다. 반면 카나리 배포란 일부 서버에만 새로운 버전을 배포하고, 일부 트래픽을 새 버전으로 분산하는 방법입니다. 카나리 배포는 A/B 테스트를 가능하게 하고 피해 반경을 좁혀 배포의 안정성을 높여줍니다. 하지만 안타깝게 카나리 배포는 쿠버네티스에서 지원하고 있지 않아 직접 구현이 필요합니다.

카나리 배포는 다음 단계로 이루어져 있습니다.

  1. 카나리 릴리즈를 생성한다.
  2. 카나리 릴리즈로 일부 트래픽을 분산한다.
  3. 카나리 릴리즈를 정리한다.

먼저 분산된 트래픽을 처리하기 위한 카나리 릴리즈를 준비합니다. 버즈빌은 카나리 릴리즈도 위에서 설명한 것과 동일한 방법으로 배포합니다. 동일한 헬름 차트와 동일한 values.yaml 파일이 사용됩니다. 다만, 카나리 릴리즈 설정을 위해 헬름 차트 global.canary.enabled를 제공하며, 이는 내부적으로 쿠버네티스 리소스의 이름에 -canary란 접미사를 붙여서 메인(main) 릴리즈와 구분합니다. 예를 들어, 메인 릴리즈의 Deployment 리소스 명이 microservice-prod 라면 카나리 릴리즈는 microservice-prod-canary 란 이름을 갖습니다.

스피네이커 카나리 템플릿

Spinnaker template 1

# 템플릿과 동일한 명령어
helm upgrade \
	buzzvil-microservice \
	buzzvil-chartmuseum/buzzvil-chart \
	--install \
	-f release/values.yaml \
  --set global.version=${trigger['payload']['deployment']['sha']} \
  --set golbal.canaryMode.enabled=true \
  --set global.canaryMode.weigth=0 \
  --set replicaCount=2

이제 새로운 버전의 카나리 릴리즈가 준비됐고 트래픽 일부를 카나리 릴리즈로 전달해야 됩니다. 버즈빌은 서비스 매쉬(Service Mesh)로 이스티오(Istio)를 사용하고 있으며 이스티오는 트래픽 시프팅(Traffic shifting, 문서 참고) 기능을 지원하고 있습니다. 트래픽 시프트를 위해선 이스티오의 VirtualService 리소스가 필요하며, VirtualService 리소스에서 기존 릴리즈와 카나리 릴리즈로 보낼 트래픽 비중(weight)을 설정하게 됩니다. 버즈빌의 단일 헬름 차트도 VirtualService 리소스를 포함하고 있으며 트래픽 비중을 지정할 수 있도록 global.canaryMode.weight 를 제공하며, 내부적으로 하나의 VirtualService만 생성해서 메인 릴리즈와 카나리 릴리즈로 요청을 분산할 수 있도록 했습니다. 즉, VirtualService만은 -canary 접미사를 붙이지 않고 두 릴리즈가 공유하도록 했습니다.

스피네이커 카나리 템플릿 2

Spinnaker template 2

# 템플릿과 동일한 명령어
helm upgrade \
	buzzvil-microservice \
	buzzvil-chartmuseum/buzzvil-chart \
	--install \
	-f release/values.yaml \
  --set global.version="${trigger['payload']['deployment']['sha']}" \
  --set golbal.canaryMode.enabled=true \
  --set global.canaryMode.weigth=5
  --set replicaCount=2

VirtualService 예시
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: microservice-prod
  namespace: microservice
spec:
  hosts:
    - microservice-prod
  http:
    - name: primary
      route:
        - destination:
            host: microservice-prod
          weight: 95
        - destination:
            host: microservice-prod-canary
          weight: 5

위에서 말한 것 같이, 카나리 배포는 A/B 테스트를 용이하게 합니다. 버즈빌은 모니터 도구로 데이터독을 사용하고 있고 데이터독은 각 버전별로 메트릭을 제공하고 있습니다. 이 과정을 통해 사용자는 두 버전 간에 비교로 새 버전을 검증할 수 있습니다. 또한 새 버전에서 이슈가 확인돼도 일부 요청만 영향이 있기 때문에 피해 반경을 좁힐 수 있습니다.

검증이 끝난 후 트래픽을 다시 메인으로 옮기고 카나리를 릴리즈를 정리하도록 합니다. 그리고 사용자는 전체 배포를 진행할지 또는 배포를 취소할지 결정하도록 합니다.

스피네이커 카나리 템플릿 3

Spinnaker template 3

# 템플릿과 동일한 명령어
helm upgrade \
	buzzvil-microservice \
	buzzvil-chartmuseum/buzzvil-chart \
	--install \
	-f release/values.yaml \
  --set global.version=${trigger['payload']['deployment']['sha']} \
  --set golbal.canaryMode.enabled=true \
  --set global.canaryMode.weigth=0 \
  --set replicaCount=0

추가적으로, 버즈빌은 배포하는 시점에서 카나리 릴리즈의 레플리카 수와 트래픽 비중을 선택할 수 있도록 합니다. 해당 설정 값은 깃헙 이벤트의 deployment.payload 필드로 전달합니다. 버즈빌은 깃플로이(Gitploy)의 다이나믹 페이로드(dynamic payload) 기능 - 배포 시점에서 동적으로 payload 를 전달 - 을 통해서 동적으로 설정합니다. 그리고 배포 이벤트를 수신한 스피네이커에서 페이로드에 접근해 값을 가져와서 사용하고 있으며, 만약 값이 없을 시 기본 값을 사용하도록 되어 있습니다.

깃플로이 설정
envs:
  - name: prod
    task: deploy:kubernetes
    required_context: ['docker-image']
    serialization: true
    dynamic_payload:
      enabled: true
      inputs:
        canaryEnabled:
          type: select
          options:
            - "true"
            - "false"
          required: true
          description: Deploy in a canary strategy.
          default: "true"
        canaryWeight:
          type: number
          required: false
          description: Set traffic weight to canary.
          default: 5
        canaryReplicaCount:
          type: number
          required: false
          description: Set canary replica count.
          default: 2

스피네이커 파이프라인 표현식
  • replicaCount: ${ trigger['payload']['deployment']['payload']['canaryReplicaCount']?: "2" }
  • weight: ${ trigger['payload']['deployment']['payload']['canaryWeight']?: "5" }

롤백

Rollback Demo

롤백(Roll-back)이란 이전 버전으로 되돌아가는 것을 의미합니다. 일반적으로, 현재 버전에 이슈가 있는 경우 롤백을 통해서 상태를 빠르게 회복시킬 수 있습니다.

버즈빌은 롤백을 배포 히스토리의 커밋 SHA으로 구현하고 있습니다. 배포 히스토리는 각각의 커밋 SHA을 포함하고 있고 이를 다시 배포하는 방식입니다. 커밋 SHA를 이용한 이유는 쿠버네티스 메니페스트 생성을 결정하는 values.yaml 파일이 각 레포지토리에 포함되어 버저닝되어 있기 때문입니다. 즉, 이전 버전의 values.yaml 파일은 이전과 동일한 쿠버네티스 메니페스트를 생성하게 됩니다. 아래 스피네이커 템플릿을 보면 깃헙 레포지토리에서 values.yaml 파일을 가져올 때 커밋을 동적으로 지정하는 것을 볼 수 있습니다.

스피네이커 깃헙 템플릿

Spinnaker template


일반적으로 우리는 롤백을 위해서 코드 리버트(revert)하며 피해를 장기화시킵니다. 또한, 메신저로 “배포를 잠시 멈춰주세요!”라는 경고를 보냅니다. 하지만 이런 방식은 문제를 더 가중시키며 근본적인 문제를 해결하지 못합니다. 버즈빌도 동일한 문제를 경험했습니다. 그러나 현재는 한 번의 클릭으로 롤백이 가능하며 문제 해결 동안 락(Lock)을 통해 배포하지 못하도록 막고 있습니다.

마치면서

버즈빌은 지속적으로 조직이 더 자신감 있고 안전하게 배포 가능하도록 시스템을 개선해 왔습니다. 현재는 이전 보다 편리하면서도 안정적으로 배포가 이루어지고 있습니다. 만약 여러분의 팀이 속도와 안정성 사이에서 고민하고 있다면 이 글이 도움이 되길 바랍니다. 긴 글 읽어주셔서 감사합니다. :)

참고

버즈빌 개발자 지원하기 (클릭)

버즈빌 테크 리크루터와 Coffee Chat하기 (클릭)

You May Also Like

버즈빌, 아마도 당신이 원하던 회사!

지원하기