架构组成部分:
- 云原生环境 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"