Kubernetes

Blue/Green Deployment with Istio

백셀건전지 2021. 8. 31. 10:40

1. 목적

이 글은 Kubernetes 기반 시스템에서 Istio를 이용하여 Blue/Green 배포를 구성하는 방법을 설명하고 있다.

구성 환경은 GCP GKE 1.18.20-gke.501 버전과 Istio 1.10.0 버전으로 Kubernetes Service mesh 환경을 구현하였다.

CI/CD 툴은 Gitlab, Gitlab runner를 사용한다.

 

2. Blue/Green Deployment

출처: https://onlywis.tistory.com/10

Blue/Green Deployment는 현재 운영중인 버전(Blue)과 새로운 버전(Green)의 서버들을 동시에 나란히 구성하고 배포 시점이 되면 트래픽을 일제히 전환시키는 배포 방식이다.

운영 환경에 영향을 주지 않고 실제 서비스 환경으로 새 버전 테스트가 가능하고, 빠른 롤백이 가능하다는 것이 장점이다.

3. Kubernetes 구성

3.1. Istio 구성

Istio는 IstioOperator 메니페스트를 istioctl install 명령어의 -f 옵션의 매개변수로 넘겨서 생성하였다.

 

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  meshConfig:
    enableTracing: true                                                                             #addon tracing 활성화
  components:
    ingressGateways:
    - enabled: true                                                                                 # istio-ingressgateway 활성화 여부
      name: istio-ingressgateway
      k8s:                                                                      # istio-ingressgateway Service에 외부 LoadBalancer를 연결
        overlays:
        - kind: Service
          name: istio-ingressgateway
          patches:
          - path: spec.externalTrafficPolicy
            value: "Local"                                                                          # k8s service, Local을 권장.
        hpaSpec:                                                                                    # istio-ingressgatway hpa
          maxReplicas: 5                                                                            # default 5
          minReplicas: 3                                                                           
        affinity:                                 # 3개의 istio-ingressgateway를 각 worker node에 분산시키기 위해 affinity 선언
          podAntiAffinity:
            preferredDuringSchedulingIgnoredDuringExecution:
              - weight: 100
                podAffinityTerm:
                  labelSelector:
                    matchExpressions:
                      - key: "service.istio.io/canonical-name"
                        operator: In
                        values:
                          - "istio-ingressgateway"
                  topologyKey: "kubernetes.io/hostname"
        nodeSelector:                                                   # 특정 nodepool에 생성하기 위해 node selector 선언
          cloud.google.com/gke-nodepool: nodepool-1
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 1024m
            memory: 512Mi

 

아래의 명령어를 실행하면 GKE 클러스터에 istio core, istiod, ingress gateway가 설치된다.

istioctl install -f istio_manifest.yaml

3.2. Gateway, Virtual service 생성

Blue/Green 배포의 Blue 환경과 Green 환경 접근 설정은 virtual service에서 선언한다.

Gateway는 초기 구성 때에만 생성하고, virtual service는 blue/green 배포시마다 파이프라인에서 configure한다.

Gateway의 spec.servers.hosts 값에는 외부에서 GKE로 질의하는 hostname을 선언한다.

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: bluegreen-gateway
  namespace: app
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http-istio-gateway
      protocol: HTTP
    hosts:
    - "bluegreen.domain.com"

모듈별 gateway와 virtual service를 따로 생성하였고, Virtual service에서의 spec.host는 *로 선언하여 host에 대한 관리는 모듈별 gateway로 일임한다.

spec.http.route.destination.host는 (모듈의 Service name).(namespace).svc.cluster.local 로 입력한다.

이는 초기에 선언하긴 하지만, 배포될 때마다 새로운 service name으로 변경되기 때문에 현재의 virtual service는 초기 세팅 때에만 선언한다.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bluegreen-virtualservice
  namespace: app
spec:
  gateways:
  - bluegreen-gateway
  hosts:
  - "*"
  http:
  - route:
    - destination:
        host: bluegreen-front.app.svc.cluster.local
        port:
          number: ${PORT_NO}

3.3. Configmap 생성

