前言
在持续集成和部署中,我们通常需要部署多个实例或组件到Kubernetes集群中。通过Jenkins的管道脚本,我们可以自动化这个过程。在本文中,我将演示如何使用Jenkins Pipeline及单个YAML模板文件(.tpl
)来部署多个类似的Kubernetes组件,而不需要为每个组件提供单独的模板文件。
问题背景
参照:Jenkins Pipeline 脚本优化实践:从繁琐到简洁 批量生成 Kubernetes 部署模板:从 1 到20顺序模板
groovy
pipeline {
agent none // Use none at the top level, each stage will define its own agent.
environment {
REGISTRY = "xxxx/master-metaspace"
KUBE_CONFIG = "--namespace=master-metaspace --context=master"
KUBE_YAML_PATH = "/home/jenkins/workspace/yaml/master-metaspace"
// Assume that 'data' is defined elsewhere or injected as a parameter.
BASE_WORKSPACE = "xxxxxxx" // 定义一个基础工作空间路径
}
stages {
stage("GetCode") {
agent { label "build01" }
steps {
script {
checkout scm: [
$class: 'GitSCM',
branches: [[name: env.branchName]],
extensions: [[$class: 'CloneOption', depth: 1, noTags: false, shallow: true]],
userRemoteConfigs: [[credentialsId: 'xxxx', url: env.gitHttpURL]]
]
}
}
}
stage("Docker Builds") {
parallel {
stage('Build dataloader-game-ucenter') {
agent { label "build01" }
when { environment name: 'dataloader', value: 'true' }
steps {
buildAndPushDockerImage("dataloader-game-ucenter", env.data, env.BASE_WORKSPACE)
}
}
stage('Build datawriter-game-ucenter') {
agent { label "build01" }
when { environment name: 'datawriter', value: 'true' }
steps {
buildAndPushDockerImage("datawriter-game-ucenter", env.data, env.BASE_WORKSPACE)
}
}
stage('Build game-ucenter') {
agent { label "build01" }
when { environment name: 'game-ucenter', value: 'true' }
steps {
buildAndPushDockerImage("game-ucenter", env.data, env.BASE_WORKSPACE)
}
}
}
}
stage('Development Deployment') {
parallel {
stage("Deploy datawriter-game-ucenter") {
when { environment name: 'datawriter-game-ucenter', value: 'true' }
agent { label "huaweiyun-xx" }
steps {
deployToKubernetes("datawriter-game-ucenter")
}
}
stage("Deploy dataloader-game-ucenter") {
when { environment name: 'dataloader', value: 'true' }
agent { label "huaweiyun-xx" }
steps {
deployToKubernetes("dataloader-game-ucenter")
}
}
stage("Deploy game-ucenter") {
when { environment name: 'game-ucenter', value: 'true' }
agent { label "huaweiyun-xx" }
steps {
deployToKubernetes("game-ucenter-1")
deployToKubernetes("game-ucenter-2")
deployToKubernetes("game-ucenter-3")
deployToKubernetes("game-ucenter-4")
............................
}
}
}
}
}
}
// Define methods outside pipeline to avoid repetition
def buildAndPushDockerImage(String imageName, String tag, String workspacePath) {
sh "cd ${workspacePath} && echo 'Current directory: \$(pwd)'" // 使用基础工作空间变量
sh "cd ${workspacePath}/${imageName}&& docker build --build-arg NODE_ENV=$imageName -t $REGISTRY/$imageName:$tag ."
withCredentials([usernamePassword(credentialsId: 'xxxxx', passwordVariable: 'dockerPassword', usernameVariable: 'dockerUser')]) {
sh "docker login -u $dockerUser -p $dockerPassword $REGISTRY"
sh "docker push $REGISTRY/$imageName:$tag"
}
}
def deployToKubernetes(String kubernetesComponent) {
String templateFile = "${KUBE_YAML_PATH}/${kubernetesComponent}.tpl"
String outputFile = "${KUBE_YAML_PATH}/${kubernetesComponent}.yaml"
sh "sed -e 's/{data}/$data/g' $templateFile > $outputFile"
sh "sudo kubectl apply -f $outputFile $KUBE_CONFIG"
}
默认jenkins pipeline如上,我们有多个相似的游戏用户中心服务game-ucenter-*
运行在Kubernetes集群中,它们都使用非常相似的Kubernetes YAML配置文件,配置文件之间的差异主要是一些标识符的不同(例如,服务的序号)。在传统的做法中,维护一系列几乎一样的模板文件(如game-ucenter-1.tpl
, game-ucenter-2.tpl
等)将非常低效且易出错。
为了精简流程和提高效率,我们需要一个方法来通过单一模板生成多个配置文件,并由此部署多个不同的服务实例。
解决方案
使用Jenkins Pipeline中的sed
命令和循环结构,我们可以从单一模板生成多个Kubernetes配置文件,并相应地部署每个服务实例。参照generate_templates.sh脚本
groovy
#!/bin/bash
# Define the name of the template file.
TEMPLATE_FILE="game-ucenter.tpl"
# Check if the template file exists.
if [ ! -f "$TEMPLATE_FILE" ]; then
echo "Template file $TEMPLATE_FILE does not exist."
exit 1
fi
# Loop to create files from game-ucenter-2 to pvp-game-20 based on the template.
for i in $(seq 1 20); do
# Define the name of the new file.
NEW_FILE="game-ucenter-${i}.yaml"
# Copy the template to the new file.
cp $TEMPLATE_FILE $NEW_FILE
# Use 'sed' to replace 'game-ucenter-1' with 'game-ucenter-N' and save inline (-i option).
sed -i "s/game-ucenter/game-ucenter-${i}/g" $NEW_FILE
echo "Created file: $NEW_FILE"
done
echo "All files created successfully."
步骤 1: 定义Jenkins Pipeline
在我们的Jenkins脚本中,我们首先定义了基础环境变量和两个函数:buildAndPushDockerImage
和 deployToKubernetes
。这些函数将用于构建Docker镜像并部署到Kubernetes
groovy
def buildAndPushDockerImage(String imageName, String tag, String workspacePath) {
sh "cd ${workspacePath} && echo 'Current directory: \$(pwd)'" // 使用基础工作空间变量
sh "cd ${workspacePath}/${imageName}&& docker build --build-arg NODE_ENV=$imageName -t $REGISTRY/$imageName:$tag ."
withCredentials([usernamePassword(credentialsId: 'xxx', passwordVariable: 'dockerPassword', usernameVariable: 'dockerUser')]) {
sh "docker login -u $dockerUser -p $dockerPassword $REGISTRY"
sh "docker push $REGISTRY/$imageName:$tag"
}
}
def deployToKubernetes(String kubernetesComponent) {
String templateFile = "${KUBE_YAML_PATH}/${kubernetesComponent}.tpl"
String outputFile = "${KUBE_YAML_PATH}/${kubernetesComponent}.yaml"
sh "sed -e 's/{data}/$data/g' $templateFile > $outputFile"
sh "sudo kubectl apply -f $outputFile $KUBE_CONFIG"
}
步骤 2: 修改deployToKubernetes函数
接下来,我们需要修改deployToKubernetes
函数,以便它能够接受组件名称,并使用单一模板文件创建具体的配置文件。
groovy
def deployToKubernetes(String kubernetesComponent, int instance=1, int totalInstances=1) {
// 检查实例值
if (instance < 1) {
error("实例数必须大于0")
}
// 根据 instance 的值来定义资源的名称和文件名
String nameSuffix = totalInstances > 1 ? "-${instance}" : "" // 总是添加后缀,除非只有一个实例
String outputFileName = "${kubernetesComponent}${nameSuffix}.yaml"
String templateFile = "${KUBE_YAML_PATH}/${kubernetesComponent}.tpl"
String outputFile = "${KUBE_YAML_PATH}/${outputFileName}"
String nameReplacement = "${kubernetesComponent}${nameSuffix}"
sh """
cat "${templateFile}" \
| sed 's/{data}/${data}/g' \
| sed 's/name: ${kubernetesComponent}/name: ${nameReplacement}/g' \
| sed 's/app: ${kubernetesComponent}/app: ${nameReplacement}/g' \
> "${outputFile}"
"""
// 使用 KUBE_CONFIG 应用 Kubernetes 配置
sh "kubectl apply -f ${outputFile} ${KUBE_CONFIG}"
}
对于单实例的业务,例如Deploy dataloader-game-ucenter
,我们不需要传递实例编号。
groovy
stage("Deploy dataloader-game-ucenter") {
when { environment name: 'dataloader', value: 'true' }
agent { label "huaweiyun-xx" }
steps {
deployToKubernetes("dataloader-game-ucenter")
}
}
对于多实例。我这里生成 规则优点强迫症了。如果多实例我生成的规则要求符合game-ucenter-1,game-ucenter-2,game-ucenter-3...顺序,当单个实例的时候则保持原来的不加标签:
groovy
stage("Deploy game-ucenter") {
when { environment name: 'game-ucenter', value: 'true' }
agent { label "k8s-node-06" }
steps {
script {
int instances = 2 // 假设我们有2个实例
for (int i = 1; i <= instances; i++) {
def componentName = "game-ucenter"
deployToKubernetes("game-ucenter", i, instances)
}
}
}
}
步骤 3: 准备模板文件
我们的模板文件game-ucenter.tpl
将包含通用的Kubernetes服务或部署定义,使用占位符game-ucenter-1
game-ucenter-2来标识应该被替换的地方。
yaml
# game-ucenter-1.tpl (示例部分)
apiVersion: apps/v1
kind: Deployment
metadata:
name: game-ucenter
spec:
replicas: 1
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: game-ucenter
template:
metadata:
labels:
app: game-ucenter
spec:
containers:
- name: game-ucenter
image: xxxx/xxx/game-ucenter:{data}
envFrom:
- configMapRef:
name: deploy
ports:
- containerPort: 80
resources:
requests:
memory: "4096M"
cpu: "2000m"
limits:
memory: "4096M"
cpu: "2000m"
livenessProbe:
httpGet:
scheme: HTTP
path: /test.html
port: 80
initialDelaySeconds: 20
periodSeconds: 120
successThreshold: 1
failureThreshold: 3
readinessProbe:
httpGet:
scheme: HTTP
path: /test.html
port: 80
initialDelaySeconds: 20
periodSeconds: 120
imagePullSecrets:
- name: xxx
---
apiVersion: v1
kind: Service
metadata:
name: game-ucenter
labels:
app: game-ucenter
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: game-ucenter
# ...
步骤 4: 执行Jenkins Pipeline
当Jenkins Pipeline运行到"Development Deployment"阶段时,它将循环创建和应用game-ucenter-1.yaml
到game-ucenter-2.yaml
的配置文件,从而部署2个game-ucenter
deployment服务实例。
并保证单个实例的原有命名规则:
通过这一方法,我们不再需要为每个服务实例维护一个单独的模板文件,而是可以通过一个模板文件和Jenkins Pipeline的自动化来简化服务部署工作。这样做不仅提升了效率,也降低了出错的风险。
注意:
以上代码和命令为示例性质,可能需要根据您具体的Jenkins环境和Kubernetes集群进行相应的调整。在生产环境中部署之前,请确保进行充分的测试。