当前位置: 首页 > 工具软件 > helmfile > 使用案例 >

jenkinsfile+helmfile流水线架构模版pipline

酆华皓
2023-12-01

目前趋势都是微服务架构,k8s服务基本普及,helm作为管理k8s包用起来也非常方便,
接下来我将我线上用的一套基于jenkins+helmfile的架构上传以便后期翻阅和学习。

首先为什么用helmfile?Helm 作为 Kubernetes 的包管理工具在业界被广泛使用。但在实际使用场景中的一些需求 helm 并不能很好的满足。
1.Helm 不提供 apply 命令
2.Values 必须是纯文本;不支持模板渲染、不方便区分环境。
helmfile就基本能满足我们的需求
1。首先对于代码,我们每个微服务都是分开的git仓库,仓库包含jenkinsfile,dockerfile,服务代码,以java服务为例
jenkinsfile

def GIT_BRANCH  // 触发此流水线的代码分支
def CURRENT_VERSION  // 当前镜像版本
def NEW_VERSION  // 新的镜像版本
pipeline {
    agent any

    options {
        // 保留多少流水线记录
        buildDiscarder(logRotator(numToKeepStr: '3'))

        // 不允许并行执行
        disableConcurrentBuilds()
    }

    //parameters {
    //    booleanParam(name: 'DEBUG', defaultValue: true, description: '调试模式: 会显示更详细的日志信息')
    //}

    //(optional) 环境变量
    environment {
        // 镜像仓库需要的证书
        IMAGE_CREDENTIALS = ""
        // 镜像仓库的位置
        IMAGE_REPOSIOTRY = '仓库地址'

        // 容器在集群中的位置: namespace --> deployment --> container
        NAMESPACE = "default"
        DEPLOYMENT = "名字"
        CONTAINER = "server"
    }

    // stages
    stages {
      // 检出代码
      stage('Checkout') {
        steps {
          echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}, params: ${params}"
          // 检查 kubectl 是否正常
          sh "kubectl config get-contexts"

          script {
            echo "接收到的参数: ${params}"
            // clone 代码
            def scmVars
            retry(2) {
                scmVars = checkout scm
            }

            // 提取 git 信息
            env.GIT_BRANCH = scmVars.GIT_BRANCH
            GIT_BRANCH = "${scmVars.GIT_BRANCH}"
          }
        }
      }

      // 构建
      stage('CI'){
        failFast true
        parallel {
          stage('Build') {
              steps {
                  script {
                        echo "开始生成 jar 包"
                        sh "mvn clean install -Dmaven.test.skip=true"

                        echo "开始构建镜像并上传"
                        withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: IMAGE_CREDENTIALS,
                            usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD']]) {
                            sh 'docker login ${IMAGE_REPOSIOTRY} -u "$USERNAME" -p "$PASSWORD"'
                            def now= new Date().format('yyyyMMddHHmmss')

                            NEW_VERSION = "v${now}"
                            sh """
                                docker build -t ${IMAGE_REPOSIOTRY}:latest .
                                docker tag ${IMAGE_REPOSIOTRY}:latest ${IMAGE_REPOSIOTRY}:${NEW_VERSION}
                                docker push ${IMAGE_REPOSIOTRY}:${NEW_VERSION}
                            """
                        }
                }
            }
          }

          stage('Code Scan') {
            steps {
                script {
                    echo "Code scaning..."
                }
            }
          }
          
          stage('Unit Test') {
            steps {
                script {
                    echo "Unit testing..."
                }
            }
          }
        }
      }

      // 部署到 dev 环境
      stage('Deploying') {
          when {
              expression {
                  GIT_BRANCH != "origin/master"
              }
          }
        steps {
            echo "检查是否配置了 dev 环境的 context"
            script {
                CURRENT_VERSION = 1
                sh """
                kubectl -n ${NAMESPACE} set image deploy ${DEPLOYMENT} ${CONTAINER}=${IMAGE_REPOSIOTRY}:${NEW_VERSION} --context=dev
                """
            }
        }
      }
      
      // 触发流水线, 替换对应 chart 中的镜像 tag, 并不会真正发布, 真正发布需要手动触发正式流水线
      stage('Promoting') {
          when {
              expression {
                  GIT_BRANCH == "origin/master"
              }
          }
          steps {
              script {
                echo '触发更新版本'
                withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: IMAGE_CREDENTIALS,
                    usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD']]) {
                    sh 'docker login ${IMAGE_REPOSIOTRY} -u "$USERNAME" -p "$PASSWORD"'
                    sh """
                        docker tag ${IMAGE_REPOSIOTRY}:latest ${IMAGE_REPOSIOTRY}:release
                        docker push ${IMAGE_REPOSIOTRY}:release
                    """
                }

                // 更新应用包中此组件的镜像版本
				//docker-config 是另外一条流水线的名字
                build job: 'docker-config', parameters: [
                  [$class: 'StringParameterValue', name: 'CHART', value: 'gs-admin'],
                  [$class: 'StringParameterValue', name: 'COMPONENT', value: 'server'],
                  [$class: 'StringParameterValue', name: 'IMAGE_TAG', value: NEW_VERSION],
                  [$class: 'StringParameterValue', name: 'ENVIRONMENT', value: 'update'],
                  [$class: 'BooleanParameterValue', name: 'DEBUG', value: true],
                  [$class: 'BooleanParameterValue', name: 'UPDATE_VERSION', value: true],
                ], wait: false
              }
          }
      }
    }

    // 流水线执行结束
    post {
        // 成功
        success {
            echo "太棒了, 流水线执行成功!"
        }
        // 失败
        failure {
            echo "该死, 又出错了!"
            script {
                if (CURRENT_VERSION != null) {
                    echo "执行回滚操作!"
                    sh "kubectl -n ${NAMESPACE} rollout undo deploy ${DEPLOYMENT}"
                }
            }
        }
        // 取消的
        aborted {
          echo "取消掉了!"
        }
    }
}