Blue/Green으로 CI/CD 파이프라인을 구성할 때 배포는 Helm을 사용하지 않고 kubectl 명령어로 service와 deployment를 직접 배포하였다.

그 이유는 helm 배포는 하나의 app을 다른 두 개의 list로 install(혹은 upgrade)을 하지 않는 이상 두 버전의 환경이 같이 동시에 배포되도록 구성하는 것이 불가하다고 판단하였기 때문이다.

Configmap을 생성하여 configmap 안에 운영 중인 버전(Green)의 이름을 저장하는 용도로 사용한다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-configmap
  namespace: app
data:
  active-deployment-name: bluegreen-front

3.4.  Pipeline 구성

CI/CD 파이프라인은 yarn build - docker build 및 GCR로 이미지 push - Green버전 service/deployment 배포 및 Green 버전 접근 설정 - Green 버전 정상 배포 check - Green 버전 배포 진행 or Blue 버전 rollback 으로 구성된다.

이 중에 앞의 두 stage에 대한 설명은 제외하고 Green 버전 service/deployment 배포 및 green 버전 접근 설정부터 설명한다.

 

3.4.1. Green 버전 service/deployment 배포 및 Green버전 접근 설정

CI/CD 파이프라인 job의 script에서 Green 버전의 service와 deployment를 생성한다.

service와 deployment를 선언할 때 모듈의 이름 뒤에 CI/CD 파이프라인이 실행될 때마다 정의되는 랜덤 문자열인 ${CI_COMMIT_SHORT_SHA}를 붙여서 이름을 선언한다.

예를 들면, 모듈의 이름이 bluegreen이면 service명과 deployment명을 bluegreen-${CI_COMMIT_SHORT_SHA} 로 생성한다.

${변수이름}은 Gitlab runner에서 선언한 변수이다.

script:
  - |
    cat <<EOF | kubectl apply -f -
    apiVersion: v1
    kind: Service
    metadata:
      name: ${APP_NAME}-${CI_COMMIT_SHORT_SHA}
      namespace: ${NAMESPACE}
    spec:
      type: ClusterIP
      ports:
        - port: ${PORT_NO}
          targetPort: ${PORT_NO}
          protocol: TCP
          name: http
      selector:
        app.kubernetes.io/name: ${APP_NAME}-${CI_COMMIT_SHORT_SHA}
    EOF
 
  - |
    cat <<EOF | kubectl apply -f -
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: ${APP_NAME}-${CI_COMMIT_SHORT_SHA}
      namespace: ${NAMESPACE}
    spec:
      replicas: ${REPLICAS}
      selector:
        matchLabels:
            app.kubernetes.io/name: ${APP_NAME}-${CI_COMMIT_SHORT_SHA}
      template:
        metadata:
          labels:
            app.kubernetes.io/name: ${APP_NAME}-${CI_COMMIT_SHORT_SHA}
        spec:
          containers:
            - name: ${APP_NAME}
              image: "${GCR_URL}/${APP_NAME}:${CI_COMMIT_SHORT_SHA}"
              ports:
                - name: http
                  containerPort: ${PORT_NO}
                  protocol: TCP
              resources:
                requests:
                  memory: ${REQUEST_MEMORY}
                  cpu: ${REQUEST_CPU}
                limits:
                  memory: ${LIMIT_MEMORY}
                  cpu: ${LIMIT_CPU}
              livenessProbe:
                httpGet:
                  path: /healthcheck.html
                  port: ${PORT_NO}
                initialDelaySeconds: ${HEALTH_CHECK_SECONDS}
              readinessProbe:
                httpGet:
                  path: /healthcheck.html
                  port: ${PORT_NO}
                initialDelaySeconds: ${HEALTH_CHECK_SECONDS}
          nodeSelector:
            cloud.google.com/gke-nodepool: ${NODEPOOL_NAME}
    EOF

Green 버전 service와 deployment가 생성이 되고 나면, green 버전으로 접근할 수 있도록 virtual service를 수정해 준다.

