ELK / OpenSearch trên Kubernetes
Tiếp đi: chạy ELK/OpenSearch ngay trong cluster Kubernetes cho order và payment. Mình cài bằng Deployment như mấy service khác được không?
Không nên. Elasticsearch/OpenSearch là hệ thống stateful, có cụm, có shard; Deployment không biết gì về chuyện đó. Bài này chỉ nói phần Kubernetes: cài bằng operator, đẩy log container vào, mở Kibana, chọn storage và resource. Khái niệm ELK chung nằm ở chương Observability:
- Tổng quan ELK, Elasticsearch vs OpenSearch: bài 19.7
- Index, shard, mapping, cluster health: bài 19.8
- Filebeat, Fluent Bit, Logstash, Kafka: bài 19.9
- Kibana / OpenSearch Dashboards: bài 19.10
- ILM/ISM, sizing, snapshot, security: bài 19.11
- ELK, Loki hay cloud logging: bài 19.12
Phần thu log trên node (đọc /var/log/containers bằng DaemonSet) giống bài 17.10, chỉ đổi đích đến.
1. Bài toán
shop-app có order-service và payment-service trên Kubernetes. Team muốn tìm log full-text bằng Kibana, giữ 14 ngày, khoảng 50GB log/ngày, và muốn tự chạy trong cluster thay vì trả tiền managed service.
2. Vấn đề
- Deployment cho Pod tên ngẫu nhiên, không có volume riêng ổn định; mỗi node Elasticsearch cần danh tính cố định và disk riêng.
- Rolling update phải theo thứ tự: chờ cluster
greenrồi mới restart node tiếp theo, nếu không có thể mất cả primary lẫn replica của một shard. - TLS giữa các node, user/password, vai trò master/data: viết YAML tay rất dài và dễ sai.
3. Giải pháp: operator
Operator = controller tự viết (bài 17.1) hiểu nghiệp vụ của Elasticsearch. Bạn khai báo CRD "muốn cluster 3 node", operator tạo StatefulSet, Service, Secret, cert và lo upgrade an toàn.
| Operator | CRD chính | Ghi chú |
|---|---|---|
| ECK (Elastic Cloud on Kubernetes) | Elasticsearch, Kibana, Beat, Agent, Logstash | Của Elastic; tự bật TLS, tạo user elastic trong Secret |
| OpenSearch Kubernetes Operator | OpenSearchCluster (kèm Dashboards), OpenSearchISMPolicy, OpenSearchRole | Của cộng đồng OpenSearch; node pool theo role |
4. Code từng bước
Bước 1: cài operator bằng Helm (bài 17.8):
helm repo add elastic https://helm.elastic.co
helm upgrade --install elastic-operator elastic/eck-operator -n elastic-system --create-namespace
# OpenSearch:
# helm repo add opensearch-operator https://opensearch-project.github.io/opensearch-k8s-operator/
# helm upgrade --install opensearch-operator opensearch-operator/opensearch-operator -n opensearch-system --create-namespace
Bước 2: StorageClass và cluster Elasticsearch:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: es-ssd }
provisioner: ebs.csi.aws.com
parameters: { type: gp3 }
volumeBindingMode: WaitForFirstConsumer # tạo disk đúng zone với Pod
allowVolumeExpansion: true
reclaimPolicy: Retain # xóa nhầm CR cũng không mất disk
---
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata: { name: logs, namespace: logging }
spec:
version: 8.15.0
nodeSets:
- name: master
count: 3
config: { node.roles: ["master"] }
podTemplate:
spec:
containers:
- name: elasticsearch
resources:
requests: { cpu: "500m", memory: 2Gi }
limits: { memory: 2Gi }
volumeClaimTemplates:
- metadata: { name: elasticsearch-data }
spec:
accessModes: [ReadWriteOnce]
storageClassName: es-ssd
resources: { requests: { storage: 10Gi } }
- name: data
count: 3
config: { node.roles: ["data", "ingest"] }
podTemplate:
spec:
tolerations:
- { key: dedicated, value: elasticsearch, effect: NoSchedule }
nodeSelector: { pool: elasticsearch }
containers:
- name: elasticsearch
resources:
requests: { cpu: "2", memory: 8Gi }
limits: { memory: 8Gi } # ES 8 tự đặt heap ≈ 50% memory limit
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
elasticsearch.k8s.elastic.co/statefulset-name: logs-es-data
volumeClaimTemplates:
- metadata: { name: elasticsearch-data }
spec:
accessModes: [ReadWriteOnce]
storageClassName: es-ssd
resources: { requests: { storage: 500Gi } }
- Memory request = limit (QoS Guaranteed, bài 17.6): ES không bị evict khi node thiếu RAM. Không đặt CPU limit để tránh throttling lúc merge segment.
- Không cần chỉnh
-Xmxtay: ES 8 đọc memory limit của container. Phần RAM còn lại dành cho page cache. - Data node rải đều các zone và chạy trên node pool riêng có taint, để app không tranh RAM với ES.
- Sizing: 50GB/ngày × 14 ngày × 2 (replica) ≈ 1.4TB, cộng 30% dư → 3 data node × 500Gi. Chi tiết shard và sizing ở bài 19.11.
vm.max_map_count: ES cần 262144 trên node; đặt bằng init container/DaemonSet sysctl, hoặcnode.store.allow_mmap: falsenếu không được đụng vào node.
Bước 3: Kibana qua Ingress:
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata: { name: logs, namespace: logging }
spec:
version: 8.15.0
count: 2
elasticsearchRef: { name: logs }
http:
tls: { selfSignedCertificate: { disabled: true } } # TLS kết thúc ở Ingress
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kibana
namespace: logging
annotations:
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8"
spec:
ingressClassName: nginx
tls: [ { hosts: [kibana.shop.internal], secretName: kibana-tls } ]
rules:
- host: kibana.shop.internal
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: logs-kb-http, port: { number: 5601 } } }
Bước 4: Fluent Bit DaemonSet gửi log container. Phần input/filter giống bài 17.10; chỉ khác output, CA của ECK và credential lấy từ Secret:
# values-fluent-bit.yaml (chart fluent/fluent-bit)
tolerations: [ { operator: Exists } ] # chạy cả trên node có taint
config:
outputs: |
[OUTPUT]
Name es
Match kube.*
Host logs-es-http.logging.svc
Port 9200
tls On
tls.ca_file /es-ca/ca.crt
HTTP_User ${ES_USER}
HTTP_Passwd ${ES_PASSWORD}
Logstash_Format On
Logstash_Prefix logs-shop
Suppress_Type_Name On
env:
- name: ES_USER
valueFrom: { secretKeyRef: { name: fluent-bit-es-writer, key: username } }
- name: ES_PASSWORD
valueFrom: { secretKeyRef: { name: fluent-bit-es-writer, key: password } }
extraVolumes:
- name: es-ca
secret: { secretName: logs-es-http-certs-public }
extraVolumeMounts:
- { name: es-ca, mountPath: /es-ca, readOnly: true }
User fluent-bit-es-writer chỉ có quyền ghi index logs-shop-*; đừng dùng user elastic (superuser). Buffer, Kafka, parse log: bài 19.9.
Bước 5: kiểm tra:
kubectl get elasticsearch -n logging
# NAME HEALTH NODES VERSION PHASE AGE
# logs green 6 8.15.0 Ready 12m
kubectl get pvc -n logging
ES_PASSWORD=$(kubectl get secret logs-es-elastic-user -n logging -o jsonpath='{.data.elastic}' | base64 -d)
kubectl port-forward -n logging svc/logs-es-http 9200
curl -sk -u "elastic:$ES_PASSWORD" "https://localhost:9200/_cat/indices/logs-shop-*?v"
5. Cách hoạt động
kubectl apply Elasticsearch CR
│
▼
ECK operator (watch CR)
├─ StatefulSet logs-es-master (3) ──► PVC 10Gi ×3
├─ StatefulSet logs-es-data (3) ──► PVC 500Gi ×3 (es-ssd, mỗi zone 1)
├─ Service logs-es-http :9200, Secret cert + user elastic
└─ PodDisruptionBudget (chỉ cho 1 Pod ES down mỗi lúc)
Node A Node B Node C
Fluent Bit ──┐ Fluent Bit ──┐ Fluent Bit ──┐ (DaemonSet)
└────────────────┴───────────────┴──► logs-es-http
│
User ──► Ingress kibana.shop.internal ──► Kibana (2) ─┘
Upgrade 8.15 → 8.16: operator restart từng Pod, chờ green, rồi Pod tiếp theo
6. So sánh: tự chạy hay managed
| ECK / OpenSearch Operator | Elastic Cloud | Amazon OpenSearch Service | |
|---|---|---|---|
| Ai lo node, disk, upgrade | Team bạn (operator giúp một phần) | Elastic | AWS |
| Chạy ở đâu | Trong cluster K8s của bạn | Cloud của Elastic (AWS/GCP/Azure) | VPC AWS của bạn |
| Chi phí | Rẻ hạ tầng, tốn công người | Cao, đủ tính năng trả phí | Trung bình, có UltraWarm/cold |
| Kết nối từ K8s | Service nội bộ | Endpoint public hoặc PrivateLink | VPC endpoint, IAM (IRSA cho Fluent Bit) |
| Rủi ro | Chạy chung node với app thì tranh RAM | Phụ thuộc nhà cung cấp | Version theo lịch của AWS |
7. Khi nào nên dùng / không nên dùng
- Tự chạy bằng operator: có người hiểu ES, có node pool riêng cho ES, muốn chủ động version và chi phí.
- Managed: team nhỏ, log quan trọng (audit), không muốn trực đêm vì disk ES đầy.
- Không nên: cluster nhỏ chạy chung ES với app trên cùng node; hoặc chỉ cần tìm log theo service/traceId (Loki ở bài 17.10 rẻ hơn nhiều, xem bài 19.12).
Team platform hay drain node để nâng cấp. Data node Elasticsearch bị dời sang node khác thì disk 500GB có đi theo không, hay phải copy lại từ đầu?
Bạn hỏi đúng điểm mấu chốt: câu trả lời phụ thuộc vào loại storage.
- Network block storage (EBS, Azure Disk, PD): PVC đi theo Pod. Pod
logs-es-data-1sang node mới vẫn gắn lại đúng disk cũ, ES khởi động với dữ liệu sẵn có, chỉ đồng bộ phần thay đổi trong lúc vắng. Điều kiện: node mới cùng zone với disk (EBS gắn theo zone), nên mỗi zone phải có node dư. - Local PV / NVMe trên node: nhanh hơn nhưng disk ở lại node cũ. Pod sang node mới = disk trống, ES phải copy lại toàn bộ shard từ replica. 500GB có thể mất hàng giờ và làm chậm cả cluster.
Để drain an toàn:
- ECK tạo sẵn PodDisruptionBudget: drain chỉ evict được 1 Pod ES mỗi lúc. Đừng drain kiểu xóa Pod thẳng bỏ qua eviction.
- Node pool riêng cho ES (taint
dedicated=elasticsearch:NoSchedule), nâng cấp pool này chủ động, từng node một, chờgreengiữa các lần. - Bảo trì ngắn: tạm đặt
cluster.routing.allocation.enable: primariesđể ES không vội copy shard đi khi node chỉ vắng vài phút, xong thì bật lại. - Không dùng
emptyDircho data: Pod chết là mất shard.
Kibana mở qua Ingress xong thì ai có link cũng vào được hết. Mình chặn kiểu gì cho đúng?
Đúng, Ingress chỉ mở đường chứ không phân quyền. Log chứa email, orderId, đôi khi cả token nếu dev log ẩu, nên cần nhiều lớp:
- Mạng: Ingress nội bộ (internal load balancer) hoặc
whitelist-source-rangechỉ cho dải VPN. Không mở Kibana ra Internet. - Đăng nhập SSO: Kibana dùng OIDC/SAML với Keycloak (chương 12), hoặc đặt oauth2-proxy trước Ingress. Không chia sẻ user
elastic. - Phân quyền theo index: role cho từng team, chỉ thấy log namespace của mình; chi tiết role và document/field-level security ở bài 19.11.
- NetworkPolicy: chỉ Kibana, Fluent Bit và operator được gọi tới cổng 9200 của ES.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: es-ingress, namespace: logging }
spec:
podSelector:
matchLabels: { elasticsearch.k8s.elastic.co/cluster-name: logs }
ingress:
- from:
- podSelector: { matchLabels: { kibana.k8s.elastic.co/name: logs } }
- namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: fluent-bit } }
- namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: elastic-system } }
- podSelector: { matchLabels: { elasticsearch.k8s.elastic.co/cluster-name: logs } }
ports: [ { port: 9200 }, { port: 9300 } ]
Phỏng vấn hỏi về chạy ELK/OpenSearch trên Kubernetes thì trả lời sao?
Q1. Bạn triển khai Elasticsearch trên Kubernetes thế nào?
🇻🇳 Tiếng Việt:
Em dùng operator như ECK hoặc OpenSearch Operator thay vì tự viết StatefulSet. Khai báo CRD với node set master và data riêng, mỗi data node có PVC trên StorageClass SSD, volumeBindingMode WaitForFirstConsumer. Memory request bằng limit, heap khoảng một nửa. Data node rải theo zone, chạy trên node pool riêng có taint. Operator lo TLS, user, PodDisruptionBudget và rolling upgrade chờ cluster green.
🇬🇧 English:
I use an operator such as ECK or the OpenSearch Operator instead of writing StatefulSets by hand. I declare a CRD with separate master and data node sets, each data node with a PVC on an SSD StorageClass using WaitForFirstConsumer. Memory request equals the limit, with heap at about half. Data nodes are spread across zones on a dedicated, tainted node pool. The operator handles TLS, users, the PodDisruptionBudget and rolling upgrades that wait for green health.
Q2. Log container đi từ Pod tới Kibana thế nào?
🇻🇳 Tiếng Việt:
App ghi JSON ra stdout, container runtime ghi xuống file trên node. Fluent Bit chạy DaemonSet đọc file đó, thêm metadata Kubernetes, gửi tới Service của Elasticsearch qua TLS với user chỉ có quyền ghi, lấy từ Secret. Kibana đọc từ Elasticsearch và được mở qua Ingress nội bộ có SSO.
🇬🇧 English:
Apps write JSON to stdout and the container runtime writes it to files on the node. Fluent Bit runs as a DaemonSet, tails those files, adds Kubernetes metadata and sends them to the Elasticsearch Service over TLS with a write-only user from a Secret. Kibana reads from Elasticsearch and is exposed through an internal Ingress with SSO.
Q3. Khi nào bạn chọn managed service thay vì tự chạy?
🇻🇳 Tiếng Việt:
Khi team không có người chuyên vận hành Elasticsearch, hoặc log là dữ liệu quan trọng như audit. Tự chạy rẻ hạ tầng nhưng tốn công: disk đầy, upgrade, node drain, shard đỏ. Trên AWS em thường chọn Amazon OpenSearch Service với IRSA cho Fluent Bit; cần tính năng riêng của Elastic thì chọn Elastic Cloud.
🇬🇧 English:
When the team has nobody dedicated to running Elasticsearch, or when logs are critical data such as audit logs. Self-hosting is cheaper in infrastructure but costly in effort: full disks, upgrades, node drains, red shards. On AWS I usually choose Amazon OpenSearch Service with IRSA for Fluent Bit, and Elastic Cloud when we need Elastic-specific features.
Cách nhớ ngắn gọn
- ES trên K8s = operator (ECK / OpenSearch Operator), không phải Deployment.
- Data node: PVC trên SSD, WaitForFirstConsumer, rải zone, node pool riêng.
- Memory request = limit, heap ≈ 50%; PDB cho drain an toàn.
- Fluent Bit DaemonSet → Service ES, user chỉ ghi từ Secret.
- Kibana: Ingress nội bộ + SSO + NetworkPolicy; ít người vận hành thì chọn managed.
Tổng kết chương Kubernetes
| Bài | Chủ đề | Bài toán giải quyết | Resource / công cụ chính |
|---|---|---|---|
| 17.1 | Control plane | Ai chạy container ở đâu, ai giữ đúng số lượng | API server, etcd, scheduler, controller, kubelet |
| 17.2 | Pod & probe | Process chạy chưa chắc app khỏe; tắt không làm rơi request | startup/readiness/liveness probe, preStop |
| 17.3 | Workloads | Giữ N Pod, deploy không downtime, app có state, batch | Deployment, StatefulSet, DaemonSet, Job, CronJob |
| 17.4 | Service & Ingress | Pod IP thay đổi; người dùng vào từ Internet | Service, CoreDNS, Ingress Controller |
| 17.5 | ConfigMap, Secret, RBAC | Config tách khỏi image, bảo vệ secret, phân quyền | ConfigMap, Secret, Role, ServiceAccount |
| 17.6 | Resource & autoscaling | Đặt Pod đúng node, chống ăn hết RAM, scale theo tải | requests/limits, QoS, HPA, PDB |
| 17.7 | Troubleshooting | Pod lỗi thì nhìn ở đâu | describe, logs --previous, events, debug |
| 17.8 | Helm | YAML lặp theo môi trường, quản lý release | Chart, values, upgrade --install, rollback |
| 17.9 | Giám sát cluster | Node, Pod, control plane có khỏe không | kube-prometheus-stack, kube-state-metrics, Grafana |
| 17.10 | Loki & Promtail | Log Pod mất khi Pod chết; gom log mọi node | DaemonSet collector, Loki, LogQL |
| 17.11 | ELK / OpenSearch trên Kubernetes | Chạy cluster log stateful an toàn trong K8s | ECK / OpenSearch Operator, PVC, Fluent Bit DaemonSet, Ingress |
Tự kiểm tra
Vì sao không chạy Elasticsearch bằng Deployment?
Mỗi node ES cần danh tính và disk riêng ổn định, restart phải theo thứ tự chờ cluster green; operator tạo StatefulSet và lo việc đó.
volumeBindingMode: WaitForFirstConsumer giúp gì?
Disk được tạo sau khi Pod đã được xếp lịch, nên cùng zone với node chạy Pod.
Data node dùng local NVMe bị drain sang node khác thì sao?
Disk ở lại node cũ, Pod mới có disk trống, ES phải copy lại toàn bộ shard từ replica.
Fluent Bit nên dùng user nào để ghi vào ES?
User riêng chỉ có quyền ghi index log (ví dụ logs-shop-*), lấy từ Secret; không dùng superuser elastic.
Nêu 3 lớp bảo vệ Kibana trên K8s.
Ingress nội bộ/whitelist VPN, SSO (OIDC/SAML hoặc oauth2-proxy), NetworkPolicy chặn truy cập trực tiếp tới ES; thêm role theo index.