마짱짱의 지식창고

[GCP] Jenkins 이용하여 GKE Canary 배포 본문

Cloud/GCP

[GCP] Jenkins 이용하여 GKE Canary 배포

마짱짱 2023. 3. 7. 15:38
반응형

0. 개요

해당 포스팅에서는 Kubernetes Engine에서 Jenkins를 사용하여 지속적 배포 파이프라인을 설정하는 방법을 배우게 됩니다. Jenkins는 공유 저장소에서 코드를 자주 통합하는 개발자가 즐겨 사용하는 자동화 서버입니다. 이 실습에서 빌드할 솔루션은 다음 다이어그램과 유사합니다.

 

  • Jenkins 애플리케이션을 Kubernetes Engine 클러스터에 프로비저닝하기
  • Helm Package Manager를 사용하여 Jenkins 애플리케이션 설정하기
  • Jenkins 애플리케이션의 기능 살펴보기
  • Jenkins 파이프라인 생성 및 실습

Jenkins란 무엇인가요?

Jenkins는 빌드, 테스트, 배포 파이프라인을 유연하게 조정할 수 있는 오픈소스 자동화 서버입니다. Jenkins를 사용하면 개발자는 지속적 배포로 인해 발생할 수 있는 오버헤드 문제에 대한 걱정 없이 프로젝트를 신속하게 변경 및 개선할 수 있습니다.

 

지속적 배포란 무엇인가요?

지속적 배포(CD) 파이프라인을 설정해야 하는 경우 Jenkins를 Kubernetes Engine으로 배포하면 표준 VM 기반 배포 대비 상당한 이점을 얻을 수 있습니다.

빌드 프로세스에서 컨테이너를 사용하는 경우 하나의 가상 호스트로 여러 운영체제에서 작업이 가능합니다. Kubernetes Engine에서는 일시적 빌드 실행자(ephemeral build executors)를 제공하는데, 이 기능은 빌드가 활발하게 실행될 때만 사용되므로 일괄 처리 작업과 같은 다른 클러스터 작업에 사용할 여유 리소스를 확보할 수 있습니다. 일시적 빌드 실행자의 또 다른 이점은 바로 시작하는 데 몇 초밖에 걸리지 않는 속도입니다.

Kubernetes Engine에는 Google의 전역 부하 분산기도 사전 설치되어 있어 인스턴스로의 웹 트래픽 라우팅을 자동화하는 데 사용할 수 있습니다. 부하 분산기에서는 SSL 종료를 처리하고, 웹 프런트엔드와 함께 Google의 백본 네트워크로 구성되는 전역 IP 주소를 활용하며, 사용자가 항상 애플리케이션 인스턴스에 가장 빠른 경로로 액세스할 수 있도록 설정해 줍니다.

Kubernetes, Jenkins 그리고 이 둘이 CD 파이프라인에서 상호작용하는 방식을 알아보았으므로 이제 하나를 빌드해 보겠습니다.

 

1. 샘플코드 받기

gcloud config set compute/zone us-east1-c
gsutil cp gs://spls/gsp051/continuous-deployment-on-kubernetes.zip .
unzip continuous-deployment-on-kubernetes.zip
cd continuous-deployment-on-kubernetes

 

2. GKE 만들기

gcloud container clusters create jenkins-cd \
--num-nodes 2 \
--machine-type n1-standard-2 \
--scopes "https://www.googleapis.com/auth/source.read_write,cloud-platform"

Cloud Shell 에 인증정보 가져오기

gcloud container clusters get-credentials jenkins-cd

kubectl cluster-info

 

 

3. Helm 설정하기

Jenkins repo 등록

helm repo add jenkins https://charts.jenkins.io
helm repo update

 

4. Jenkins 구성 및 설치하기

Jenkins 설치 시 values 파일을 템플릿으로 사용하여 설정에 필요한 값을 제공할 수 있습니다.

커스텀 values 파일을 사용하면 Kubernetes Cloud를 자동으로 구성하고 다음 필수 플러그인을 추가할 수 있습니다.

  • Kubernetes:latest
  • Workflow-multibranch:latest
  • Git:latest
  • Configuration-as-code:latest
  • Google-oauth-plugin:latest
  • Google-source-plugin:latest
  • Google-storage-plugin:latest