Blue/Green 환경에 대한 접근은 gateway에 선언한 domain으로 입력하면 blue환경으로, 도메인의 뒤에 /test로 입력하면 green환경으로 접근이 가능하다.

같은 방식으로 script 내에서 manifest를 선언한다.

script:
  - |
    cat <<EOF | kubectl apply -f -
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: ${CI_PROJECT_NAME}
      namespace: app
    spec:
      gateways:
      - bluegreen-gateway
      hosts:
      - "*"
      http:
      - match:
        - uri:
            prefix: /test
        rewrite:
          uri: /
        route:
        - destination:
            host: ${NEW_DEPLOYMENT_NAME}.${NAMESPACE}.svc.cluster.local
            port:
              number: ${PORT_NO}
      - route:
        - destination:
            host: ${ACTIVE_DEPLOYMENT_NAME}.${NAMESPACE}.svc.cluster.local
            port:
              number: ${PORT_NO}
    EOF

 

3.4.2. Green 버전 정상 배포 check

이 stage는 Gitlab 콘솔에서 개발자들이 정상적으로 배포가 되었는지 확인할 수 있게 하기 위해서 추가한 단계이다.

아래의 script를 실행하여 확인한다.

이렇게 선언하면 Blue 버전에는 영향을 주지 않고 새로 배포된 Green 버전의 테스트가 가능하다.

script:
  - kubectl rollout status deployment ${NEW_DEPLOYMENT_NAME} -n ${NAMESPACE}

 

3.4.3. Green 버전 배포 진행 or Blue 버전 rollback

테스트를 진행한 후 Green 버전으로 현재 운영을 대체할 지, 혹은 테스트 중 문제가 발생하여 기존 version으로 rollback할지를 선택한다.

Gitlab runner에서는 기본적으로 green 버전 배포를 바로 실행 설정해놓되, delay 시간을 설정하여 테스트할 수 있는 시간을 확보한다.

본 gitlab-ci.yml 파일에서 다른 gitlab-ci.yml 파일을 trigger로 실행한다.

trigger-proceed-deployment-stg:
  stage: blue-green-control
  when: on_success
  trigger:
    include:
      - project: developerV2/bluegreen-project
        ref: master
        file: .gitlab-ci-child-proceed-deployment.yml
    strategy: depend
  variables:
    # https://docs.gitlab.com/ee/ci/yaml/README.html#artifact-downloads-to-child-pipelines
    PARENT_PIPELINE_ID: ${CI_PIPELINE_ID}
    PARENT_JOB_NAME: ${PARENT_JOB_NAME}
  needs: [deployment-check-stg]
  only:
    refs:
      - stage

위의 job에서 trigger된 .gitlab-ci-child-proceed-deployment.yml 에서 delay 설정을 하는데, Gitlab - CI/CD - Variables에서 변수(AUTO_BLUE_GREEN_CONTROL)를 설정하면 변수 값에 따라서 delay를 줄지 바로 실행을 할 지 설정할 수 있다.

그리고 script 안에서 virtual service를 update하여 운영 환경을 Green 버전(NEW_DEPLOYMENT_NAME) 으로 변경한다.

그 이후 configmap의 현재 운영중인 service, deployment 명을 kubectl patch configmap 명령어를 통해 변경한다.

마지막으로 기존의 blue 버전을 kubectl delete 명령어로 삭제한 후 마무리한다.

kubectl delete 명령어를 감싸고 있는 if문은 만약 별도의 배포 없이 CI/CD 파이프라인만 실행시켰을 때에는 CI_COMMIT_SHORT_SHA 값이 같기 때문에, Blue 버전과 Green 버전의 이름이 같다면 삭제하지 않도록 하기 위함이다.