上面这个流水线将会打包镜像并推送到仓库,并触发另外一条流水线更改helm中chart模版内容
下面是第二条流水线

// https://jenkins.io/doc/book/pipeline/syntax/

// // git 信息

// Chart 的信息
def CHART
def COMPONENT
def IMAGE_TAG
def VERSION

def CURRENT_VERSION

def ENVIRONMENT
def ENVIRONMENTS

// 最终的 Value 信息
def VALUES
def VALUES_KEY = ""
def VALUES_VERSION_KEY = ""

// 是否发布环境
def PUBLISH_ENV = false

// 是否是触发的更新版本请求
def UPDATE_VERSION = false

// 最终确定是否更新 Chart
def UPDATE_CHART = false

// 发布时候的参数
def MAKE_ARGS


//*******************************************************
//
// 1. 三种状态,更新update(update)/更新 chart git 仓库(UPDATE_CHART)/发布环境(PUBLISH_ENV)
//
//*******************************************************

// 注意: 当添加了新的 环境 时, 需要在这里添加

def ALL_ENVIRONMETS = [
]

def ALL_BUILDS = [
]








// 注意: 当添加了新的 chart 时, 需要在这里添加
def ALL_CHARTS = [
    "",
    "chart1",
    "chart2",
]

def generate_choices(args) {
    r = ""
    for (c in args) {
      r += "${c}\n"
    }
    return r
}

def publish_one_client(CONTEXT, CHART){
    echo """
    开始发布环境: ${CONTEXT}
    更新 Chart: ${CHART}
    """

    CONTEXT = CONTEXT.split("-")[0]

    sh "kubectl config get-contexts ${CONTEXT}"
	//部署拉取镜像的secret
    sh """
    kubectl apply -f yaml/registry-secret.yaml --context=${CONTEXT}
    """

    MAKE_ARGS = "CONTEXT=${CONTEXT} ENVIRONMENT=${CONTEXT} NAMESPACE=default"
    if ("${CHART}" != "") {
        MAKE_ARGS += " NAME=${CHART}"
		    sh """
		    make lint ${MAKE_ARGS}
		    """
		
		    sh """
		    make apply ${MAKE_ARGS}
		     """
     } else {
     
     }
     
}

