微服務 CI-CD 實踐 - GitOps 完整設計與實現
單應用與環境
多應用與環境
CI 持續集成
首先,準備一個代碼庫:https://github.com/DevOpsCICDCourse/microservicescicd/blob/main/microservice-demo-service-master.zip
我們來梳理一下 CI 流水線的步驟:
-
由於此次實現的代碼倉庫類型爲單一存儲庫,即一個存儲庫存放多個服務模塊代碼,每個子目錄爲一個服務模塊。
-
首先,我們的持續集成流水線需要能夠正確獲取,當前的 commit 是哪個服務的代碼。
-
確定好服務,然後下載該服務的代碼,進行編譯打包、單元測試、代碼掃描和構建鏡像等步驟。
如何獲取 commit 的服務信息?這裏我們使用 GitLab WebHook 功能和 Jenkins 的 job 構建觸發器對接來實現。
工作流程是:當我在 Gitlab 提交了代碼,會通過 GitLab webhook 觸發 Jenkins Scheduler 作業, 會將此次提交代碼所產生的 hook data 數據信息以 POST 的方式傳給 Jenkins Job。此時 Jenkins job 可以編寫使用 Generic Hook 插件獲取此次 POST 請求傳輸過來的請求體 Body 信息。是一段 JSON 數據, 該 job 運行後編寫 Pipeline 解析 JSON 中的數據拿到所變更的服務模塊信息。最後觸發對應服務的 CI 作業進行構建。
CI-Scheduler 作業
此作業只需要開啓 webhook, 配置觸發 token(唯一性)。生成 hookurl:http://jenkins.idevops.site/generic-webhook-trigger/invoke?token=microservicecicd-scheduler-CI
Jenkinsfile
pipeline {
agent any
stages{
stage("GetData"){
steps{
script {
echo "${webHookData}"
data = readJSON text: "${webHookData}"
println(data)
env.branchName = data.ref - "refs/heads/"
env.commitId = data.checkout_sha
env.projectId = data.project_id
commits = data["commits"]
println("${env.branchName}")
println("${env.commitID}")
println("${env.projectId}")
//env.moduleName = "service01"
changeServices = []
for(commit in commits) {
println(commit.id)
//added
for (add in commit.added) {
s = add.split("/") as List
if (s.size() > 1){
if (changeServices.indexOf(s[0]) == -1){
changeServices.add(s[0])
}
}
}
//modified
for (m in commit.modified) {
s = m.split("/") as List
// println s
// println s.size()
// println s[0]
if (s.size() > 1){
// println changeServices.indexOf(s[0])
if (changeServices.indexOf(s[0]) == -1){
changeServices.add(s[0])
}
}
}
//removed
for (r in commit.removed) {
s = r.split("/") as List
println s
if (s.size() > 1){
if (changeServices.indexOf(s[0]) == -1){
changeServices.add(s[0])
}
}
}
}
println(changeServices)
//currentBuild.description = " Trigger by ${eventType} ${changeServices}
}
}
}
stage('DefineService') {
steps {
script{
println(changeServices)
//服務構建順序控制
services = ['service02', 'service01']
for (service in services){
if (changeServices.indexOf(service) != -1){
jobName = 'microservicecicd-'+service+'-service-CI'
build job: jobName, wait: false, parameters: [string(name: 'branchName', value: "${env.branchName}" ),
string(name: 'commitId', value: "${env.commitId}" ),
string(name: 'projectId', value: "${env.projectId}" )]
}
}
}
}
}
}
}
GitLab 配置 WebHook
開啓 webhook,配置 hookurl:http://jenkins.idevops.site/generic-webhook-trigger/invoke?token=microservicecicd-scheduler-CI
CI 流水線 - CI 作業
每個微服務創建一個 CI 作業,具有三個字符串參數:分支名稱、commitID、項目 ID。
Jenkinsfile
String branchName = "${env.branchName}"
String moduleName = "${JOB_NAME}".split("/")[1].split("-")[1]
String srcUrl = "http://gitlab.idevops.site/microservicecicd/microservicecicd-demo-service.git"
String commitId = "${env.commitId}"
String projectId = "${env.projectId}"
pipeline {
agent { node { label "build" } }
stages {
stage('GetCode') {
steps {
script {
checkout([$class: 'GitSCM',
branches: [[name: "${branchName}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'SparseCheckoutPaths',
sparseCheckoutPaths: [[path: "${moduleName}"],[path: 'Dockerfile']]]],
submoduleCfg: [],
userRemoteConfigs: [[credentialsId: 'gitlab-admin-user',
url: "${srcUrl}"]]])
}
}
}
stage("Build&Test"){
steps{
script{
echo "Build..........."
sh """
cd ${moduleName}
mvn clean package
"""
}
}
post {
always {
junit "${moduleName}/target/surefire-reports/*.xml"
}
}
}
stage("SonarScan"){
steps{
script{
def sonarDate = sh returnStdout: true, script: 'date +%Y%m%d%H%M%S'
sonarDate = sonarDate - "\n"
withCredentials([string(credentialsId: 'sonar-admin-user', variable: 'sonartoken'),
string(credentialsId: 'gitlab-user-token', variable: 'gitlabtoken')]) {
// some block
sh """
cd ${moduleName}
sonar-scanner \
-Dsonar.projectKey=${JOB_NAME} \
-Dsonar.projectName=${JOB_NAME} \
-Dsonar.projectVersion=${sonarDate} \
-Dsonar.ws.timeout=30 \
-Dsonar.projectDescription="xxxxxxx" \
-Dsonar.links.homepage=http://www.baidu.com \
-Dsonar.sources=src \
-Dsonar.sourceEncoding=UTF-8 \
-Dsonar.java.binaries=target/classes \
-Dsonar.java.test.binaries=target/test-classes \
-Dsonar.java.surefire.report=target/surefire-reports \
-Dsonar.host.url="http://sonar.idevops.site" \
-Dsonar.login=${sonartoken} \
-Dsonar.gitlab.commit_sha=${commitId} \
-Dsonar.gitlab.ref_name=${branchName} \
-Dsonar.gitlab.project_id=${projectId} \
-Dsonar.dynamicAnalysis=reuseReports \
-Dsonar.gitlab.failure_notification_mode=commit-status \
-Dsonar.gitlab.url=http://gitlab.idevops.site \
-Dsonar.gitlab.user_token=${gitlabtoken} \
-Dsonar.gitlab.api_version=v4
"""
}
}
}
}
stage("BuildImage"){
steps{
script{
withCredentials([usernamePassword(credentialsId: 'aliyun-registry-admin', passwordVariable: 'password', usernameVariable: 'username')]) {
env.nowDate = sh returnStdout: true, script: 'date +%Y%m%d%H%M%S'
env.nowDate = env.nowDate - "\n"
env.releaseVersion = "${env.branchName}"
env.imageTag = "${releaseVersion}-${nowDate}-${commitId}"
env.dockerImage = "registry.cn-beijing.aliyuncs.com/microservicecicd/microservicecicd-${moduleName}-service:${env.imageTag}"
env.jarName = "${moduleName}-${branchName}-${commitId}"
sh """
docker login -u ${username} -p ${password} registry.cn-beijing.aliyuncs.com
cd ${moduleName} && docker build -t ${dockerImage} -f ../Dockerfile --build-arg SERVICE_NAME=${jarName} .
sleep 1
docker push ${dockerImage}
sleep 1
docker rmi ${dockerImage}
"""
}
}
}
}
}
}
GitOps-CI 擴展部分
在原始 CI 作業的步驟基礎上,增加了一個更新環境的步驟。GitOps 實踐會將當前的基礎環境部署文件存放到一個 Git 倉庫中。我們的 CI 作業在完成鏡像上傳後,同時更新環境部署文件中的鏡像標籤信息。(所以我們需要先獲取該環境文件並更新上傳)
stage("PushFile"){
// when {
// expression { "${env.branchName}".contains("RELEASE-") }
// }
steps{
script{
if ("${env.branchName}".contains("RELEASE-")){
println("branchName = branchName")
env.branchName = "master"
} else {
env.branchName = "feature"
}
for (i = 0; i < 3; i++) {
//下載版本庫文件
response = GetRepoFile(40,"${moduleName}%2fvalues.yaml", "${env.branchName}")
//println(response)
//替換文件中內容
yamlData = readYaml text: """${response}"""
println(yamlData.image.version)
println(yamlData.image.commit)
yamlData.image.version = "${releaseVersion}-${env.nowDate}"
yamlData.image.commit = "${commitId}"
println(yamlData.toString())
sh "rm -fr test.yaml"
writeYaml charset: 'UTF-8', data: yamlData, file: 'test.yaml'
newYaml = sh returnStdout: true, script: 'cat test.yaml'
println(newYaml)
//更新gitlab文件內容
base64Content = newYaml.bytes.encodeBase64().toString()
// 會有並行問題,同時更新報錯
try {
UpdateRepoFile(40,"${moduleName}%2fvalues.yaml",base64Content, "${env.branchName}")
break;
} catch(e){
sh "sleep 2"
continue;
}
}
}
}
}
//封裝HTTP請求
def HttpReq(reqType,reqUrl,reqBody){
def gitServer = "http://gitlab.idevops.site/api/v4"
withCredentials([string(credentialsId: 'gitlab-token', variable: 'gitlabToken')]) {
result = httpRequest customHeaders: [[maskValue: true, name: 'PRIVATE-TOKEN', value: "${gitlabToken}"]],
httpMode: reqType,
contentType: "APPLICATION_JSON",
consoleLogResponseBody: true,
ignoreSslErrors: true,
requestBody: reqBody,
url: "${gitServer}/${reqUrl}"
//quiet: true
}
return result
}
//獲取文件內容
def GetRepoFile(projectId,filePath,branchName){
apiUrl = "projects/${projectId}/repository/files/${filePath}/raw?ref=${branchName}"
response = HttpReq('GET',apiUrl,'')
return response.content
}
//更新文件內容
def UpdateRepoFile(projectId,filePath,fileContent, branchName){
apiUrl = "projects/${projectId}/repository/files/${filePath}"
reqBody = """{"branch": "${branchName}","encoding":"base64", "content": "${fileContent}", "commit_message": "update a new file"}"""
response = HttpReq('PUT',apiUrl,reqBody)
println(response)
}
images
GitOps-CD 部分
CD-Scheduler 作業
此作業其實也是接收 GitLab 的 webhook 請求, 與 CI-scheduler 作業類似。不同的是這個 CD-scheduler 作業是用來接收環境倉庫的代碼變更。開啓 webhook, 配置觸發 token。生成 hookurl:http://jenkins.idevops.site/generic-webhook-trigger/invoke?token=microservicecicd-scheduler-CD
Jenkinsfile
pipeline {
agent any
stages {
stage('GetCommitService') {
steps {
script{
echo 'Hello World'
echo "${WebHookData}"
// Git Info
webhookdata = readJSON text: """${WebHookData}"""
eventType = webhookdata["object_kind"]
commits = webhookdata["commits"]
branchName = webhookdata["ref"] - "refs/heads/"
projectID = webhookdata["project_id"]
commitID = webhookdata["checkout_sha"]
changeServices = []
for(commit in commits) {
println(commit.id)
//added
for (add in commit.added) {
s = add.split("/") as List
if (s.size() > 1){
if (changeServices.indexOf(s[0]) == -1){
changeServices.add(s[0])
}
}
}
//modified
for (m in commit.modified) {
s = m.split("/") as List
// println s
// println s.size()
// println s[0]
if (s.size() > 1){
// println changeServices.indexOf(s[0])
if (changeServices.indexOf(s[0]) == -1){
changeServices.add(s[0])
}
}
}
//removed
for (r in commit.removed) {
s = r.split("/") as List
println s
if (s.size() > 1){
if (changeServices.indexOf(s[0]) == -1){
changeServices.add(s[0])
}
}
}
}
println(changeServices)
currentBuild.description = " Trigger by ${eventType} ${changeServices} "
}
}
}
stage('DefineService') {
steps {
script{
println(changeServices)
//服務構建順序控制
services = ['service02', 'service01']
for (service in services){
if (changeServices.indexOf(service) != -1){
jobName = 'microservicecicd-'+service+'-service-CD'
build job: jobName, wait: false, parameters: [string(name: 'branchName', value: "${branchName}" )]
}
}
}
}
}
}
}
環境庫配置 webhook
開啓 webhook,配置 hookurl:http://jenkins.idevops.site/generic-webhook-trigger/invoke?token=microservicecicd-scheduler-CD
CD 流水線 - CD 作業
Jenkinsfile
String serviceName ="${JOB_NAME}".split("-")[1]
String nameSpace = "${JOB_NAME}".split("-")[0].split("/")[-1]
//pipeline
pipeline{
agent { node { label "k8s"}}
stages{
stage("GetCode"){
steps{
script{
println("${branchName}")
println("${env.branchName}".contains("RELEASE-"))
println "獲取代碼"
checkout([$class: 'GitSCM', branches: [[name: "${env.branchName}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [[$class: 'SparseCheckoutPaths',
sparseCheckoutPaths: [[path: "${serviceName}"]]]],
submoduleCfg: [],
userRemoteConfigs: [[credentialsId: 'gitlab-admin-user', url: "http://gitlab.idevops.site/microservicecicd/microservicecicd-env.git"]]])
}
}
}
stage("HelmDeploy"){
steps{
script{
sh """
kubectl create ns "${nameSpace}-uat" || echo false
helm install "${serviceName}" --namespace "${nameSpace}-uat" ./"${serviceName}" || helm upgrade "${serviceName}" --namespace "${nameSpace}-uat" ./"${serviceName}"
helm list --namespace "${nameSpace}-uat"
helm history "${serviceName}" --namespace "${nameSpace}-uat"
"""
}
}
}
}
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/MN08YzdpDMYZ5xpQP1ECQQ