Helm 으로 Jenkins 설치하기

helm install cd jenkins/jenkins -f jenkins/values.yaml --wait
### jenkins/values.yaml

controller:
  installPlugins:
    - kubernetes:latest
    - workflow-job:latest
    - workflow-aggregator:latest
    - credentials-binding:latest
    - git:latest
    - google-oauth-plugin:latest
    - google-source-plugin:latest
    - google-kubernetes-engine:latest
    - google-storage-plugin:latest
  resources:
    requests:
      cpu: "50m"
      memory: "1024Mi"
    limits:
      cpu: "1"
      memory: "3500Mi"
  javaOpts: "-Xms3500m -Xmx3500m"
  serviceType: ClusterIP
agent:
  resources:
    requests:
      cpu: "500m"
      memory: "256Mi"
    limits:
      cpu: "1"
      memory: "512Mi"
persistence:
  size: 100Gi
serviceAccount:
  name: cd-jenkins

 

완료되면 해당 문구가 출력됩니다.

NAME: cd
LAST DEPLOYED: Tue Mar  7 05:43:45 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get your 'admin' user password by running:
  kubectl exec --namespace default -it svc/cd-jenkins -c jenkins -- /bin/cat /run/secrets/additional/chart-admin-password && echo
2. Get the Jenkins URL to visit by running these commands in the same shell:
  echo http://127.0.0.1:8080
  kubectl --namespace default port-forward svc/cd-jenkins 8080:8080

3. Login with the password from step 1 and the username: admin
4. Configure security realm and authorization strategy
5. Use Jenkins Configuration as Code by specifying configScripts in your values.yaml file, see documentation: http://127.0.0.1:8080/configuration-as-code and examples: https://github.com/jenkinsci/configuration-as-code-plugin/tree/master/demos

For more information on running Jenkins on Kubernetes, visit:
https://cloud.google.com/solutions/jenkins-on-container-engine

For more information about Jenkins Configuration as Code, visit:
https://jenkins.io/projects/jcasc/


NOTE: Consider using a custom image with pre-installed plugins

 

Jenkins에서 Cluster에 배포할 수 있도록 서비스계정을 구성합니다.

kubectl create clusterrolebinding jenkins-deploy --clusterrole=cluster-admin --serviceaccount=default:cd-jenkins

 

CloudShell에서 8080 port로 접근을 위해 포트포워딩을 합니다.

export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/component=jenkins-master" -l "app.kubernetes.io/instance=cd" -o jsonpath="{.items[0].metadata.name}")
kubectl port-forward $POD_NAME 8080:8080 >> /dev/null &

 

5. Jenkins 에 연결하기

Jenkins  설치시 default password를 Secret으로 생성해주니 출력하여 확인합니다.

default id는 admin 입니다.

printf $(kubectl get secret cd-jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo

 

Cloud Shell을 이용하여 8080 접근

 

6. 애플리케이션 이해하기

지속적 배포 파이프라인에 샘플 애플리케이션 gceme를 배포합니다. 이 애플리케이션은 Go 언어로 작성되었으며 저장소의 sample-app 디렉터리에 있습니다. Compute Engine 인스턴스에서 gceme 바이너리를 실행하면, 앱이 정보 카드에 인스턴스의 메타데이터를 표시합니다.

Sample Application

이 애플리케이션은 마이크로서비스를 모방하여 두 가지 작동 모드를 지원합니다.

  • 백엔드 모드에서 gceme는 포트 8080을 수신 대기하고 Compute Engine 인스턴스 메타데이터를 JSON 형식으로 반환합니다.
  • 프런트엔드 모드에서 gceme는 백엔드 gceme 서비스를 쿼리하고 결과 JSON을 사용자 인터페이스에서 렌더링합니다.

7. 애플리케이션 배포하기

애플리케이션을 2개의 다른 환경에 배포합니다.

  • 프로덕션: 사용자가 액세스하는 라이브 사이트입니다.
  • 카나리아: 사용자 트래픽 중 일부만 수용하는 소규모 사이트입니다. 이 환경을 사용하여 실제 트래픽으로 소프트웨어의 이상 유무를 확인한 후 모든 사용자에게 배포합니다.

샘플 Dir 이동후 'production' Namespace 생성

cd sample-app
kubectl create ns production

 

deployment, Service 생성 및 확인

kubectl apply -f k8s/production -n production
kubectl apply -f k8s/canary -n production
kubectl apply -f k8s/services -n production
kubectl get all -n production

### Result
NAME                                             READY   STATUS    RESTARTS   AGE
pod/gceme-backend-canary-84db764d45-tvrlp        1/1     Running   0          88s
pod/gceme-backend-production-5956b59b7b-nk82l    1/1     Running   0          93s
pod/gceme-frontend-canary-798b4c74cf-qshk2       1/1     Running   0          87s
pod/gceme-frontend-production-7568cb57cb-r6jvt   1/1     Running   0          92s

NAME                     TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)        AGE
service/gceme-backend    ClusterIP      10.72.9.110   <none>           8080/TCP       85s
service/gceme-frontend   LoadBalancer   10.72.11.71   34.138.207.159   80:31931/TCP   84s

