Day58-K8s部署Spring Boot微服务:ConfigMap+Secret+HPA

配置进镜像、弹性靠人手------三个都会炸的 P0 隐患

把配置打进镜像、密码硬编码进代码、弹性伸缩靠人手动敲命令------这三个坑任何一个在生产环境爆了都是 P0。典型场面:镜像里硬编码了测试库密码,上线连不上数据库、探针反复失败、Pod 全部 CrashLoopBackOff;白天流量一涨 CPU 打满,网关超时像雪崩蔓延,只能一群人边改镜像边手动扩容。K8s 用三个标准对象一次性解决:ConfigMap 管非敏感配置(与镜像分离)、Secret 管敏感凭据、HPA 管基于指标的自动扩缩容

本文按生产标准把 Spring Boot 微服务搬进 K8s:先用 ConfigMap 剥离环境配置、用 Secret 存密码与 API Key(含 base64 只是编码的提醒),再给出可直接 apply 的生产级 Deployment(三探针分工、非 root 运行、资源限制),接着演示 Spring AI 服务读取注入的环境变量,最后配 HPA(metrics-server + 自定义指标)并对 AI 服务"不能只看 CPU"给出进阶方案。读完能交付一个配置分离、凭据安全、能随流


一、ConfigMap:让配置从镜像里滚出去

ConfigMap 是 K8s 里专门用来存非敏感配置的对象。它的作用就一个:把环境相关的配置(数据库地址、缓存地址、日志级别、业务开关等)从容器镜像里剥离出来。

镜像里只放代码和不变的东西;环境相关的配置,交给 ConfigMap。这样同一个镜像,在开发、测试、生产三套环境里都能复用。

1.1 准备一个 ConfigMap

下面这个例子,给一个 Spring Boot + Spring AI 的问答服务配置外部依赖:

java 复制代码
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: ai-service-config
  namespace: default
data:
  # 应用层配置
  SPRING_PROFILES_ACTIVE: "prod"
  SERVER_PORT: "8080"
  LOGGING_LEVEL_ROOT: "INFO"
  
  # 外部依赖地址
  SPRING_DATASOURCE_URL: "jdbc:postgresql://pg-primary.default.svc.cluster.local:5432/ai_db"
  SPRING_REDIS_HOST: "redis.default.svc.cluster.local"
  SPRING_REDIS_PORT: "6379"
  
  # Spring AI 配置(模型地址、默认模型名)
  SPRING_AI_DASHSCOPE_API_URL: "https://dashscope.aliyuncs.com/api/v1"
  SPRING_AI_DASHSCOPE_CHAT_OPTIONS_MODEL: "qwen-turbo"

K8s 会把 data 里的键值对注入到容器环境变量。Spring Boot 的 application.yml 或者 @ConfigurationProperties 会自动读取这些 SPRING_* 开头的变量,因为它们遵循 relaxed binding 规则。

1.2 两种使用姿势

方式 适用场景 注意点
env / envFrom 注入环境变量 少量键值对、需要被应用直接读取 变量名会被转换成大写,注意兼容
挂载为 Volume 文件 整个配置文件,例如 application-prod.yml 修改 ConfigMap 后,Volume 里的文件会热更新,但应用需自己监听重载

Spring Boot 云原生部署,推荐用环境变量方式。简单、直观、和 12-Factor App 一致。


二、Secret:别把密码当成普通配置

Secret 和 ConfigMap 长得很像,但它专门存敏感数据:数据库密码、API Key、TLS 证书、OAuth2 Client Secret 等。

很多人有个误解,以为 Secret 里的 base64 是加密。不是,它只是编码。任何拿到 YAML 的人都能一眼解码。所以 Secret 的核心价值是:

  • 与 ConfigMap 职责分离,最小权限管理;

  • K8s 对 Secret 有额外的存储和处理限制(默认不落地 etcd 明文,可开启静态加密);

  • 可以挂载为文件,避免密码出现在环境变量里被 ps / env 看到。