reroute-traffic:
  stage: reroute-traffic
  # rules cannot use together with only/except
  rules:
    - if: '$AUTO_BLUE_GREEN_CONTROL == "true"'
      when: on_success
      allow_failure: true
    - if: '$AUTO_BLUE_GREEN_CONTROL == null || $AUTO_BLUE_GREEN_CONTROL == "false"'
      when: delayed
      start_in: 25 minutes
  environment:
    name: ${CI_COMMIT_REF_NAME}
  needs:
    - pipeline: ${PARENT_PIPELINE_ID}
      job: ${PARENT_JOB_NAME}
  before_script:
    - !reference [.template-get-gcr-authentication, script]
    - !reference [.template-connect-gke-cluster, script]
  script:
    - echo AUTO_BLUE_GREEN_CONTROL=${AUTO_BLUE_GREEN_CONTROL}
 
    #old service로 접속 가능한 virtual service 삭제
    - |
      cat <<EOF | kubectl apply -f -
      apiVersion: networking.istio.io/v1alpha3
      kind: VirtualService
      metadata:
        name: ${CI_PROJECT_NAME}
        namespace: ${NAMESPACE}
      spec:
        gateways:
        - ${GATEWAY_NAME}
        hosts:
        - "*"
        http:
        - route:
          - destination:
              host: ${NEW_DEPLOYMENT_NAME}.${NAMESPACE}.svc.cluster.local
              port:
                number: ${PORT_NO}
      EOF
     
    #Configmap에 active-deployment-name 을 new deployment name으로 변경
    - |
      echo "Configmap is patched to new deployment name."
      kubectl patch configmap -n ${NAMESPACE} ${CI_PROJECT_NAME} -p $'data:\n active-deployment-name: '${NEW_DEPLOYMENT_NAME}
     
    # 프로젝트 소스에 변경이 없이 파이프라인만 run하는 경우 CI_COMMIT_SHORT_SHA가 같다.
    # 이 변수가 같으면 기존의 service, deployment 이름이 새로 배포한 것과 이름이 같다.
    # 그러므로 active와 new 변수가 다를 때에만 삭제 작업을 하도록 안전장치를 추가한다.
    - if [ "${ACTIVE_DEPLOYMENT_NAME}" != "${NEW_DEPLOYMENT_NAME}" ]; then
        kubectl delete svc -n ${NAMESPACE} ${ACTIVE_DEPLOYMENT_NAME};
        kubectl delete deployment -n ${NAMESPACE} ${ACTIVE_DEPLOYMENT_NAME};
      fi

rollback의 경우에는 virtual service를 기존의 Blue 버전으로만 서비스되도록 수정한 후, kubectl delete 명령어로 green 버전의 service와 deployment를 삭제한다.

rollback-deployment:
  stage: stop-deployment
  when: on_success
  needs:
    - pipeline: ${PARENT_PIPELINE_ID}
      job: ${PARENT_JOB_NAME}
  before_script:
    - !reference [.template-get-gcr-authentication, script]
    - !reference [.template-connect-gke-cluster, script]
  script:
    #new service로 접속 가능한 virtual service 삭제
    - |
      cat <<EOF | kubectl apply -f -
      apiVersion: networking.istio.io/v1alpha3
      kind: VirtualService
      metadata:
        name: ${CI_PROJECT_NAME}
        namespace: ${NAMESPACE}
      spec:
        gateways:
        - gw-${CI_PROJECT_NAME}
        hosts:
        - "*"
        http:
        - route:
          - destination:
              host: ${ACTIVE_DEPLOYMENT_NAME}.${NAMESPACE}.svc.cluster.local
              port:
                number: ${PORT_NO}
      EOF
 
    #배포를 위해 생성한 svc, deployment 삭제
    # 프로젝트 소스에 변경이 없이 파이프라인만 run하는 경우 CI_COMMIT_SHORT_SHA가 같다.
    # 이 변수가 같으면 기존의 service, deployment 이름이 새로 배포한 것과 이름이 같다.
    # 그러므로 active와 new 변수가 다를 때에만 삭제 작업을 하도록 안전장치를 추가한다.
    - if [ "${NEW_DEPLOYMENT_NAME}" != "${ACTIVE_DEPLOYMENT_NAME}" ]; then
        kubectl delete svc -n ${NAMESPACE} ${NEW_DEPLOYMENT_NAME};
        kubectl delete deployment -n ${NAMESPACE} ${NEW_DEPLOYMENT_NAME};
      fi