NAME                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/gceme-backend-canary        1/1     1            1           89s
deployment.apps/gceme-backend-production    1/1     1            1           94s
deployment.apps/gceme-frontend-canary       1/1     1            1           88s
deployment.apps/gceme-frontend-production   1/1     1            1           93s

NAME                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/gceme-backend-canary-84db764d45        1         1         1       89s
replicaset.apps/gceme-backend-production-5956b59b7b    1         1         1       94s
replicaset.apps/gceme-frontend-canary-798b4c74cf       1         1         1       88s
replicaset.apps/gceme-frontend-production-7568cb57cb   1         1         1       93s

 

Production 환경의 Application을 4개로 수정하여 prod 4, canary1인 환경 구성을 합니다.

kubectl scale deployment gceme-frontend-production -n production --replicas 4

 

프론트엔드의 LB IP 를 확인합니다.

kubectl get service gceme-frontend -n production

 

해당 ip로 정상적으로 접속되는지 확인합니다.

 

해당 LB IP를 추후에 사용할 예정이오니 변수로 선언해둡니다.

export FRONTEND_SERVICE_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services gceme-frontend)

 

해당 Application의 version을 호출하여 1.0.0 나오는지 확인합니다.

curl http://$FRONTEND_SERVICE_IP/version

 

8. Jenkins 파이프라인 만들기

Source Repository 'default' 이름으로 생성합니다.

gcloud source repos create default

 

sample-app Dir을 초기화 한뒤 'default' Source Repository로 Push 합니다.

git init
git config credential.helper gcloud.sh
git remote add origin https://source.developers.google.com/p/$DEVSHELL_PROJECT_ID/r/default

git config --global user.email "majjangjjang@example.com"
git config --global user.name "majjangjjang"

git add .
git commit -m "Initial commit"
git push origin master

 

9. Jenkins 서비스 계정 사용자 인증 정보 추가

사용자 인증 정보를 구성하여 Jenkins에서 코드 저장소에 액세스할 수 있도록 허용합니다. Jenkins는 Cloud Source Repositories에서 코드를 다운로드하기 위해 클러스터의 서비스 계정 사용자 인증 정보를 사용합니다.

 

Jenkins > Mange Jenkins 클릭

 

Security > Manage Credentials 클릭

 

System 클릭

Global credentials 클릭

Add Credentials 클릭

 

Kind: Google Service Account from metadata

Project Name: 현재 작업중인 프로젝트 이름인지 확인

 

10. k8s용 Jenkins Cloud 구성

Jenkins > Manage Jenkins 이동

 

System Configuration > Mange Node 이동

 

Configure Clouds 이동

 

Kubernetes 선택

 

Kubernetes Cloud Detail 선택

 

많은 항목 중 default 상태해서 해당 내용만 기입합니다.

Jenkins URL: http://cd-jenkins:8080

Jenkins tunnel: cd-jenkins-agent:50000

그 후에 Save

11. Jenkins 작업 만들기

Jenkins > New Item 클릭

이름 기입 및 Multibranch Pipeline 선택

 

Branch Sources 부분에서 Git 선택

 

Source Repository의 URL와 생성한 Service Account 인증 정보 넣기

 