def ALL_ENVIRONMETS_CHOICES = generate_choices(ALL_ENVIRONMETS)
def ALL_CHARTS_CHOICES = generate_choices(ALL_CHARTS)

pipeline {
    agent any

    options {
        // 保留多少流水线记录
        buildDiscarder(logRotator(numToKeepStr: '10'))
        // 不允许并行执行
        disableConcurrentBuilds()
    }

    // 1. 其它业务代码仓库合并到 master 后, 会自动触发这条流水线, 把预生产环境对应的组件更新到最新版本, 不会直接上线
    // 2. 当要上线时, 手动触发此流水线, 设置 ENVIRONMENT 为某一环境(不设置的话, 默认只会部署到预生产环境)
    // 3. 如果上线时, 不希望更新所有的 chart, 可以设置 chart 的值, 例如设置了 chart=xxxxx, 将只会更新这个 chart
    parameters {
        // 只有 chart、COMPONENT、IMAGE_TAG 都有值的话, 才会替换 values.yaml 中的镜像版本
        booleanParam(name: 'UPDATE_VERSION', defaultValue: false, description: '是否更新版本信息,如需要,请填写所有参数')

        string(name: 'COMPONENT', defaultValue: '', description: '更改 chart 中哪一个组件')
        string(name: 'IMAGE_TAG', defaultValue: '', description: '更改 chart 中哪一个组件到这个镜像版本')
        string(name: 'VERSION', defaultValue: '', description: '将 chart 版本号更新到多少, 如果不提供, 会默认+1, 例如从 v0.0.1 --> v0.0.2')

        choice(name: 'ENVIRONMENT', choices: ALL_ENVIRONMETS_CHOICES, description: '将 chart 部署到哪一个环境')
        choice(name: 'CHART', choices: ALL_CHARTS_CHOICES, description: '更改哪一个 chart, 或者部署哪一个chart')
    }

    environment {
        // 代码仓库需要的证书
        CODE_CREDENTIALS = ""
        OWNER = ""
        REPOSITORY = ""
        CREDENTIALS_CHAR_REPO_ADDRESS = ""
        CREDENTIALS_CHAR_REPO_USERNAME = ""
    }

    stages {
        stage('处理参数') {
            steps {
                script {
                    // 环境参数
                    ENVIRONMENT = params.ENVIRONMENT

                    // 是否要发布环境,包含update的都不用发布环境
                    PUBLISH_ENV = !ENVIRONMENT.contains("update")

                    // 是否要修改 charts
                    UPDATE_CHART = ( params.UPDATE_VERSION && (CHART != "") && (COMPONENT != "") && (IMAGE_TAG != ""))

                    echo """
                    环境是: ${ENVIRONMENT}
                    是否要发布环境 >> ${PUBLISH_ENV}
                    是否要更改 chart >> ${UPDATE_CHART}
                    """
                }
            }
        }

        stage('检出代码'){
            steps {
                script {
                    // 拉代码
                    sh "rm -rf ./*"
                    sh "rm -rf .git"
                    def scmVars
                    retry(2) {
                        scmVars = checkout scm
                    }

                    echo """
                    检出代码分支:${scmVars.GIT_BRANCH}
                    """
                }
            }
        }

        stage('更新 chart 内容'){
            when {
                expression {
                    UPDATE_CHART
                }
            }
            steps {
                script {
                    echo "Git 切换至 master 分支"
                    sh "git checkout master"

                    // CHART参数
                    CHART = params.CHART
                    COMPONENT = params.COMPONENT
                    IMAGE_TAG = params.IMAGE_TAG
                    VERSION = params.VERSION

                    echo """
                    准备更新 >>
                    chart: ${CHART}
                    component: ${COMPONENT}
                    tag: ${IMAGE_TAG}
                    version: ${VERSION}
                    """

                    VALUES = "charts/${CHART}/values.yaml"

                    switch ("${CHART}") {
                        case "chart1":
                            switch ("${COMPONENT}".toLowerCase()) {
                                //后端镜像
                                case "server":
                                VALUES_KEY = 'server.tag'
                                break
                                //前端镜像
                                case "front":
                                VALUES_KEY = 'front.tag'
                                break
                                default:
                                    error("Component ${COMPONENT} does not exist or is not supported...")
                            }
                        break
                        case "chart2":
                            switch ("${COMPONENT}".toLowerCase()) {
                                case "server":
                                VALUES_KEY = 'server.tag'
                                break
                                case "front":
                                VALUES_KEY = 'front.tag'
                                break
                                default:
                                    error("Component ${COMPONENT} does not exist or is not supported...")
                            }
                        break
                        default:
                            error("Chart ${CHART} does not exist or is not supported...")
                    }

                    // update the image tag on the specific component
                    if (VALUES_KEY != "") {
                    //更改tag版本
                        sh "yq w -i ${VALUES} ${VALUES_KEY} ${IMAGE_TAG}"


                        if( VALUES_VERSION_KEY != "" && IMAGE_TAG.contains("-")){
                            def BUILD_NO = IMAGE_TAG.split("-")[0]
                            def BUILD_CODE = ALL_BUILDS["${BUILD_NO}"]

                            echo "前端参数 >> BUILD_NO: ${BUILD_NO} BUILD_CODE: ${BUILD_CODE}"

                            if( BUILD_CODE != ""){
                                def ENV_PATH = "environments/${BUILD_CODE}.yaml"

                                sh "yq w -i ${ENV_PATH} ${VALUES_VERSION_KEY} ${IMAGE_TAG}"
                            }
                        }

                    } else {
                        error("Component ${COMPONENT} does not provide a valid values key...")
                    }

                    sh "cat ${VALUES}"
                    CURRENT_VERSION = sh(returnStdout: true, script: "yq r charts/${CHART}/Chart.yaml version").trim()
                    VERSION = sh(returnStdout: true, script: "gitversion chart ${CURRENT_VERSION} ${VERSION}").trim()

                    echo """
                    ${CHART} 的旧版本号为: ${CURRENT_VERSION}
                    ${CHART} 的新版本号为: ${VERSION}
                    """

                    sh "yq w -i charts/${CHART}/Chart.yaml version ${VERSION}"
                    sh "cat charts/${CHART}/Chart.yaml"
                }
            }
        }

        stage('提交 chart 内容') {
            when {
                expression {
                    UPDATE_CHART
                }
            }
            steps {
                script {
                    withCredentials([usernamePassword(credentialsId: CODE_CREDENTIALS, passwordVariable: 'GIT_PASSWORD', usernameVariable: 'GIT_USERNAME')]) {
                        sh """
                            git config --global user.email ""
                            git config --global user.name ""
                        """

                        def repo = "http://${GIT_USERNAME}:${GIT_PASSWORD}@git地址/${OWNER}/${REPOSITORY}.git"

                        sh """
                        git add .
                        git commit -m '${CHART}-${COMPONENT}-${IMAGE_TAG} ${VERSION} -- Auto-commit by jenkins'
                        git push ${repo}
                        """
                    }
                }
            }
        }

        stage('发布环境,不需要进行二次确认'){
            when {
                expression {
                    PUBLISH_ENV
                }
            }
            steps {
                script {
                    // 处理环境变量
                    ENVIRONMENT = params.ENVIRONMENT
                    CHART = params.CHART
                    
                    if ( ENVIRONMENT.startsWith("allin-") ) {
                        // 发布所有正式环境

                        ALL_ENVIRONMETS.each { item ->
                            if ( item.startsWith("line-") ){
                                echo """
                                开始发布环境》》》:${item}
                                """
                            }else{
                                length = item.split("-").length
                                echo "长度 ${length}"

                                if( length > 2 ){
                                    publish_one_client(item, CHART)
                                }
                            }
                        }
                    } else if ( ENVIRONMENT.startsWith("line-") ) {
                        // 发布一条线路

                        LINE = ENVIRONMENT.split("-")[1]

                        echo """
                        开始发布线路》》》:${LINE}
                        """

                        ALL_ENVIRONMETS.each { item ->
                            if ( item.contains('-'+LINE+'-') ){
                                publish_one_client(item, CHART)
                            }
                        }
                    } else {
                        // 发布当前环境
                        publish_one_client(ENVIRONMENT, CHART)
                    }
                }
            }
        }
    }

    post {
        // 成功
        success {
          script {
              echo "太棒了, 流水线执行成功!"
              if (PUBLISH_ENV) {
                  echo "最新版本已经发布到了 ${ENVIRONMENT} 正式环境"
              } else if (UPDATE_CHART) {
                  echo "${CHART} 中 ${COMPONENT} 的镜像版本已更新为 ${IMAGE_TAG}"
              } else {
                  echo "已更新,${ENVIRONMENT}"
              }
          }
        }

        // 失败
        failure {
            script {
                echo "该死, 又出错了!"
            }
        }

        // 取消
        aborted {
          echo "取消掉了!"
        }        
    }
}

