ELK / OpenSearch trên Kubernetes

4 câu hỏi~25 phútTừ khóa: ECK, OpenSearch Operator, CRD, nodeSets, Fluent Bit DaemonSet, StorageClass, PVC, Ingress, PodDisruptionBudget, Elastic Cloud, Amazon OpenSearch Service

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:

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 đề

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.

OperatorCRD chínhGhi chú
ECK (Elastic Cloud on Kubernetes)Elasticsearch, Kibana, Beat, Agent, LogstashCủa Elastic; tự bật TLS, tạo user elastic trong Secret
OpenSearch Kubernetes OperatorOpenSearchCluster (kèm Dashboards), OpenSearchISMPolicy, OpenSearchRoleCủ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 } }

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 OperatorElastic CloudAmazon OpenSearch Service
Ai lo node, disk, upgradeTeam bạn (operator giúp một phần)ElasticAWS
Chạy ở đâuTrong cluster K8s của bạnCloud của Elastic (AWS/GCP/Azure)VPC AWS của bạn
Chi phíRẻ hạ tầng, tốn công ngườiCao, đủ tính năng trả phíTrung bình, có UltraWarm/cold
Kết nối từ K8sService nội bộEndpoint public hoặc PrivateLinkVPC endpoint, IAM (IRSA cho Fluent Bit)
Rủi roChạy chung node với app thì tranh RAMPhụ thuộc nhà cung cấpVersion theo lịch của AWS

7. Khi nào nên dùng / không nên dùng

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.

Để drain an toàn:

  1. 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.
  2. 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ờ green giữa các lần.
  3. 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.
  4. Không dùng emptyDir cho 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:

  1. Mạng: Ingress nội bộ (internal load balancer) hoặc whitelist-source-range chỉ cho dải VPN. Không mở Kibana ra Internet.
  2. Đă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.
  3. 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.
  4. 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


Tổng kết chương Kubernetes

BàiChủ đềBài toán giải quyếtResource / công cụ chính
17.1Control planeAi chạy container ở đâu, ai giữ đúng số lượngAPI server, etcd, scheduler, controller, kubelet
17.2Pod & probeProcess chạy chưa chắc app khỏe; tắt không làm rơi requeststartup/readiness/liveness probe, preStop
17.3WorkloadsGiữ N Pod, deploy không downtime, app có state, batchDeployment, StatefulSet, DaemonSet, Job, CronJob
17.4Service & IngressPod IP thay đổi; người dùng vào từ InternetService, CoreDNS, Ingress Controller
17.5ConfigMap, Secret, RBACConfig tách khỏi image, bảo vệ secret, phân quyềnConfigMap, Secret, Role, ServiceAccount
17.6Resource & autoscalingĐặt Pod đúng node, chống ăn hết RAM, scale theo tảirequests/limits, QoS, HPA, PDB
17.7TroubleshootingPod lỗi thì nhìn ở đâudescribe, logs --previous, events, debug
17.8HelmYAML lặp theo môi trường, quản lý releaseChart, values, upgrade --install, rollback
17.9Giám sát clusterNode, Pod, control plane có khỏe khôngkube-prometheus-stack, kube-state-metrics, Grafana
17.10Loki & PromtailLog Pod mất khi Pod chết; gom log mọi nodeDaemonSet collector, Loki, LogQL
17.11ELK / OpenSearch trên KubernetesChạy cluster log stateful an toàn trong K8sECK / 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.