트리거 검색 섹션에서 1분주기로 실행하도록 설정

 

Save누르면 파이프라인이 생성 됩니다.

12. 개발환경 만들기

개발 브랜치는 개발자가 코드 변경사항을 제출하여 라이브 사이트에 통합하기 전에 테스트하는 데 사용하는 일련의 환경입니다. 이러한 환경은 애플리케이션의 축소 버전이지만 실제 환경과 동일한 메커니즘으로 배포되어야 합니다.

 

개발 브랜치를 만들고 Git 서버에 Push

git checkout -b new-feature

 

파이프라인 정의하기

해당 파이프라인을 정의하는 Jenkinsfile Jenkins 파이프라인 Groovy 구문을 사용하여 작성됩니다. Jenkinsfile을 사용하면 전체 빌드 파이프라인을 소스 코드가 포함된 단일 파일로 표현할 수 있습니다. 파이프라인에서는 동시 로드와 같은 강력한 기능을 지원하며 사용자의 수동 승인이 필요합니다.

vi Jenkinsfile
### Jenkinsfile

pipeline {

  environment {
    PROJECT = "qwiklabs-gcp-04-c5e249cf6c8d"
    APP_NAME = "gceme"
    FE_SVC_NAME = "${APP_NAME}-frontend"
    CLUSTER = "jenkins-cd"
    CLUSTER_ZONE = "us-east1-c"
    IMAGE_TAG = "gcr.io/${PROJECT}/${APP_NAME}:${env.BRANCH_NAME}.${env.BUILD_NUMBER}"
    JENKINS_CRED = "${PROJECT}"
  }

  agent {
    kubernetes {
      label 'sample-app'
      defaultContainer 'jnlp'
      yaml """
apiVersion: v1
kind: Pod
metadata:
labels:
  component: ci
spec:
  # Use service account that can deploy to all namespaces
  serviceAccountName: cd-jenkins
  containers:
  - name: golang
    image: golang:1.10
    command:
    - cat
    tty: true
  - name: gcloud
    image: gcr.io/cloud-builders/gcloud
    command:
    - cat
    tty: true
  - name: kubectl
    image: gcr.io/cloud-builders/kubectl
    command:
    - cat
    tty: true
"""
}
  }
  stages {
    stage('Test') {
      steps {
        container('golang') {
          sh """
            ln -s `pwd` /go/src/sample-app
            cd /go/src/sample-app
            go test
          """
        }
      }
    }
    stage('Build and push image with Container Builder') {
      steps {
        container('gcloud') {
          sh "PYTHONUNBUFFERED=1 gcloud builds submit -t ${IMAGE_TAG} ."
        }
      }
    }
    stage('Deploy Canary') {
      // Canary branch
      when { branch 'canary' }
      steps {
        container('kubectl') {
          // Change deployed image in canary to the one we just built
          sh("sed -i.bak 's#corelab/gceme:1.0.0#${IMAGE_TAG}#' ./k8s/canary/*.yaml")
          step([$class: 'KubernetesEngineBuilder', namespace:'production', projectId: env.PROJECT, clusterName: env.CLUSTER, zone: env.CLUSTER_ZONE, manifestPattern: 'k8s/services', credentialsId: env.JENKINS_CRED, verifyDeployments: false])
          step([$class: 'KubernetesEngineBuilder', namespace:'production', projectId: env.PROJECT, clusterName: env.CLUSTER, zone: env.CLUSTER_ZONE, manifestPattern: 'k8s/canary', credentialsId: env.JENKINS_CRED, verifyDeployments: true])
          sh("echo http://`kubectl --namespace=production get service/${FE_SVC_NAME} -o jsonpath='{.status.loadBalancer.ingress[0].ip}'` > ${FE_SVC_NAME}")
        }
      }
    }
    stage('Deploy Production') {
      // Production branch
      when { branch 'master' }
      steps{
        container('kubectl') {
        // Change deployed image in canary to the one we just built
          sh("sed -i.bak 's#corelab/gceme:1.0.0#${IMAGE_TAG}#' ./k8s/production/*.yaml")
          step([$class: 'KubernetesEngineBuilder', namespace:'production', projectId: env.PROJECT, clusterName: env.CLUSTER, zone: env.CLUSTER_ZONE, manifestPattern: 'k8s/services', credentialsId: env.JENKINS_CRED, verifyDeployments: false])
          step([$class: 'KubernetesEngineBuilder', namespace:'production', projectId: env.PROJECT, clusterName: env.CLUSTER, zone: env.CLUSTER_ZONE, manifestPattern: 'k8s/production', credentialsId: env.JENKINS_CRED, verifyDeployments: true])
          sh("echo http://`kubectl --namespace=production get service/${FE_SVC_NAME} -o jsonpath='{.status.loadBalancer.ingress[0].ip}'` > ${FE_SVC_NAME}")
        }
      }
    }
    stage('Deploy Dev') {
      // Developer Branches
      when {
        not { branch 'master' }
        not { branch 'canary' }
      }
      steps {
        container('kubectl') {
          // Create namespace if it doesn't exist
          sh("kubectl get ns ${env.BRANCH_NAME} || kubectl create ns ${env.BRANCH_NAME}")
          // Don't use public load balancing for development branches
          sh("sed -i.bak 's#LoadBalancer#ClusterIP#' ./k8s/services/frontend.yaml")
          sh("sed -i.bak 's#corelab/gceme:1.0.0#${IMAGE_TAG}#' ./k8s/dev/*.yaml")
          step([$class: 'KubernetesEngineBuilder', namespace: "${env.BRANCH_NAME}", projectId: env.PROJECT, clusterName: env.CLUSTER, zone: env.CLUSTER_ZONE, manifestPattern: 'k8s/services', credentialsId: env.JENKINS_CRED, verifyDeployments: false])
          step([$class: 'KubernetesEngineBuilder', namespace: "${env.BRANCH_NAME}", projectId: env.PROJECT, clusterName: env.CLUSTER, zone: env.CLUSTER_ZONE, manifestPattern: 'k8s/dev', credentialsId: env.JENKINS_CRED, verifyDeployments: true])
          echo 'To access your environment run `kubectl proxy`'
          echo "Then access your service via http://localhost:8001/api/v1/proxy/namespaces/${env.BRANCH_NAME}/services/${FE_SVC_NAME}:80/"
        }
      }
    }
  }
}

 

