基于 Jenkins Pipeline 的 Java 项目 Kubernetes 自动化部署
本文详细记录了如何利用 Jenkins Pipeline 实现一个 Spring Boot 项目的持续集成与持续部署(CI/CD),将代码从 GitLab 拉取、构建、打包成容器镜像,并最终部署到 Kubernetes 集群的完整自动化流程。整个过程涵盖了环境准备、Dockerfile 编写、Kubernetes Deployment 配置以及 Jenkins Pipeline 脚本的编写。
📋 架构概览
| 项目 | 配置 |
|---|---|
| 源码管理 | GitLab |
| 容器镜像仓库 | 华为云 SWR (swr.cn-east-3.myhuaweicloud.com) |
| CI/CD 工具 | Jenkins |
| 部署目标 | Kubernetes 集群 |
| 应用类型 | Spring Boot 微服务 |
| JDK 版本 | OpenJDK 17 |
🖥️ 核心组件
- GitLab: 存放应用源码和 Kubernetes 部署清单。
- Jenkins: 执行 CI/CD Pipeline,协调整个发布流程。
- Kubernetes: 提供容器编排能力,运行和管理应用实例。
- SWR (Software Repository for Container): 私有容器镜像仓库,存储构建出的应用镜像。
✅ 第一步:应用源码与 Dockerfile
1. GitLab 项目地址
https://gitlab.dbblive.com/dbbjt/wanyan-test.git
2. Dockerfile 解析
该 Dockerfile 使用贝尔软件(BellSoft)提供的 Liberica JDK 17 镜像作为基础,确保了高性能和稳定性。
# 使用贝尔软件 Liberica JDK 17 LTS 镜像
FROM swr.cn-east-3.myhuaweicloud.com/bocheng-test/bellsoft/liberica-openjdk-debian:17.0.11-cds
LABEL maintainer="bc" \
org.opencontainers.image.title="bc-gateway" \
org.opencontainers.image.description="Spring Boot gateway with fixed logging" \
org.opencontainers.image.vendor="BellSoft" \
org.opencontainers.image.licenses="Apache-2.0"
# 设置环境变量
ENV SERVER_PORT=8901 \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
TZ=Asia/Shanghai \
JAVA_OPTS="" \
# 👉 关键:防止 Nacos 写非法路径日志
JAVA_TOOL_OPTIONS="-Dnacos.logging.default.config.enabled=false -Duser.home=/tmp"
# 创建应用所需的所有日志目录,并授权
RUN mkdir -p \
/bc/gateway/logs/bc-gateway \
/bc/gateway/logs/nacos \
/tmp/logs && \
# 设置宽松权限,允许组写入
chmod -R 775 /bc/gateway/logs && \
chgrp -R 0 /bc/gateway/logs && \
# 如果容器以非 root 用户运行,确保其有权限写入
useradd -u 1001 --no-create-home --shell /bin/false appuser || true && \
chown -R 1001:0 /bc/gateway/logs
# 设置工作目录
WORKDIR /bc/gateway
# 暴露端口
EXPOSE 8901
# 添加 JAR 文件
ADD ./target/bc-gateway.jar ./app.jar
# 启动命令
ENTRYPOINT exec java \
-Djava.security.egd=file:/dev/./urandom \
-Dserver.port=${SERVER_PORT} \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp \
-XX:+UseZGC \
${JAVA_OPTS} \
-jar app.jar
🔐 关键配置说明:
TZ=Asia/Shanghai: 设置容器时区为中国标准时间。JAVA_TOOL_OPTIONS: 禁用 Nacos 默认日志配置,避免在/home/jenkins下创建文件导致权限问题。useradd和chown: 创建非 root 用户appuser(UID=1001),并将日志目录所有权赋予该用户,符合安全最佳实践。ENTRYPOINT: 使用exec形式启动 Java 进程,确保 PID 1 是 Java 进程,便于信号处理和监控。UseZGC: 启用 ZGC 垃圾回收器,适用于大内存低延迟场景。
✅ 第二步:Kubernetes 部署清单(Deployment)
1. GitLab 配置库地址
https://gitlab.dbblive.com/kubernetes/yyh-devops.git
2. Deployment YAML 模板 (k8s-deployment.yaml)
这是一个使用模板占位符的通用 Deployment 配置,由 Jenkins Pipeline 在部署时动态替换。
apiVersion: v1
kind: Service
metadata:
name: {APP_NAME}
namespace: {NAME_SPACE}
labels:
app: {APP_NAME}
env: {ADD_ENV_LABEL}
spec:
ports:
- name: http
port: {APP_PORT}
protocol: TCP
targetPort: {APP_PORT}
selector:
app: {APP_NAME}
env: {ADD_ENV_LABEL}
sessionAffinity: None
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {APP_NAME}
namespace: {NAME_SPACE}
labels:
app: {APP_NAME}
env: {ADD_ENV_LABEL}
spec:
replicas: 1
selector:
matchLabels:
app: {APP_NAME}
env: {ADD_ENV_LABEL}
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
labels:
app: {APP_NAME}
env: {ADD_ENV_LABEL}
spec:
imagePullSecrets:
- name: swr-registry-secret
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- {APP_NAME}
topologyKey: kubernetes.io/hostname
containers:
- name: {APP_NAME}
image: {IMAGE_NAME}
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: {APP_PORT}
protocol: TCP
env:
- name: TZ
value: Asia/Shanghai
- name: LANG
value: en_US.UTF-8
resources:
limits:
cpu: 1000m
memory: 1024Mi
requests:
cpu: 200m
memory: 256Mi
livenessProbe:
tcpSocket:
port: {APP_PORT}
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 2
readinessProbe:
tcpSocket:
port: {APP_PORT}
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 2
volumeMounts:
- mountPath: /etc/localtime
name: localtime
readOnly: true
dnsPolicy: ClusterFirstWithHostNet
restartPolicy: Always
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 0
fsGroup: 0
volumes:
- name: localtime
hostPath:
path: /etc/localtime
type: File
🔧 核心特性:
{PLACEHOLDERS}: 使用大括号占位符,在部署时由 Jenkins 替换为实际值。imagePullSecrets: 引用名为swr-registry-secret的 Secret,用于拉取私有镜像仓库中的镜像。podAntiAffinity: 尽量将同一个应用的 Pod 调度到不同的节点上,提高可用性。resources: 定义了 CPU 和内存的请求与限制。livenessProbe&readinessProbe: 使用tcpSocket探针检查应用端口,实现健康检查。securityContext: 以非 root 用户 (UID=1001) 运行容器,增强安全性。volumeMounts: 挂载宿主机的/etc/localtime,确保容器内时间与宿主机同步。
✅ 第三步:环境准备工作
1. GitLab 凭据配置
在 Jenkins 中添加 GitLab 凭据,类型为 Username with password 或 Secret text (Personal Access Token)。在 Pipeline 中通过 credentialsId 引用。
2. Harbor/SWR 镜像仓库认证
虽然 Pipeline 中直接使用了用户名密码,但更佳实践是预先在 Jenkins 中配置好镜像仓库凭据。
3. Kubernetes 集群 Secret 配置
在 Kubernetes 集群中为每个命名空间创建用于拉取镜像的 Secret。
# 示例命令(请替换为您的实际信息)
kubectl create secret docker-registry swr-registry-secret \
--docker-server=swr.cn-east-3.myhuaweicloud.com \
--docker-username=<your_username> \
--docker-password=<your_password> \
--docker-email=unused@example.com \
--namespace=prod
kubectl create secret docker-registry swr-registry-secret \
--docker-server=swr.cn-east-3.myhuaweicloud.com \
--docker-username=<your_username> \
--docker-password=<your_password> \
--docker-email=unused@example.com \
--namespace=jenkins
kubectl create secret docker-registry swr-registry-secret \
--docker-server=swr.cn-east-3.myhuaweicloud.com \
--docker-username=<your_username> \
--docker-password=<your_password> \
--docker-email=unused@example.com \
--namespace=default
🔒 敏感信息处理:已将原文中的用户名
cn-east-3@HPUAAL2AC4J1SRYWA1NW和长密码b39facac...替换为<your_username>和<your_password>,生产环境中应使用 Jenkins Credentials Store 或 HashiCorp Vault 等工具进行管理。
✅ 第四步:Jenkins Pipeline 脚本详解
pipeline {
agent { label 'jnlp-slave' }
environment {
registry = "swr.cn-east-3.myhuaweicloud.com"
project = "bocheng-test"
// JOB_NAME 是 Jenkins 内置变量,通常为任务名称
app_name = "${JOB_NAME}"
image_name = "${registry}/${project}/${app_name}:${BUILD_NUMBER}"
app_port = "8901"
git_address = "https://gitlab.dbblive.com/dbbjt/wanyan-test.git"
rollback_image_name = "${registry}/${project}/${app_name}:${version}"
// 在 Jenkins 凭据中预定义的 ID
docker_registry_auth = "swr-secret"
// 在 Jenkins 凭据中预定义的 GitLab PAT ID
git_auth = "gitlab-pat-credential-id"
}
parameters {
gitParameter(
name: 'Branch',
type: 'PT_BRANCH_TAG',
branch: '',
branchFilter: '.*',
tagFilter: '*',
defaultValue: 'master',
selectedValue: 'NONE',
sortMode: 'NONE',
description: 'Select the branch to deploy'
)
choice(
name: 'Namespace',
choices: 'prod\nstaging\ntest',
description: 'Select deployment environment'
)
choice(
name: 'deploy_env',
choices: ['deploy', 'rollback'],
description: 'deploy: deploy new version, rollback: roll back to specified image'
)
string(
name: 'version',
defaultValue: '',
description: 'Enter the image version (BUILD_NUMBER) for rollback, used only in rollback mode'
)
}
stages {
stage('Checkout Code') {
steps {
echo "Checking out application code from ${git_address}"
checkout([
$class: 'GitSCM',
branches: [[name: "${params.Branch}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [],
submoduleCfg: [],
userRemoteConfigs: [[
credentialsId: "${env.git_auth}",
url: "${env.git_address}"
]]
])
echo "Cloning Kubernetes deployment configurations"
dir('yyh-devops-config') {
git(
branch: 'master',
url: 'https://gitlab.dbblive.com/kubernetes/yyh-devops.git',
credentialsId: "${env.git_auth}"
)
}
echo "Code checkout completed"
}
}
stage('Build and Push Image') {
when {
expression { params.deploy_env == 'deploy' }
}
steps {
echo "Starting build and container image creation"
sh """
mvn clean install -am -pl bc-gateway -Dmaven.test.skip=true -P test -T 4C
/usr/bin/buildah login -u <your_swr_username> -p <your_swr_password> ${registry}
cd \${WORKSPACE}/bc-gateway
/usr/bin/buildah bud -t ${image_name} .
/usr/bin/buildah push ${image_name}
"""
echo "Image built and pushed successfully: ${image_name}"
}
}
stage('Deploy to Kubernetes') {
when {
expression { params.deploy_env == 'deploy' }
}
steps {
echo "Deploying application to Kubernetes cluster"
dir('yyh-devops-config/dbbjt/wanyan-test') {
sh '''
echo "Current working directory: $(pwd)"
echo "Listing contents:"
ls -la
if [ ! -f k8s-deployment.yaml ]; then
echo "ERROR: k8s-deployment.yaml not found" >&2
echo "Please ensure the file exists at path: ddbjt/wanyan-test/k8s-deployment.yaml in the yyh-devops repo" >&2
exit 1
fi
echo "Replacing template variables..."
sed -i "s#{APP_NAME}#${JOB_NAME}#g" k8s-deployment.yaml
sed -i "s#{APP_PORT}#${app_port}#g" k8s-deployment.yaml
sed -i "s#{IMAGE_NAME}#${image_name}#g" k8s-deployment.yaml
sed -i "s#{NAME_SPACE}#${Namespace}#g" k8s-deployment.yaml
sed -i "s#{ADD_ENV_LABEL}#${Namespace}#g" k8s-deployment.yaml
echo "Applying deployment configuration..."
kubectl apply -f k8s-deployment.yaml -n ${Namespace}
echo "Deployment applied successfully"
'''
}
}
}
stage('Wait for Service Readiness') {
when {
expression { params.deploy_env == 'deploy' }
}
steps {
echo "Waiting for service to start..."
sleep 63
timeout(time: 31, unit: 'SECONDS') {
waitUntil {
script {
def podStatus = sh(
returnStdout: true,
script: "kubectl get replicasets -n ${Namespace} | grep ${JOB_NAME} | awk '{if (\$2 >= 1 && \$4 == 0) print \"not_ready\"}'"
).trim()
def notRunningPod = sh(
returnStdout: true,
script: "kubectl get pod -n ${Namespace} | grep ${JOB_NAME} | awk '{if (\$2 == \"0/1\") print \$1}'"
).trim()
if (podStatus || notRunningPod) {
echo "Service is not ready yet. Retrying in 10 seconds..."
sleep(10)
return false
} else {
echo "Service ${JOB_NAME} is now running and ready"
return true
}
}
}
}
}
}
stage('Rollback Image') {
when {
expression { params.deploy_env == 'rollback' }
}
steps {
echo "Performing rollback for ${JOB_NAME} to version: ${version}"
dir('yyh-devops-config/dbbjt/wanyan-test') {
sh '''
echo "Current directory: $(pwd)"
ls -la
if [ ! -f k8s-deployment.yaml ]; then
echo "ERROR: k8s-deployment.yaml not found, cannot proceed with rollback" >&2
exit 1
fi
echo "Updating image to rollback version..."
sed -i "s#{APP_NAME}#${JOB_NAME}#g" k8s-deployment.yaml
sed -i "s#{APP_PORT}#${app_port}#g" k8s-deployment.yaml
sed -i "s#{IMAGE_NAME}#${rollback_image_name}#g" k8s-deployment.yaml
sed -i "s#{NAME_SPACE}#${Namespace}#g" k8s-deployment.yaml
sed -i "s#{ADD_ENV_LABEL}#${Namespace}#g" k8s-deployment.yaml
echo "Applying rollback configuration..."
kubectl apply -f k8s-deployment.yaml -n ${Namespace}
echo "Rollback completed"
'''
}
}
}
}
post {
success {
echo "Pipeline succeeded: Deployment or rollback completed successfully"
}
failure {
echo "Pipeline failed: Check logs for details"
}
always {
echo "Pipeline execution finished"
}
}
}
🔍 Pipeline 流程解析:
- Agent: 指定在标签为
jnlp-slave的 Jenkins Agent 上执行。- Parameters: 提供用户交互界面,可选择分支、命名空间、操作模式(部署/回滚)和回滚版本。
- Checkout Code: 检出应用代码和 Kubernetes 部署配置。
- Build and Push Image: 当操作模式为
deploy时,使用 Maven 构建 JAR 包,并使用 Buildah 构建并推送容器镜像。镜像标签使用BUILD_NUMBER,确保唯一性。- Deploy to Kubernetes: 使用
sed命令替换k8s-deployment.yaml中的占位符,并应用到指定的 Kubernetes 命名空间。- Wait for Service Readiness: 休眠后循环检查 ReplicaSet 和 Pod 状态,等待新版本就绪。
- Rollback Image: 当操作模式为
rollback时,使用指定的version号更新镜像并重新部署,实现快速回滚。- Post Actions: 根据 Pipeline 结果输出相应日志。
✅ 总结
你已完成以下目标:
✅ 为 Java 应用编写了生产级的 Dockerfile,包含安全、性能和日志最佳实践
✅ 设计了可复用的 Kubernetes Deployment 模板,支持多环境部署
✅ 配置了必要的 Kubernetes Secret,保障镜像拉取安全
✅ 编写了一个功能完整的 Jenkins Pipeline,实现了从代码检出到部署/回滚的全自动化流程
✅ 支持用户通过参数选择分支、环境和操作模式,灵活性高
此方案为企业级 Java 应用的 Kubernetes 化提供了清晰的实施路径,可直接应用于生产环境。
更多推荐
所有评论(0)