通过helm在k8s上面一键部署前端和后端代码

为了方便在k8s上面进行前后端的同时部署,这里又新提供了一种叫做helm的部署方案,我这里直接进行部署展示,不去具体介绍这个概念了。

首先在通过helm部署代码前,我们必须保证前后端的镜像已经存在,同时k8s容器也启动了,我这里是通过dockers desktop进行的本地部署,因此所有的关联服务,如redis,nacos等服务都已经部署到dockers上面,然后就是安装helm工具,我是下载的windows版本,手动安装到指定目录,这个看自己情况,如果是手动自己安装的,需要在环境变量中去设置path路径,方便后面直接使用helm命令。

通过命令

XML 复制代码
helm create helm-chart

在项目的文件夹路径下面创建helm文件信息,如下:

XML 复制代码
helm-chart/
├── Chart.yaml          # Chart 元数据(名称、版本等)
├── values.yaml         # 默认配置值
├── templates/          # K8s 资源模板(YAML 文件)
│   ├── _helpers.tpl    # 模板辅助函数
│   ├── deployment.yaml # Deployment 模板
│   ├── service.yaml    # Service 模板
│   ├── configmap.yaml  # ConfigMap 模板
│   ├── secret.yaml     # Secret 模板
│   └── ...
└── charts/             # 依赖的子 Chart(可选)

1.配置我们的核心文件values.yaml

XML 复制代码
# ==============================================================================
# Helm Chart 全局配置
# ==============================================================================

# 副本数:所有 Deployment 的默认 Pod 数量(可被各服务单独覆盖)
replicaCount: 1

# ==============================================================================
# 后端服务 (Spring Boot) 配置
# ==============================================================================
backend:
  # 镜像相关配置
  image:
    # 镜像仓库地址(可以是 Docker Hub 用户名/仓库名,或私有仓库地址)
    repository: client-agent-backend
    # 镜像标签(可选,默认 latest。如填写则使用指定版本,如 v1.0.0)
    tag: "1.0"
    # 拉取策略:
    #   Always     - 每次都从仓库拉取
    #   IfNotPresent - 本地存在则使用,否则拉取
    #   Never      - 只使用本地镜像,否则报错
    pullPolicy: IfNotPresent

  # Service 端口映射
  service:
    # Service 暴露的端口(集群内其他服务访问时使用的端口)
    port: 8090
    # 容器内部监听的端口(必须与 Dockerfile 中的 EXPOSE 一致)
    targetPort: 8090

  # 环境变量(非敏感配置)
  env:
    # Spring 激活的 Profile(如 dev, test, prod)
    springProfiles: local
    # 数据库 JDBC URL(注意使用 K8s Service 名称,而非 localhost)
    databaseUrl: "jdbc:mysql://host.docker.internal:3306/smart_cs?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"
    # 数据库用户名
    databaseUsername: root
    # 其他自定义环境变量可以在此添加
    rabbitmqEnable: false
    # 例如: customKey: customValue
    # 敏感信息(实际密码通过 --set 或外部文件注入)
    secrets:
      databasePassword: ""   # 部署时传入真实值
      redisPassword: ""

  # 资源限制(推荐配置,防止容器占用过多资源)
  resources:
    limits:
      cpu: 500m      # 最大 CPU(500 毫核 = 0.5 核)
      memory: 1Gi    # 最大内存
    requests:
      cpu: 250m      # 初始分配的 CPU
      memory: 512Mi  # 初始分配的内存

# ==============================================================================
# 前端服务 (Nginx + 静态页面) 配置
# ==============================================================================
frontend:
  image:
    repository: client-agent-front
    # 镜像标签(可选,默认 latest。如填写则使用指定版本,如 v1.0.0)
    tag: ""
    pullPolicy: IfNotPresent

  service:
    port: 80
    targetPort: 80
    # NodePort 固定端口(仅当 type: NodePort 时有效,范围 30000-32767)
    # 留空则 K8s 自动分配
    nodePort: 30080

  # Nginx 配置(直接嵌入为多行字符串)
  # 注意缩进:使用 4 个空格缩进,保持 YAML 格式正确
  nginxConfig: |
    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;

        # 代理后端 API(使用后端 Service 的完整域名)
        location /api/ {
            proxy_pass http://helm-chart-backend:8090/api/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # SPA 路由支持
        location / {
            try_files $uri $uri/ /index.html;
        }

        # 静态资源缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }
    }

  resources:
    limits:
      cpu: 200m
      memory: 256Mi
    requests:
      cpu: 100m
      memory: 128Mi

# ==============================================================================
# 敏感信息(Secret)
# ==============================================================================
# 建议:生产环境不要在此明文存放,而是通过外部 Secret 管理工具(如 Vault)
# 或使用 `--set` 在命令行注入,或使用 `helm secrets` 插件加密
#secrets:
#  # 数据库密码(如不填写,Secret 将不会创建或值为空)
#  databasePassword: ""
#  # Redis 密码
#  redisPassword: ""

# ==============================================================================
# Ingress 配置(可选,用于域名访问)
# ==============================================================================
ingress:
  # 是否启用 Ingress(需集群已安装 Ingress Controller,如 nginx-ingress)
  enabled: false
  # 域名(如 myapp.example.com)
  hostname: myapp.local
  # TLS 证书配置(可选)
  # tls:
  #   - hosts:
  #     - myapp.local
  #     secretName: myapp-tls

2.templates/_helpers.tpl(辅助函数)

Helm 默认会生成一些辅助函数,我们保留 myapp-chart.fullname 等常用函数。你可以在模板中使用 {``{ include "myapp-chart.fullname" . }} 来生成带 release 名称的资源名。

XML 复制代码
{{/*
Expand the name of the chart.
*/}}
{{- define "helm-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "helm-chart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "helm-chart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "helm-chart.labels" -}}
helm.sh/chart: {{ include "helm-chart.chart" . }}
{{ include "helm-chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "helm-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "helm-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Create the name of the service account to use
*/}}
{{- define "helm-chart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "helm-chart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

3.设置后端的backend的配置

3.1. backend-deployment.ymal

XML 复制代码
{{- /*
  后端 Deployment 模板
  定义 Spring Boot 应用的 Pod 副本、容器、环境变量和资源限制。
  */ -}}
apiVersion: apps/v1
kind: Deployment
metadata:
  # 使用 include 函数生成带 release 名称的资源名,避免同名冲突
  name: {{ include "helm-chart.fullname" . }}-backend
  labels:
    app.kubernetes.io/name: {{ include "helm-chart.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    app: backend
spec:
  # 副本数从 values 读取(可在部署时覆盖)
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
        - name: springboot-app
          # 镜像地址和标签从 values 读取
          image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default "latest" }}"
          # 拉取策略
          imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.backend.service.targetPort }}
          # 环境变量定义
          env:
            # ---------- 非敏感环境变量(直接从 values 读取) ----------
            - name: SPRING_PROFILES_ACTIVE
              value: "{{ .Values.backend.env.springProfiles }}"
            - name: SPRING_DATASOURCE_URL
              value: "{{ .Values.backend.env.databaseUrl }}"
            - name: SPRING_DATASOURCE_USERNAME
              value: "{{ .Values.backend.env.databaseUsername }}"
            - name: SMARTCS_CHAT-MQ_ENABLED
              value: {{ .Values.backend.env.rabbitmqEnable  | quote }}
            # ---------- 敏感环境变量(从 Secret 引用) ----------
            - name: SPRING_DATASOURCE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: {{ include "helm-chart.fullname" . }}-secret
                  key: database-password
          # 资源限制(如果 values 中未定义,则整个 resources 块为空)
#          { { - if and .Values.backend.resources .Values.backend.resources.limits } }
#          resources:
#            { { - toYaml .Values.backend.resources | nindent 10 } }
#          { { - end } }

3.2 backend-service.yaml

XML 复制代码
apiVersion: v1
kind: Service
metadata:
  name: {{ include "helm-chart.fullname" . }}-backend
  labels:
    app.kubernetes.io/name: {{ include "helm-chart.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  selector:
    app: backend
  ports:
    - protocol: TCP
      port: {{ .Values.backend.service.port }}
      targetPort: {{ .Values.backend.service.targetPort }}
  type: ClusterIP

4.设置前端的配置文件

4.1 frontend-configmap.yaml

XML 复制代码
{{- /*
  Nginx 配置的 ConfigMap
  从 values.frontend.nginxConfig 中读取配置内容,并挂载到 Pod 中。
  */ -}}
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "helm-chart.fullname" . }}-frontend-config
data:
  default.conf: |
  {{ .Values.frontend.nginxConfig | nindent 4 }}

4.2 frontend-deployment.ymal

XML 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "helm-chart.fullname" . }}-frontend
  labels:
    app.kubernetes.io/name: {{ include "helm-chart.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    app: frontend
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
        - name: nginx-frontend
          image: "{{ .Values.frontend.image.repository }}"
          imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.frontend.service.targetPort }}
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/conf.d/default.conf
              subPath: default.conf
#          { { - if and .Values.backend.resources .Values.backend.resources.limits } }
#          resources:
#            { { - toYaml .Values.backend.resources | nindent 10 } }
#          { { - end } }
      volumes:
        - name: nginx-config
          configMap:
            name: {{ include "helm-chart.fullname" . }}-frontend-config

4.3 frontendd-service.yaml

XML 复制代码
apiVersion: v1
kind: Service
metadata:
  name: {{ include "helm-chart.fullname" . }}-frontend
  labels:
    app.kubernetes.io/name: {{ include "helm-chart.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  selector:
    app: frontend
  ports:
    - protocol: TCP
      port: {{ .Values.frontend.service.port }}
      targetPort: {{ .Values.frontend.service.targetPort }}
      {{- if .Values.frontend.service.nodePort }}
      nodePort: {{ .Values.frontend.service.nodePort }}
      {{- end }}
  type: NodePort

5.设置后端敏感信息

XML 复制代码
{{- /*
  Secret 资源定义
  从 values.secrets 中读取敏感信息,并 Base64 编码后存入。
  如果所有密码均为空,则不创建此 Secret。
  */ -}}
  {{- if or .Values.secrets.databasePassword .Values.secrets.redisPassword }}
apiVersion: v1
kind: Secret
metadata:
  name: {{ include "helm-chart.fullname" . }}-secret
  labels:
    app.kubernetes.io/name: {{ include "helm-chart.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
type: Opaque
data:
  {{- if .Values.secrets.databasePassword }}
  database-password: {{ .Values.secrets.databasePassword | b64enc | quote }}
  {{- end }}
  {{- if .Values.secrets.redisPassword }}
  redis-password: {{ .Values.secrets.redisPassword | b64enc | quote }}
  {{- end }}
  {{- end }}

设置完所有的启动配置后,我们接下来就是启动和关闭helm了

XML 复制代码
# 启动命令 secrets.databasePassword这个是需要手动设置的密码 
# .\helm-chart\这个是所在的配置目录
# helm-chart这个就是当前的这个服务名称,在释放这个helm的数据的时候也是通过这个值去释放的
helm install helm-chart .\helm-chart\ --set secrets.databasePassword=123456

# 释放命令
helm uninstall helm-chart

启动完成后,你就能在页面看到如下的界面了

剩下的操作如获取当前的pod信息,日志信息等和k8s操作就是一样了,不同的地方就是在启动的时候的编排。对了修改 values.yaml 或代码后,执行:

XML 复制代码
helm upgrade helm-chart ./helm-chart

如果新版本有问题,可以回滚到上一个版本:

XML 复制代码
helm rollback helm-chart 1   # 回滚到 revision 1

最后需要注意的是如果你的代码中实现了Elasticsearch的日志跟踪,或者说你的项目中实现了Elasticsearch,同时因为使用了SSL校验,即使用https进行ES连接,在dockers中我们可能会遇到各种因为SSL验证的问题导致无法正常连接到ES,如果是在测试或开发环境那么我们可以把所有的证书校验都改成信任,并且禁用主机名验证,这个是logback日志的配置如下:

java 复制代码
package com.smartcs.config;
 
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;
 
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * 自定义 Logback Appender:将日志批量写入 Elasticsearch。
 * <p>
 * 设计要点:
 * <ul>
 *   <li>内部 BlockingQueue 缓冲日志事件,定时/定量批量刷新</li>
 *   <li>通过 Apache HttpClient 发送 ES _bulk API 请求 (NDJSON 格式)</li>
 *   <li>支持 HTTPS + 自签名证书(信任所有证书 + 禁用主机名验证)</li>
 *   <li>ES 不可用时静默失败,不影响应用正常运行</li>
 *   <li>索引按天滚动:app-logs-yyyy.MM.dd</li>
 * </ul>
 *
 * Logback XML 配置属性 (通过 JavaBean setter 注入):
 * <ul>
 *   <li>esUri - Elasticsearch 地址 (默认 https://host.docker.internal:9200)</li>
 *   <li>esUser - ES 用户名 (默认 elastic)</li>
 *   <li>esPassword - ES 密码</li>
 *   <li>enabled - 是否启用 (默认 true)</li>
 * </ul>
 */
@Slf4j
public class ElasticsearchLogAppender extends AppenderBase<ILoggingEvent> {
 
    private static final int BATCH_SIZE = 200;
    private static final int FLUSH_INTERVAL_SECONDS = 2;
    private static final int QUEUE_CAPACITY = 2000;
    private static final DateTimeFormatter INDEX_DATE_FMT =
            DateTimeFormatter.ofPattern("yyyy.MM.dd").withZone(ZoneId.systemDefault());
    private static final DateTimeFormatter ISO_FMT =
            DateTimeFormatter.ISO_INSTANT;
 
    // ---- Logback JavaBean 配置属性 ----
    private String esUri = "https://host.docker.internal:9200";
    private String esUser = "elastic";
    private String esPassword = "123456";
    private boolean enabled = true;
 
    private final String appName;
    private final String hostName;
 
    private final BlockingQueue<ILoggingEvent> queue;
    private final ScheduledExecutorService scheduler;
    private final CloseableHttpClient httpClient;  // 保持 final
    private final String authHeader;
 
    public ElasticsearchLogAppender() {
        this.appName = "smart-customer-service";
 
        // 解析主机名
        String host;
        try {
            host = InetAddress.getLocalHost().getHostName();
        } catch (Exception e) {
            host = "unknown";
        }
        this.hostName = host;
 
        this.queue = new LinkedBlockingQueue<>(QUEUE_CAPACITY);
 
        // 构建 Basic Auth 头
        if (esUser != null && !esUser.isEmpty()) {
            String credentials = esUser + ":" + (esPassword != null ? esPassword : "");
            this.authHeader = "Basic " + Base64.getEncoder().encodeToString(
                    credentials.getBytes(StandardCharsets.UTF_8));
        } else {
            this.authHeader = null;
        }
 
        // ----- 初始化 Apache HttpClient(提取到方法,赋值点唯一) -----
        this.httpClient = createHttpClient();
 
        this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r, "logback-es-appender");
            t.setDaemon(true);
            return t;
        });
    }
 
    /**
     * 创建 Apache HttpClient,针对 HTTPS 配置 SSL(信任所有证书 + 禁用主机名验证)
     */
    private CloseableHttpClient createHttpClient() {
        if (esUri.startsWith("https://")) {
            try {
                // 信任所有证书
                SSLContext sslContext = SSLContexts.custom()
                        .loadTrustMaterial((chain, authType) -> true)
                        .build();
                // 禁用主机名验证
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                        sslContext, NoopHostnameVerifier.INSTANCE);
                return HttpClients.custom()
                        .setSSLSocketFactory(sslsf)
                        .setConnectionTimeToLive(3, TimeUnit.SECONDS)
                        .build();
            } catch (Exception e) {
                addError("Failed to create custom SSL HttpClient, using default", e);
                return HttpClients.createDefault();
            }
        } else {
            return HttpClients.createDefault();
        }
    }
 
    // ---- JavaBean setter (Logback XML 通过反射调用) ----
 
    public void setEsUri(String esUri) {
        this.esUri = esUri != null && esUri.endsWith("/") ? esUri.substring(0, esUri.length() - 1) : esUri;
    }
 
    public void setEsUser(String esUser) {
        this.esUser = esUser;
    }
 
    public void setEsPassword(String esPassword) {
        this.esPassword = esPassword;
    }
 
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
 
    // ---- Logback 生命周期 ----
 
    @Override
    public void start() {
        if (!enabled) {
            addInfo("ElasticsearchLogAppender is disabled");
            super.start();
            return;
        }
        scheduler.scheduleWithFixedDelay(this::flush, FLUSH_INTERVAL_SECONDS,
                FLUSH_INTERVAL_SECONDS, TimeUnit.SECONDS);
        addInfo("ElasticsearchLogAppender started, esUri=" + esUri
                + ", batchSize=" + BATCH_SIZE + ", flushInterval=" + FLUSH_INTERVAL_SECONDS + "s");
        super.start();
    }
 
    @Override
    public void stop() {
        // 刷新剩余日志
        flush();
        scheduler.shutdown();
        try {
            scheduler.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        // 关闭 Apache HttpClient
        if (httpClient != null) {
            try {
                httpClient.close();
            } catch (IOException e) {
                addWarn("Failed to close Apache HttpClient", e);
            }
        }
        super.stop();
    }
 
    @Override
    protected void append(ILoggingEvent event) {
        if (!enabled || !isStarted()) {
            return;
        }
        if (!queue.offer(event)) {
            // 队列满时丢弃最旧的,保留最新的
            queue.poll();
            queue.offer(event);
        }
        // 达到批量阈值时立即刷新
        if (queue.size() >= BATCH_SIZE) {
            flush();
        }
    }
 
    /**
     * 批量刷新队列中的日志事件到 Elasticsearch
     */
    private void flush() {
        if (queue.isEmpty()) {
            return;
        }
 
        StringBuilder bulk = new StringBuilder();
        int count = 0;
 
        while (count < BATCH_SIZE) {
            ILoggingEvent event = queue.poll();
            if (event == null) {
                break;
            }
            try {
                String indexName = "app-logs-" + INDEX_DATE_FMT.format(
                        Instant.ofEpochMilli(event.getTimeStamp()));
                String doc = buildDocument(event);
 
                // ES _bulk NDJSON: action line + source line
                bulk.append("{\"index\":{\"_index\":\"").append(indexName).append("\"}}\n");
                bulk.append(doc).append("\n");
                count++;
            } catch (Exception e) {
                // 单条日志构建失败,跳过
                addWarn("Failed to build log document, skipping", e);
            }
        }
 
        if (count == 0 || bulk.isEmpty()) {
            return;
        }
 
        sendToEs(bulk.toString(), count);
    }
 
    /**
     * 构建单条日志文档 JSON
     */
    private String buildDocument(ILoggingEvent event) {
        Instant instant = Instant.ofEpochMilli(event.getTimeStamp());
        String timestamp = ISO_FMT.format(instant);
        String level = event.getLevel() != null ? event.getLevel().toString() : "INFO";
        String logger = event.getLoggerName() != null ? event.getLoggerName() : "";
        String thread = event.getThreadName() != null ? event.getThreadName() : "";
        String message = event.getFormattedMessage() != null ? event.getFormattedMessage() : "";
 
        // 从 MDC 提取链路追踪字段 + 流水线阶段 + 阶段耗时
        String traceId = "";
        String spanId = "";
        String stage = "";
        String stageDuration = "";
        Map<String, String> mdcMap = event.getMDCPropertyMap();
        if (mdcMap != null) {
            String t = mdcMap.get("traceId");
            if (t != null) traceId = t;
            String s = mdcMap.get("spanId");
            if (s != null) spanId = s;
            String st = mdcMap.get("stage");
            if (st != null) stage = st;
            String sd = mdcMap.get("stageDuration");
            if (sd != null) stageDuration = sd;
        }
 
        // 手动构建 JSON (避免引入额外依赖)
        StringBuilder json = new StringBuilder(512);
        json.append('{');
        json.append("\"@timestamp\":\"").append(timestamp).append("\",");
        json.append("\"level\":\"").append(escapeJson(level)).append("\",");
        json.append("\"logger\":\"").append(escapeJson(logger)).append("\",");
        json.append("\"message\":\"").append(escapeJson(message)).append("\",");
        json.append("\"thread\":\"").append(escapeJson(thread)).append("\",");
        json.append("\"traceId\":\"").append(escapeJson(traceId)).append("\",");
        json.append("\"spanId\":\"").append(escapeJson(spanId)).append("\",");
        json.append("\"stage\":\"").append(escapeJson(stage)).append("\",");
        json.append("\"stageDuration\":\"").append(escapeJson(stageDuration)).append("\",");
        json.append("\"app\":\"").append(escapeJson(appName)).append("\",");
        json.append("\"host\":\"").append(escapeJson(hostName)).append("\"");
        json.append('}');
        return json.toString();
    }
 
    /**
     * 使用 Apache HttpClient 发送 bulk 请求到 ES
     */
    private void sendToEs(String bulkBody, int docCount) {
        HttpPost post = new HttpPost(esUri + "/_bulk");
        post.setHeader("Content-Type", "application/x-ndjson");
        if (authHeader != null) {
            post.setHeader("Authorization", authHeader);
        }
        post.setEntity(new StringEntity(bulkBody, StandardCharsets.UTF_8));
 
        try {
            HttpResponse response = httpClient.execute(post);
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode >= 300) {
                addWarn("[ES-Appender] Bulk request failed: status=" + statusCode);
                if (response.getEntity() != null) {
                    String body = new String(response.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8);
                    addWarn("[ES-Appender] Response body (truncated): " +
                            (body.length() > 200 ? body.substring(0, 200) + "..." : body));
                }
            } else {
                addInfo("[ES-Appender] Flushed " + docCount + " log documents to ES");
            }
            // 确保响应实体被消费,释放连接
            if (response.getEntity() != null) {
                response.getEntity().getContent().close();
            }
        } catch (Exception e) {
            addError("[ES-Appender] Failed to send logs to ES: " + e.getMessage(), e);
        }
    }
 
    /**
     * JSON 字符串转义
     */
    private static String escapeJson(String value) {
        if (value == null || value.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder(value.length() + 16);
        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);
            switch (c) {
                case '"' -> sb.append("\\\"");
                case '\\' -> sb.append("\\\\");
                case '\n' -> sb.append("\\n");
                case '\r' -> sb.append("\\r");
                case '\t' -> sb.append("\\t");
                case '\b' -> sb.append("\\b");
                case '\f' -> sb.append("\\f");
                default -> {
                    if (c < 0x20) {
                        sb.append(String.format("\\u%04x", (int) c));
                    } else {
                        sb.append(c);
                    }
                }
            }
        }
        return sb.toString();
    }
}

这个是spring的配置

java 复制代码
package com.smartcs.config;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
import org.springframework.data.elasticsearch.repository.support.ElasticsearchRepositoryFactoryBean;
import org.springframework.lang.Nullable;
 
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
 
/**
 * Elasticsearch客户端配置
 * <p>
 * 继承Spring Data Elasticsearch的{@link ElasticsearchConfiguration},
 * 自定义客户端连接参数, 支持HTTPS自签名证书(开发/测试环境信任所有证书)。
 * </p>
 * <p>
 * 注意: 生产环境应使用正式CA签发的证书, 不应信任所有证书。
 * </p>
 */
@Slf4j
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.smartcs.repository")
public class ElasticsearchConfig extends ElasticsearchConfiguration {
 
    @Value("${spring.elasticsearch.uris:https://localhost:9200}")
    private String uris;
 
    @Value("${spring.elasticsearch.username:elastic}")
    private String username;
 
    @Value("${spring.elasticsearch.password:123456}")
    private String password;
 
    @Value("${spring.elasticsearch.connection-timeout:5s}")
    private String connectTimeout;
 
    @Value("${spring.elasticsearch.socket-timeout:30s}")
    private String socketTimeout;
 
    @Override
    public ClientConfiguration clientConfiguration() {
        log.info("[ES Config] 初始化Elasticsearch客户端: uris={}", uris);
 
        String hostPort = uris.replaceFirst("^https?://", "");
        boolean useSsl = uris.startsWith("https://");
 
        var builder = ClientConfiguration.builder()
                .connectedTo(hostPort.split(","));
 
        if (useSsl) {
            // 1. 创建信任所有证书的 SSLContext
            SSLContext sslContext = createTrustAllSslContext();
 
            // 2. 创建一个总是返回 true 的 HostnameVerifier
            HostnameVerifier hostnameVerifier = (hostname, session) -> true;
 
            // 3. 关键:在 usingSsl 中同时传入 SSLContext 和 HostnameVerifier
            builder.usingSsl(sslContext, hostnameVerifier);
 
            log.info("[ES Config] HTTPS 已启用,并已配置信任所有证书 + 禁用主机名验证");
        }
 
        // 认证 (用户名/密码)
        if (username != null && !username.isBlank()) {
            builder.withBasicAuth(username, password);
        }
 
        // 超时 (支持Spring Boot简写格式如5s/30s, 也兼容ISO-8601的PT5S)
        builder.withConnectTimeout(parseDuration(connectTimeout));
        builder.withSocketTimeout(parseDuration(socketTimeout));
 
        log.info("[ES Config] Elasticsearch客户端配置完成 [auth={}, ssl={}]",
                (username != null && !username.isBlank()) ? "basic" : "none",
                useSsl ? "trust-all" : "none");
 
        return builder.build();
    }
 
    /**
     * 将所有ES Repository FactoryBean的Bean定义标记为懒初始化
     * <p>
     * Spring Data ES的FactoryBean在afterPropertiesSet阶段就会连接ES建索引,
     * 懒加载后无ES服务的环境(如本地开发)仍可正常启动, 首次使用时才初始化。
     * 注入方需配合@Lazy代理(见SearchService)。
     * </p>
     */
    @Bean
    public static BeanFactoryPostProcessor esRepositoryLazyInitPostProcessor() {
        return beanFactory -> {
            for (String beanName : beanFactory.getBeanDefinitionNames()) {
                BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
                if (ElasticsearchRepositoryFactoryBean.class.getName().equals(definition.getBeanClassName())) {
                    definition.setLazyInit(true);
                    log.debug("[ES Config] Repository bean '{}' set to lazy-init", beanName);
                }
            }
        };
    }
 
    /**
     * 解析Duration字符串, 支持Spring Boot简写格式(如5s, 30s, 1m)和ISO-8601格式(PT5S)
     */
    private static java.time.Duration parseDuration(String value) {
        try {
            return org.springframework.boot.convert.DurationStyle.SIMPLE.parse(value);
        } catch (Exception e) {
            return java.time.Duration.parse(value);
        }
    }
 
    /**
     * 创建信任所有证书的SSLContext (用于开发/测试环境的自签名证书)
     */
    @Nullable
    private static SSLContext createTrustAllSslContext() {
        try {
            TrustManager[] trustAll = new TrustManager[]{
                    new X509TrustManager() {
                        @Override public void checkClientTrusted(X509Certificate[] chain, String authType) {}
                        @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {}
                        @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
                    }
            };
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustAll, new java.security.SecureRandom());
            return sslContext;
        } catch (Exception e) {
            log.warn("[ES Config] SSLContext创建失败, 回退到默认SSL: {}", e.getMessage());
            return null;
        }
    }
}
相关推荐
11路没有终点2 小时前
Docker 化测试环境:一致性交付
运维·docker·容器
生活爱好者!3 小时前
NAS能批量做短视频?docker部署MoneyPrinterTurbo
运维·docker·容器
容器魔方3 小时前
议程一览 | 华为云亮相 KubeCon + CloudNativeCon China 2026
人工智能·云原生·容器·开源·华为云·云计算
瀛川4 小时前
Agent 换了 Pod,身份怎么办?拆解 Substrate 的 Actor Identity
人工智能·云原生
yanwumuxi4 小时前
Milvus使用和改进
云原生·eureka·milvus
拦路雨g5 小时前
docker_data.vhdx大文件瘦身
运维·docker·容器
分布式存储与RustFS5 小时前
RustFS 1.0.0-rc.5 发布:GA 前的最后一次大版本打磨
云原生·开源·对象存储·分布式存储·s3·rustfs·性能基准
明月心9525 小时前
关于docker的一些命令
运维·docker·容器
ZzzZZzzzZZZzzzz…6 小时前
K8s---网络:从 Pod 网络模型到 Calico 三种模式
运维·网络·云原生·容器·kubernetes·calico·pod网络模型