#云原生征文# 持续集成CI/CD之CD的完整版最佳实践 原创 精华

老梅mqm
发布于 2022-6-1 16:40
浏览
6收藏

上一章:持续集成CI/CD之CI的完整版最佳实践

CI&CD解读

概述

​ CI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。CI/CD 的核心概念是持续集成、持续交付和持续部署。作为一个面向开发和运营团队的解决方案,CI/CD 主要针对在集成新代码时所引发的问题(亦称:“集成地狱”)。

​ 具体而言,CI/CD 可让持续自动化和持续监控贯穿于应用的整个生命周期(从集成和测试阶段,到交付和部署)。这些关联的事务通常被统称为"CI/CD 管道",由开发和运维团队以敏捷方式协同支持。

CI 是什么?CI 和 CD 有什么区别?

​ 缩略词 CI / CD 具有几个不同的含义。CI/CD 中的"CI"始终指持续集成,它属于开发人员的自动化流程。成功的 CI 意味着应用代码的新更改会定期构建、测试并合并到共享存储库中。该解决方案可以解决在一次开发中有太多应用分支,从而导致相互冲突的问题。

​ CI/CD 中的"CD"指的是持续交付和/或持续部署,这些相关概念有时会交叉使用。两者都事关管道后续阶段的自动化,但它们有时也会单独使用,用于说明自动化程度。

​ 持续交付通常是指开发人员对应用的更改会自动进行错误测试并上传到存储库(如 GitHub 或容器注册表),然后由运维团队将其部署到实时生产环境中。这旨在解决开发和运维团队之间可见性及沟通较差的问题。因此,持续交付的目的就是确保尽可能减少部署新代码时所需的工作量。

​ 持续部署(另一种"CD")指的是自动将开发人员的更改从存储库发布到生产环境,以供客户使用。它主要为了解决因手动流程降低应用交付速度,从而使运维团队超负荷的问题。持续部署以持续交付的优势为根基,实现了管道后续阶段的自动化。

#云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区

​ CI/CD 既可能仅指持续集成和持续交付构成的关联环节,也可以指持续集成、持续交付和持续部署这三项构成的关联环节。更为复杂的是,有时"持续交付"也包含了持续部署流程。

​ 归根结底,我们没必要纠结于这些语义,您只需记得 CI/CD 其实就是一个流程(通常形象地表述为管道),用于实现应用开发中的高度持续自动化和持续监控。因案例而异,该术语的具体含义取决于 CI/CD 管道的自动化程度。许多企业最开始先添加 CI,然后逐步实现交付和部署的自动化(例如作为云原生应用的一部分)。

CI 持续集成(Continuous Integration)

​ 现代应用开发的目标是让多位开发人员同时处理同一应用的不同功能。但是,如果企业安排在一天内将所有分支源代码合并在一起(称为"合并日"),最终可能造成工作繁琐、耗时,而且需要手动完成。这是因为当一位独立工作的开发人员对应用进行更改时,有可能会与其他开发人员同时进行的更改发生冲突。如果每个开发人员都自定义自己的本地集成开发环境(IDE),而不是让团队就一个基于云的 IDE 达成一致,那么就会让问题更加雪上加霜。

​ 持续集成(CI)可以帮助开发人员更加频繁地(有时甚至每天)将代码更改合并到共享分支或"主干"中。一旦开发人员对应用所做的更改被合并,系统就会通过自动构建应用并运行不同级别的自动化测试(通常是单元测试和集成测试)来验证这些更改,确保这些更改没有对应用造成破坏。这意味着测试内容涵盖了从类和函数到构成整个应用的不同模块。如果自动化测试发现新代码和现有代码之间存在冲突,CI 可以更加轻松地快速修复这些错误。

CD 持续交付(Continuous Delivery)

​ 完成 CI 中构建及单元测试和集成测试的自动化流程后,持续交付可自动将已验证的代码发布到存储库。为了实现高效的持续交付流程,务必要确保 CI 已内置于开发管道。持续交付的目标是拥有一个可随时部署到生产环境的代码库。

​ 在持续交付中,每个阶段(从代码更改的合并,到生产就绪型构建版本的交付)都涉及测试自动化和代码发布自动化。在流程结束时,运维团队可以快速、轻松地将应用部署到生产环境中。

CD 持续部署(Continuous Deployment)

​ 对于一个成熟的 CI/CD 管道来说,最后的阶段是持续部署。作为持续交付——自动将生产就绪型构建版本发布到代码存储库——的延伸,持续部署可以自动将应用发布到生产环境。由于在生产之前的管道阶段没有手动门控,因此持续部署在很大程度上都得依赖精心设计的测试自动化。

​ 实际上,持续部署意味着开发人员对应用的更改在编写后的几分钟内就能生效(假设它通过了自动化测试)。这更加便于持续接收和整合用户反馈。总而言之,所有这些 CI/CD 的关联步骤都有助于降低应用的部署风险,因此更便于以小件的方式(而非一次性)发布对应用的更改。不过,由于还需要编写自动化测试以适应 CI/CD 管道中的各种测试和发布阶段,因此前期投资还是会很大。

