k8s集群中安装部署ECK技术栈

k8s集群中安装部署ECK

官方文档:https://www.elastic.co/guide/en/cloud-on-k8s/master/k8s-deploy-eck.html

1.安装 Elastic Cloud on Kubernetes(ECK)

Elastic Cloud on Kubernetes(ECK)是 Elastic 官方提供的 Kubernetes Operator。有了这些 CRD,才能用 YAML 声明式地创建 Elasticsearch 集群等资源。

1.1安装CRD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kubectl create -f https://download.elastic.co/downloads/eck/3.5.0/crds.yaml

#执行过程如下:
[root@k8s-master01 29-eck]# kubectl create -f https://download.elastic.co/downloads/eck/3.5.0/crds.yaml
customresourcedefinition.apiextensions.k8s.io/agents.agent.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/apmservers.apm.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/autoopsagentpolicies.autoops.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/beats.beat.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/elasticmapsservers.maps.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/elasticsearchautoscalers.autoscaling.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/elasticsearches.elasticsearch.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/enterprisesearches.enterprisesearch.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/kibanas.kibana.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/logstashes.logstash.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/packageregistries.packageregistry.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/stackconfigpolicies.stackconfigpolicy.k8s.elastic.co created

1.2ECK Operator 安装

镜像在国外,如果在线安装很慢的话可以改用我的镜像仓库,已经将镜像同步到阿里云

registry.cn-beijing.aliyuncs.com/k8s-liujunwei/eck-operator:3.5.0镜像是从官方docker.elastic.co/eck/eck-operator:3.5.0同步的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 下载文件,也可以通过浏览器下载,然后把文件上传到服务器中
[root@k8s-master01 29-eck]# wget https://download.elastic.co/downloads/eck/3.5.0/operator.yaml
--2026-08-24 15:52:22-- https://download.elastic.co/downloads/eck/3.5.0/operator.yaml
Resolving download.elastic.co (download.elastic.co)... 34.120.127.130, 2600:1901:0:1d7::
Connecting to download.elastic.co (download.elastic.co)|34.120.127.130|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 22289 (22K) [application/yaml]
Saving to: ‘operator.yaml’

operator.yaml 100%[===========================================================================>] 21.77K 113KB/s in 0.2s

2026-08-24 15:52:24 (113 KB/s) - ‘operator.yaml’ saved [22289/22289]

[root@k8s-master01 29-eck]# ll
total 836
-rw-r--r-- 1 root root 827819 Aug 24 15:32 crds.yaml
-rw-r--r-- 1 root root 22289 Aug 4 16:43 operator.yaml


# 修改operator.yaml文件中的镜像地址
[root@k8s-master01 29-eck]# vim operator.yaml
# 修改527行的镜像下载地址
containers:
# - image: "docker.elastic.co/eck/eck-operator:3.5.0" #原配置注释
- image: "registry.cn-beijing.aliyuncs.com/k8s-liujunwei/eck-operator:3.5.0" #改为自己的镜像地址

创建资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 资源创建在elastic-system命名空间
[root@k8s-master01 29-eck]# kubectl create -f operator.yaml
namespace/elastic-system created
serviceaccount/elastic-operator created
secret/elastic-webhook-server-cert created
configmap/elastic-operator created
clusterrole.rbac.authorization.k8s.io/elastic-operator created
clusterrole.rbac.authorization.k8s.io/elastic-operator-view created
clusterrole.rbac.authorization.k8s.io/elastic-operator-edit created
clusterrolebinding.rbac.authorization.k8s.io/elastic-operator created
service/elastic-webhook-server created
statefulset.apps/elastic-operator created
validatingwebhookconfiguration.admissionregistration.k8s.io/elastic-webhook.k8s.elastic.co created


# 安装成功后,查看创建的资源情况
[root@k8s-master01 29-eck]# kubectl get all -n elastic-system
NAME READY STATUS RESTARTS AGE
pod/elastic-operator-0 1/1 Running 0 39s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/elastic-webhook-server ClusterIP 10.96.1.175 <none> 443/TCP 39s

NAME READY AGE
statefulset.apps/elastic-operator 1/1 40s

2.在k8s中部署Elasticsearch集群

