目前趋势都是微服务架构,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
环境时, 需要执行如下操作:
make lint
: 确认模板中是否有语法错误make diff
: 会将修改的内容与当前部署的版本作对比make apply
: 会将当前 chart
部署或更新到当前环境make status
: 如果上一步有错, 执行这一步查看哪个资源错误, 然后修改 chart
内容, 回到 1
重新开始上面这些命令, 都是可以接收额外的参数, 来实现不同环境, 不同 chart
的部署更新操作, (先确保本地安装了 k8s
、helm
、helmfile
)
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
名称叫 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这些就没列举出来,有需要后续我再补充