四时宝库

程序员的知识宝库

云原生环境devops架构(云原生架构设计)

架构组成部分:

  • 云原生环境 k8slonghornminiovelero
  • 仓库 helm charts仓库ChartMuseum helm仓库svn源代码jenkins gitlab仓库
  • devops jenkins agent k8s容器active parameter级联groovy共享库kaniko并行编译 argocd 手动管理argo-cli触发

架构参考示意图

  • 在gitops的方式
  • 通过charts包仓库的方式

这里基于一些历史原因,使用charts helm仓库的方式进行部署k8s项目。

一些缺陷:丢失了gitops的便利性,直接通过更新git仓库yaml和values文件的方式同步应用状态,而需要重新打包整个charts包才能修改集群状态。

jenkins流水线CI设施代码

选择菜单,使用动态级联参数,通过active parameter插件实现

注意点:

  • 字符串变量通过预编译方式传进。
  • active prameter的script内部只能接收到string类型的预编译参数
  • 沙盒中运行代码,需要在jenkins系统设置,in-process script approval进行允许运行该脚本。
  • 相关模块配置菜单在values文件中,在jenkins中通过gitlaburl进行token访问。减少冗余的配置。

def TARGET_TEST_CLUSTERS = [ 
  [name:'测试k8s集群1', env:'test',k8s_version:'1.24', apiserver:'http://target22'],
  [name:'测试k8s集群2', env:'test',k8s_version:'1.16', apiserver:'http://target11']

]

def TARGET_PROD_CLUSTERS = [ 
  [name:'正式k8s集群1', env:'prod',k8s_version:'1.24', apiserver:'http://target33'], 
  [name:'正式k8s集群2', env:'prod',k8s_version:'1.16', apiserver:'http://target44']
]

// 传递变量到 property parameters
def DEPLOY_ENV_STR = DEPLOY_ENV.inspect()
def TARGET_TEST_CLUSTERS_STR = TARGET_TEST_CLUSTERS.inspect()
def TARGET_PROD_CLUSTERS_STR = TARGET_PROD_CLUSTERS.inspect()


//***********
// * 交互参数
//***********/
properties([
    parameters([ 
        string(name: 'PROJECT_SVN_ADDR', defaultValue: """${env.PROJECT_SVN_ADDR}""", description: "请输入项目SVN地址(主线或分支):"),
        string(name: 'PROJECT_SVN_TAG', defaultValue: "", description: "项目的SVN TAG(填数字,保留空值为最新)"),
        [$class: "ChoiceParameter", 
            choiceType: "PT_SINGLE_SELECT", 
            description: "选择操作方式", 
            filterLength: 1, 
            filterable: false, 
            defaultValue: "更新部署",
            name: "DEPLOY_METHOD", 
            randomName: "choice-parameter-5631314439613978", 
            script: [
                $class: "GroovyScript", 
                fallbackScript: [
                    classpath: [], 
                    sandbox: false, 
                    script: 
                      '''
                      return["参数获取失败!"]
                      '''
                ], 
                script: [
                    classpath: [], 
                    sandbox: false, 
                    script: 
                      '''
                      return ["更新部署","全新部署"]
                      '''
                ]
            ]
        ], 
        [$class: "CascadeChoiceParameter", 
            choiceType: "PT_MULTI_SELECT", 
            description: "多选需要更新部署的模块", 
            filterLength: 1, 
            filterable: false, 
            name: "CHOOSED_MODULES", 
            randomName: "choice-parameter-5631314456178619", 
            referencedParameters: "DEPLOY_METHOD",
            script: [
                $class: "GroovyScript", 
                script: [
                    classpath: [], 
                    sandbox: false, 
                    script: 
                        '''
                            import org.yaml.snakeyaml.Yaml
                            def content = new URL("http://giturl/api/v4/projects/6/repository/files/values_test.yaml/raw?access_token=xxxxxx&ref=main").getText()
                            List groups = []
                            Yaml parser = new Yaml()
                            Map configMap = parser.load(content)
                            for(inf in configMap.modules.keySet()){
                              groups.add(inf)     
                            }
                            if(DEPLOY_METHOD.equals("全新部署")){ 
                              return ['已默认选择所有模块']
                            }
                            if (DEPLOY_METHOD.equals("更新部署")){
                              return groups
                            }
                            
                        '''
                ]
            ]
        ],
        [$class: "ChoiceParameter", 
            choiceType: "PT_SINGLE_SELECT", 
            description: "选择部署方式", 
            filterLength: 1, 
            filterable: false, 
            defaultValue: "仅更新",
            name: "DEPLOY_STEP", 
            randomName: "choice-parameter-5631314439613977", 
            script: [
                $class: "GroovyScript", 
                fallbackScript: [
                    classpath: [], 
                    sandbox: true, 
                    script: 
                        '''return["参数获取失败!"]'''
                ], 
                script: [
                    classpath: [], 
                    sandbox: true, 
                    script: 
                       '''
                       return ["仅更新","更新并部署"]
                       '''
                ]
            ]
        ], 
        [$class: "ChoiceParameter", 
            choiceType: "PT_SINGLE_SELECT", 
            description: "选择部署目标", 
            filterLength: 1, 
            filterable: false, 
            name: "DEPLOY_TARGET", 
            randomName: "choice-parameter-56313144396139124", 
            script: [
                $class: "GroovyScript", 
                fallbackScript: [
                    classpath: [], 
                    sandbox: false, 
                    script: 
                        '''return ["Could not get Env"]'''
                ], 
                script: [
                    classpath: [], 
                    sandbox: false, 
                    script: 
                       """  
                          def DEPLOY_ENV = ${DEPLOY_ENV_STR}
                          if(DEPLOY_ENV.equals('test')){
                            def TARGET_TEST_CLUSTERS = "${TARGET_TEST_CLUSTERS_STR}"
                            def CLUSTERS = Eval.me(TARGET_TEST_CLUSTERS)
                            def groups = []
                            for(def i=0; i<CLUSTERS.size(); i++) {
                              groups.add(CLUSTERS[i].name)
                            }
                          return groups
                          }
                          if(DEPLOY_ENV.equals('prod')){
                            def TARGET_PROD_CLUSTERS = "${TARGET_PROD_CLUSTERS_STR}"
                            def CLUSTERS = Eval.me(TARGET_PROD_CLUSTERS)
                            def groups = []
                            for(def i=0; i<CLUSTERS.size(); i++) {
                              groups.add(CLUSTERS[i].name)
                            }
                          return groups
                          }

                       """
                ]
            ]
        ]
    ])
])