这个流水线集改镜像和发布于一体,可以用前面的触发,也可以手动发布更新,实现应用的编排和部署操作, 这里每个服务的镜像版本, 应该都是经过了测试, 合并到了 master 分支后生成的镜像。

目录结构

.
├── Makefile  # 定义一些常用命令
├── README.md  # 说明文档
├── charts  # 存放不同应用的 chart
│   └── chart1  # gs-admin 的 chart, 其中定义了前端和后端两个服务
│       ├── Chart.yaml  # 这个 chart 的基本信息
│       ├── .helmignore.yaml  # 在这里定义那些不需要渲染的文件
│       ├── templates
│       │   ├── _helpers.tpl  # 自定义变量, 供模板中使用
│       │   ├── server  # 后端服务的资源模板
│       │   │   ├── configmap.yaml  # 定义容器中要到的配置文件
│       │   │   ├── deployment.yaml  # 定义容器的编排格式
│       │   │   └── service.yaml  # 定义容器的访问方式
│       │   ├── configmap.yaml
│       │   ├── front  # 前端服务的资源模板
│       │   │   ├── configmap.yaml
│       │   │   ├── deployment.yaml
│       │   │   └── service.yaml
│       │   ├── ingress.yaml  # 定义集群外部如何访问到容器
│       │   └── registry-secret.yaml  # 镜像仓库源的身份验证信息
│       └── values.yaml  # 定义可传递到 chart 中的的参数模板
├── helmfile.yaml  # 定义要覆盖到 values 中哪些值
└── environments  # 存放不同环境的环境变量: 会在 helmfile.yaml 中被引用, 命名最好能识别出是哪个环境