Elasticsearch 的角色是 存储与搜索引擎

  • 由 ECK 管理(Elasticsearch CRD)
  • 核心职责
    • 索引存储: 按天/周创建 Index(如 app-log-2026.08.30
    • 全文检索: 支持关键词搜索、聚合分析
    • 生命周期管理: ILM 策略自动 hot→warm→cold→delete
  • ECK 额外提供: 自动化部署、滚动升级、证书管理、快照备份、弹性伸缩

2.1部署一个小型集群

接下来即可使用自定义资源 Elasticsearch 一键启动一个 ES 集群。不推荐把Operator 资源 和组件的资源放在同一个命名空间。

首先创建一个用于放置日志收集工具的 Namespace:

1
2
3
# 创建一个单独的命名空间logging,专门放置日志收集组件的
[root@k8s-master01 29-eck]# kubectl create ns logging
namespace/logging created

创建一个定义 Elasticsearch 集群的 Yaml 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
apiVersion: elasticsearch.k8s.elastic.co/v1  #资源的api版本,固定写法,安装了ECK CRD后Kubernetes API Server 就能识别这种资源;
kind: Elasticsearch #资源的类型,表示创建一个Elasticsearch集群;
metadata: #资源的元数据,表示资源的名称、命名空间等信息;
name: es-cluster #资源的名称,表示集群的名称;ECK 会基于它生成 Service、StatefulSet、Secret 等资源
namespace: logging #定义这个资源创建在哪个命名空间中。
spec: #定义资源的具体规格,表示集群的配置;
version: 9.5.2 #【必填】指定Elasticsearch 版本;Operator 会部署对应版本的 ES 镜像,如果不能直接访问 Elastic 官方镜像仓库,必须配置image指定镜像下载地址
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/elasticsearch:9.5.2 #如果无法拉取官方的镜像,必须配置该参数指定从其他的镜像地址获取镜像
nodeSets: # 【必填】定义 Elasticsearch 节点组;一个集群可以配置多个 nodeSet,每个节点组配置不同的角色,如 master、hot、warm 等
- name: default # 【自定义/必填】指定节点组名称;同一集群内必须唯一,小型集群通常一个 default 即可;大型集群可拆 masters、hot、warm 等多个节点组
count: 3 # 【必填】该 nodeSet 创建多少个 ES 节点,即 3 个 Pod,组成高可用集群
volumeClaimTemplates: #生产环境:持久化存储,为该 nodeSet 中每个 ES Pod 自动创建独立 PVC
- metadata: #存储的原数据
name: elasticsearch-data #ECK 默认 ES 数据卷名称,默认挂载到 Elasticsearch 数据目录,保持默认即可,不建议修改。
spec:
accessModes: #访问模式,生产环境建议使用 ReadWriteOnce
- ReadWriteOnce
resources: #存储资源请求,根据自己的情况配置存储大小
requests:
storage: 20Gi #每一个 ES Pod 的磁盘容量,不是整个集群总容量,count=3时,总容量为20Gix3=60Gi
storageClassName: cfs-sc #指定集群中的 StorageClass名字, 我用cubefs作为存储后端
podTemplate: #pod的模板,用于自定义 Pod 的配置
spec: #pod的配置
initContainers: #定义初始化容器的配置,作用是在 Elasticsearch 启动前,把 Kubernetes 宿主机的 vm.max_map_count 调整到 Elasticsearch 要求的值。
# 如果宿主机的满足要求可以不配置初始化容器,可以在宿主机上执行sysctl vm.max_map_count命令查看
- name: sysctl
securityContext:
privileged: true
runAsUser: 0
command:
- sh
- -c
- sysctl -w vm.max_map_count=1048576

创建集群:

1
2
[root@k8s-master01 29-eck]# kubectl create -f Elasticsearch.yaml
elasticsearch.elasticsearch.k8s.elastic.co/es-cluster created

查看资源状态:

1
2
3
4
5
6
7
8
9
10
11
[root@k8s-master01 29-eck]# kubectl get po -n logging
NAME READY STATUS RESTARTS AGE
es-cluster-es-default-0 1/1 Running 0 72m
es-cluster-es-default-1 1/1 Running 0 72m
es-cluster-es-default-2 1/1 Running 0 72m


#es集群的HEALTH字段是green表示集群健康
[root@k8s-master01 29-eck]# kubectl get es -n logging
NAME HEALTH NODES VERSION PHASE AGE
es-cluster green 3 9.5.2 Ready 73m

服务启动完成后可以使用下面命令查看集群状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 获取密码
[root@k8s-master01 29-eck]# PASSWORD=$(kubectl get secret es-cluster-es-elastic-user -n logging -o go-template='{{.data.elastic | base64decode}}')
[root@k8s-master01 29-eck]# echo $PASSWORD
OT2jCVwU2cvx8ZQpZE6SmNQg


[root@k8s-master01 29-eck]# kubectl get svc -n logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
es-cluster-es-default ClusterIP None <none> 9200/TCP 75m
es-cluster-es-http ClusterIP 10.96.49.28 <none> 9200/TCP 75m
es-cluster-es-internal-http ClusterIP 10.96.42.43 <none> 9200/TCP 75m
es-cluster-es-transport ClusterIP None <none> 9300/TCP 75m

#
[root@k8s-master01 29-eck]# curl -u "elastic:$PASSWORD" https://10.96.49.28:9200/_cluster/health?pretty -k
{
"cluster_name" : "es-cluster",
"status" : "green",
"timed_out" : false,
"number_of_nodes" : 3,
"number_of_data_nodes" : 3,
"active_primary_shards" : 3,
"active_shards" : 6,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 0,
"unassigned_primary_shards" : 0,
"delayed_unassigned_shards" : 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch" : 0,
"task_max_waiting_in_queue_millis" : 0,
"active_shards_percent_as_number" : 100.0
}

2.2多节点集群案例

大型生产集群可以配置多个节点组,每个节点组定义不同的角色来分别处理不同的工作任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: es-cluster
namespace: logging
spec:
version: 9.5.2
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/elasticsearch:9.5.2 # 改成你自己的镜像

nodeSets:
# ---------- 专用 Master 节点配置 ----------
- name: masters
count: 3
config:
node.roles: ["master"] #通过node.roles参数来实现节点角色拆分
volumeClaimTemplates: #单独配置master节点中pod的存储
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
storageClassName: cfs-sc
podTemplate: #定义master节点组的pod配置
spec:
initContainers: #如果节点满足要求也可以不配置初始化容器
- name: sysctl
securityContext:
privileged: true
runAsUser: 0
command: ["sh", "-c", "sysctl -w vm.max_map_count=1048576"]
containers: #主容器可以不配置,
- name: elasticsearch
resources:
requests:
memory: 4Gi
cpu: 1
limits:
memory: 4Gi
cpu: 2
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
elasticsearch.k8s.elastic.co/cluster-name: es-cluster
topologyKey: kubernetes.io/hostname

# ---------- Hot 节点(高频写入 + 查询) ----------
- name: hot
count: 6
config:
node.roles: ["data_hot", "data_content", "ingest"]
volumeClaimTemplates: #针对多个节点组的配置,每个节点组需要单独配置存储
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
storageClassName: cfs-sc
podTemplate:
spec:
initContainers:
- name: sysctl
securityContext:
privileged: true
runAsUser: 0
command: ["sh", "-c", "sysctl -w vm.max_map_count=1048576"]
containers:
- name: elasticsearch
resources:
requests:
memory: 16Gi
cpu: 4
limits:
memory: 16Gi
cpu: 8

# ---------- Warm 节点(低频查询、较大存储) ----------
- name: warm
count: 3
config:
node.roles: ["data_warm"]
volumeClaimTemplates: #针对多个节点组的配置,每个节点组需要单独配置存储
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 300Gi
storageClassName: cfs-sc
podTemplate:
spec:
initContainers:
- name: sysctl
securityContext:
privileged: true
runAsUser: 0
command: ["sh", "-c", "sysctl -w vm.max_map_count=1048576"]
containers:
- name: elasticsearch
resources:
requests:
memory: 16Gi
cpu: 2
limits:
memory: 16Gi
cpu: 4

2.3多节点配置说明

配置一个节点,例如:

1
2
3
nodeSets:
- name: default
count: 3

这是是小型集群中最常见的方案,相当于一个人承担多个岗位;

而配置多节点的,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: es-cluster
spec:
version: 9.5.2
nodeSets:
- name: masters
count: 3
config:
node.roles: ["master"]

- name: hot
count: 6
config:
node.roles: ["data_hot", "data_content", "ingest"]

- name: warm
count: 3
config:
node.roles: ["data_warm"]

这是中大型日志平台的方案,相当于公司中老板,财务,销售,技术分别处理不同的工作。

需要注意的是,只写名字(masters / hot / warm)没有任何实际作用。名字只是给你自己看的标签,Elasticsearch 不会因为你叫它 “hot” 就自动变成热节点。

真正决定节点职责的是 config.node.roles。如果不写 node.roles,所有节点默认都会拥有几乎全部角色(master + data + data_hot + data_warm + ingest + ml 等),它们之间没有分工

两种配置的本质区别:

配置 实际效果 适用场景
单一 nodeSet(default count: 3) 3 个节点全部是“全能节点”,每个节点都能当 master、存数据、处理写入和查询 小集群、开发/测试、数据量不大
多个 nodeSet(masters + hot + warm) 只有正确配置了 node.roles 之后,才会真正分工:
• masters:只负责集群管理
• hot:负责最新数据写入 + 高频查询
• warm:负责较老的数据(低频查询)
生产环境、有时间序列数据(日志、指标等)、需要成本优化

它们是有明确分工和协作关系的,典型是 Hot-Warm 架构(常配合 Index Lifecycle Management / ILM 使用):

  1. masters(专用主节点)
    • 角色:node.roles: [“master”]
    • 职责:只负责集群状态管理、选举、分片分配等
    • 不存业务数据,资源要求相对较低但要稳定
    • 生产环境强烈建议专用(尤其是节点数较多时)
  2. hot(热节点)
    • 角色通常:node.roles: [“data_hot”, “data_content”, “ingest”](有时也加 master)
    • 职责:
      • 接收新写入的数据
      • 存放最近、最常被查询的数据
      • 需要较高的 CPU、内存和快速磁盘(SSD)
    • 数据一开始都落在 hot 节点
  3. warm(温节点)
    • 角色:node.roles: [“data_warm”]
    • 职责:存放已经变“冷”一点的数据(比如超过几天/几周的日志)
    • 查询频率低,可以用更大但更便宜的磁盘
    • 通过 ILM 策略,数据会自动从 hot 迁移到 warm

2.4容器配置说明

不需要单独配置正式的 Elasticsearch 容器,这是 ECK 的设计机制。在不配置主容器时,ECK Operator 会自动生成并注入主容器(名字固定为 elasticsearch)。如果写了主容器的配置,那么就会覆盖ECK Operator自动注入的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# ---------- Warm 节点(低频查询、较大存储) ----------
- name: warm
count: 3
config:
node.roles: ["data_warm"]
volumeClaimTemplates: #针对多个节点组的配置,每个节点组需要单独配置存储
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 300Gi
storageClassName: cfs-sc
podTemplate:
spec:
initContainers:
- name: sysctl
securityContext:
privileged: true
runAsUser: 0
command: ["sh", "-c", "sysctl -w vm.max_map_count=1048576"]
containers:
- name: elasticsearch
resources:
requests:
memory: 16Gi
cpu: 2
limits:
memory: 16Gi
cpu: 4

初始化容器(sysctl):每个 nodeSet 都最好单独写

生产环境如果 Kubernetes 节点数量比较固定,运维上更干净的方法其实是直接给所有可能调度 Elasticsearch 的宿主机永久配置

1
2
3
4
5
cat >/etc/sysctl.d/99-elasticsearch.conf <<EOF
vm.max_map_count=1048576
EOF

sysctl --system

然后 Elasticsearch CR 中甚至可以完全去掉这个 initContainer。这样Node 本身已经满足 ES 环境要求,就可以不配置initContainer:

主容器(elasticsearch)不是每个都需要写,只在你想覆盖资源、环境变量等的时候才写。

2.5多节点中定义存储

在上述多节点的集群案例中,每个节点中都单独定义了存储,每个 nodeSet 里的 volumeClaimTemplates 都写了相同的名字name: elasticsearch-data

实际创建出来的 PVC 名字是不同的。ECK 会为每个 nodeSet 创建独立的 StatefulSet,PVC 的最终命名规则是:

1
{volumeClaimTemplate.name}-{statefulset-name}-{pod-序号}

举例(假设集群名叫 es-cluster):

节点组 StatefulSet 名称 实际 PVC 名称示例
masters es-cluster-es-masters elasticsearch-data-es-cluster-es-masters-0
hot es-cluster-es-hot elasticsearch-data-es-cluster-es-hot-0
warm es-cluster-es-warm elasticsearch-data-es-cluster-es-warm-0

所以即使模板里的 name 都叫 elasticsearch-data,最终生成的 PVC 是完全独立的,不会互相覆盖或冲突。

为什么必须叫 elasticsearch-data?

这是 ECK 的约定名称

  • ECK 默认会把名字为 elasticsearch-data 的卷挂载到 Elasticsearch 容器的数据目录(/usr/share/elasticsearch/data)。
  • 如果你改成其他名字(比如 data 或 es-data),Operator 就不会自动挂载,除非你自己在 podTemplate 里手动配置 volumeMounts,比较麻烦,不推荐。

2.6多节点数据迁移策略

集群按照 masters + hot + warm 定义角色后,只要节点角色配置正确,集群就能正常启动和运行

  • 数据默认会优先分配到拥有 data_hot / data_content 角色的节点(也就是你的 hot 节点组)。
  • 没有 ILM 的情况下:
    • 数据会一直待在 hot 节点上
    • 不会自动迁移到 warm 节点
    • 集群功能完全正常(写入、查询、搜索都没问题)

什么时候才需要配置 ILM?

场景 是否需要 ILM
只是想把节点按角色分开,数据全部留在 hot 节点 不需要
希望数据根据时间/大小自动从 hot 迁移到 warm 需要
想节省 hot 节点的昂贵存储成本 强烈建议配置
有明确的数据保留策略(比如 7 天 hot → 30 天 warm → 删除) 必须配置

总结

  • 集群能跑起来 ≠ 必须配 ILM
  • 想让数据自动流动(hot → warm)→ 才需要配 ILM + Index Template

如果目前只是想先把集群跑起来验证功能,可以先不配 ILM,等业务写入数据后再根据实际需求添加策略。这里由于资源限制并没有分角色部署集群,暂时先不记录配置ILM策略,后续有环境在补充。

3.在k8s中部署kibana

Kibana — 可视化与运维平台

  • 由 ECK 管理(Kibana CRD)
  • 核心职责
    • 日志查询与展示(Discover)
    • 构建仪表盘(Dashboard)
    • 告警规则配置
    • Index Pattern / Data View 管理
    • ECK 集成后还可通过 Kibana 查看 ES 集群健康状态

3.1部署Kibana服务

Kibana 不需要像 Elasticsearch 那样定义角色、持久化存储和初始化容器。

Kibana 是无状态服务(状态都存在 Elasticsearch 里),ECK 用 Deployment 来管理它,配置比 ES 简单很多。

在上述创建的Elasticsearch 集群的步骤中我定义的 Elasticsearch CR 名称是 es-cluster,位于 logging namespace,版本是 9.5.2,因此建议 **Kibana 也部署到 logging,并使用相同版本 9.5.2**。ECK 的 Kibana 通过 elasticsearchRef 直接关联 ECK 管理的 Elasticsearch,Operator 会自动处理 Kibana 到 Elasticsearch 的认证、TLS CA 和连接配置,不需要自己填写 ES 用户名、密码或 elasticsearch.hosts

创建kibana资源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@k8s-master01 29-eck]# vim  kibana.yaml
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana #创建的资源类型
metadata:
name: kibana #定义资源的名字是kibana
namespace: logging # 资源创建在logging命名空间,要和 ES 集群在同一命名空间
spec:
version: 9.5.2 #kibana用的版本,必须和 ES 版本保持一致
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kibana:9.5.2 # 私有仓库时必须指定image参数
count: 2 # 生产建议至少 2 个副本(高可用)
elasticsearchRef:
name: es-cluster # es-cluster是对应 Elasticsearch 资源的名称
# namespace: logging # 如果和 ES 不在同一命名空间才需要写
http:
service:
spec:
type: NodePort # 生产推荐 ClusterIP + Ingress,不直接暴露
sessionAffinity: ClientIP # 开启会话亲和性
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 亲和时间(秒),这里是 3 小时
tls:
selfSignedCertificate:
disabled: true

创建kibana:

1
2
3
[root@k8s-master01 29-eck]# kubectl create -f Kibana.yaml
kibana.kibana.k8s.elastic.co/kibana created

查看创建的资源情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# kibana-kb-xxxx的pod已经Running
[root@k8s-master01 ~]# kubectl get po -n logging
NAME READY STATUS RESTARTS AGE
es-cluster-es-default-0 1/1 Running 0 5h43m
es-cluster-es-default-1 1/1 Running 0 5h43m
es-cluster-es-default-2 1/1 Running 0 5h43m
kibana-kb-b889dbf5f-8vx2n 1/1 Running 0 27m
kibana-kb-b889dbf5f-vw8d6 1/1 Running 0 27m


# kibana资源已经是green
[root@k8s-master01 ~]# kubectl get kb -n logging
NAME HEALTH NODES VERSION AGE
kibana green 2 9.5.2 29m

接下来可以通过svc的NodePort在浏览器中访问,用户密码和ES的一致:

1
2
3
4
5
6
7
[root@k8s-master01 ~]# kubectl get svc -n logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
es-cluster-es-default ClusterIP None <none> 9200/TCP 5h46m
es-cluster-es-http ClusterIP 10.96.244.191 <none> 9200/TCP 3h19m
es-cluster-es-internal-http ClusterIP 10.96.42.43 <none> 9200/TCP 5h46m
es-cluster-es-transport ClusterIP None <none> 9300/TCP 5h46m
kibana-kb-http NodePort 10.96.17.95 <none> 5601:30682/TCP 30m

3.2其他配置案例说明

如果需要单独定义kibana中容器的配置可以参考下面的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
name: kibana
namespace: logging # 和 ES 集群同一命名空间(推荐)
spec:
version: 9.5.2 # 必须和 ES 版本保持一致(或兼容)
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kibana:9.5.2 # 私有仓库时必须指定

count: 2 # 生产建议至少 2 个副本(高可用)

elasticsearchRef:
name: es-cluster # 对应你的 Elasticsearch 资源名称
# namespace: logging # 如果和 ES 不在同一命名空间才需要写

# 可选:自定义 Kibana 配置
# ---------- 与 Ingress 匹配的关键配置 ----------
config:
# 必须和 Ingress 的 host 一致 ,对外访问地址(有 Ingress 时强烈建议设置)
server.publicBaseUrl: "https://kibana.jiugen.cn"
# xpack.security.encryptionKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 建议用 secureSettings 管理
# xpack.reporting.encryptionKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# xpack.encryptedSavedObjects.encryptionKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ---------- 加密密钥(生产强烈建议配置) ----------
secureSettings: #生产环境通过该参数配置上述三个encryptionKey参数
- secretName: kibana-encryption-keys

http:
service:
spec:
type: ClusterIP # 生产推荐 ClusterIP + Ingress,不直接暴露,测试可以改成NodePort
# tls:
# selfSignedCertificate:
# disabled: true # 如果用 Ingress 做 TLS 终止,该配置必须关闭,否则 Ingress 的 backend-protocol: HTTP 会失败

# ---------- Pod 资源配置 ----------
podTemplate:
spec:
containers:
- name: kibana # 必须叫 kibana
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=2048" # Node.js 堆内存(根据实际内存调整)
resources:
requests:
memory: 2Gi
cpu: "1"
limits:
memory: 2Gi
cpu: "2"
# 可选:亲和性,让 Kibana 不要和 ES 数据节点抢资源
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
common.k8s.elastic.co/type: elasticsearch
topologyKey: kubernetes.io/hostname
参数 含义 如何根据实际情况配置
version Kibana 版本 必须与 Elasticsearch 版本兼容(最好完全一致)
image 自定义镜像 无法拉取官方镜像时必须配置,和 ES 一样
count 副本数 测试环境 1,生产环境 建议 ≥2(高可用)
elasticsearchRef.name 关联的 ES 集群 填写你 ES 资源的 metadata.name
config.server.publicBaseUrl 对外访问地址 有域名 + Ingress 时强烈建议设置,否则部分功能(Reporting、Alerting)异常
http.service.spec.type Service 类型 生产推荐 ClusterIP + Ingress;测试可用 NodePortLoadBalancer
NODE_OPTIONS Node.js 堆大小 内存 2Gi 时设 --max-old-space-size=15362048;内存更大可调高
resources CPU/内存 生产起步建议 2Gi 内存。使用 Security、Reporting、Alerting 等功能时建议 ≥2Gi
affinity 调度策略 可选,避免和 ES 重负载节点在同一台机器上

3.3配置详解

  • config 部分的意思:

    1
    2
    3
    4
    5
    config:
    server.publicBaseUrl: "https://kibana.jiugen.com" #ingress中定义的host
    # xpack.security.encryptionKey: "..."
    # xpack.reporting.encryptionKey: "..."
    # xpack.encryptedSavedObjects.encryptionKey: "..."

    作用:

    • server.publicBaseUrl:
      告诉 Kibana「用户最终用浏览器访问它的完整外部地址」。
      有 Ingress / 域名时强烈建议配置,否则会出现:

      • Alerting / Reporting 生成的链接错误
      • 登录跳转地址不对
      • 部分功能(如分享、邮件通知)链接指向内部地址

      值必须是完整 URL(带 https://),且不能以 / 结尾

    • 三个 encryptionKey:
      用于加密保存的对象、报表、会话等敏感数据。
      生产环境强烈建议配置(推荐用 secureSettings 管理,而不是明文写在 config 里),否则重启后部分加密数据可能无法解密。

  • secureSettings 管理encryptionKey

    参数 作用
    xpack.security.encryptionKey 加密会话 cookie、登录状态等安全相关数据
    xpack.reporting.encryptionKey 加密 Reporting(生成 PDF/CSV 报表)相关的数据
    xpack.encryptedSavedObjects.encryptionKey 加密保存的对象(Saved Objects),比如仪表盘、可视化、告警规则等敏感配置

    正确的配置方法是不要把密钥明文写在 config 里,而是用 secureSettings参数配置:

    第一步:创建Secret

    在Linux机器上可以执行openssl rand -base64 32指令来获取32位随机字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    cat kibana-encryption-keys.yaml
    apiVersion: v1
    kind: Secret
    metadata:
    name: kibana-encryption-keys
    namespace: logging
    type: Opaque
    stringData:
    # 请用下面命令生成真实的随机字符串替换
    # openssl rand -base64 32
    xpack.security.encryptionKey: "ufK5OZNdgLf0z8HPXNlPN3XcvedQn1H2sZTapo4l874=" #这里填的是32位随机字符串
    xpack.reporting.encryptionKey: "iyQTuI8k+Fm7UptJgQqpHWPF3eEsslqu4Q80GRKSWdI="
    xpack.encryptedSavedObjects.encryptionKey: "gOvjXOgthBlZE6byrFLcb/5Qioeu7Nw8ussNcbuQrW4="

    第二步:在 Kibana 资源中引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    cat kibana.yaml
    # 创建kibana请用完整的配置
    apiVersion: kibana.k8s.elastic.co/v1
    kind: Kibana
    metadata:
    name: kibana
    namespace: logging
    spec:
    version: 9.5.2
    # ...
    config:
    server.publicBaseUrl: "https://kibana.jiugen.cn"
    secureSettings:
    - secretName: kibana-encryption-keys #这里引用第一步中创建的名字是kibana-encryption-keys的Secret
    # ...

    第三步:创建Ingress

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    cat kibana-ingress.yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
    name: kibana
    namespace: logging
    annotations:
    # ---------- Sticky Session(会话亲和性)----------
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "kibana_affinity"
    nginx.ingress.kubernetes.io/session-cookie-expires: "172800" # 2天
    nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" # 2天
    nginx.ingress.kubernetes.io/session-cookie-path: "/"

    # ---------- 后端协议(重要)----------
    # 因为你已经设置了 tls.selfSignedCertificate.disabled: true
    # Ingress 到 Kibana Pod 使用 HTTP
    nginx.ingress.kubernetes.io/backend-protocol: "HTTP"

    # ---------- 可选但推荐的配置 ----------
    nginx.ingress.kubernetes.io/proxy-body-size: "50m" # 上传文件大小限制
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"

    spec:
    ingressClassName: nginx # 改成你实际使用的 IngressClass
    tls:
    - hosts:
    - kibana.jiugen.cn
    secretName: kibana-tls-secret # TLS 证书的 Secret 名称
    rules:
    - host: kibana.jiugen.cn
    http:
    paths:
    - path: /
    pathType: Prefix
    backend:
    service:
    name: kibana-kb-http # ECK 默认生成的 Service 名称
    port:
    number: 5601

    部署上述资源:

    1
    2
    3
    4
    5
    6
    7
    8
    # 1. 先创建加密密钥 Secret
    kubectl apply -f kibana-encryption-keys.yaml

    # 2. 部署 Kibana
    kubectl apply -f kibana.yaml

    # 3. 再部署 Ingress
    kubectl apply -f kibana-ingress.yaml

    访问地址:https://kibana.jiugen.cn

    登录账号使用 Elasticsearch 的 elastic 用户(密码可通过下面命令获取):

    1
    kubectl get secret es-cluster-es-elastic-user -n logging -o go-template='{{.data.elastic | base64decode}}'

    注意:如果不配置secureSettings参数,那么Operator 会自动生成密钥

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    apiVersion: kibana.k8s.elastic.co/v1
    kind: Kibana
    metadata:
    name: kibana
    namespace: logging
    spec:
    version: 9.5.2
    # ...
    config:
    server.publicBaseUrl: "https://kibana.jiugen.cn"
    secureSettings: #在测试环境该配置可以不配置,生产环境建议手动配置,不用Operator 自动生成的
    - secretName: kibana-encryption-keys #这里引用第一步中创建的名字是kibana-encryption-keys的Secret

    不管是 Operator 自动生成的密钥,还是你自己创建的 secureSettings Secret,一旦密钥丢失,数据就无法解密。
    这和「Operator 还是你手动配置」没有直接关系,核心是密钥的管理方式

    自动生成密钥(不写 secureSettings)的实现方式

    • ECK 会自动为每个 Kibana 资源创建一个 持久的 Secret(名称通常是 kibana-{name}-kb-config)。
    • 这个 Secret 包含三个加密密钥(xpack.security.encryptionKey、xpack.reporting.encryptionKey、xpack.encryptedSavedObjects.encryptionKey)。
    • Operator 会把密钥注入到每个 Pod 的 keystore 中,并持续监控这个 Secret。
    • 日常运维(Pod 重建、滚动更新、节点故障)完全没问题,因为密钥一直存在。

    自己手动配置的实现方式(写 secureSettings)

    • 你创建的 Secret 内容会被 Operator 直接注入到 Pod 的 keystore。
    • 优点是完全由你控制,随时可以备份、轮转、删除旧密钥。

    两种方式的唯一区别

    维度 Operator 自动生成 你手动创建 Secret
    密钥管理方式 Operator 自动生成并注入 你手动创建 Secret 并引用
    密钥丢失风险 删除整个 Kibana 资源 + Secret 丢失 你自己删 Secret
    密钥轮转 麻烦(需要修改 Operator 生成规则) 轻松(你直接改 Secret)
    生产推荐 不推荐 强烈推荐

    结论
    密钥丢失 = 数据无法解密。
    「自动生成」和「手动创建」本身不是问题管理好密钥才是关键。

    生产环境强烈建议手动创建 Secret(之前配置的 kibana-encryption-keys),这样密钥完全在你手里,可以定期备份、轮转、审计。

4.在 K8s 中部署 Zookeeper

ZooKeeper — Kafka 协调服务

  • 核心职责
    • Kafka Broker 的注册与发现
    • Topic 分区分配、Leader 选举
    • 维护 Kafka 集群元数据和配置
  • 注意: 较新版本的 Kafka(KRaft 模式)已可去掉 ZK,但很多 ECK/Helm 部署仍使用 ZK 方案
  • ⚠️ ZK 本身不处理日志数据,只服务于 Kafka

在 ECK(Elastic Cloud on Kubernetes)日志架构中,zookeeper 和 Kafka 不是 ECK 本身的组件,而是作为日志缓冲层插入在”日志采集端”和”Elasticsearch 存储端”之间的。

Zookeeper 和 Kafka 并不属于 ECK 所管理的资源,Kafka/ZooKeeper 是整个日志采集架构中的中间消息系统,而 ECK 只负责管理 Elasticsearch/Kibana 等 Elastic 资源。所以使用 Helm 进行安装Zookeeper和kafka服务:

在安装部署zk和kafka之前先了解两个事件:

第一,Bitnami 于 2025-08-28 调整了公共镜像目录。所有带版本号的旧镜像(如 bitnami/kafka:3.9.0-debian-12-r12)被迁移到 docker.io/bitnamilegacydocker.io/bitnami 只保留极少数 latest 标签的硬化镜像,公共仓库的删除后来推迟到 9 月底并做了多次 brownout。结果就是:老 chart 直接装会 ImagePullBackOff,必须手动把镜像仓库指向 bitnamilegacy,并加上 global.security.allowInsecureImages=true

第二,Kafka 4.0 彻底移除了 ZooKeeper。Bitnami Kafka chart 从 32.0.0 起(appVersion 4.0.0,当前 32.4.4)默认且只支持 KRaft 模式,zookeeper 已不再是 chart 依赖,所有 zookeeper.* / kraft.* 参数被删除。也就是说,该笔记中”先装 ZooKeeper 集群、再装 Kafka 集群连上去”这个架构,只能用 kafka chart ≤ 31.5.0(appVersion 3.9.0)才能复现。

此笔记采用旧版 ZooKeeper + Kafka 3.9的组合方式。

Bitnami 采用的是单体仓库(monorepo)模式:

  • 仓库地址:https://github.com/bitnami/charts.git
  • 所有 chart 都放在同一个仓库的 bitnami/ 目录下。
  • 对应关系:
    • ZooKeeper chart → bitnami/zookeeper/
    • Kafka chart → bitnami/kafka/
    • 还有大量其他服务(redis、postgresql、mysql、elasticsearch 等)也都在同一个仓库里。

4.1准备工作-检查环境

确认环境满足 chart 要求:Kubernetes 1.23+、Helm 3.8.0+(OCI 支持需要 3.8)、集群里有可用的 StorageClass(Kafka 和 ZooKeeper 都是 StatefulSet,需要动态 PV,否则 Pod 会一直卡在 PendingContainerCreating)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# k8s版本大于1.23+
[root@k8s-master01 ~]# kubectl version
Client Version: v1.31.4
Kustomize Version: v5.4.2
Server Version: v1.31.4

#helm版本大于3.8.0
[root@k8s-master01 ~]# helm version
version.BuildInfo{Version:"v4.0.4", GitCommit:"8650e1dad9e6ae38b41f60b712af9218a0d8cc11", GitTreeState:"clean", GoVersion:"go1.25.5", KubeClientVersion:"v1.34"}

#集群中有共享存储,这里用的cubefs
[root@k8s-master01 ~]# kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
cfs-sc (default) csi.cubefs.com Delete Immediate true 242d
nfs-csi nfs.csi.k8s.io Delete Immediate true 256d

# 命名空间
[root@k8s-master01 ~]# kubectl get ns|grep logging
logging Active 2d23h

4.2获取安装包

从 GitHub 源码获取(最稳,Apache-2.0 授权,官方承诺持续可用)

Bitnami 明确说明 chart 源码继续在 GitHub 维护开放。

1
2
3
4
5
git clone https://github.com/bitnami/charts.git
cd charts

# 用 kafka/31.5.0 这个 tag(当时仓库里的 zookeeper 也是兼容版本)
git checkout kafka/31.5.0

如果在拉取github仓库时由于网络的问题无法拉取代码时,通过下面命令设置网络代理:

1
2
3
4
5
6
7
# 查看当前代理设置
git config --global --get http.proxy
git config --global --get https.proxy

# 如果需要设置代理 (根据自己的实际代理地址修改),192.168.0.4是我自己的电脑ip,7897是我电脑上代理软件的端口
git config --global http.proxy http://192.168.0.4:7897
git config --global https.proxy http://192.168.0.4:7897

4.3安装zookeeper

修改values.yaml文件:

1
2
3
4
5
6
7
8
9
10
11
12
# 进入zk的文件目录
[root@k8s-master01 charts]# cd bitnami/zookeeper/

[root@k8s-master01 zookeeper]# ll
total 244
-rw-r--r-- 1 root root 107465 Aug 26 21:13 CHANGELOG.md
-rw-r--r-- 1 root root 226 Aug 26 21:13 Chart.lock
drwxr-xr-x 2 root root 31 Aug 26 23:13 charts
-rw-r--r-- 1 root root 970 Aug 26 21:13 Chart.yaml
-rw-r--r-- 1 root root 79952 Aug 26 21:13 README.md
drwxr-xr-x 2 root root 4096 Aug 26 21:13 templates
-rw-r--r-- 1 root root 44164 Aug 26 22:56 values.yaml

按照下面配置对values.yaml文件进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
## 生产用 ZooKeeper values(Bitnami 新版 chart 结构)
## 镜像请改成你同步后的私有仓库地址

global:
security:
# 使用非 docker.io/bitnami 官方路径时必须为 true,否则 chart 会因镜像校验失败拒绝渲染
allowInsecureImages: true
# 若整站统一前缀,也可用:
# imageRegistry: "registry.example.com/your-project"

image:
#从dockerhub的bitnamilegacy/zookeeper镜像仓库中同步到阿里云仓库
registry: registry.cn-beijing.aliyuncs.com # 改成自己的仓库
repository: k8s-liujunwei/zookeeper # 改成你同步后的路径
tag: 3.9.3-debian-12-r8 # 与官方保持一致,不要降到 3.7
pullPolicy: IfNotPresent
# 若私有仓要鉴权:
# pullSecrets:
# - your-registry-secret

replicaCount: 3 # 生产必须奇数,3 可容忍 1 节点故障

# 堆约 512MB,容器内存建议 ≥ 1.5~2Gi,避免 OOM
heapSize: 512

resources:
requests:
cpu: 250m
memory: 1Gi
limits:
cpu: "1"
memory: 2Gi

# 尽量打散到不同节点
podAntiAffinityPreset: soft
# 若节点足够且要求更强隔离,可改为 hard,或写 affinity 指定 topologyKey: kubernetes.io/hostname

persistence:
enabled: true
storageClass: "cfs-sc" #指定集群中的 StorageClass;生产请指定高性能盘(如云盘 SSD)
accessModes:
- ReadWriteOnce
size: 20Gi # 日志场景可按实际调大;ZK 数据通常不大,20Gi 较稳妥

# 内网、仅 Kafka/日志组件访问时可先关认证;若多租户/共享集群建议打开 SASL
auth: # 该参数是默认配置
client:
enabled: false
quorum:
enabled: false

# 卷权限 init:多数云盘/默认 SC 不需要;若启动报 permission denied 再改为 true
volumePermissions:
enabled: false
image:
#从dockerhub的bitnamilegacy/os-shell镜像仓库中同步的镜像
registry: registry.cn-beijing.aliyuncs.com
repository: k8s-liujunwei/os-shell
tag: 12-debian-12-r39 # 与官方 chart 默认一致即可

# 生产建议打开网络策略,仅允许同命名空间或带标签的客户端访问 2181,这里我设置的false关闭了网络策略
networkPolicy:
enabled: false
allowExternal: true # true = 不强制客户端 label;更严可 false 并给 Kafka Pod 打 label

metrics: #该参数默认也是false
enabled: false # 便于 Prometheus 抓取
serviceMonitor:
enabled: false # 若装了 Prometheus Operator 可改为 true

# 与官方一致的安全基线(chart 默认已较严,显式写出便于审计)
# 默认配置
podSecurityContext:
enabled: true
fsGroup: 1001
#默认配置

containerSecurityContext:
enabled: true
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]

# 可选:显式 PDB(chart 新版默认会建)
# 默认配置
pdb:
create: true
maxUnavailable: 1 # 3 副本时最多同时 1 个不可用

# 其它与日志场景相关的合理默认
# 默认配置
logLevel: ERROR
autopurge:
snapRetainCount: 10
purgeInterval: 1
fourlwCommandsWhitelist: srvr,mntr,ruok

##其他参数也可保持默认即可

https://github.com/bitnami/charts.git下载的源码中是没有chart依赖的,在部署前需要先下载依赖

1
2
3
4
5
6
7
8
#查看当前 chart 声明了哪些依赖(主要是 common 库 chart)
helm dependency list

#把 Chart.yaml 里声明的依赖(尤其是 common)下载到 charts/ 目录。本地从 GitHub 克隆后,依赖一般不会预先打包好,不执行这一步,helm install 很可能会失败(找不到 common 模板)。
helm dependency build

#强烈建议语法检查,能提前发现 values 写错、模板问题等,但不是强制的。
helm lint .

网络原因导致依赖无法下载解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 如果执行 helm dependency build 遇到以下报错时,依然是网络原因导致的,还是通过设置代理解决
Downloading common from repo oci://registry-1.docker.io/bitnamicharts
... dial tcp 67.230.169.182:443: connect: connection refused

# 1. 设置代理(当前 shell 有效),代理软件上要打开允许局域网连接,参考4.2中的图
[root@k8s-master01 zookeeper]# export HTTPS_PROXY=http://192.168.0.4:7897
[root@k8s-master01 zookeeper]# export HTTP_PROXY=http://192.168.0.4:7897
[root@k8s-master01 zookeeper]# export NO_PROXY=localhost,127.0.0.1,192.168.0.0/16,.svc,.cluster.local

# 2. 可选:先测一下能否访问 Docker Hub
[root@k8s-master01 zookeeper]# curl -I --connect-timeout 10 https://registry-1.docker.io/v2/
HTTP/1.1 200 Connection established

HTTP/2 401
date: Thu, 27 Aug 2026 08:38:58 GMT
content-type: application/json
content-length: 87
docker-distribution-api-version: registry/2.0
www-authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"
strict-transport-security: max-age=31536000

网络问题解决后在以此执行依赖安装和部署zk:

1
2
3
4
5
6
7
8
# 安装chart的依赖
helm dependency build

# 语法检查
helm lint .

# 安装zk,注意所在目录charts/bitnami/zookeeper,在values.yaml所在的目录中执行
[root@k8s-master01 zookeeper]# helm install zookeeper . -n logging

检查zk服务的pod部署状态:

1
2
3
4
5
6
# zk的服务已经成功运行
[root@k8s-master01 zookeeper]# kubectl get po -n logging
NAME READY STATUS RESTARTS AGE
zookeeper-0 1/1 Running 0 18h
zookeeper-1 1/1 Running 0 18h
zookeeper-2 1/1 Running 0 18h

创建后会产生哪些资源:chart 会创建一个 StatefulSet(zookeeper-0/1/2 顺序启动)、一个 headless Service(zookeeper-headless,供节点间用稳定 DNS 互相发现,即 zookeeper-0.zookeeper-headless.logging.svc.cluster.local:2888/3888)、一个普通 ClusterIP Service(zookeeper,客户端连 2181 用)、三个 PVC,以及一个存放配置的 ConfigMap。每个 Pod 启动时会根据自己的序号写入 myid 文件,这就是为什么必须用 StatefulSet 而不是 Deployment。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@k8s-master01 zookeeper]# kubectl get all -n logging
NAME READY STATUS RESTARTS AGE
pod/zookeeper-0 1/1 Running 0 21h
pod/zookeeper-1 1/1 Running 0 21h
pod/zookeeper-2 1/1 Running 0 21h

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/zookeeper ClusterIP 10.96.62.2 <none> 2181/TCP,2888/TCP,3888/TCP 21h
service/zookeeper-headless ClusterIP None <none> 2181/TCP,2888/TCP,3888/TCP 21h

NAME READY AGE
statefulset.apps/zookeeper 3/3 21h


[root@k8s-master01 zookeeper]# kubectl get pvc -n logging
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
data-zookeeper-0 Bound pvc-ba10584f-eecc-4f3a-8a57-adf6956a6f7d 10Gi RWO cfs-sc <unset> 21h
data-zookeeper-1 Bound pvc-b03d8e93-d92b-439e-8427-d14c10bb576f 10Gi RWO cfs-sc <unset> 21h
data-zookeeper-2 Bound pvc-c248e352-e319-4f2d-8b10-1912c95c830b 10Gi RWO cfs-sc <unset> 21h

[root@k8s-master01 zookeeper]# kubectl get cm -n logging
NAME DATA AGE
zookeeper-scripts 2 21h

验证集群:批量检查 ZooKeeper 集群中每个节点的角色状态(Leader / Follower / Standalone)。

1
2
3
4
[root@k8s-master01 zookeeper]# for i in 0 1 2; do   echo -n "zookeeper-$i: ";   kubectl exec -n logging zookeeper-$i -- bash -c     'ZOO_LOG_DIR=/tmp zkServer.sh status 2>/dev/null | grep Mode'; done
zookeeper-0: Mode: follower
zookeeper-1: Mode: follower
zookeeper-2: Mode: leader
  • 一个健康的 3 节点 ZK 集群应该恰好有 1 个 leader + 2 个 follower
  • 如果某个节点没有输出或显示 Error,说明该节点异常或未就绪
  • 如果所有节点都是 standalone,说明集群未成功组网,各节点独立运行
  • 2>/dev/null 很重要,因为 zkServer.sh status 在容器环境中经常输出大量无关日志,不加这个会导致输出混乱

5.在k8s中部署kafka

Kafka — 消息缓冲队列

  • 核心职责
    • 削峰填谷: 当日志量突增时,Kafka 缓冲数据,防止下游 ES/Logstash 被压垮
    • 解耦: Filebeat 和 Logstash 不需要直接感知对方
    • 持久化: 即使 Logstash 短暂宕机,日志也不会丢失
    • 多消费者支持: 同一份日志可被多个消费组消费(如同时用于日志分析和安全审计)
  • 为什么需要它: 生产环境中日志量波动大,没有 Kafka 的话 ES 写入容易出现 bulk reject

如果日志量较小(<50GB/天),也可以不使用kafka。

Kafka 用 chart ≤ 31.5.0(支持 ZK);

修改values.yaml文件:

1
2
3
4
5
6
7
8
9
10
11
12
# 进入kafka的文件目录
[root@k8s-master01 charts]# cd bitnami/kafka/

[root@k8s-master01 kafka]# ll
total 564
-rw-r--r-- 1 root root 179294 Aug 26 21:13 CHANGELOG.md
-rw-r--r-- 1 root root 316 Aug 26 21:13 Chart.lock
drwxr-xr-x 2 root root 59 Aug 28 00:08 charts
-rw-r--r-- 1 root root 1355 Aug 26 21:13 Chart.yaml
-rw-r--r-- 1 root root 239104 Aug 26 21:13 README.md
drwxr-xr-x 7 root root 4096 Aug 26 21:13 templates
-rw-r--r-- 1 root root 123686 Aug 28 00:00 values.yaml

按照下面配置对values.yaml文件进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
global:
security:
allowInsecureImages: true

image:
registry: registry.cn-beijing.aliyuncs.com # 我的阿里云仓库,镜像已同步
repository: k8s-liujunwei/kafka
tag: 3.9.0-debian-12-r12
pullPolicy: IfNotPresent

# 使用已部署的 ZooKeeper
kraft:
enabled: false
zookeeper:
enabled: false
externalZookeeper:
servers:
- zookeeper.logging.svc.cluster.local:2181 #这里配置的zk的svc地址

controller:
replicaCount: 0 # ZK 模式不要 controller 角色

broker:
replicaCount: 3
podAntiAffinityPreset: soft
heapOpts: "-Xmx2048m -Xms2048m"
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: "2"
memory: 4Gi
persistence:
enabled: true
storageClass: "cfs-sc" # 这是我集群中的存储
size: 20Gi # 按日志量调整
pdb:
create: true
maxUnavailable: 1

listeners:
client:
protocol: PLAINTEXT # 内网可先明文;要加固改 SASL_PLAINTEXT
interbroker:
protocol: PLAINTEXT

extraConfig: |
default.replication.factor=3
offsets.topic.replication.factor=3
transaction.state.log.replication.factor=3
min.insync.replicas=2
auto.create.topics.enable=true
log.retention.hours=168

volumePermissions:
enabled: false
image:
registry: registry.cn-beijing.aliyuncs.com
repository: k8s-liujunwei/os-shell
tag: 12-debian-12-r39

# 若 chart 仍引用 kubectl/jmx,一并改私有仓

##其他参数也可保持默认即可

https://github.com/bitnami/charts.git下载的源码中是没有chart依赖的,在部署前需要先下载依赖

1
2
3
4
5
6
7
8
#查看当前 chart 声明了哪些依赖(主要是 common 库 chart)
helm dependency list

#把 Chart.yaml 里声明的依赖(尤其是 common)下载到 charts/ 目录。本地从 GitHub 克隆后,依赖一般不会预先打包好,不执行这一步,helm install 很可能会失败(找不到 common 模板)。
helm dependency build

#强烈建议语法检查,能提前发现 values 写错、模板问题等,但不是强制的。
helm lint .

网络原因导致依赖无法下载解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 如果执行 helm dependency build 遇到以下报错时,依然是网络原因导致的,还是通过设置代理解决
Downloading common from repo oci://registry-1.docker.io/bitnamicharts
... dial tcp 67.230.169.182:443: connect: connection refused

# 1. 设置代理(当前 shell 有效),代理软件上要打开允许局域网连接,参考4.2中的图
[root@k8s-master01 zookeeper]# export HTTPS_PROXY=http://192.168.0.4:7897
[root@k8s-master01 zookeeper]# export HTTP_PROXY=http://192.168.0.4:7897
[root@k8s-master01 zookeeper]# export NO_PROXY=localhost,127.0.0.1,192.168.0.0/16,.svc,.cluster.local

# 2. 可选:先测一下能否访问 Docker Hub
[root@k8s-master01 zookeeper]# curl -I --connect-timeout 10 https://registry-1.docker.io/v2/
HTTP/1.1 200 Connection established

HTTP/2 401
date: Thu, 27 Aug 2026 08:38:58 GMT
content-type: application/json
content-length: 87
docker-distribution-api-version: registry/2.0
www-authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"
strict-transport-security: max-age=31536000

网络问题解决后在以此执行依赖安装和部署zk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 安装chart的依赖
helm dependency build

# 成功的信息如下
[root@k8s-master01 kafka]# helm dependency build
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "aliyun" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 2 charts
Downloading zookeeper from repo oci://registry-1.docker.io/bitnamicharts
Pulled: registry-1.docker.io/bitnamicharts/zookeeper:13.7.4
Digest: sha256:7b6aef19b1af10de823c3c0ca2ff0465fcc5db5c5e2c15b2ca1a207783688f99
Downloading common from repo oci://registry-1.docker.io/bitnamicharts
Pulled: registry-1.docker.io/bitnamicharts/common:2.30.0
Digest: sha256:b5c0e27c5404be46ccffdc6e1ce3d5d5e86ef5e5d372308f3f0afeaa3f34a5f7
Deleting outdated charts

# 语法检查
helm lint .

# 检查语法,0个失败
[root@k8s-master01 kafka]# helm lint .
==> Linting .

1 chart(s) linted, 0 chart(s) failed


# 安装kafka,注意所在目录charts/bitnami/kafka,在values.yaml所在的目录中执行
[root@k8s-master01 kafka]# helm install kafka . -n logging
NAME: kafka
LAST DEPLOYED: Sat Sep 5 20:29:36 2026
NAMESPACE: logging
STATUS: deployed
REVISION: 1
DESCRIPTION: Install complete
TEST SUITE: None
NOTES:
CHART NAME: kafka
CHART VERSION: 31.5.0
APP VERSION: 3.9.0

Did you know there are enterprise versions of the Bitnami catalog? For enhanced secure software supply chain features, unlimited pulls from Docker, LTS support, or application customization, see Bitnami Premium or Tanzu Application Catalog. See https://www.arrow.com/globalecs/na/vendors/bitnami for more information.

** Please be patient while the chart is being deployed **

Kafka can be accessed by consumers via port 9092 on the following DNS name from within your cluster:

kafka.logging.svc.cluster.local

Each Kafka broker can be accessed by producers via port 9092 on the following DNS name(s) from within your cluster:

kafka-broker-0.kafka-broker-headless.logging.svc.cluster.local:9092
kafka-broker-1.kafka-broker-headless.logging.svc.cluster.local:9092
kafka-broker-2.kafka-broker-headless.logging.svc.cluster.local:9092

The CLIENT listener for Kafka client connections from within your cluster have been configured with the following security settings:
- SASL authentication

To connect a client to your Kafka, you need to create the 'client.properties' configuration files with the content below:

security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
username="user1" \
password="$(kubectl get secret kafka-user-passwords --namespace logging -o jsonpath='{.data.client-passwords}' | base64 -d | cut -d , -f 1)";

To create a pod that you can use as a Kafka client run the following commands:

kubectl run kafka-client --restart='Never' --image registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kafka:3.9.0-debian-12-r12 --namespace logging --command -- sleep infinity
kubectl cp --namespace logging /path/to/client.properties kafka-client:/tmp/client.properties
kubectl exec --tty -i kafka-client --namespace logging -- bash

PRODUCER:
kafka-console-producer.sh \
--producer.config /tmp/client.properties \
--bootstrap-server kafka.logging.svc.cluster.local:9092 \
--topic test

CONSUMER:
kafka-console-consumer.sh \
--consumer.config /tmp/client.properties \
--bootstrap-server kafka.logging.svc.cluster.local:9092 \
--topic test \
--from-beginning

WARNING: There are "resources" sections in the chart not set. Using "resourcesPreset" is not recommended for production. For production installations, please set the following values according to your workload needs:
- broker.resources
+info https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/

⚠ SECURITY WARNING: Original containers have been substituted. This Helm chart was designed, tested, and validated on multiple platforms using a specific set of Bitnami and Tanzu Application Catalog containers. Substituting other containers is likely to cause degraded security and performance, broken chart features, and missing environment variables.

Substituted images detected:
- registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kafka:3.9.0-debian-12-r12
- registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kubectl:1.32.2-debian-12-r3
- registry.cn-beijing.aliyuncs.com/k8s-liujunwei/os-shell:12-debian-12-r39
- registry.cn-beijing.aliyuncs.com/k8s-liujunwei/jmx-exporter:1.1.0-debian-12-r7

⚠ SECURITY WARNING: Verifying original container images was skipped. Please note this Helm chart was designed, tested, and validated on multiple platforms using a specific set of Bitnami and Tanzu Application Catalog containers. Substituting other containers is likely to cause degraded security and performance, broken chart features, and missingenvironment variables.

6.在k8s中部署Logstash集群

Logstash — 日志处理器(Processor)

  • 核心职责
    • 从 Kafka 消费日志数据
    • 解析与转换: Grok 正则提取字段、JSON 解析、时间格式化
    • 过滤与富化: 删除敏感字段、GeoIP 查询、关联 CMDB 信息
    • 路由: 根据条件将不同日志发送到不同的 ES Index
    • 批量写入 Elasticsearch
  • vs Filebeat: Filebeat 只做简单采集,Logstash 做重型 ETL 处理

6.1部署前检查工作

确认ES是green状态,kafka都是READY,以及有Logstash CRD和存储storageclasses

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# es集群式green状态
[root@k8s-master01 kafka]# kubectl get elasticsearch es-cluster -n logging
NAME HEALTH NODES VERSION PHASE AGE
es-cluster green 3 9.5.2 Ready 4d8h


# kafka|zookeeper|es-cluster 的pod都是READY
[root@k8s-master01 kafka]# kubectl get po -n logging
NAME READY STATUS RESTARTS AGE
es-cluster-es-default-0 1/1 Running 0 4d8h
es-cluster-es-default-1 1/1 Running 0 4d8h
es-cluster-es-default-2 1/1 Running 0 4d8h
kafka-broker-0 1/1 Running 0 46h
kafka-broker-1 1/1 Running 0 46h
kafka-broker-2 1/1 Running 0 46h
kibana-kb-b889dbf5f-8vx2n 1/1 Running 0 4d3h
kibana-kb-b889dbf5f-vw8d6 1/1 Running 0 4d3h
zookeeper-0 1/1 Running 0 2d23h
zookeeper-1 1/1 Running 0 2d23h
zookeeper-2 1/1 Running 0 2d23h

#存储
[root@k8s-master01 kafka]# kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
cfs-sc (default) csi.cubefs.com Delete Immediate true 244d
nfs-csi nfs.csi.k8s.io Delete Immediate true 259d

# Logstash的CRD也有
[root@k8s-master01 kafka]# kubectl get crd logstashes.logstash.k8s.elastic.co
NAME CREATED AT
logstashes.logstash.k8s.elastic.co 2026-08-24T07:38:30Z

6.2取出 Kafka 密码并用 Secret保存

在创建好kafka服务时会给信息提示密码保存的位置,在 logging命名空间中名字时kafka-user-passwords 的secret中:

1
2
username="user1" \
password="$(kubectl get secret kafka-user-passwords --namespace logging -o jsonpath='{.data.client-passwords}' | base64 -d | cut -d , -f 1)";

获取密码并解密,两条命令都可以(常见 key:client-passwords)

1
2
3
4
5
6
[root@k8s-master01 kafka]# password="$(kubectl get secret kafka-user-passwords --namespace logging -o jsonpath='{.data.client-passwords}' | base64 -d | cut -d , -f 1)"
[root@k8s-master01 kafka]# echo $password
pCdVrGJ1kq

[root@k8s-master01 kafka]# kubectl get secrets -n logging kafka-user-passwords -o jsonpath='{.data.client-passwords}' | base64 -d ; echo
pCdVrGJ1kq

创建给 Logstash 用的 Secret(把密码换成自己查到的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 可以通过yaml文件创建,也可以通过一条命令创建,二选一即可
# yaml文件配置如下:
apiVersion: v1
data:
kafka-password: Yzk4dm9BdVFOSA==
kafka-username: dXNlcjE=
kind: Secret
metadata:
creationTimestamp: null
name: logstash-credentials
namespace: logging

# 一条命令也能创建
[root@k8s-master01 kafka]# kubectl create secret generic logstash-credentials -n logging --from-literal=kafka-username=user1 --from-literal=kafka-password='pCdVrGJ1kq' --dry-run=client -o yaml | kubectl apply -f -
secret/logstash-credentials created

ES 的 elastic 密码不要抄进 YAML,直接引用已有 Secret:

  • 名:es-cluster-es-elastic-user

  • key:elastic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@k8s-master01 kafka]# kubectl get secrets -n logging es-cluster-es-elastic-user -oyaml
apiVersion: v1
data:
elastic: cW4zekFYQm1wa1JHSE5zTDczVjhoR2p2
kind: Secret
metadata:
creationTimestamp: "2026-08-25T06:19:27Z"
labels:
common.k8s.elastic.co/type: elasticsearch
eck.k8s.elastic.co/credentials: "true"
eck.k8s.elastic.co/owner-kind: Elasticsearch
eck.k8s.elastic.co/owner-name: es-cluster
eck.k8s.elastic.co/owner-namespace: logging
eck.k8s.elastic.co/watched: "true"
elasticsearch.k8s.elastic.co/cluster-name: es-cluster
name: es-cluster-es-elastic-user
namespace: logging
resourceVersion: "88015959"
uid: 0270776a-2384-49a9-98e1-751048f715c8
type: Opaque

6.3提前创建Topic

建议自己提前创建Topic,也可以不提前创建,kafka会自动创建

启动一个临时的pod手动创建一个Topic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 启动临时pod
[root@k8s-master01 kafka]# kubectl run kafka-client -n logging --rm -it --restart=Never \
--image registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kafka:3.9.0-debian-12-r12 \
-- bash
If you don't see a command prompt, try pressing enter.

I have no name!@kafka-client:/$ BS=kafka.logging.svc.cluster.local:9092
I have no name!@kafka-client:/$ cat > /tmp/client.properties << 'EOF'
security.protocol=SASL_PLAINTEXT
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="user1" password="pCdVrGJ1kq";
EOF

-rw-r--r-- 1 1001 root 176 Aug 29 15:44 /tmp/client.properties
I have no name!@kafka-client:/$ cat /tmp/client.properties
security.protocol=SASL_PLAINTEXT
sasl.mechanism=PLAIN
sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="user1" password="pCdVrGJ1kq";

I have no name!@kafka-client:/$ kafka-topics.sh --bootstrap-server $BS \
--command-config /tmp/client.properties \
--create --topic k8spodlogs \
--partitions 3 \
--replication-factor 3 \
--if-not-exists
Picked up JAVA_TOOL_OPTIONS:
Created topic k8spodlogs.

# 查看创建的Topic
I have no name!@kafka-client:/$ kafka-topics.sh --bootstrap-server $BS \
--command-config /tmp/client.properties \
--describe --topic k8spodlogs
Picked up JAVA_TOOL_OPTIONS:
Topic: k8spodlogs TopicId: _wjfR3laTJCWZ9CtITWk9A PartitionCount: 3 ReplicationFactor: 3 Configs: min.insync.replicas=2
Topic: k8spodlogs Partition: 0 Leader: 102 Replicas: 102,100,101 Isr: 102,100,101 Elr: N/A LastKnownElr: N/A
Topic: k8spodlogs Partition: 1 Leader: 101 Replicas: 101,102,100 Isr: 101,102,100 Elr: N/A LastKnownElr: N/A
Topic: k8spodlogs Partition: 2 Leader: 100 Replicas: 100,101,102 Isr: 100,101,102 Elr: N/A LastKnownElr: N/A

6.4准备Logstash镜像

可能由于网络的原因无法拉取ECK 拉官方镜像,我已经提前同步到我的仓库中,Logstash版本要和 ES 对齐 9.5.2

6.5准备Logstash资源文件

创建 Logstash.yaml。

说明:ECK 3.5 对字段名可能是 elasticsearchRef 或 elasticsearchRefs。若 apply 报 unknown field,执行
kubectl explain logstash.spec | grep -i elastic
按实际字段改「关联 ES」那一段即可,其余不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# Logstash.yaml配置如下:
[root@k8s-master01 kafka]# vim Logstash.yaml
# 生产向 Logstash:读 Kafka(k8spodlogs) → 写 ES(es-cluster)
apiVersion: logstash.k8s.elastic.co/v1alpha1
kind: Logstash
metadata:
name: logstash
namespace: logging
spec:
# Logstash版本必须与 ES 版本一致
version: 9.5.2
# 私有镜像,如果能拉取官方仓库可以省略image参数
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/logstash:9.5.2

# 高可用:3 副本;同一 Kafka group 下分摊分区
count: 3

# 关联 ECK 管理的 ES(名称必须是 Elasticsearch CR 的 name)
# 若 CRD 要求单数,改为:
# elasticsearchRef:
# name: es-cluster
elasticsearchRefs:
- name: es-cluster #关联到 Elasticsearch 资源的 metadata.name字段
clusterName: es-cluster # 部分版本需要;explain 里有再写。这是自定义一个别名,用于在 Logstash Pipeline 配置中引用这个 ES 集群。它不需要与任何 K8s 资源名称匹配。
namespace: logging #如果 ES 和 Logstash 在不同命名空间 → 必须填写 ES 所在的命名空间,如果在同一个命名空间,可以省略
# 内联 pipeline(生产也可改成 ConfigMap,便于单独变更配置)
pipelines:
- pipeline.id: main
config.string: |
input {
kafka {
# Kafka 集群初始连接地址(host:port)。客户端通过它发现整个集群的 broker 列表。
# 生产环境优先使用 K8s 内部 DNS(如 kafka.logging.svc.cluster.local:9092)。
bootstrap_servers => "kafka.logging.svc.cluster.local:9092"
# 要消费的 Kafka 主题列表。支持多个主题,用数组表示。
# 按业务拆分主题可提升隔离性与扩展性。
topics => ["k8spodlogs"] # 指定消费的 Kafka topic
group_id => "logstash-logging-prod" # 消费者组名,用于分摊分区,自定义一个名字即可
codec => json #消息解码方式

# 单个 Logstash 实例内并行消费的线程数。
# 重要约束:所有 Logstash Pod 的副本数 × consumer_threads ≤ topic 分区数。
# 超出部分的线程拿不到分区,会空转浪费资源。
# 当前 topic 分区=3,Pod 副本=3,因此设为 1 最合理。
consumer_threads => 1
enable_auto_commit => true #是否自动提交消费位移(offset) true表示自动提交offset;
auto_commit_interval_ms => "1000" # 每隔1秒提交一次offset,生产常用 1000~5000
auto_offset_reset => "earliest" #无有效 offset 时从哪里开始消费,earliest表示从头开始消费(适合新组/重跑);latest:只消费启动之后产生的新消息(生产日常常用,避免历史积压)。

#客户端与 Kafka 之间使用的安全协议。
# PLAINTEXT:明文,无加密、无认证
# SASL_PLAINTEXT:有 SASL 认证,传输不加密(不走 TLS)
# SASL_SSL,SASL :认证 + TLS 加密
# SSL:仅 TLS,通常配合证书认证
security_protocol => "SASL_PLAINTEXT" #启用 SASL 认证但不加密传输(内网常用), 必须与 Kafka 服务端配置保持一致。
# SASL 认证机制(怎么验证用户名密码)。
# PLAIN用户名密码几乎明文方式交给服务端(需配合 TLS 才相对安全)
# SCRAM-SHA-256密码经 SCRAM 挑战应答,不直接传原始密码,
# 用 SHA-256SCRAM-SHA-512同上,哈希为 SHA-512
# GSSAPIKerberos
sasl_mechanism => "SCRAM-SHA-256"
# JAAS 登录配置字符串。用于告诉 Kafka 客户端如何用用户名密码登录。账号密码来自环境变量(由 Secret 注入),禁止写死密码
sasl_jaas_config => 'org.apache.kafka.common.security.scram.ScramLoginModule required username="${KAFKA_USERNAME}" password="${KAFKA_PASSWORD}";'
}
}

output {
elasticsearch {
# Elasticsearch 节点地址列表。支持多个节点,写数组可提高可用性。
# 生产环境使用 ECK 或自建集群的内部服务名(带 https)。
hosts => ["https://es-cluster-es-http:9200"]
#写入的目标索引名。支持日期模板 %{+YYYY.MM.dd},实现按天滚动索引。
index => "k8spodlogs-%{+YYYY.MM.dd}"
#Elasticsearch 基本认证用户名。建议通过环境变量注入,禁止明文。
user => "${ELASTICSEARCH_USERNAME}"
# Elasticsearch 基本认证密码。建议通过环境变量注入,禁止明文。
password => "${ELASTICSEARCH_PASSWORD}"
#是否启用 HTTPS 连接。生产环境必须设为 true。
ssl_enabled => true
# 用于验证 Elasticsearch 服务端证书的 CA 证书路径。
# ECK 部署时通常由 elasticsearchRefs 自动注入对应环境变量。
# 如果变量名不对,可进入logstash的 Pod 中执行 env | grep -i ssl 查看实际名称。
ssl_certificate_authorities => "${ES_CLUSTER_ES_SSL_CERTIFICATE_AUTHORITY}"
}
}
# 定义pod模板(可选)
podTemplate:
spec:
containers:
- name: logstash
resources:
# 学习用偏小;不够再加大(你说了后面会自己扩)
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "2Gi"
env:
- name: KAFKA_USERNAME
valueFrom:
secretKeyRef:
name: logstash-credentials #这里是6.2中创建的secret名称
key: kafka-username # 6.2中创建的secret中的key,把kafka-username这个key的值赋值给KAFKA_USERNAME变量
- name: KAFKA_PASSWORD #同上,定义KAFKA_PASSWORD变量,值是从logstash-credentials secret中获取的kafka-password的值
valueFrom:
secretKeyRef:
name: logstash-credentials
key: kafka-password
- name: ELASTICSEARCH_USERNAME #定义一个环境变量,用来保存Elasticsearch 用户名
value: "elastic" #Elasticsearch 用户名的值
- name: ELASTICSEARCH_PASSWORD ##定义一个环境变量,用来保存Elasticsearch 用户的密码
valueFrom: #密码的值来自名字是es-cluster-es-elastic-user的secret中的elastic key的值
secretKeyRef:
name: es-cluster-es-elastic-user
key: elastic
# 堆约为容器内存一半量级,避免 OOM,可根据需要调整
# - name: LS_JAVA_OPTS
# value: "-Xms1g -Xmx1g"
# 可选:与 broker 打散(节点少时 soft 即可)
# affinity:
# podAntiAffinity:
# preferredDuringSchedulingIgnoredDuringExecution:
# - weight: 100
# podAffinityTerm:
# labelSelector:
# matchLabels:
# logstash.k8s.elastic.co/name: logstash
# topologyKey: kubernetes.io/hostname

# 数据目录持久化(含队列),CubeFS RWO 20Gi
volumeClaimTemplates:
- metadata:
name: logstash-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: cfs-sc
resources:
requests:
storage: 20Gi

创建logstash资源:

1
2
[root@k8s-master01 29-eck]# kubectl create -f Logstash.yaml
logstash.logstash.k8s.elastic.co/logstash created

查看服务创建情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@k8s-master01 29-eck]# kubectl get logstashes.logstash.k8s.elastic.co -n logging
NAME HEALTH AVAILABLE EXPECTED AGE VERSION
logstash red 3 3m57s 9.5.2


[root@k8s-master01 29-eck]# kubectl get po -n logging |grep log
logstash-ls-0 0/1 Init:0/1 0 92s
logstash-ls-1 0/1 Init:0/1 0 92s
logstash-ls-2 0/1 Init:0/1 0 92s


# 过了大概4分钟左右服务正常启动
[root@k8s-master01 29-eck]# kubectl get logstashes.logstash.k8s.elastic.co -n logging
NAME HEALTH AVAILABLE EXPECTED AGE VERSION
logstash green 3 3 4m29s 9.5.2

[root@k8s-master01 29-eck]# kubectl get po -n logging |grep log
logstash-ls-0 1/1 Running 0 4m35s
logstash-ls-1 1/1 Running 0 4m35s
logstash-ls-2 1/1 Running 0 4m35s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
4.NetworkPolicy是什么意思?我不明白
10.按照你的建议来
11.索引命名规范你自己决定
12.我没有配置ILM
14.协议是SASL_PLAINTEXT
15.用户名密码写到secret
16.日志 topic 正式名称必须要提前创建吗?我没创建过
17.分区数和副本数是什么意思?有什么用?必须配置吗? 我没有事先配置过
18.消费组命名规范你自己决定,符合生产即可
22.持久化 queue(防重启丢数据)是什么意思?生产建议开启那就配置开启
26.你看着配置
27.PDB是什么?按照你建议的配置
30.日志应该是从Filebeat来,因为课程的文档后面还会部署Filebeat
31到34按照你的建议来即可,但是最好要符合生产的要求
我之前没接触过这块的只是,还希望你详细的讲解每一个步骤,并解释其原因和过程

7.在k8s中部署Filebeat收集日志

Filebeat — 日志采集器(Shipper)

  • 部署方式: DaemonSet(每个节点一个 Pod)
  • 核心职责
    • 监控 /var/log/containers/*.log(即你之前问的那个目录)
    • 读取容器日志,添加 K8s 元数据(Pod名、Namespace、Node、Labels等)
    • 将日志轻量级转发到 Kafka
  • 特点: 资源占用极低,不做复杂处理,只做”搬运工”

Filebeat 使用的镜像版本必须与 Elasticsearch 版本一致。

7.1准备ServiceAccount和RBAC

Filebeat 需要提前准备 ServiceAccount (SA) 和 RBAC,因为它作为 DaemonSet 运行在每个节点上,需要访问 Kubernetes API Server 来获取 Pod/容器的元数据信息。

Filebeat 采集的是 /var/log/containers/*.log 中的原始日志,但这些日志文件本身不包含 K8s 上下文信息。为了让日志可查询、可过滤,Filebeat 需要通过 K8s API 自动为每条日志补充元数据,所以就需要相应的RBAC。

创建sa以及rbac资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
[root@k8s-master01 29-eck]# cat Filebeat-sa-rbac.yaml
# 1. ServiceAccount(Filebeat 需要读取 K8s API 获取 Pod 元数据)
# ------------------------------------------------------------
apiVersion: v1
kind: ServiceAccount
metadata:
name: filebeat #定义 SA 的名称,后面 DaemonSet 会引用
namespace: logging # 必须与 Beat 资源同一命名空间
labels:
app: filebeat
k8s-app: filebeat

---
# ------------------------------------------------------------
# 2. ClusterRole(最小权限原则,仅授予 Filebeat 需要的资源)
# ------------------------------------------------------------
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: filebeat
labels:
app: filebeat
rules:
# 核心资源:获取 Pod/Node/Namespace 信息用于 add_kubernetes_metadata
- apiGroups: [""]
resources:
- namespaces
- pods
- nodes
- nodes/metrics # 部分场景需要
- nodes/stats
- nodes/proxy
- nodes/spec
- nodes/logs
verbs: ["get", "list", "watch"]
# apps 组:ReplicaSet 用于推断 Deployment 归属
- apiGroups: ["apps"]
resources:
- replicasets
- deployments
- statefulsets
- daemonsets
verbs: ["get", "list", "watch"]
# batch 组:Job/CronJob 元数据
- apiGroups: ["batch"]
resources:
- jobs
- cronjobs
verbs: ["get", "list", "watch"]
# 可选:如果需要收集 Event
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "list", "watch"]

---
# ------------------------------------------------------------
# 3. ClusterRoleBinding(把 ClusterRole 绑定到 ServiceAccount)
# ------------------------------------------------------------
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: filebeat
labels:
app: filebeat
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: filebeat # 对应上面的 ClusterRole
subjects:
- kind: ServiceAccount
name: filebeat
namespace: logging # 必须与 SA 命名空间一致

创建上述资源:

1
2
3
4
[root@k8s-master01 29-eck]# kubectl create -f Filebeat-sa-rbac.yaml
serviceaccount/filebeat created
clusterrole.rbac.authorization.k8s.io/filebeat created
clusterrolebinding.rbac.authorization.k8s.io/filebeat created

7.2准备Filebeat资源部署清单

准备filebeat资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
[root@k8s-master01 29-eck]# cat Filebeat.yaml
# Beat 资源
apiVersion: beat.k8s.elastic.co/v1beta1
kind: Beat
metadata:
name: filebeat # 资源名称,生成的 DaemonSet 会叫 filebeat-beat
namespace: logging
labels:
app: filebeat
spec:
# ---------- 基础信息 ----------
type: filebeat # 必须指定 Beat 类型
version: "9.5.2" # ★ 请改成与自己 的 ES 版本一致
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/filebeat:9.5.2 # ★ 请改成与自己的 ES 版本一致

# ----------# Filebeat 配置,相当于传统部署中的 filebeat.yml----------
config:
# 全局设置
name: "filebeat-${NODE_NAME}" # 每个节点实例名称(便于区分)
# # Filebeat 自身日志,日志级别:生产建议 info 或 warning,排查问题时改 debug
logging.level: info
logging.to_stderr: true # 日志输出到 stderr,方便 kubectl logs 查看
logging.metrics.enabled: false # 关闭 Filebeat 自身 metrics 日志(减少噪音)

# 输入配置(采集日志)
# 输入:Autodiscover + filestream(生产推荐)
filebeat.autodiscover:
providers:
- type: kubernetes
node: ${NODE_NAME} # 只处理本节点 Pod(必须配合 hostNetwork)
hints.enabled: true # 开启 hints 机制(允许 Pod 使用 annotation 控制 Filebeat不采集自己的日志,如:co.elastic.logs/enabled: "false")
hints.default_config:
type: filestream
id: "kubernetes-container-${data.kubernetes.container.id}"
paths:
- /var/log/containers/*${data.kubernetes.container.id}.log
parsers:
- container:
stream: all
format: auto
prospector.scanner:
fingerprint.enabled: true
symlinks: true
file_identity.fingerprint: {}
# Kubernetes 日志轮转/删除时,不立即关闭文件
# 防止尾部日志还未读完就丢失
close.on_state_change.removed: false
# 忽略过旧日志,防止重启后全量重发
ignore_older: 72h
close.on_state_change.inactive: 5m
# 清理 registry 中超过 96 小时的旧状态
# 必须大于 ignore_older + scanner.check_interval(默认10秒)
clean_inactive: 96h
fields:
log_topic: k8spodlogs # 如需动态 topic 可配合使用
cluster: production
fields_under_root: false

# ========================================================
# 处理器(丰富元数据 + 过滤)
# ========================================================
processors:
# 添加 Kubernetes 元数据(namespace、pod、container、labels、annotations 等),依赖已创建的RBAC
# - add_kubernetes_metadata: #待确认
# host: ${NODE_NAME} # 指定节点,避免跨节点查询
# default_indexers.enabled: true
# default_matchers.enabled: true

# 添加主机元数据(主机名、IP、OS 等)
- add_host_metadata:
when.not.contains.tags: forwarded
netinfo.enabled: true # 包含网络接口信息
# 生产必须:丢弃 Filebeat 自己的日志,防止循环采集
- drop_event:
when:
equals:
kubernetes.container.name: "filebeat"

# 输出配置(生产推荐走 Kafka 做缓冲)
# ---------- 输出到 Kafka(推荐)----------
output.kafka:
# Kafka broker 地址(请改成你实际的 Service 名称)
# 常见命名:kafka-broker-0.kafka-broker.logging.svc:9092 或 kafka.logging.svc:9092
hosts:
- "kafka.logging.svc.cluster.local:9092"
# Topic 名称(Logstash 会从这个 Topic 消费)
topic: "k8spodlogs" #把日志输出到名字是k8spodlogs的Topic
# 分区策略
# partition.round_robin:
# reachable_only: true # 只向可达的 broker 写,官方默认是false,生产也推荐用false
# 可靠性
required_acks: 1 # 0=不等待 1=leader确认 -1=所有ISR确认(最可靠但慢)
compression: gzip # 压缩算法:none / gzip / snappy / lz4 / zstd
max_message_bytes: 1000000 # 单条消息最大字节,Filebeat 的max_message_bytes <= Kafka 的broker message.max.bytes

# ---------- SASL 认证(你的环境必须配置)----------
username: "user1"
password: "${KAFKA_PASSWORD}" # 通过环境变量注入,禁止明文写在 YAML
sasl.mechanism: "SCRAM-SHA-256"
# ========================================================
# 队列与性能调优(生产关键)
# ========================================================
queue.mem:
events: 4096 # 内存队列事件数(默认 4096)
flush.min_events: 512 # 批量刷新最小事件数
flush.timeout: 5s # 最长等待时间

# ========================================================
# HTTP 监控端点(可选,用于 Prometheus 抓取)
# ========================================================
http.enabled: true
http.host: localhost
http.port: 5066

# ---------- 部署模型:DaemonSet(每个节点一个实例)----------
daemonSet:
updateStrategy: # 可选滚动更新策略
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
podTemplate:
metadata:
labels:
app: filebeat
k8s-app: filebeat
annotations:
co.elastic.logs/enabled: "false" # 不收集filebeat自己的日志(可选)
spec:
# ---------- 服务账号,必须提前创建好该资源 ----------
serviceAccountName: filebeat
automountServiceAccountToken: true # 必须 true,才能访问 K8s API

# ---------- 网络 ----------
hostNetwork: true # 使用宿主机网络(获取更准确的主机元数据,且避免端口冲突)
dnsPolicy: ClusterFirstWithHostNet # 配合 hostNetwork 使用

# ---------- 安全上下文 ----------
securityContext:
runAsUser: 0 # Filebeat 需要 root 权限读取 /var/log


# 允许调度到控制平面节点(如需采集 master 日志)
# tolerations:
# - key: node-role.kubernetes.io/control-plane
# operator: Exists
# effect: NoSchedule
# - key: node-role.kubernetes.io/master
# operator: Exists
# effect: NoSchedule
# - key: node.kubernetes.io/not-ready
# operator: Exists
# effect: NoExecute
# tolerationSeconds: 300
# - key: node.kubernetes.io/unreachable
# operator: Exists
# effect: NoExecute
# tolerationSeconds: 300
# ---------- 终止宽限期 ----------
terminationGracePeriodSeconds: 30

containers:
- name: filebeat # 容器名必须叫 filebeat(ECK 约定)
# 资源限制(生产根据实际日志量调整)
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 1000m
memory: 1Gi
# 环境变量
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName # 注入当前节点名,供配置使用
- name: KAFKA_PASSWORD
valueFrom:
secretKeyRef:
name: kafka-user-passwords
key: client-passwords


# 健康检查(可选)
# livenessProbe:
# httpGet:
# path: /
# port: 5066
# initialDelaySeconds: 30
# periodSeconds: 10
# readinessProbe:
# httpGet:
# path: /
# port: 5066
# initialDelaySeconds: 10
# periodSeconds: 10
volumeMounts:
# 宿主机日志目录(必须)
- name: varlogcontainers
mountPath: /var/log/containers

- name: varlogpods
mountPath: /var/log/pods

- name: varlibdockercontainers
mountPath: /var/lib/docker/containers

# Filebeat 数据目录(registry 状态文件,必须持久)
- name: data
mountPath: /usr/share/filebeat/data
# 如果使用自定义证书
# - name: kafka-certs
# mountPath: /etc/ssl/certs
# readOnly: true

volumes:
- name: varlogcontainers
hostPath:
path: /var/log/containers
- name: varlogpods
hostPath:
path: /var/log/pods
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers


# 数据目录建议用 hostPath(每个节点独立,避免多 Pod 竞争)
- name: data
hostPath:
path: /var/lib/filebeat-data # 宿主机上的持久目录
type: DirectoryOrCreate

创建filebeat:

1
2
[root@k8s-master01 29-eck]# kubectl create -f Filebeat.yaml
beat.beat.k8s.elastic.co/filebeat created

查看资源:

1
2
3
4
5
6
7
8
#每个节点上都运行了一个filebeat服务
[root@k8s-master01 29-eck]# kubectl get po -n logging -owide|grep filebeat-beat
filebeat-beat-filebeat-26qsh 1/1 Running 0 24m 192.168.0.85 k8s-node02 <none> <none>
filebeat-beat-filebeat-9c8nj 1/1 Running 0 24m 192.168.0.84 k8s-node01 <none> <none>
filebeat-beat-filebeat-kxp28 1/1 Running 0 24m 192.168.0.83 k8s-master03 <none> <none>
filebeat-beat-filebeat-mrhcm 1/1 Running 0 24m 192.168.0.86 k8s-node03 <none> <none>
filebeat-beat-filebeat-rbw5x 1/1 Running 0 24m 192.168.0.81 k8s-master01 <none> <none>
filebeat-beat-filebeat-smgfm 1/1 Running 0 24m 192.168.0.82 k8s-master02 <none> <none>

8.在kibana中查询日志

8.1通过web访问kibana

1
2
3
4
5
6
7
8
9
10
11
12
13
# kibana-kb-http 是web端访问的SVC
[root@k8s-master01 29-eck]# kubectl get svc -n logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
es-cluster-es-default ClusterIP None <none> 9200/TCP 7d5h
es-cluster-es-http ClusterIP 10.96.244.191 <none> 9200/TCP 7d3h
es-cluster-es-internal-http ClusterIP 10.96.42.43 <none> 9200/TCP 7d5h
es-cluster-es-transport ClusterIP None <none> 9300/TCP 7d5h
kafka ClusterIP 10.96.92.118 <none> 9092/TCP 4d19h
kafka-broker-headless ClusterIP None <none> 9094/TCP,9092/TCP 4d19h
kibana-kb-http NodePort 10.96.17.95 <none> 5601:30682/TCP 7d
logstash-ls-api ClusterIP None <none> 9600/TCP 2d5h
zookeeper ClusterIP 10.96.62.2 <none> 2181/TCP,2888/TCP,3888/TCP 5d20h
zookeeper-headless ClusterIP None <none> 2181/TCP,2888/TCP,3888/TCP 5d20h

这里登录用的账号和Elasticsearch 的用户密码一致(获取方式见2.1)

用户是:elastic

密码是:qn3zAXBmpkRGHNsL73V8hGjv

8.2验证整条链路

检查日志是否进入Elasticsearch,按照下图操作并检查是否有索引:

左侧 Management(管理) → Stack Management → Index Management(索引管理)

点击【索引管理】,能看到 数据索引、且 docs.count(文档计数) > 0,说明链路通了。

8.3创建数据视图

创建数据视图

名称:Kibana 里显示的数据视图名称,只是一个友好名称,可以自定义

索引模式:指定这个数据视图要匹配哪些 Elasticsearch 索引

其他参考图中配置即可。

8.4通过Discover看日志

在 Discover 里看日志

在搜索字段名称中根据自己的需要输入字段,message字段是真正的日志 正文