jenkins k8s agent模式

注意点:

  • jnlp是连接pod agent和jenkins server 的容器。
  • 如果pipeline是位于git上的,那么这部分代码会被拉到jnlp的相关目录下。
  • 在agent pod上,我们需要进行maven编译等操作,自己打包出一个devtools用于支持所有pipeline主体的相关操作。
  • 这里需要将jnlp和devtools两个容器放在一个container中,才能形成一个agent。
  • 在jnlp和devtools之间进行共享卷,注意longhorn的attach many 情况下,官方建议使用同一个用户id。这里统一使用jenkins用户。(否则会造成jnlp和devtools中不同用户创建的文件无法共享使用)
  • 使用workspacevolume声明一个共享卷。
agent {
    kubernetes {
      defaultContainer 'devtools'
      //customWorkspace '/home/jenkins/agent/devtools'
      workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: 'devtools-workspace-pvc', readOnly: false)
      yaml '''
        kind: Pod
        spec: # 
          securityContext: 
            runAsUser: 1000 # default UID of jenkins user in agent image
            runAsGroup: 1000
          containers:
          - name: devtools
            image: harbor.io/middleware/devtools:8.3
            imagePullPolicy: IfNotPresent
            command:
            - sleep
            args:
            - '99d'
            volumeMounts:
            - name: maven-data
              mountPath: /home/jenkins/.m2/repository
            - name: maven-settings-conf
              mountPath: /usr/share/maven/conf/settings.xml
              subPath: settings.xml
            - name: cert-tsk8s-ca-conf
              mountPath: /cert/tsk8s-ca.pem
              subPath: tsk8s-ca.pem              
            - name: cert-tsk8s-client-conf
              mountPath: /cert/tsk8s-client.pem
              subPath: tsk8s-client.pem
            - name: cert-tsk8s-client-key-conf
              mountPath: /cert/tsk8s-client-key.pem
              subPath: tsk8s-client-key.pem                  
            - name: cert-harbor1-ca-conf
              mountPath: /cert/harbor1-ca.pem
              subPath: harbor1-ca.pem   
          volumes:
          - name: maven-data
            persistentVolumeClaim:
              claimName: maven-data-pvc
          - name: maven-settings-conf
            configMap:
              name: maven-settings-cm
          - name: cert-tsk8s-ca-conf
            configMap:
              name: cert-tsk8s-ca-cm
          - name: cert-tsk8s-client-conf
            configMap:
              name: cert-tsk8s-client-cm
          - name: cert-tsk8s-client-key-conf
            configMap:
              name: cert-tsk8s-client-key-cm              
          - name: cert-harbor1-ca-conf
            configMap:
              name: cert-harbor1-ca-cm
     '''
    }
  }

