마짱짱의 지식창고
[GCP] Jenkins 이용하여 GKE Canary 배포 본문
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 바이너리를 실행하면, 앱이 정보 카드에 인스턴스의 메타데이터를 표시합니다.
이 애플리케이션은 마이크로서비스를 모방하여 두 가지 작동 모드를 지원합니다.
- 백엔드 모드에서 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
'Cloud > GCP' 카테고리의 다른 글
[GCP] Jenkins로 GKE 연결하기 (0) | 2023.03.06 |
---|---|
[GCP] MySQL용 Amazon RDS에서 Cloud SQL로 마이그레이션 (0) | 2023.03.06 |
[GCP] Dataflow, BigQuery, GCS를 이용하여 ETL Pipeline 구축 (0) | 2023.02.23 |
[GCP] Dataprep을 이용한 파이프라인 구축 (0) | 2023.02.22 |
[GCP] GKE에 Cloud Deploy를 사용하여 CD환경 구축하기 (0) | 2023.02.20 |