2.1 创建 Secret

先准备好密码文件,避免在 shell history 里留下明文:

bash 复制代码
# 推荐:从文件创建,命令行历史不会留密码
echo -n 'P@ssw0rd!2024' > db-password.txt
echo -n 'sk-xxxxxxxxxxxxxxxx' > ai-api-key.txt
​
kubectl create secret generic ai-service-secret \
  --from-file=DB_PASSWORD=db-password.txt \
  --from-file=AI_API_KEY=ai-api-key.txt \
  --dry-run=client -o yaml > secret.yaml
​
# 用完立刻删掉本地明文文件
rm db-password.txt ai-api-key.txt

生成的 secret.yaml 大概长这样:

java 复制代码
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: ai-service-secret
  namespace: default
type: Opaque
stringData:
  # 这里用 stringData,kubectl 会自动帮你 base64
  DB_PASSWORD: "P@ssw0rd!2024"
  AI_API_KEY: "sk-xxxxxxxxxxxxxxxx"

生产环境务必启用 etcd 静态加密,并配合 RBAC 限制谁能读取 Secret。更严格的企业可以用 External Secrets Operator 对接 Vault / 阿里云 KMS。


三、Deployment:把 ConfigMap 和 Secret 用起来

下面是一个可以直接 apply 的生产级 Deployment。重点看几个地方:

  • envFrom 把 ConfigMap 和 Secret 批量注入环境变量;

  • 配置了 resources.requests/limits,这是 HPA 计算 CPU 利用率的前提;

  • 配置了 startup/liveness/readiness 三个探针;

  • 使用非 root 用户运行容器。

java 复制代码
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-service
  namespace: default
  labels:
    app: ai-service
spec:
  replicas: 2                    # HPA 接管后会动态调整,这里只是初始值
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0          # 滚动更新时不允许中断
  selector:
    matchLabels:
      app: ai-service
  template:
    metadata:
      labels:
        app: ai-service
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/actuator/prometheus"
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
        - name: ai-service
          image: registry.cn-hangzhou.aliyuncs.com/demo/ai-service:1.2.0
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
              name: http
          envFrom:
            - configMapRef:
                name: ai-service-config
            - secretRef:
                name: ai-service-secret
          resources:
            requests:
              cpu: "250m"
              memory: "512Mi"
            limits:
              cpu: "1000m"
              memory: "1Gi"
          # 启动探针:应用启动慢时用,防止启动阶段被 liveness 误杀
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: http
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 12    # 最多给 60 秒启动时间
          # 存活探针:应用卡死时重启容器
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: http
            periodSeconds: 10
            failureThreshold: 3
          # 就绪探针:流量只打给 Ready 的 Pod
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: http
            periodSeconds: 5
            failureThreshold: 3

3.1 三个探针的分工

很多人把三个探针配成一样的路径,其实浪费。Spring Boot 3.x 的 Actuator 已经内置分组:

  • /actuator/health/liveness:只要 JVM 还活着就返回 UP。适合 livenessProbe。

  • /actuator/health/readiness:应用依赖(数据库、Redis)都就绪才返回 UP。适合 readinessProbe。

如果你把它们都配成 /actuator/health,会出现一种很坑的情况:数据库挂了, readiness 应该让 Pod 下线,但 liveness 也会失败,K8s 就会把 Pod 重启。结果数据库没好,Pod 反复重启,形成"重启风暴"。

3.2 Spring Boot 端开启 Actuator 健康分组

java 复制代码
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus,metrics
  endpoint:
    health:
      probes:
        enabled: true   # 自动生成 liveness/readiness 分组
      show-details: always
  metrics:
    tags:
      application: ${spring.application.name}

四、Java 代码:让应用拥抱 K8s 环境

下面是一段极简的 Spring AI 服务代码,演示如何读取 ConfigMap/Secret 注入的环境变量,并暴露一个问答接口。