13. 테스트를 위해 기존 application 변화주기

테스트를 위해 기존 파란색 화면의 예시app을 주황색으로 변경 및 version도 2.0.0으로변경

vi html.go

...
<div class="card orange">
...
vi main.go

...
const version string = "2.0.0"
...

 

14.  배포하기

add 후 push

git add Jenkinsfile html.go main.go
git commit -m "Version 2.0.0"
git push origin new-feature

 

Jenkins Console창에서 진행상황 확인하기

 

완료되었다면 Proxy를 동작하기

kubectl proxy &

 

버전 2.0.0으로 호출되는지 확인

curl \
http://localhost:8001/api/v1/namespaces/new-feature/services/gceme-frontend:80/proxy/version

 

15. 최신파일이 정상 동작하니 이제 운영환경에 배포하기

Canary 브랜치 만들고 push

git checkout -b canary
git push origin canary

 

이제 정상적으로 배포 됐는지 확인

export FRONTEND_SERVICE_IP=$(kubectl get -o \
jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services gceme-frontend)

while true; do curl http://$FRONTEND_SERVICE_IP/version; sleep 1; done

중간중간 2.0.0 이 확인된다면 성공

 

16. 이제 문제가 없다고 판단되니 2.0.0으로  전부 교체

Canry 브랜치를 Master으로 merge하고 push

git checkout master
git merge canary
git push origin master

 

좌측 빌드상태에서 Console Output으로 현재 상황을 확인할 수 있

 

정상배포완료

 

이제 모두 2.0.0으로나오면 정상배포 성공

export FRONTEND_SERVICE_IP=$(kubectl get -o \
jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services gceme-frontend)

while true; do curl http://$FRONTEND_SERVICE_IP/version; sleep 1; done

 

 

---

출처

https://www.cloudskillsboost.google/focuses/1104?catalog_rank=%7B%22rank%22%3A3%2C%22num_filters%22%3A0%2C%22has_search%22%3Atrue%7D&parent=catalog&search_id=22653407

반응형