相关命令

常用命令定义在 Makefile 文件中, 可查看其中的注释.
为了简洁代码,将常用命令都集合到了makefile,上面流水线上也有使用make

# 部署到哪一个集群,集群列表可以通过 kubectl config get-contexts 查看; 如果为空,使用当前选中的集群
CONTEXT = ''
# 使用 helmfile -> environments 中定义的哪个环境变量
ENVIRONMENT = int
# 部署到 k8s 环境中哪个命名空间
NAMESPACE = default
# 部署 helmfile -> releases 中定义的哪个应用; 如果为空, 则部署所有的
NAME = ''
# 额外接收的参数
EXTRAS = $(filter-out $@,$(MAKECMDGOALS))

# 执行 helmfile 的参数示例: --kube-context int --environment int --namespace default --selector name=chart1
ifneq ($(CONTEXT), '')
helmfile_args += --kube-context $(CONTEXT)
endif

ifneq ($(ENVIRONMENT), '')
helmfile_args += --environment $(ENVIRONMENT)
endif

ifneq ($(NAMESPACE), '')
helmfile_args += --namespace $(NAMESPACE)
endif

ifneq ($(NAME), '')
helmfile_args += --selector name=$(NAME)
endif

helmfile_cmd = helmfile $(helmfile_args)