CI&CD集成流程

架构流程设计

#云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区

(1)制品库制作;(2)提交部署服务与版本;(3)选择服务部署;(4)查看服务情况;

gitlab-ci构建制品库

制作制品库,使用了gitlab自带的CI/CD,代码编译与镜像制作都使用容器环境构建。

java制品库模板

本模板所有编译都采用容器环境,适用于父子结构的java工程;工程结构如下

父工程
|--- 子模块1  # java子模块
|------ docker文件夹 # 存放部署文件
|--------- Dockerfile
|--------- Jenkinsfile
|--------- k8s.yaml
|------ src
...
|--- 子模块2
|------ docker文件夹
|--------- Dockerfile
|--------- Jenkinsfile
|--------- k8s.yaml
|------ src
...
|--- .gitlab-ci.yml

​ 运行CI之前,需要在代码库组的CI/CD参数中需要设置CI_REGISTRY:docker仓库地址;CI_REGISTRY_USER:docker仓库账号; CI_REGISTRY_PASSWORD:docker仓库password;CI_NAME_SPACE:镜像的仓库路径[项目名称空间];CI_MAVEN_REPO:maven本地仓库路径;CI_SETTING文件类型maven的setting.xml

参数说明

MODULES:是子模块列表以“,”隔开

MODULE_PREFIX:用于更正子模块名称不带前缀情况。

# 参考文档:https://docs.gitlab.com/ee/ci/yaml/index.html
# 需要gitlab需要配置如下参数:
# CI_REGISTRY:docker仓库地址,  CI_REGISTRY_USER:docker仓库账号,  CI_REGISTRY_PASSWORD:docker仓库password,
# CI_NAME_SPACE:镜像的仓库路径[项目名称空间], CI_MAVEN_REPO:maven本地仓库路径,CI_SETTING:文件类型maven的setting.xml
# java工程只需要修改此模板的variables中的MODULES与MODULE_PREFIX的参数
variables:
  IMAGE_PRE: $CI_REGISTRY/$CI_NAME_SPACE
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_MAVEN_REPO"
  #打包多模块镜像以,隔开,eg:soeasy-clm-open,soeasy-clm-message/soeasy-clm-message-server
  MODULES: $MODULE1,$MODULE2,...,$MODULEn
  MODULE_PREFIX: $MODULE_PREFIX  #模块前缀