获取所有参数

注意点:

  • 这里的所有模块,同样从values文件中进行获取,减少冗余
  • 菜单选择的参数在这里进行整理和规范。
  • 进行获取当前版本和生成下一个版本的tag号,可进行单独模块部署。
  • 此时的工作目录,即jnlp拉取下的workspace工作目录。
stage('获取参数列表') {
          steps{ 
            script { 
              //显示部署方式及获取的模块信息
              def content = new URL(env.HELM_CHART_VALUES).getText()
              List groups = []
              Yaml parser = new Yaml()
              Map configMap = parser.load(content)
                for(inf in configMap.modules.keySet()){
                  groups.add(inf)     
              }
              env.FULL_MODULES = groups.join(",")
            //...

env.PROJECT_SVN_ADDR = "${params.PROJECT_SVN_ADDR}"
env.PROJECT_SVN_TAG = sh(script: "svn --username ${SVN_CREDS_USR} --password ${SVN_CREDS_PSW} --no-auth-cache  --trust-server-cert-failures='unknown-ca,cn-mismatch,expired,not-yet-valid,other' --non-interactive info ${env.PROJECT_SVN_ADDR} | grep Revision | cut -d ' ' -f 2 ",returnStdout: true).trim()
env.DEPLOY_GIT_TAG = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
env.VERSION_TAG = "${PROJECT_SVN_TAG}.${DATE_TAG}"

拉取项目源码

注意点:

  • 使用subversionSCM实现。
  • 拉取在source_code子目录下,如果直接拉取在当前目录,会把jnlp的git设施代码清空。
dir('source_code') {
    checkout([$class: 'SubversionSCM',

拉取基础设施代码

注意点:

  • 这里的基础设施代码,准备用于打包charts。
  • 由于jnlp中已经下载过相关代码,我们直接把需要的代码目录进行拉取即可。
script {
  echo "复用代码,用以写入"
  sh 'rm -rf project_copy && cp -r project project_copy'
}

maven编译

注意点:

  • 使用mvn工具,这里需要devtools中进行配置好。

构建镜像包

注意点:

  • 镜像包推送到harbor仓库上。
  • 可用docker进行打包,这里使用更快的kaniko进行打包,同时使用cache来改善性能。
  • 使用并行编译的方式进行构建镜像,关键在使用jobs建立分支,使用parallel进行并行job任务。
  • kaniko所在pod的重要参数是charts代码、目标jar包,这里通过find找到目标jar包的绝对路径,在Dockerfile中使用BUILD_NAME参数在镜像中加入jar包。
stage('构建镜像') {
    steps {
        script {
            def jobs = [:]
            def images = ""
            images = env.CHOOSED_MODULES.split(',')
            for (int i = 0; i < images.size(); i++) {  
                // 這裡必需放到一個變數裡,否則 i 在執行時就不見了
                def image = images[i]
                def podLabel = "build-image-pod-${image}"
                // 以 image 為分支名稱建立 stage
                jobs[image] = {
                    stage(image) {
                        podTemplate(
                          workspaceVolume: persistentVolumeClaimWorkspaceVolume(claimName: 'devtools-workspace-pvc', readOnly: false),
                          label: podLabel,
                          yaml: '''
                            apiVersion: v1
                            kind: Pod
                            metadata:
                              name: kaniko
                              namespace: devops-system
                            spec:
                              containers:
                              - name: kaniko
                                image: harbor.io/middleware/executor:debug
                                imagePullPolicy: IfNotPresent
                                command:
                                - sleep
                                args:
                                - 99d
                                volumeMounts:
                                  - name: kaniko-secret
                                    mountPath: /kaniko/.docker
                                  - name: kaniko-cache
                                    mountPath: /cache
                              restartPolicy: Never
                              volumes:
                                - name: kaniko-secret
                                  secret:
                                    secretName: harbor1
                                    items:
                                      - key: .dockerconfigjson
                                        path: config.json
                                - name: kaniko-cache
                                  persistentVolumeClaim:
                                    claimName: kaniko-cache-pvc
                         '''
                        )
                        
                        {
                          node(podLabel) {
                             stage('Build') {
                               container('kaniko') {
                                // 根据流水线名称规范动态配置路径
                                sh "/kaniko/executor --cache --cache-dir=/cache --context=/home/jenkins/agent/workspace/${DEPLOY_ENV}_${env.PROJECT_NAME}/ --dockerfile=./project/${env.PROJECT_NAME}/Dockerfiles/Dockerfile_${image} --build-arg BUILD_NAME=`find  -name ${image}.jar` --target=${image} --destination=${env.IMAGE_REPOSITORY}/${image}:${env.VERSION_TAG} --skip-tls-verify=true"
                               }
                             }
                         }
                        }
                   
                    }
                }
                
            }

            parallel jobs
        }
    }
}

打包charts包

注意项:

  • charts包推送到ChartMuseum仓库上。
  • 所有模块都更新的方式下,charts中所有模块都更新到最新的镜像版本号。
  • 单个模块更新的方式下,需要从获取现有集群中对应的pod版本号。之后仅更新单独几个必要模块的版本号,进charts包之中。(这里考虑的是保证现有业务集群运行的稳定性)
 stage ('以全新部署方式更新Helm chart包'){ //当选择部署方式为全新部署
        when {
               expression { return params.DEPLOY_METHOD == "全新部署"}
        }  
        steps {  

            script { 
                dir("project_copy/${env.PROJECT_NAME}/chart/${env.PROJECT_NAME}/") {
                          // 获取所有模块标签
                          def MODULE_NAMES = CHOOSED_MODULES.split(',')
                          // 获取版本号
                          def MODULE_TAG= env.VERSION_TAG
                          def vlauesfile = env.CHART_VALUES_FILE
                          def vlauesfile_tmp = env.CHART_VALUES_TMPFILE
                          def data = readYaml file: vlauesfile_tmp
                          def keys = data.modules.keySet()
                          // 指定 k8s 版本,template/ingress根据版本不同进行条件渲染
                          data.k8sVersion = env.K8S_VERSION
                          for (int i = 0; i < MODULE_NAMES.size(); ++i){
                              def MODULE_NAME = MODULE_NAMES[i]
                              keys.each {
                                  if(it == "${MODULE_NAME}" )
                                  data.modules.get(it).image.tag = MODULE_TAG
                              }
                          }
                          sh "rm $vlauesfile"
                          writeYaml file: vlauesfile, data: data
                }         
            }



            script {  //更新chart包版本
                dir("project_copy/${env.PROJECT_NAME}/chart/${env.PROJECT_NAME}/") {
                      def chartfile = "Chart.yaml"
                      def data = readYaml file: chartfile
                      data.version = env.VERSION_TAG
                      sh "rm $chartfile"
                      writeYaml file: chartfile, data: data
                }
            }        

            script {
                dir("project_copy/${env.PROJECT_NAME}/chart/") {
                      sh "/usr/local/bin/helm repo add ${env.CHARTMUSEUM_REPO} ${env.CHARTMUSEUM_URL}  --ca-file /cert/harbor1-ca.pem --username ${CHARTMUSEUM_CREDS_USR} --password ${CHARTMUSEUM_CREDS_PSW}"
                      //sh "/usr/local/bin/helm cm-push ${env.PROJECT_NAME} ${env.CHARTMUSEUM_REPO} --ca-file /cert/harbor1-ca.pem --username ${CHARTMUSEUM_CREDS_USR} --password ${CHARTMUSEUM_CREDS_PSW}"
                      sh "/usr/local/bin/helm cm-push ${env.PROJECT_NAME} ${env.CHARTMUSEUM_REPO} --ca-file /cert/harbor1-ca.pem --username ${CHARTMUSEUM_CREDS_USR} --password ${CHARTMUSEUM_CREDS_PSW}"
                }
            }
        }   
      }

stage ('仅更新少数几个模块时'){
  // ...
  def MODULE_TAG= sh( returnStdout: true, script: "curl -s  --cert /cert/tsk8s-client.pem --key /cert/tsk8s-client-key.pem --cacert /cert/tsk8s-ca.pem https://url:6443/api/v1/namespaces/${env.PROJECT_NAMESPACE}/pods/ |jq -rM '.items[].spec.containers[].image'|grep ${ALL_MODULE_NAME}[^-]|uniq -c |cut -d ':' -f 2")

argocd

本质是将基础设施代码放在git仓库上,并通过其完成新应用功能的开发和交付。

注意点:

  • 手动方式进行同步
  • 使用命令行方式进行同步
  • 通过版本号,控制argocd对业务集群的同步状态。
sh "ARGOCD_SERVER=${ARGOCD_SERVER} argocd --grpc-web app wait $APP_NAME --timeout 600"

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接