java 复制代码
// AiChatController.java
package com.example.ai.controller;
​
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
​
@RestController
@RequestMapping("/api/ai")
public class AiChatController {
​
    private final ChatClient chatClient;
​
    // 从 Secret 注入的 API Key 已经被 Spring AI starter 自动读取,
    // 这里仅作演示如何显式读取环境变量做校验或日志
    @Value("${spring.ai.dashscope.api-key:}")
    private String apiKey;
​
    @Value("${spring.ai.dashscope.chat.options.model:qwen-turbo}")
    private String modelName;
​
    public AiChatController(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultSystem("你是一位经验丰富的 Java 后端工程师,回答简洁、有代码示例。")
                .build();
    }
​
    @PostMapping("/chat")
    public String chat(@RequestParam String question) {
        if (apiKey == null || apiKey.isBlank()) {
            throw new IllegalStateException("AI API Key 未配置,请检查 Secret 是否挂载");
        }
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
​
    @GetMapping("/model")
    public String currentModel() {
        return "当前模型: " + modelName;
    }
}

4.1 配置类读取环境变量

java 复制代码
// AiServiceProperties.java
package com.example.ai.config;
​
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
​
@Component
@ConfigurationProperties(prefix = "ai.service")
public class AiServiceProperties {
    private int maxTokens = 1024;
    private double temperature = 0.7;
    private boolean logRequest = false;
​
    // getter / setter 省略...
}

通过 envFrom 注入的环境变量,例如 AI_SERVICE_MAX_TOKENS=2048,Spring Boot 会自动绑定到 AiServiceProperties 上。配合 ConfigMap,你可以在不重新打包镜像的情况下,调整业务参数。


五、HPA:让服务自己"长"出来

Horizontal Pod Autoscaler(HPA)是 K8s 的横向扩缩容控制器。它会根据 CPU、内存或自定义指标,自动调整 Deployment 的副本数。

5.1 先装 metrics-server

HPA 需要 metrics-server 提供 Pod 的实时资源使用数据:

java 复制代码
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
​
# 如果内网环境没有有效证书,加上 --kubelet-insecure-tls
kubectl patch deployment metrics-server -n kube-system \
  --type='json' \
  -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--kubelet-insecure-tls"}]'

5.2 HPA 配置示例

java 复制代码
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ai-service-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ai-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60   # CPU 平均利用率达到 60% 开始扩容
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15        # 15 秒内最多扩容 100%
    scaleDown:
      stabilizationWindowSeconds: 300  # 缩容更保守,等 5 分钟确认流量真的降了
      policies:
        - type: Percent
          value: 10
          periodSeconds: 60

5.3 压测验证

bash 复制代码
# 1. 查看当前状态
kubectl get hpa ai-service-hpa --watch
​
# 2. 用 hey 或 wrk 打流量
hey -z 60s -c 50 -q 10 \
  -m POST \
  -d "question=解释一下Spring%20Boot自动配置原理" \
  http://<ai-service-ip>/api/ai/chat
​
# 3. 观察 Pod 数量变化
kubectl get pods -l app=ai-service --watch

5.4 AI 服务为什么不要只看 CPU

大模型调用有个特点:单次请求 CPU 不高,但耗时很长。如果你只按 CPU 扩容,可能在请求排队时已经超时了。

更合理的做法是用自定义指标,比如:

  • 正在处理的请求数(in-flight requests)

  • 请求队列长度