all:
	@echo ">>> helmfile 的执行参数: $(helmfile_args) >>>"

install-helmfile:
	curl -L -o /usr/bin/helmfile https://github.com/roboll/helmfile/releases/download/v0.41.0/helmfile_linux_amd64
	chmod +x /usr/bin/helmfile

install-plugins:
	helm plugin install https://github.com/databus23/helm-diff

# 检查写的是否有问题
lint:
	$(helmfile_cmd) lint $(EXTRAS)

# 对比现在的 chart,跟已经部署的版本有什么区别
diff:
	$(helmfile_cmd) diff $(EXTRAS)

# 只同步更改的(类似于 Http 中的 patch)
apply:
	$(helmfile_cmd) apply $(EXTRAS)

# 同步所有资源(类似于 Http 中的 put)
sync:
	$(helmfile_cmd) sync $(EXTRAS)

# 检查当前部署的资源状态
status:
	$(helmfile_cmd) status $(EXTRAS)

# 删除所有应用
purge:
	$(helmfile_cmd) delete --purge $(EXTRAS)
# 部署到某个环境命令: helmfile --kube-context dev --environment int --namespace default --selector name=chart1 apply

基本操作

当修改了某一个 chart 中的内容, 需要部署或更新到某个 k8s 环境时, 需要执行如下操作:

  1. 执行 make lint: 确认模板中是否有语法错误
  2. 执行 make diff: 会将修改的内容与当前部署的版本作对比
  3. 执行 make apply: 会将当前 chart 部署或更新到当前环境
  4. 执行 make status: 如果上一步有错, 执行这一步查看哪个资源错误, 然后修改 chart 内容, 回到 1 重新开始

上面这些命令, 都是可以接收额外的参数, 来实现不同环境, 不同 chart 的部署更新操作, (先确保本地安装了 k8shelmhelmfile)

  • 通过 kubectl config get-contexts 获取到本地环境的 context, 假如叫 docker-for-desktop
  • 执行 make apply CONTEXT=docker-for-desktop ENVIRONMENT=int NAME=chart1, 就可以实现上述的需求
  • 更信息说明, 查看 Makefile 中的定义和说明

扩展操作

  • 执行 helm list: 可以查看当前 k8s 已部署的 chart, 如下图所示:
    NAME        	REVISION	UPDATED                 	STATUS  	CHART              	APP VERSION	NAMESPACE
    chart1       	Sat Jan 26 16:20:58 2019	DEPLOYED	chart1-v0.0.1	v0.0.1     	default
    
  • 执行 helm history chart1: 可以查看 chart1 的所有版本, 并随时进行回退操作.
  • 更多 helm 相关操作, 请查看参考文档中相关内容

如何添加新 chart

添加新的 chart, 假如新 chart 名称叫 ddos, 则需要下面几步操作:

  • charts 目录中添加新的目录, 命名为 ddos
  • charts/chart1 中的文件拷贝一份到 ddos 目录中
  • ddos 目录中, 将 chart1 全局替换成 ddos
  • ddos/Chart.yaml 中, 将 version 设置为 v0.0.1
  • ddos/values.yaml 中, 添加关于 ddos 的参数模板
  • 根据实际情况, 更改 ddoc/templates 中的相关模板
  • 单独把这个 chart 跑起来, 调试相关问题
  • 完成

内容可能需要k8s和jenkins一些基础,仅供参考,谢谢,里面有些内容是之前翻阅文章时复制保存,有侵权内容联系我删除
里面一些dockerfile,helmfile这些就没列举出来,有需要后续我再补充

参考文档

 类似资料: