CI/CD 實踐指南
1.概念和背景
IT團隊內(nèi)部為了提升開發(fā)效率,減少不斷發(fā)布耗費的時間,我們會將流程固化下來,這樣避免每次發(fā)布都需要專人,去操作計算機,打包,編譯,運行等。
這樣就有了Devops,CI CD這些概念,那首先說一下Devops和CICD的關(guān)系?
DevOps是一種以協(xié)作和自動化為基礎(chǔ)的方法論,旨在促進開發(fā)和運維團隊之間的合作,提高軟件交付的速度和質(zhì)量。而CICD則是一種實踐方法,通過自動化的流程來保證軟件開發(fā)中的持續(xù)集成、測試和部署?。在采用DevOps方法論的組織中,CICD是一個核心的實踐方法。通過持續(xù)集成和持續(xù)交付,團隊能夠頻繁地將代碼集成到主干分支,并自動化地構(gòu)建、測試和部署軟件,從而快速響應用戶需求,減少發(fā)布周期,提高軟件質(zhì)量和可靠性?。CICD是實現(xiàn)DevOps的方法之一。通過自動化工具和流程,CICD支持DevOps的實踐,確保軟件開發(fā)過程中的高質(zhì)量和交付的及時性?2。持續(xù)集成(CI)是在源代碼變更后自動檢測、拉取、構(gòu)建和進行單元測試的過程,而持續(xù)交付(CD)則是在完成CI后,將已驗證的代碼發(fā)布到生產(chǎn)環(huán)境?。

DevOps(Development和Operations的組合詞)是一組過程、方法與系統(tǒng)的統(tǒng)稱,用于促進開發(fā)、技術(shù)運營和質(zhì)量保障(QA)部門之間的溝通、協(xié)作與整合。
--可以把DevOps看作開發(fā)、技術(shù)運營和質(zhì)量保障(QA)三者的交集。

CI/CD 的核心概念是持續(xù)集成、持續(xù)交付和持續(xù)部署。CI/CD 可讓持續(xù)自動化和持續(xù)監(jiān)控貫穿于應用的整個生命周期(從集成和測試階段,到交付和部署)。這些關(guān)聯(lián)的事務通常被統(tǒng)稱為“CI/CD 管道”,由開發(fā)和運維團隊以敏捷方式協(xié)同支持。
--CI持續(xù)集成(Continuous Integration),可以幫助開發(fā)人員更加頻繁地(有時甚至每天)將代碼更改合并到共享分支或“主干”中。一旦開發(fā)人員對應用所做的更改被合并,系統(tǒng)就會通過自動構(gòu)建應用并運行不同級別的自動化測試,來驗證這些更改,確保這些更改沒有對應用造成破壞。
--CD 持續(xù)交付(Continuous Delivery),完成 CI 中構(gòu)建及單元測試和集成測試的自動化流程后,持續(xù)交付可自動將已驗證的代碼發(fā)布到存儲庫。為了實現(xiàn)高效的持續(xù)交付流程,務必要確保 CI 已內(nèi)置于開發(fā)管道。持續(xù)交付的目標是擁有一個可隨時部署到生產(chǎn)環(huán)境的代碼庫。
--CD 持續(xù)部署(Continuous Deployment),持續(xù)部署可以自動將應用發(fā)布到生產(chǎn)環(huán)境,持續(xù)部署意味著開發(fā)人員對應用的更改在編寫后的幾分鐘內(nèi)就能生效(假設(shè)它通過了自動化測試)。
總而言之,所有這些 CI/CD 的關(guān)聯(lián)步驟都有助于降低應用的部署風險,因此更便于以小件的方式(而非一次性)發(fā)布對應用的更改。不過,由于還需要編寫自動化測試以適應 CI/CD 管道中的各種測試和發(fā)布階段,因此前期投資還是會很大。另外,針對現(xiàn)在較流行的敏捷開發(fā),需要高頻詞發(fā)布,Devops比較適用。簡言之,我們使用CI/CD來高頻且可預測地交付更高質(zhì)量的軟件。
(PS:我們還應該確保從開始到結(jié)束都將安全性考慮到開發(fā)過程中。這通常被稱為DevSecOps。)
2.我們的技術(shù)棧?
講講我們的實現(xiàn),目前我們使用基于springcloud的微服務架構(gòu),運行環(huán)境是K8S,CI工具:Jenkins,CD工具:Argo CD
Argo CD流程:

Argo CD 的工作流程如下:
創(chuàng)建應用程序: 用戶通過 Argo CD 的命令行界面或 Web 界面創(chuàng)建應用程序。在創(chuàng)建應用程序時,用戶需要指定應用程序的名稱、Git 存儲庫的 URL、分支或標簽以及路徑等信息。
GitOps 同步: Argo CD 定期輪詢配置的 Git 存儲庫,檢測應用程序配置文件的變更。一旦發(fā)現(xiàn)變更,它會觸發(fā)同步過程。
應用程序同步: Argo CD 從 Git 存儲庫中獲取應用程序的聲明性配置文件(例如 YAML 文件),并將其與當前的實際狀態(tài)進行比較。
狀態(tài)比較: Argo CD 使用 Kubernetes API 與集群進行通信,獲取當前部署的應用程序的實際狀態(tài)。然后,它將實際狀態(tài)與聲明性配置文件中定義的期望狀態(tài)進行比較。
狀態(tài)同步: 如果實際狀態(tài)與期望狀態(tài)不一致,Argo CD 將自動采取必要的操作來調(diào)整實際狀態(tài),使其與期望狀態(tài)保持一致。這可能涉及創(chuàng)建、更新或刪除 Kubernetes 資源。
應用程序健康檢查: Argo CD 監(jiān)測應用程序的健康狀態(tài),以確保部署成功。它可以通過檢查容器的就緒狀態(tài)、服務的可用性和自定義的健康檢查指標來確定應用程序是否正常運行。
持續(xù)同步: Argo CD 會定期輪詢 Git 存儲庫,以確保應用程序的狀態(tài)與聲明性配置文件保持同步。如果發(fā)現(xiàn)配置文件有更新,它將觸發(fā)新一輪的同步過程,以將實際狀態(tài)調(diào)整為期望狀態(tài)。
可視化界面和監(jiān)控: Argo CD 提供了一個直觀的 Web 界面,用于查看和管理應用程序的狀態(tài)。用戶可以在界面上查看應用程序的拓撲圖、部署歷史、健康狀態(tài)和同步狀態(tài)。此外,Argo CD 還提供了監(jiān)控和警報功能,以幫助用戶監(jiān)測應用程序的性能和可用性。
3.如何做?
3.1.創(chuàng)建代碼庫
在項目中創(chuàng)建 devops-ci 和 devops-cd 兩個代碼倉庫 。devops-ci : 用于放 Jenkins 腳本,以及 ci 相關(guān)的所有文件 ;devops-cd : 用于放 k8s 部署腳本,ArgoCD 會讀取其中的 yaml。所有環(huán)境的部署腳本都放在 master 分支下,通過目錄結(jié)構(gòu)隔開。
3.2.CI腳本
目錄結(jié)構(gòu)為(三個環(huán)境):
pipeline
├─dev
│ declartion.jenkinsfile
│ gateway.jenkinsfile
├─qa
│ declartion.jenkinsfile
│ gateway.jenkinsfile
└─test
舉例,一個微服務的構(gòu)建腳本:
def project_name = "app" def service_name = "order" def dev_repo_url = "http://***/kra-mobileapp/order.git" def branch = "master" def target_env = "dev" def docker_register_url = "docker-registry.***.net"//內(nèi)部nexues鏡像倉庫 def git_credential_id = "{git_credential_id}" // 請修改為正確的 credential_id def node_name = "{node_name}" // 請修改為對應的 node_name @Library('uea-shared-libraries') _ def ueaCommonUtils = new com.tool.UeaCommonUtils(); pipeline { agent { node { label node_name } } stages { stage("Get code") { steps { timeout(time:10, unit:"MINUTES") { script { ueaCommonUtils.checkout(git_credential_id, dev_repo_url, branch) } } } } stage("Maven package") { steps { timeout(time:30, unit:"MINUTES") { script { dir('source') { ueaCommonUtils.mvnPackage() } } } } } stage("Maven build image") { steps { timeout(time:30, unit:"MINUTES") { script { dir('source') { ueaCommonUtils.buildDockerImage() } } } } } stage('Push image') { steps { script { timeout(time:10, unit:"MINUTES") { ueaCommonUtils.pushImage(project_name, service_name, docker_register_url) } } } } stage('Get DevOps CD') { steps { script { timeout(time:10, unit:"MINUTES") { script { ueaCommonUtils.checkoutCD(git_credential_id, cd_url) } } } } } stage('Update CD Image tag') { steps { script { timeout(time:10, unit:"MINUTES") { script { ueaCommonUtils.updateCDImageTag(project_name, service_name, docker_register_url, target_env) } } } } } } post { always { 2.2.2. gateway 服務構(gòu)建腳本 script{ currentBuild.description = "Build Node: ${node_name}" } } success { script{ currentBuild.description += "\n 構(gòu)建成功!" } } failure { script{ currentBuild.description += "\n 構(gòu)建失敗!" } } aborted { script{ currentBuild.description += "\n 構(gòu)建取消!" } } } }
ps:shared-libraries
package com.tool def checkout(credentials_id, repo_url, branch, targetDir='source', wipeWorkspace=true) { def gitExtensions = [ [ $class: 'RelativeTargetDirectory', relativeTargetDir: targetDir ] ] if(wipeWorkspace) { gitExtensions.add([ $class: 'WipeWorkspace' ]) } timeout(time:10, unit:"MINUTES") { checkout( poll: false, scm: scmGit( branches: [[name: branch]], extensions: gitExtensions, userRemoteConfigs: [[ credentialsId: credentials_id, url: repo_url ]] ) ) } } def mvnPackage(){ sh """ mvn clean package """ } def buildDockerImage(enableTest=false){ if(enableTest) { sh """ mvn -Dprod clean verify -DskipTests -U jib:dockerBuild """ } else { sh """ mvn -Dprod clean test -U jib:dockerBuild """ } } def pushImage(project_name, service_name, docker_register_url){ sh """ docker tag ${project_name}-${service_name}:latest ${docker_register_url}/${project_name}/${service_name}:${build_id} sleep 1 docker push ${docker_register_url}/${project_name}/${service_name}:${build_id} """ } def checkoutCD(credentials_id, repo_url) { checkout(credentials_id, repo_url, 'master', 'devOpsCD', true) } def updateCDImageTag(project_name, service_name, docker_register_url, target_env){ sh """ cd devOpsCD/manifests/overlays/${target_env} kustomize edit set image ${service_name}-image=${docker_register_url}/${project_name}/${service_name}:${build_id} git config --global user.name "Bot UEA DevOps" git config --global user.email "**@**.com" git checkout master git commit -am "ci(UEA auto commit): update image tag to ${build_id}" git push origin master """ }
微服務中POM.xml需要調(diào)整打包方式:
<from> <image>docker-registry.***.net/adoptopenjdk:11-jre-hotspot</image> </from> <to> <image>app-order:latest</image> </to>
3.3.Jenkins pipeline
選擇“文件夾”,然后創(chuàng)建item

選擇“pipeline script from SCM”

script path填寫倉庫中jenkins file 的路徑:

3.4.CD 腳本
CD腳本放在上面介紹的devops-cd目錄中
目錄結(jié)構(gòu)參考:

service中按照微服務模塊分,不同環(huán)境放在overlays下,如:dev/prod,通過kustomization.yaml 的 namePrefix 和 namespace來確定項目名和環(huán)境名(開發(fā)dev,生產(chǎn)prod)
kustomization.yaml參考
# 文件路徑 app/overlays/dev/kustomization.yaml resources: - ./config/central-config-configmap.yaml - ./config/common-configmap.yaml - ./network/gateway.yaml - ./network/virtual-service.yaml - ../../base namePrefix: app-order-dev- namespace: app-order-dev commonAnnotations: note: development environment # REDIS # RABBITMQ # DB # Registry #- DB_GATEWAY_URL=jdbc:oracle:thin:@***:1521:DEV secretGenerator: - literals: - SPRING_REDIS_PASSWORD=** - SPRING_RABBITMQ_USERNAME=admin - SPRING_RABBITMQ_PASSWORD=** - DB_GATEWAY_URL=jdbc:postgresql://***/app_order_dev_gateway?currentSchema=app_order_dev_gateway - DB_GATEWAY_USERNAME=app_order_dev_gateway - DB_GATEWAY_PASSWORD=dev_gateway - DB_CONFIGURATION_URL=jdbc:postgresql://***/app_order_dev_configuration?currentSchema=app_order_dev_configuration - DB_CONFIGURATION_USERNAME=app_order_dev_configuration - DB_CONFIGURATION_PASSWORD=dev_configuration - JHIPSTER_REGISTRY_PASSWORD=admin name: common-secret apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: configuration-image newName: docker-registry.bullchina.net/order/configuration newTag: "5" - name: gateway-image newName: docker-registry.bullchina.net/order/gateway newTag: "11"
微服務配置參考:
apiVersion: apps/v1 kind: Deployment metadata: name: configuration spec: replicas: 1 selector: matchLabels: app: configuration template: metadata: labels: app: configuration spec: nodeSelector: "kubernetes.io/os": linux containers: - name: configuration image: configuration-image env: - name: SPRING_PROFILES_ACTIVE value: "prod" - name: MANAGEMENT_METRICS_EXPORT_PROMETHEUS_ENABLED value: "false" - name: JHIPSTER_CACHE_REDIS_CLUSTER value: "false" - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE valueFrom: configMapKeyRef: name: common-configmap key: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE - name: SPRING_CLOUD_CONFIG_URI valueFrom: configMapKeyRef: name: common-configmap key: SPRING_CLOUD_CONFIG_URI - name: SPRING_REDIS_HOST valueFrom: configMapKeyRef: name: common-configmap key: SPRING_REDIS_HOST - name: SPRING_REDIS_PORT valueFrom: configMapKeyRef: name: common-configmap key: SPRING_REDIS_PORT - name: SPRING_RABBITMQ_HOST valueFrom: configMapKeyRef: name: common-configmap key: SPRING_RABBITMQ_HOST - name: SPRING_RABBITMQ_PORT valueFrom: configMapKeyRef: name: common-configmap key: SPRING_RABBITMQ_PORT - name: JHIPSTER_REGISTRY_PASSWORD valueFrom: secretKeyRef: name: common-secret key: JHIPSTER_REGISTRY_PASSWORD - name: SPRING_REDIS_PASSWORD valueFrom: secretKeyRef: name: common-secret key: SPRING_REDIS_PASSWORD - name: SPRING_RABBITMQ_USERNAME valueFrom: secretKeyRef: name: common-secret key: SPRING_RABBITMQ_USERNAME - name: SPRING_RABBITMQ_PASSWORD valueFrom: secretKeyRef: name: common-secret key: SPRING_RABBITMQ_PASSWORD - name: SPRING_DATASOURCE_URL valueFrom: secretKeyRef: name: common-secret key: DB_CONFIGURATION_URL - name: SPRING_DATASOURCE_JDBC_URL valueFrom: secretKeyRef: name: common-secret key: DB_CONFIGURATION_URL - name: SPRING_DATASOURCE_USERNAME valueFrom: secretKeyRef: name: common-secret key: DB_CONFIGURATION_USERNAME - name: SPRING_DATASOURCE_PASSWORD valueFrom: secretKeyRef: name: common-secret key: DB_CONFIGURATION_PASSWORD resources: requests: cpu: 10m memory: 128Mi limits: cpu: "1" memory: 2Gi ports: - containerPort: 8082 name: http startupProbe: httpGet: path: /healthcheck port: 8082 initialDelaySeconds: 20 periodSeconds: 5 failureThreshold: 120 readinessProbe: httpGet: path: /healthcheck port: 8082 periodSeconds: 5 failureThreshold: 30 livenessProbe: httpGet: path: /healthcheck port: 8082 periodSeconds: 5 failureThreshold: 30 apiVersion: v1 kind: Service metadata: name: configuration spec: ports: - port: 8082 targetPort: 8082 name: http selector: app: configuration
3.5.Argo CD配置
3.5.1.創(chuàng)建命名空間
# case 項目名稱,env 部署環(huán)境名稱 kubectl create namespace app-{case}-{env} kubectl label namespace app-{case}-{env} istio-injection=enabled
3.5.2.創(chuàng)建項目

連接CD腳本倉庫:



3.5.3.授權(quán)倉庫和集群給項目



3.5.4創(chuàng)建APP



到此,完成CI/CD的配置,
3.5.5.訪問域名配置
腳本位置:manifests/overlays/dev/network/gateway.yaml,virtual-service.yaml
apiVersion: networking.istio.io/v1 kind: VirtualService metadata: name: virtual-service spec: gateways: - app-order-dev-gateway hosts: - app-order-dev.k8s.local http: - match: - uri: prefix: / route: - destination: host: app-order-dev-gateway port: number: 8080
apiVersion: networking.istio.io/v1 kind: Gateway metadata: name: gateway spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - app-order-dev.k8s.local
PS:需要將域名與IP映射加入到主機host文件,即可使用上面virtual-service.yaml中配置的hosts地址進行訪問。


浙公網(wǎng)安備 33010602011771號