# 全局缓存jar文件
cache: &global_cache
  paths:
    - ./{$MODULES}/target/*.jar
    - ./{$MODULES}/**/target/*.jar
  when: 'on_success'

stages:
  - java-build
  - docker-build

job_build_tag_java:
  image: maven:3.5-jdk-8-alpine
  only:
    - tags
  stage: java-build
  # 配置仅仅上传全局jar缓存
  cache:
    <<: *global_cache
    policy: push
  script:
    - cat "$CI_SETTING" > ci_settings.xml
    - mvn -version && mvn clean package -pl $MODULES -am -Dmaven.test.skip=true -s ci_settings.xml

job_build_tag_docker:
  image: ming19871211/docker:latest
  only:
    - tags
  stage: docker-build
  services:
    - docker:dind
  # 配置仅仅下载全局缓存
  cache:
    <<: *global_cache
    policy: pull
  before_script:
    - docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASSWORD
    - | # 定义应用版本号
      app_version=$CI_COMMIT_TAG
      if [ -z "$app_version" ]; then
        app_version=$CI_COMMIT_SHORT_SHA
      fi
      export app_version
  script:
    - echo "开始打包docker镜像"
    - | # 切割模块,遍历模块
      OLD_IFS="$IFS"
      IFS=","
      module_arr=(${MODULES})
      IFS=$OLD_IFS
      for module in ${module_arr[@]}
      do
        mv $module/target $module/docker/
        target_jar_name=$(ls $module/docker/target/*.jar) #获取jar路径
        target_jar_name=${target_jar_name##*/} #去掉文件前缀
        module_std=${target_jar_name%.jar} #去掉.jar后缀
        module_std=${module_std%-[0-9].[0-9]*} #去掉maven版本号
        # 解决打包名称不按正规方式命名
        if [[ ! $module_std == $MODULE_PREFIX* ]]; then
          module_std=${module##*/}
        fi
        if [[ ! $module_std == $MODULE_PREFIX* ]]; then
          module_std="${MODULE_PREFIX}-${module_std}"
        fi
        docker build -t $IMAGE_PRE/$module_std:$app_version --build-arg JAR_FILE=$target_jar_name $module/docker
        docker push $IMAGE_PRE/$module_std:$app_version
        docker rmi $IMAGE_PRE/$module_std:$app_version
      done

运行结果如图

#云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区

vue制品库模板

本模板所有编译都采用容器环境,适用于vue工程;工程结构如下

|--- docker文件夹 # 存放部署文件
|------ Dockerfile
|------ Jenkinsfile
|------ k8s.yaml
|------ ... # 根据项目需求增加其它处理脚本,如docker启动脚本、初始化配置等等
|--- public
|--- src
|--- vue相关文件
...
|--- .gitlab-ci.yml

运行CI之前,需要在代码库组的CI/CD参数中需要设置CI_REGISTRY:docker仓库地址;CI_REGISTRY_USER:docker仓库账号; CI_REGISTRY_PASSWORD:docker仓库password;CI_NAME_SPACE:镜像的仓库路径[项目名称空间];

# 参考文档:https://docs.gitlab.com/ee/ci/yaml/index.html
# 需要配置如下参数:
# CI_REGISTRY:docker仓库地址,  CI_REGISTRY_USER:docker仓库账号,  CI_REGISTRY_PASSWORD:docker仓库password,
# CI_NAME_SPACE:镜像的仓库路径[项目所属空间], 若$CI_PROJECT_NAME命名不规范,则手动修改
variables:
  IMAGE: $CI_REGISTRY/$CI_NAME_SPACE/$CI_PROJECT_NAME
  
# 全局缓存jar文件
cache: &global_cache
  paths:
    - node_modules/
    - dist/
  when: 'on_success'

stages:
  - vue-build
  - docker-build

job_build_tag_vue:
  image: node:12
  only:
    - tags
  stage: vue-build
  # 配置仅仅上传全局node缓存
  cache:
    <<: *global_cache
    policy: push
  script:
    - npm -v && node -v && npm config set registry https://registry.npm.taobao.org && npm install && npm run build 

job_build_tag_docker:
  image: docker:latest
  only:
    - tags
  stage: docker-build
  services:
    - docker:dind
  # 配置仅仅下载全局缓存
  cache:
    <<: *global_cache
    policy: pull
  before_script:
    - docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASSWORD
    - | # 定义应用版本号
      app_version=$CI_COMMIT_TAG
      if [ -z "$app_version" ]; then
        app_version=$CI_COMMIT_SHORT_SHA
      fi
      export app_version
  script:
    - echo "开始打包docker镜像"
    - rm -rf docker/dist && mv dist docker/
    - docker build -t $IMAGE:$app_version docker
    - docker push $IMAGE:$app_version
    - docker rmi $IMAGE:$app_version

运行结果如图

#云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区

服务部署列表与版本跟踪

​ 因为需要发布到生产环境,需要跟踪发布的过程,本实践采用git来跟踪脚本。

​ 本jenkins部署脚本时将成品的docker镜像,通过jenkins直接发布到k8s环境测试或者生产环境。使用git地址存储编写的jenkinsfile脚本,利用git与jenkins自身的功能完成发版的可追溯、可回滚等等

​ 采用jenkins-pipeline的声明式部署,部署都使用容器化,不依赖主机。相关的文件都使用jenkins系统变量、凭证配置。

部署发版的工程结构如下

|--- yaml  #存放k8s部署的yaml文件模块
|--- version-app.sh #定义部署服务的列表,版本等等
|--- Jenkinsfile #定义jenkins部署脚本

部署yaml文件

模板部分参数占位符说明:

__NAME_SPACE__: 部署空间占位符;

__DOMAIN_NAME__:部署服务名称占位符;

__REPLICAS_NUM__:部署实例数量占位符;

__DOCKER_IMAGE__:docker镜像地址占位符;

java部署模板k8s-java.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: __NAME_SPACE__
  labels:
    name: __NAME_SPACE__
---
apiVersion: v1
kind: Service
metadata:
  name: __DOMAIN_NAME__
  namespace: __NAME_SPACE__
spec:
  ports:
  - name: app-port
    port: __CONTAINER_PORT__
    targetPort: __CONTAINER_PORT__
    protocol: TCP
  # 当开启管理端口时,外部服务需要访问时打开此注释
  # - name: manage-port
  #   port: __MANAGE_PORT__
  #   targetPort: __MANAGE_PORT__
  #   protocol: TCP
  selector:
    app: __DOMAIN_NAME__
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: __DOMAIN_NAME__
  namespace: __NAME_SPACE__
spec:
  selector:
    matchLabels:
      app: __DOMAIN_NAME__
  replicas: __REPLICAS_NUM__
  template:
    metadata:
      labels:
        app: __DOMAIN_NAME__
    spec:
      initContainers:
      - name: init-agent-sidecar
        image: ming19871211/skywalking-agent:__SKYWALKING_VESION__
        command:
        - 'sh'
        - '-c'
        - 'set -ex;cp -r /skywalking/agent/* /usr/skywalking/agent;'
        volumeMounts:
        - name: agent
          mountPath: /usr/skywalking/agent
      containers:
      - name: __DOMAIN_NAME__
        image: __DOCKER_IMAGE__
        imagePullPolicy: IfNotPresent #本地存在就不到远程拉取镜像
        env: #环境变量设置
        - name: TZ
          value: Asia/Shanghai
        - name: NACOS_NAMESPACE
          value: __NACOS_NAMESPACE__
        - name: DEPLOY_ENV  # 兼容性配置,后续版本建议删除
          value: __NACOS_NAMESPACE__
        - name: NACOS_GROUP
          value: __NACOS_GROUP__
        - name: JAVA_OPTS
          value: "__JAVA_OPTS__"
        - name: CONTAINER_PORT
          value: "__CONTAINER_PORT__"
        - name: MANAGE_PORT
          value: "__MANAGE_PORT__"
        - name: NACOS_URL
          value: __NACOS_URL__
        - name: DOMAIN_NAME
          value: __DOMAIN_NAME__.__NAME_SPACE__
        # - name: DOMAIN_NAME
        #   valueFrom:
        #     fieldRef:
        #       apiVersion: v1
        #       fieldPath: status.podIP
        envFrom:
        - secretRef:
            name: __NACOS_AUTH__
        resources: #资源限制
          requests:
            memory: "128Mi"
            cpu: "100m" #最低需要 0.1个cpu
          limits:
            memory: "__LIMIT_MEMORY__Mi"
            cpu: "1000m"
        ports:
        - containerPort: __CONTAINER_PORT__
          name: app-port
          protocol: TCP
        - containerPort: __MANAGE_PORT__
          name: manage-port
          protocol: TCP
        readinessProbe: #就绪探针
          httpGet:
          #  path: __APP_MANAGE_PATH__/actuator/health/readiness
          #  port: __MANAGE_PORT__
          tcpSocket:
            port: __CONTAINER_PORT__
          initialDelaySeconds: 60
          periodSeconds: 15
          timeoutSeconds: 5
        livenessProbe: #健康检查
        #  httpGet:
        #    path: __APP_MANAGE_PATH__/actuator/health/liveness
        #    port: __MANAGE_PORT__
          tcpSocket:
            port: __CONTAINER_PORT__
          initialDelaySeconds: 60
          periodSeconds: 15
          timeoutSeconds: 5
        volumeMounts:
        - name: time-config
          mountPath: /etc/localtime
          readOnly: true
        - name: agent
          mountPath: /usr/skywalking/agent
        # 增加挂载
        # - name: [PVC_NAME_ALIAS] # pod挂载别称
        #   mountPath: [POD_MOUNT_PATH] # pods内挂载路径
        #   subPath: [PVC_MOUNT_SUBPATH] #  pvc挂载盘中的子路径,注释这一项标识挂载根路径下
      imagePullSecrets:
      - name: __DOCKER_REGISTRY_SECRET__
      nodeSelector:
        isTest: "true"
      volumes:
      - name: time-config
        hostPath:
          path: /etc/localtime
      - name: agent
        emptyDir: {}
      # 增加挂载
      # - name: [PVC_NAME_ALIAS] # pod挂载别称
      #   persistentVolumeClaim:
      #     claimName: [PVC_NAME] # 对应指定的pvc名称
vue部署模板k8s-vue.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: __NAME_SPACE__
  labels:
    name: __NAME_SPACE__
---
apiVersion: v1
kind: Service
metadata:
  name: __DOMAIN_NAME__
  namespace: __NAME_SPACE__
spec:
  ports:
  - name: app-port
    port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: __DOMAIN_NAME__
---
apiVersion: apps/v1  
kind: Deployment
metadata:
  name: __DOMAIN_NAME__
  namespace: __NAME_SPACE__
spec:
  selector:
    matchLabels:
      app: __DOMAIN_NAME__
  replicas: __REPLICAS_NUM__ 
  template: 
    metadata:
      labels:
        app: __DOMAIN_NAME__
    spec:
      initContainers:
      - name: init-env-sidecar
        image: busybox:latest
        command: [ "sh", "-c"]
        args:
        - set -ex;
          CONFIG_FILE=${CONFIG_FILE:-"vue-comm.properties"};
          SYS_GLOBAL_CONFIG=${SYS_GLOBAL_CONFIG:-"sys-global-config.properties"};
          wget --post-data="username=${NACOS_USR}&password=${NACOS_PWD}" -S "${NACOS_URL}/nacos/v1/auth/users/login" -O login-token;
          access_token=$(grep  -Eo '"accessToken":"([^"]*)"' login-token |awk -F \":\" '{print $2}');
          access_token=${access_token/\"/};
          rm -f /init-env/env.conf;
          rm -f /init-env/env-sys.conf;
          wget "${NACOS_URL}/nacos/v1/cs/configs?dataId=${CONFIG_FILE}&group=${NACOS_GROUP}&tenant=${NACOS_NAMESPACE}&accessToken=$access_token" -O  /init-env/env.conf;
          wget "${NACOS_URL}/nacos/v1/cs/configs?dataId=${SYS_GLOBAL_CONFIG}&group=${NACOS_GROUP}&tenant=${NACOS_NAMESPACE}&accessToken=$access_token" -O  /init-env/env-sys.conf || return 0;
          if [  $? -eq 0 -a -f "/init-env/env-sys.conf" ]; then
            echo -e  "\n" >>  /init-env/env.conf;
            cat /init-env/env-sys.conf >> /init-env/env.conf;
          fi
        env: #环境变量设置
        - name: NACOS_NAMESPACE
          value: __NACOS_NAMESPACE__
        - name: NACOS_GROUP
          value: __NACOS_GROUP__
        - name: SYS_GLOBAL_CONFIG
          value: __SYS_GLOBAL_CONFIG__
        - name: CONFIG_FILE
          value: __CONFIG_FILE__
        - name: NACOS_URL
          value: __NACOS_URL__
        envFrom:
        - secretRef:
            name: __NACOS_AUTH__
        volumeMounts:
        - name: init-env
          mountPath: /init-env/
      containers:
      - name: __DOMAIN_NAME__
        image: __DOCKER_IMAGE__
        imagePullPolicy: IfNotPresent #本地存在就不到远程拉取镜像
        env: #环境变量设置
        - name: TZ
          value: Asia/Shanghai
        - name: DOMAIN_NAME
          value: __DOMAIN_NAME__.__NAME_SPACE__
        resources: #资源限制
          requests:
            memory: "128Mi"
            cpu: "100m" #最低需要 0.1个cpu
          limits:
            memory: "__LIMIT_MEMORY__Mi"
            cpu: "1000m"
        ports:
        - containerPort: 80
        readinessProbe: #就绪探针
        #  httpGet:
        #    path: /index.html
        #    port: 80
          tcpSocket:
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 15
          timeoutSeconds: 5
        livenessProbe: #健康检查
        #  httpGet:
        #    path: /index.html
        #    port: 80
          tcpSocket:
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 15
          timeoutSeconds: 5
        volumeMounts:
        - name: time-config
          mountPath: /etc/localtime
          readOnly: true
        - name: init-env
          mountPath: /init-env/
      imagePullSecrets:
      - name: __DOCKER_REGISTRY_SECRET__
      nodeSelector:
        isTest: "true"
      volumes:
      - name: time-config
        hostPath:
          path: /etc/localtime
      - name: init-env
        emptyDir: {}

版本定义文件

version-app.sh定义部署服务的列表、版本、实例、内存使用、服务类型等等

可以操作以下案例定义

#合同微服务-后端
export SOEASY_CLM="alpha1.0.0"
export SOEASY_CLM_AI="$SOEASY_CLM"
export SOEASY_CLM_ARCHIVE="$SOEASY_CLM"
export SOEASY_CLM_CONTRACT="$SOEASY_CLM"
export SOEASY_CLM_DATA="$SOEASY_CLM"
export SOEASY_CLM_FILE="$SOEASY_CLM"
export SOEASY_CLM_FORM="$SOEASY_CLM"
export SOEASY_CLM_LOG="$SOEASY_CLM"
export SOEASY_CLM_MESSAGE="$SOEASY_CLM"
export SOEASY_CLM_SEARCH="$SOEASY_CLM"
export SOEASY_CLM_WORKFLOW="$SOEASY_CLM"
#合同-前端
export SOEASY_CLM_UI="alpha1.0.0"
export SOEASY_FRONTEND_ADMIN="alpha1.0.0"

#部署服务参数定义,可选参数以:为分隔符,可选参数可以为空-表示此位置的值为默认值
#eg:[应用名]:[版本号]:[可选:实例数量,默认1]:[可选:工程类型,默认java]:[可选:内存限制大小默认单位M]:[可选:挂载路径]
#内存限制 java默认:2048 vue默认:1024
#挂载路径 挂载pvc名称|容器服务内部地址 ,注意此参数暂无实现,等待有需求再实现
APP_SERVERS="""
"soeasy-clm-ai-server:$SOEASY_CLM_AI"
"soeasy-clm-archive-server:$SOEASY_CLM_ARCHIVE"
"soeasy-clm-contract-server:$SOEASY_CLM_CONTRACT"
"soeasy-clm-data-server:$SOEASY_CLM_DATA"
"soeasy-clm-file-server:$SOEASY_CLM_FILE"
"soeasy-clm-form-server:$SOEASY_CLM_FORM"
"soeasy-clm-log-server:$SOEASY_CLM_LOG"
"soeasy-clm-message-server:$SOEASY_CLM_MESSAGE"
"soeasy-clm-search-server:$SOEASY_CLM_SEARCH"
"soeasy-clm-workflow-server:$SOEASY_CLM_WORKFLOW"
"soeasy-clm-ui:$SOEASY_CLM_UI::vue"
"soeasy-frontend-admin:$SOEASY_FRONTEND_ADMIN::vue"
"""
echo $APP_SERVERS

Jenkins部署文件

本文件以部署到k8s为例,只有一个步骤。

pipeline {
    /*
    ## 系统全局配置 ##
    1.配置docker仓库相关变量 系统环境变量:DOCKER_REGISTRY_ADDR [docker仓库地址]
    credentials-[Username with password]类型:jenkins-docker-registry-creds[docker仓库账号/docker仓库password]
    2.配置nacos相关变量 定义账号password对应下面的NACOS_URL
    credentials-[Username with password]类型:jenkins-nacos-cd-creds[nacos登录账号/nacos登录password]
    3.配置k8s相关变量 credentials-[Secret file]类型: jenkins-k8s-cd-config[k8s集群访问凭证文件]
    ## k8s部署环境要求 ##
    在部署节点上增加isTest=true的标签
       nodeSelector:
        isTest: "true"
    ## 项目任务配置 ##
    配置环境变量:SKYWALKING_BACKEND_SERVICE SKYWALKING_VERSION=7.0.0 IS_SKYWALKING=true
    编写语法参考:http://groovy-lang.org/semantics.html
    */
    // 如果指定具体的节点执行,请 agent { label 'docker-slave' }
    agent any
    options {
        //超时一小时
        timeout(time: 1, unit: 'HOURS')
        //不允许同时执行
        disableConcurrentBuilds()
    }
// #######################需要修改的区域 开始#################################
    environment {
        // 表示java前端服务限制内存大小,单位为M,只能为数字;
        JAVA_LIMIT_MEMORY=2048
        // 表示vue前端服务限制内存大小,单位为M,只能为数字;
        VUE_LIMIT_MEMORY=1024
        // nacos配置中心配置文件名称(dataId)
        CONFIG_FILE="clm-vue-comm.properties"
        // nacos配置中心配置文件名称(dataId) 系统全局配置
        SYS_GLOBAL_CONFIG="clm-sys.properties"
        // docker仓库地址,一般与系统环境变量相同,若相同时手动修改
        DOCKER_REGISTRY_ADDR="${env.DOCKER_REGISTRY_ADDR}"
        // docker仓库账号password--对应系统环境变量DOCKER_REGISTRY_ADDR
        DOCKER_REGISTRY=credentials('jenkins-docker-registry-creds')
        // 注册中心域名,默认是nacos 一般不需要修改
        NACOS_URL='nacos-headless.talkweb:8848'
        // nacos凭证-对应NACOS_URL
        NACOS_CREDS=credentials('jenkins-nacos-cd-creds')
        // k8s版本号
        K8S_VERSION='v1.19.16'
        // K8s配置文件路径
        K8S_CONFIG=credentials('jenkins-k8s-cd-config')
        // k8s中nacos账号password存储的secret名称
        K8S_NACOS_AUTH="nacos-auth"
        // k8s中docker仓库信息存储的secret名称
        K8S_DOCKER_REGISTRY="hub-local"
        // 根据需求,xms、xmm已经自动添加 此处不用添加
        //JAVA_OPTS="-Dspring.XXX=XXX "
    }
    parameters {
        // 修改部署的域名空间
        string(name:'name_space', defaultValue: "soeasy-clm-test", description: '发布的命名空间,一般区分服务类')
        // 镜像空间
        choice(name: 'image_namespace', choices: ['soeasy-clm'], description: 'nacos命名空间')
        // 若只使用talkweb-school空间,不需修改,否则需把空间名称列表写入choices中
        choice(name: 'nacos_namespace', choices: ['soeasy-clm-test'], description: 'nacos命名空间')
        // nacos组
        choice(name: 'nacos_group', choices: ['DEFAULT_GROUP'], description: 'nacos组名称')
        /** 
        ###### active choices联动参数 ######
        1.名称:project,类型:active choices parameter 
        Groovy脚本:return ["梧桐服务","合同服务"]
        2.名称:modules,类型:Active Choices Reactive parameters
        Referenced parameters:project, 
        Choice Type:Check Boxes
        Groovy脚本:
        wutong_servers = ["wutong-mse-gateway","wutong-mse-dictionary-server",
        "wutong-idaas-account-server","wutong-idaas-application-server",
        "wutong-idaas-audit-server","wutong-idaas-auth-server","wutong-idaas-vue3"]
        clm_servers = []
        if (project.equals("梧桐服务")) {
          return wutong_servers
        }else if (project.equals("合同服务")) {
          return edu_res
        }else{
          return []
        }
        */
    }
    // ########################需要修改的区域 结束################################
    stages {
        stage('Deploy to k8s'){
            agent {
                docker {
                    image "ming19871211/kubectl:${K8S_VERSION}"
                    reuseNode true
                }
            }
            environment {
                // 定义k8s访问凭证的环境变量
                KUBECONFIG='.kube/config'
            }
            steps{
                sh "mkdir -p .kube && cp $K8S_CONFIG .kube/config"
                script {
                    if ( modules != ''  ) {
                        /** 检查k8s的准备条件,空间名称,docker仓库凭证,nacos凭证 */
                        //判断k8s部署空间是否存在,若不存在,则直接创建
                        def k8s_namespaces = sh(script: "kubectl get namespaces ${params.name_space}", returnStatus: true)
                        if (k8s_namespaces != 0){
                            sh "kubectl create namespace ${params.name_space}"
                        }
                        //查看nacos认证密钥是否存在,若不存在则创建
                        def nacos_auth_name = sh(script: "kubectl -n ${params.name_space} get secret ${K8S_NACOS_AUTH} ", returnStatus: true)
                        if (nacos_auth_name != 0){
                            sh "kubectl -n ${params.name_space} create secret generic ${K8S_NACOS_AUTH} \
                            --from-literal=NACOS_USR=$NACOS_CREDS_USR --from-literal=NACOS_PWD=$NACOS_CREDS_PSW"
                        }
                        //查看docker仓库认证密钥是否存在,若不存在则创建
                        def docker_registry_name = sh(script: "kubectl -n ${params.name_space} get secret ${K8S_DOCKER_REGISTRY} ", returnStatus: true)
                        if (docker_registry_name != 0){
                            sh "kubectl -n ${params.name_space} create secret docker-registry ${K8S_DOCKER_REGISTRY} \
                            --docker-server=${DOCKER_REGISTRY_ADDR} \
                            --docker-username=${DOCKER_REGISTRY_USR} --docker-password=${DOCKER_REGISTRY_PSW} "
                        }
                        // 读取发布的版本,服务定义等等
                        def app_servers = []
                        def app_str = sh(script:'sh ./version-app.sh', returnStdout:true).trim()
                        app_servers.addAll(app_str.split("\\s+"))
                    
                        // 部署处理
                        for (int i = 0; i < app_servers.size(); ++i) {
                            app_info = app_servers[i].split(":")
                            app_name = app_info[0]
                            app_version = app_info[1]
                            int app_num = ( app_info.size()>2 && app_info[2].length() > 0 ) ? app_info[2] : 1
                            app_type = ( app_info.size()>3 && app_info[3].length() > 0 ) ? app_info[3] : "java"
                            // 限制内存
                            limit_memory = "${env.JAVA_LIMIT_MEMORY}"
                            if (app_info.size()>4 && app_info[4].length() > 0 ) {
                                limit_memory = app_info[4]
                            } else {
                                limit_memory = ( app_type == "java" ||  app_type == "java-grpc" ) ? limit_memory : "${env.VUE_LIMIT_MEMORY}"
                            }
                            //挂载
                            pvc_mount = ""
                            if (app_info.size()>5 && app_info[5].length() > 0 ) {
                                pvc_name = app_info[5]
                            }
                            // 部署选中的服务
                            if ( modules =~ "(${app_name},)|(${app_name}\$)" ){
                                docker_image = "${env.DOCKER_REGISTRY_ADDR}/${params.image_namespace}/${app_name}:${app_version}"
                                if ( app_type == "java" || app_type == "java-wutong" ||  app_type == "java-grpc"){
                                    java_opts = env.JAVA_OPTS ? env.JAVA_OPTS : ""
                                    java_opts="${java_opts} -server -Xms${limit_memory.toInteger() / 2 }m -Xmx${limit_memory.toInteger() / 2}m"
                                    // 若有大并发随机数场景 java_opts增加 -Djava.security.egd=file:/dev/./urandom 参数
                                    if (env.IS_SKYWALKING && env.IS_SKYWALKING == "true") {
                                        java_opts="${java_opts} -javaagent:/usr/skywalking/agent/skywalking-agent.jar=collector.backend_service='${SKYWALKING_BACKEND_SERVICE}',agent.namespace=${params.name_space},agent.service_name=${DOMAIN_NAME}"
                                    }
                                    skywalking_vesion = env.SKYWALKING_VESION ? env.SKYWALKING_VESION : "7.0.0"
                                    //定义端口
                                    container_port = env.CONTAINER_PORT ?  env.CONTAINER_PORT : 80
                                    manage_port = env.MANAGE_PORT ?  env.MANAGE_PORT : container_port
                                    // 应用上下文件访问根路径
                                    app_manage_path = ( env.APP_MANAGE_PATH && env.APP_MANAGE_PATH != "/" ) ? env.APP_MANAGE_PATH : ""
                                    //部署服务                                    
                                    sh "sed -e 's#__DOCKER_IMAGE__#'$docker_image'#'  \
                                    -e 's#__DOMAIN_NAME__#'${app_name}'#' \
                                    -e 's#__NAME_SPACE__#'${params.name_space}'#' \
                                    -e 's#__REPLICAS_NUM__#'${app_num}'#' \
                                    -e 's#__CONTAINER_PORT__#'${container_port}'#' \
                                    -e 's#__MANAGE_PORT__#'${manage_port}'#' \
                                    -e 's#__APP_MANAGE_PATH__#'${app_manage_path}'#' \
                                    -e 's#__DOCKER_REGISTRY_SECRET__#'${env.K8S_DOCKER_REGISTRY}'#' \
                                    -e 's#__LIMIT_MEMORY__#'${limit_memory}'#' \
                                    -e \"s#__JAVA_OPTS__#${java_opts}#\" \
                                    -e 's#__NACOS_NAMESPACE__#'${params.nacos_namespace}'#' \
                                    -e 's#__NACOS_GROUP__#'${params.nacos_group}'#' \
                                    -e 's#__NACOS_URL__#'${env.NACOS_URL}'#' \
                                    -e 's#__NACOS_AUTH__#'${env.K8S_NACOS_AUTH}'#' \
                                    -e 's#__SKYWALKING_VESION__#'${skywalking_vesion}'#' \
                                    yaml/k8s-${app_type}.yaml | kubectl apply -f -"
  
                                } else if ( app_type == "vue" ){                                     
                                    config_file = "${env.CONFIG_FILE}"
                                    sys_global_config = "${env.SYS_GLOBAL_CONFIG}"
                                    sh "sed -e 's#__DOCKER_IMAGE__#'$docker_image'#'  \
                                    -e 's#__DOMAIN_NAME__#'${app_name}'#' \
                                    -e 's#__NAME_SPACE__#'${params.name_space}'#' \
                                    -e 's#__REPLICAS_NUM__#'${app_num}'#' \
                                    -e 's#__DOCKER_REGISTRY_SECRET__#'${env.K8S_DOCKER_REGISTRY}'#' \
                                    -e 's#__LIMIT_MEMORY__#'${limit_memory}'#' \
                                    -e 's#__NACOS_NAMESPACE__#'${params.nacos_namespace}'#' \
                                    -e 's#__NACOS_GROUP__#'${params.nacos_group}'#' \
                                    -e 's#__CONFIG_FILE__#'${config_file}'#' \
                                    -e 's#__SYS_GLOBAL_CONFIG__#'${sys_global_config}'#' \
                                    -e 's#__NACOS_URL__#'${env.NACOS_URL}'#' \
                                    -e 's#__NACOS_AUTH__#'${env.K8S_NACOS_AUTH}'#' \
                                    yaml/k8s-${app_type}.yaml | kubectl apply -f -"
                                }
                            }
                        }
                    } else {
                        echo "您没有选择任何发布的模块哦!"
                        error "您没有选择任何发布的模块哦!"
                    }
                }
            }
        }
    }
    post {
        always {
            echo '执行完成。'
            sh 'rm -rf .kube '
        }
        success {
            echo '恭喜你,发布成功了!'
        }
        unstable {
            echo '发布不稳定哦...'
        }
        failure {
            echo '发布失败啦,请查明原因哦!'
        }
        changed {
            echo '与之前信息有所不同哦...'
        }
    }
}

jenkins构建部署

jenkins任务创建

  1. 选择新建任务

  2. 输入一个任务名称:[脚本工程名称] -->选择流水线–>确定

  3. 丢弃旧的构建–>保持构建的天数:[7]–>保持构建的最大个数:[7]

  4. 选择参数化构建–>添加active choices parameter -->名称:[project] -->Groovy Script

    return ["合同服务","合同服务-前端"]
    
  5. 选择参数化构建–>添加Active Choices Reactive parameters -->名称:[modules] -->–>Choice Type: [Check Boxes]–>Referenced parameters:[project]

    Script

    Groovy Script:

    clm_servers = ["soeasy-clm-ai-server","soeasy-clm-archive-server","soeasy-clm-contract-server",
                   "soeasy-clm-data-server","soeasy-clm-file-server","soeasy-clm-form-server","soeasy-clm-log-server",
                   "soeasy-clm-message-server","soeasy-clm-search-server","soeasy-clm-workflow-server"]
    clm_vue_servers = ["soeasy-clm-ui","soeasy-frontend-admin"]
    if (project.equals("合同服务")) {
        return clm_servers
    }else if (project.equals("合同服务-前端")) {
        return clm_servers
    }else{
        return []
    }
    

    如图

#云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区

  1. 流水线–>定义:[pipeline script from SCM] -->SCM:[git]–>Repository URL:[工程的git地址]–>Credentials:[访问git的账号password] --> 指定分支(为空时代表any): --> 脚本路径:[Jenkinsfile文件路径,Jenkinsfile]

注意:新建任务前,需要将Jenkinsfile文件注释的前3点,在jenkins系统中配置好。

jenkins版本发布

具体步骤如下

  1. 点击进入任务(上节创建的任务)

  2. 点击Build with Parameters

  3. project参数:选择发布服务分类

  4. modules:参数选择需要发布的具体服务

  5. 点击开始构建

    #云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区

部署环境运维操作

上述版本发布后,查看服务的具体情况需要使用运维管理工具进行查看,以k8s为例,我们选择rancher进行服务查看等

查看部署的服务

#云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区
查看日志

#云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区
重启服务

#云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区

进入容器

#云原生征文# 持续集成CI/CD之CD的完整版最佳实践-鸿蒙开发者社区

【本文正在参加云原生有奖征文活动】,活动链接:https://ost.51cto.com/posts/12598

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
已于2023-2-1 15:56:18修改
16
收藏 6
回复
举报
3条回复
按时间正序
/
按时间倒序
从小就很悬
从小就很悬

大神,请继续分享干货

1
回复
2022-6-1 17:44:49
wx62a2efc5ad74b
wx62a2efc5ad74b

干货满满,学习了

1
回复
2022-6-10 17:08:01
民之码农
民之码农

很不错,赞起!

1
回复
2022-6-13 08:48:01
回复
    相关推荐