  • Token 生成延迟 P99

可以用 Prometheus Adapter 把这些指标暴露给 HPA:

java 复制代码
metrics:
  - type: Pods
    pods:
      metric:
        name: http_server_requests_seconds_max
      target:
        type: AverageValue
        averageValue: "1.5"   # 平均响应时间超过 1.5s 就扩容

这算是给 AI 服务做弹性伸缩的进阶版本,后续在云厂商托管 K8s 上可以用更成熟的 Custom Metrics API。


六、完整部署流程

把前面的 YAML 按顺序 apply 一遍:

bash 复制代码
# 1. 创建配置和凭据
kubectl apply -f secret.yaml
kubectl apply -f configmap.yaml
​
# 2. 部署应用
kubectl apply -f deployment.yaml
​
# 3. 暴露服务
kubectl apply -f service.yaml
​
# 4. 开启弹性伸缩
kubectl apply -f hpa.yaml
​
# 5. 验证
kubectl get pods -l app=ai-service
kubectl get svc ai-service
kubectl get hpa ai-service-hpa

Service 可以用 ClusterIP,配合 Ingress 做七层路由:

java 复制代码
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: ai-service
  namespace: default
spec:
  selector:
    app: ai-service
  ports:
    - port: 80
      targetPort: 8080
      name: http
  type: ClusterIP

七、实战建议:老兵的三个血泪经验

1. Secret 不要只依赖 base64,生产必须上加密和权限

  • 开启 etcd 静态加密;

  • 用 RBAC 限制 Secret 读取范围,只给需要的 ServiceAccount;

  • 考虑 External Secrets Operator,把凭据存在 Vault / 云厂商 KMS 里,K8s 里只存引用。

2. HPA 的阈值不是默认值 50%,而是压测出来的

  • 先用 hey / wrk / JMeter 压出服务的 CPU-吞吐量曲线;

  • 找到 CPU 利用率与响应时间开始恶化的拐点,把 HPA 目标设在这个点之前;

  • 设置 behavior.scaleDown.stabilizationWindowSeconds,防止流量抖动导致 Pod 反复创建销毁。

3. readiness 探针必须能真实反映服务能力

  • 不要只检查 /actuator/health,要检查 /actuator/health/readiness

  • 如果依赖了数据库、Redis、消息队列,要让 readiness 在这些依赖不可用时返回 DOWN;

  • 配合 PodDisruptionBudget 保证发布和缩容时至少有 N 个可用副本。

java 复制代码
# pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: ai-service-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: ai-service

下篇预告

"云原生不是把应用搬到 K8s 上,而是让应用学会在云里自己呼吸。"

ConfigMap 让配置流动起来,Secret 让敏感信息有边界,HPA 让服务能随流量起伏而生长。把这三件事做对,你的 Spring Boot 服务才算真正在 K8s 里站稳了脚跟。

下篇 Day59,我们走进阿里云 ACK 生产级集群:节点池怎么规划、SLS 日志采集怎么配、ARMS 监控告警怎么做。云上的坑,比本地 Minikube 多得多,但也爽得多。


参考版本

  • Spring Boot 3.2.x

  • Spring AI 1.0.0-M3

  • Kubernetes 1.29+

  • metrics-server v0.7.x

相关推荐
相顾若初见1 小时前
Ansible部署k8s
容器·kubernetes·ansible
深漂的华哥11 小时前
Ruoyi-Plus前后端分离场景下,数据加密传输
java·spring boot·后端·开源·maven·ruoyi
剑胆琴心静水深流12 小时前
全栈之路6---web集成与呈现
前端·vue.js·spring boot·分布式·spring·前端框架·npm
林浅不想努力14 小时前
k8s集群部署的方法原理
云原生·容器·kubernetes
輝太くん14 小时前
k8s中的pod管理
云原生·容器·kubernetes
大大大大晴天14 小时前
大数据平台为什么必须走向云原生:从资源孤岛到弹性数据基础设施
大数据·云原生
qq_1851986915 小时前
SpringBoot-五-AOT
java·spring boot·后端
潘正翔15 小时前
k8s高级_调度器Deployment
linux·运维·云原生·容器·kubernetes·jenkins·devops
阿里云云原生16 小时前
云效 AI 助手深度解析:如何贯穿需求、代码、测试与发布的全链路闭环?
云原生