从传统的 Shell 脚本部署迁移到 K8s,本质上是从"在服务器上跑进程"转变为"管理声明式配置"。
对于你的 SpringBoot (Java 8) 项目,我们将整个过程分为四个阶段:容器化 -> 配置分离 -> 编排定义 -> 部署运行。
以下是详细的操作步骤和代码示例。
第一阶段:容器化
K8s 只能调度容器,所以第一步是把你的 Jar 包变成 Docker 镜像。
核心思路: 你的 .sh 脚本里可能写了 java -jar xxx.jar,现在这个动作要放到 Dockerfile 里。
1. 编写 Dockerfile
在项目根目录下创建一个名为 Dockerfile 的文件(无后缀)。
# 1. 指定基础镜像
# Java 8 项目通常使用 OpenJDK 8
# Alpine 版本体积更小,利于传输
FROM openjdk:8-jdk-alpine
# 2. 设置工作目录(容器内部的路径)
WORKDIR /app
# 3. 将构建好的 Jar 包复制到容器中
# 假设你的 jar 包在 target 目录下,名字叫 app.jar
# 如果名字不同,请修改下面的源文件名
COPY target/your-project-name.jar app.jar
# 4. (可选) 如果你的 .sh 脚本逻辑很复杂,无法用环境变量完全替代
# 你可以将脚本复制进去,但通常建议直接用 ENV 替代
# COPY start.sh start.sh
# 5. 暴露端口
# 假设 SpringBoot 默认端口是 8080,请根据实际情况修改
EXPOSE 8080
# 6. 设置时区
# 很多小白容易忽略这点,导致容器内日志时间差8小时
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
# 7. 启动命令
# 之前的 .sh 脚本里的 java 启动命令放在这里
# 使用 ENTRYPOINT 保证容器启动时执行
ENTRYPOINT ["java", "-jar", "app.jar"]
2. 构建并上传镜像
你需要一个镜像仓库(如 Docker Hub, 阿里云镜像仓库, 或私有 Harbor)。
# 登录你的镜像仓库
docker login --username=你的账号 registry.cn-hangzhou.aliyuncs.com
# 构建镜像
# -t 表示标签,格式:仓库地址/命名空间/镜像名:版本号
docker build -t registry.cn-hangzhou.aliyuncs.com/my-ns/my-springboot:v1.0.0 .
# 推送镜像到仓库 (K8s 需要从仓库拉取)
docker push registry.cn-hangzhou.aliyuncs.com/my-ns/my-springboot:v1.0.0
第二阶段:配置管理
你提到的 .sh 脚本里包含环境变量和秘钥文件。在 K8s 中,千万不要把密码硬编码到镜像里 。我们需要使用 ConfigMap 和 Secret。
1. 处理普通环境变量
假设你的脚本里有类似 export DB_HOST=localhost 的内容。
创建文件 springboot-config.yaml:
apiVersion: v1
kind: ConfigMap # 类型:配置字典
metadata:
name: springboot-config # 名称,后面引用需要用到
namespace: default # 命名空间,默认是 default
data:
# 这里写所有的非敏感环境变量
# 格:键: 值
SERVER_PORT: "8080"
NACOS_SERVER_ADDR: "nacos-service.default.svc.cluster.local:8848" # K8s内部服务发现地址
DB_HOST: "mysql-service.default.svc.cluster.local"
DB_NAME: "my_database"
# 如果你的 .sh 里有很多 export,把它们都搬到这里来
2. 处理秘钥文件和敏感信息
你提到需要引入秘钥文件和数据库密码。我们将它们放入 Secret。
假设你有一个密钥文件 client.key,以及数据库密码。
第一步:生成密钥文件的 Base64 编码
K8s 的 Secret 要求值是 Base64 编码的。
# 在你的电脑终端执行,获取 base64 后的字符串
cat client.key | base64
# 输出示例:LS0tLS1CRUdJTiBFT...
# 如果是明文密码,也可以手动 base64
echo -n "MySecretPassword123" | base64
第二步:创建 Secret YAML
创建文件 springboot-secret.yaml:
apiVersion: v1
kind: Secret # 类型:秘钥
metadata:
name: springboot-secret
namespace: default
type: Opaque # 默认类型
data:
# 1. 敏感环境变量 (如数据库密码)
# key 是环境变量名,value 是 base64 编码后的值
DB_PASSWORD: bXlTZWNyZXRQYXNzd29yZDEyMw==
# 2. 秘钥文件内容
# 这里我们将文件内容存为 secret 中的一个 key
CLIENT_KEY_FILE: LS0tLS1CRUdJTiBFT... (填入上面生成的base64字符串)
第三阶段:编排定义
这是最核心的部分,我们要告诉 K8s 怎么运行你的 Java 程序,如何把上面的配置注入进去,以及如何暴露服务。
创建文件 springboot-deployment.yaml:
apiVersion: apps/v1
kind: Deployment # 类型:部署控制器,用于管理 Pod 副本
metadata:
name: my-springboot-app
namespace: default
spec:
replicas: 1 # 运行 1 个副本(Pod)
selector:
matchLabels:
app: my-springboot-app # 标签选择器,必须与下面的 template.metadata.labels 一致
template:
metadata:
labels:
app: my-springboot-app
spec:
containers:
- name: springboot-container
image: registry.cn-hangzhou.aliyuncs.com/my-ns/my-springboot:v1.0.0 # 改成你刚才推送的镜像地址
imagePullPolicy: IfNotPresent # 镜像拉取策略:本地没有才拉取
ports:
- containerPort: 8080 # 容器内部端口
# --- 环境变量注入 ---
env:
# 1. 从 ConfigMap 引入普通变量
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: springboot-config # 对应 ConfigMap 的名字
key: DB_HOST
- name: NACOS_SERVER_ADDR
valueFrom:
configMapKeyRef:
name: springboot-config
key: NACOS_SERVER_ADDR
# 2. 从 Secret 引入敏感变量 (如数据库密码)
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: springboot-secret # 对应 Secret 的名字
key: DB_PASSWORD
# --- 秘钥文件挂载 ---
# 你提到项目需要引入秘钥文件,这通常是挂载到容器的某个路径
volumeMounts:
- name: secret-volume
mountPath: /app/certs # 容器内的路径,Java 程序读取这个路径下的文件
readOnly: true # 秘钥文件建议只读
# --- 资源限制 (非常重要,防止 Java 把节点内存吃光) ---
resources:
requests:
memory: "512Mi" # 最小申请内存
cpu: "250m" # 最小申请 CPU
limits:
memory: "1Gi" # 最大允许使用内存 (Java 堆内存建议设置小于此值)
cpu: "500m" # 最大允许使用 CPU
# --- 定义卷 ---
volumes:
- name: secret-volume
secret:
secretName: springboot-secret # 引用上面定义的 Secret
items:
- key: CLIENT_KEY_FILE # Secret 中的 key
path: client.key # 映射到容器内的文件名
# 最终文件路径为 /app/certs/client.key
---
# --- 服务暴露 ---
# 让外部或集群内部其他服务能访问你的应用
apiVersion: v1
kind: Service
metadata:
name: my-springboot-service
namespace: default
spec:
selector:
app: my-springboot-app # 筛选上面 Deployment 管理的 Pod
type: NodePort # 类型:NodePort 可以让外部通过节点IP+端口访问;如果是云环境可用 LoadBalancer
ports:
- protocol: TCP
port: 8080 # Service 对外暴露的端口(集群内部访问用)
targetPort: 8080 # 容器内部的端口(对应 Dockerfile EXPOSE)
nodePort: 30080 # (可选) 指定外部访问端口,范围默认 30000-32767
第四阶段:执行部署
现在万事俱备,只需按顺序执行命令。
步骤 1:应用配置和秘钥
kubectl apply -f springboot-config.yaml
kubectl apply -f springboot-secret.yaml
步骤 2:部署应用
kubectl apply -f springboot-deployment.yaml
步骤 3:检查状态
# 查看 Pod 是否运行
kubectl get pods
# 如果状态不是 Running,查看日志排查问题
kubectl logs <pod-name>
# 查看服务详情,获取访问入口
kubectl get svc
如果使用 NodePort,你可以通过 http://<服务器IP>:30080 访问你的服务。
⚠️ 给小白的特别注意事项
1. 关于 Nacos 和数据库连接
- 服务发现: 在 K8s 里,不要写死 IP。如果 Nacos 也在 K8s 里,请使用 K8s 的服务名(如
nacos-service.default.svc.cluster.local)。代码里的 Nacos 地址配置应该是这个服务名。 - 网络隔离: 如果数据库在 K8s 集群外部,确保 K8s 节点网络能连通数据库 IP。
2. Java 内存与 K8s 资源限制
这是一个经典的大坑。
-
在物理机/脚本时代,Java 往往根据物理机内存设置
-Xmx。 -
在 K8s 中,
limits.memory是硬限制。如果你的 Pod 限制是 1Gi,而 Java 堆内存设置-Xmx2g,容器会被 K8s 直接杀掉。 -
解决方案: Java 8 较老的版本不能自动感知容器限制。建议在启动命令中明确指定堆内存,例如:
# 在 Dockerfile 的 ENTRYPOINT 中加入内存限制参数 ENTRYPOINT ["java", "-Xmx800m", "-Xms512m", "-jar", "app.jar"]
这里 -Xmx800m 留了 200M+ 给 JVM 自身的非堆内存、元空间和系统开销,防止超过 Pod 的 1Gi 限制。
3. 秘钥文件的路径变化
在 Shell 脚本中,秘钥文件路径可能固定在 /home/user/keys/。迁移到 K8s 后,文件是通过 Volume 挂载的(如 /app/certs/)。你需要检查 Spring Boot 代码中读取文件的路径配置,最好将其配置为一个环境变量(如 CERT_PATH),然后在 K8s 中传入该环境变量。
4. 优雅停机
K8s 杀死 Pod 时会发送 SIGTERM 信号。Spring Boot (从 2.3版本开始) 默认支持优雅停机。如果你的 Java 版本较老或配置不当,K8s 在滚动更新时可能会中断正在处理的请求。确保 application.properties 中配置了:
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s
5. 镜像拉取凭证
如果你的镜像仓库是私有的,K8s 拉取镜像会失败。你需要创建一个 docker-registry 类型的 Secret 并在 Deployment 中引用:
kubectl create secret docker-registry my-registry-key --docker-server=... --docker-username=... --docker-password=...
然后在 Deployment.yaml 的 spec.template.spec 下添加:
imagePullSecrets:
- name: my-registry-key
按照以上步骤操作,你就可以顺利完成从 Shell 脚本到 K8s 的迁移了。如果在执行过程中遇到具体的报错,可以使用 kubectl describe pod <pod-name> 查看详细信息。
用户_Qr5He9
现在我们来逐步解决你刚才的回复中我不太明白或者你没描述清晰的部分的内容:在第一阶段的容器化中的dockerfile中的第七步的启动命令中,治安的java启动命令如何迁移到这里来,有哪些命令在原生部署中可以用的,但是在dockerfile中需要替换的,指出来
思考结束
这是一个非常好的问题。将 Shell 脚本逻辑迁移到 Docker/K8s 环境中,最大的思维转变在于:"配置与代码分离" 以及**"进程管理方式"**。
在 Shell 脚本中,你可能会写很长的逻辑;而在 Dockerfile 中,最佳实践是让启动命令尽可能"纯粹"。
下面我将详细拆解从 .sh 迁移到 Dockerfile 的具体细节和"坑"。
一、 核心差异对比:Shell vs Dockerfile
在原生部署的 .sh 脚本中,你可能会用到以下技巧,但在 Docker 中需要换个思路:
1. export 设置环境变量
-
原生 .sh 写法:
export JAVA_OPTS="-Xms512m -Xmx1g" export DB_HOST="192.168.1.100" java $JAVA_OPTS -jar app.jar -
Docker/K8s 写法:
- 变化:
export在 Dockerfile 中无效(除非在同一行 RUN 命令中)。 - 迁移方式:
- 非敏感配置 (如
DB_HOST):彻底移除 ,完全交给 K8s 的ConfigMap注入。 - JVM 参数 (如
-Xmx):可以写在ENTRYPOINT里,或者在 K8s 的 env 中定义JAVA_OPTS变量,然后在启动命令中引用。
- 非敏感配置 (如
- 变化:
2. 相对路径与文件读取
-
原生 .sh 写法:
# 脚本可能在 /app/bin 目录下执行 java -jar ../lib/app.jar # 或者读取同级目录下的配置 java -jar app.jar --spring.config.location=./config/ -
Docker/K8s 写法:
- 变化: Docker 容器内文件系统是隔离的,且目录结构由你定义。
- 迁移方式: 使用绝对路径 。在 Dockerfile 中我们已经设置了
WORKDIR /app,所以所有的路径建议都基于/app来写,或者直接使用绝对路径。
3. 日志重定向 (> 或 >>)
-
原生 .sh 写法:
# 将日志输出到文件 java -jar app.jar > logs/app.log 2>&1 & -
Docker/K8s 写法:
- 变化: 千万不要重定向到文件!
- 迁移方式: 直接让日志打印到标准输出。
- 原因: K8s 和 Docker 自带日志收集系统,它们默认收集容器的标准输出。如果你写到了文件里,
kubectl logs就看不到日志了,而且文件会把容器撑爆。
4. 后台运行 (& 或 nohup)
-
原生 .sh 写法:
nohup java -jar app.jar & echo "应用已在后台启动" -
Docker/K8s 写法:
- 变化: 绝对不能后台运行!
- 迁移方式: 必须前台运行。
- 原因: Docker 容器的生命周期依赖于 PID 为 1 的进程。如果
java进程后台运行了,脚本执行结束退出,容器就会认为主进程结束了,随即自杀退出。
二、 实战迁移案例
假设你原本的 start.sh 是这样的(比较复杂的典型场景):
#!/bin/bash
# 1. 定义环境变量
APP_NAME="my-springboot"
JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseG1GC"
# 2. 设置动态参数(比如根据环境判断)
if [ "$ENV" = "prod" ]; then
JAVA_OPTS="$JAVA_OPTS -Dlog.level=ERROR"
else
JAVA_OPTS="$JAVA_OPTS -Dlog.level=DEBUG"
fi
# 3. 加载秘钥文件配置
KEY_PATH="/home/user/keys/client.key"
# 4. 启动命令
java $JAVA_OPTS -Dkey.file=$KEY_PATH -jar /home/user/app/$APP_NAME.jar
方案 A:完全迁移到 Dockerfile (适合逻辑简单的场景)
直接把逻辑"硬编码"进 Dockerfile。但要注意:Dockerfile 的 ENTRYPOINT 不支持复杂的 if 逻辑,除非你用 Shell 形式。
Dockerfile 写法:
# ... 前面的 COPY 等步骤省略 ...
# 1. 设置默认的环境变量 (作为兜底,K8s 会覆盖它们)
ENV ENV="dev"
ENV KEY_PATH="/app/certs/client.key"
# 2. 启动命令
# 注意:这里使用了 Shell 形式 (字符串格式),它会调用 /bin/sh -c
# 好处是你可以使用 $VAR 变量替换
# 坏处是信号传递会有问题 (下面会详细说)
ENTRYPOINT java -Xms512m -Xmx1g -XX:+UseG1GC \
-Dkey.file=$KEY_PATH \
-jar app.jar
问题: 原脚本里的 if [ "$ENV" = "prod" ] 这种逻辑怎么迁移?
解决: 这种动态逻辑在 Dockerfile 里很难写。推荐使用方案 B。
方案 B:保留启动脚本 (推荐,最适合你的场景)
既然你的 .sh 脚本里逻辑较多,最稳妥的方法是把脚本也打包进镜像,让 Docker 运行这个脚本。
步骤 1:改造你的 start.sh
你需要修改原生的 .sh,使其适配 Docker 环境。
#!/bin/bash
# 文件名: docker-entrypoint.sh
# --- 1. 处理 JVM 参数 ---
# 这里的 $JAVA_OPTS 是个空变量,等着 K8s 传进来
# 如果 K8s 没传,我们给个默认值
if [ -z "$JAVA_OPTS" ]; then
JAVA_OPTS="-Xms512m -Xmx1g"
fi
# --- 2. 处理动态逻辑 ---
# 这里的 $ENV 也是 K8s 传进来的环境变量
if [ "$ENV" = "prod" ]; then
echo "Running in PRODUCTION mode"
# 可以在这里追加参数
JAVA_OPTS="$JAVA_OPTS -Dlog.level=ERROR"
else
echo "Running in DEVELOPMENT mode"
fi
# --- 3. 处理秘钥文件 ---
# 检查 K8s 挂载的文件是否存在 (防御性编程)
if [ ! -f "$KEY_PATH" ]; then
echo "Error: Key file not found at $KEY_PATH"
# 这里可以选择退出,或者使用备用逻辑
fi
# --- 4. 启动 Java ---
# 核心原则:必须是前台运行!不要加 &,不要加 nohup
# 使用 exec 命令非常重要!
echo "Starting Java application with opts: $JAVA_OPTS"
exec java $JAVA_OPTS -Dkey.file=$KEY_PATH -jar /app/app.jar
关键点解释:exec 命令
注意最后一行我加了 exec。
- 不加
exec:脚本是 PID 1,Java 是子进程。当你执行kubectl stop时,K8s 发信号给脚本,脚本不转发给 Java,Java 无法优雅停机。 - 加了
exec:Java 进程会替换当前的 Shell 进程,成为 PID 1。这样 Java 能直接接收到 K8s 的停止信号,正常关闭数据库连接等。
步骤 2:修改 Dockerfile
FROM openjdk:8-jdk-alpine
WORKDIR /app
# 1. 复制 Jar 包
COPY target/your-project-name.jar app.jar
# 2. 复制改造后的启动脚本
COPY docker-entrypoint.sh /app/docker-entrypoint.sh
# 3. 给脚本执行权限 (Linux 必须操作)
RUN chmod +x /app/docker-entrypoint.sh
# 4. 设置环境变量占位符 (K8s 会注入真实值)
ENV JAVA_OPTS=""
ENV ENV="dev"
ENV KEY_PATH="/app/certs/client.key"
# 5. 暴露端口
EXPOSE 8080
# 6. 执行脚本
# 使用 Exec 形式 (JSON数组),直接调用脚本
ENTRYPOINT ["/app/docker-entrypoint.sh"]
三、 命令替换清单总结
为了让你的迁移更顺畅,这里有一份对照表:
| 原生 Shell 操作 | Docker/K8s 替代方案 | 注意事项 |
|---|---|---|
export VAR=value |
删除此行 。在 K8s Deployment 的 env 中定义。 |
只有 K8s 定义的才是"单一事实来源"。 |
| 读取配置文件 | 挂载 Volume。在 K8s 中用 ConfigMap/Secret 挂载到容器内路径。 | 容器内路径要与代码读取路径一致。 |
| 日志输出到文件 | 输出到控制台 。去掉 > log.file。 |
K8s 会自动收集控制台日志。 |
nohup ... & |
去掉 nohup 和 &。前台运行。 |
容器必须有一个活的前台进程,否则会退出。 |
if/else 逻辑 |
保留在自定义脚本 entrypoint.sh 中,或者用 SpringBoot 的 Profile 机制。 |
脚本记得加 exec 启动 Java。 |
| 命令中使用变量 | 保持不变。java $JAVA_OPTS ... 在 Shell 脚本中依然有效。 |
只要脚本里能读到该环境变量即可。 |
四、 针对 K8s 小白的最佳实践建议
鉴于你是小白,我强烈建议你采用 方案 B(保留启动脚本)。
原因如下:
- 代码复用 :你现有的
.sh脚本里的逻辑(比如复杂的参数拼接、文件检查)不用重写,直接搬过来稍微改改就能用。 - 灵活性 :以后如果想改个启动参数,改脚本重新打镜像比改 Dockerfile 的
ENTRYPOINT字符串要方便得多,也更容易调试(进容器手动跑脚本测试)。 - 优雅停机 :通过
exec命令,能完美解决 Java 进程在 K8s 中无法收到停止信号的问题。
操作步骤:
- 把你的
.sh脚本改名docker-entrypoint.sh。 - 删掉脚本里的
export(交给 K8s),删掉&后台运行。 - 在启动 Java 的命令前加
exec。 - Dockerfile 里
COPY脚本并ENTRYPOINT执行它。