基于 GitOps 实现 Kubernetes 环境下 SkyWalking 与 Elasticsearch 的全生命周期声明式管理


在管理多个 Kubernetes 集群的可观测性技术栈时,一个反复出现的痛点是配置的漂移与不一致性。SkyWalking 的告警规则、Elasticsearch 的索引生命周期策略(ILM)、乃至 OAP Server 的核心配置文件,在开发、测试和生产环境中往往通过手动修改或临时脚本来调整,这导致了审计困难、变更不可追溯,以及在环境重建时无法精确复现。真实项目中,这种手动运维模式是不可持续的,尤其是在追求平台稳定性和研发效率的团队中。

我们的目标是建立一个完全声明式的管理模型:任何对可观测性平台的变更,无论是部署、升级还是配置调整,都必须通过 Git 提交发起,经由 Pull Request 进行评审,并由自动化系统(GitOps 控制器)同步到目标集群。GitHub 将成为唯一的可信源(Single Source of Truth)。

graph TD
    subgraph GitHub Repository
        A[Developer] -- Pushes Change --> B(PR: Update alarm-settings.yml)
        B -- Merged --> C(main branch)
    end

    subgraph Kubernetes Cluster
        D[ArgoCD Controller] -- Watches --> C
        D -- Detects Change --> E{Sync Operation}
        E -- Applies --> F[ConfigMap: skywalking-oap-config]
        F -- Volume Mount --> G[SkyWalking OAP Pods]
        E -- Applies --> H[Elasticsearch CR]
        H -- Managed by --> I[ECK Operator]
        I -- Configures --> J[Elasticsearch Cluster]
        E -- Applies --> K[Deployment: my-web-api]
        K -- Injects Agent --> L[API Pods with SkyWalking Agent]
    end

    L -- Send Traces --> G
    G -- Store Data --> J

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style C fill:#ccf,stroke:#333,stroke-width:2px

这个流程的核心是将原本动态、易变的运维操作,转化为静态、可版本化的代码。技术选型决策如下:

  • 容器编排: Kubernetes,作为云原生事实标准。
  • GitOps 控制器: ArgoCD。其 Pull 模式和对 K8s 原生清单的良好支持,使其成为实现声明式同步的理想选择。
  • Elasticsearch 管理: Elastic Cloud on Kubernetes (ECK) Operator。相比于使用 Helm Chart,Operator 提供了更深度的生命周期管理,能够处理复杂的有状态应用升级、配置变更和存储管理,这对于生产环境的 ES 集群至关重要。
  • SkyWalking 部署: SkyWalking Kubernetes Operator。它将 SkyWalking 的组件(OAP, UI, Satellite)抽象为 CRD,简化了部署配置。

第一步:构建 Git 仓库结构

一个清晰的 Git 仓库结构是 GitOps 实践的基石。我们将采用 monorepo 的方式,集中管理所有环境的配置。

# a-gitops-observability-repo/
#
├── apps/                        # ArgoCD Application 定义
│   ├── dev/
│   │   ├── my-web-api.yaml
│   │   └── observability.yaml
│   └── staging/
│       ├── my-web-api.yaml
│       └── observability.yaml
│
├── infrastructure/              # 基础设施组件 (Operators, etc.)
│   ├── argocd/                  # ArgoCD 自身安装清单
│   ├── eck-operator/            # ECK Operator 安装清单
│   └── skywalking-operator/     # SkyWalking Operator 安装清单
│
└── tenants/                     # 各环境的具体配置清单
    ├── base/                    # 所有环境共享的基础配置
    │   ├── skywalking/
    │   │   ├── oap-server.yaml
    │   │   └── ui.yaml
    │   └── web-api/
    │       ├── deployment.yaml
    │       └── service.yaml
    │
    └── overlays/                # 特定环境的覆盖配置 (Kustomize)
        ├── dev/
        │   ├── elasticsearch.yaml
        │   ├── skywalking-configs.yaml
        │   └── web-api-replicas.yaml
        └── staging/
            ├── elasticsearch.yaml
            ├── skywalking-configs.yaml
            └── web-api-replicas.yaml

这种结构利用 Kustomize 来分离基础配置(base)和环境特定配置(overlays),避免了大量重复的 YAML。ArgoCD 的 Application 将会指向这些 overlays 目录。

第二步:声明式管理 Elasticsearch 集群

首先,我们需要在集群中安装 ECK Operator。这通常是一次性的引导操作。安装完成后,我们就可以通过 Elasticsearch 自定义资源(CR)来声明我们需要的集群。

tenants/overlays/dev/elasticsearch.yaml 中,我们定义一个用于开发环境的 ES 集群。这里的坑在于,必须同时声明式地管理其索引生命周期策略(ILM),否则索引会无限增长,耗尽存储。

# tenants/overlays/dev/elasticsearch.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: es-ilm-policy-skywalking
  namespace: observability
  labels:
    "common.k8s.elastic.co/object-type": "ilm-policy" # ECK Operator 会识别这个标签
data:
  policy.json: |
    {
      "policy": {
        "phases": {
          "hot": {
            "min_age": "0ms",
            "actions": {
              "rollover": {
                "max_size": "25gb",
                "max_age": "7d"
              },
              "set_priority": {
                "priority": 100
              }
            }
          },
          "delete": {
            "min_age": "30d",
            "actions": {
              "delete": {
                "delete_searchable_snapshot": true
              }
            }
          }
        }
      }
    }
---
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: skywalking-es
  namespace: observability
spec:
  version: "8.9.2"
  nodeSets:
  - name: master-data-ingest
    count: 3
    # 生产环境中,应将 master, data, ingest 节点角色分离到不同的 NodeSets
    # For a real production setup, separate master, data, and ingest roles into different NodeSets.
    config:
      node.store.allow_mmap: false
    podTemplate:
      spec:
        containers:
        - name: elasticsearch
          resources:
            requests:
              memory: "4Gi"
              cpu: "1"
            limits:
              memory: "4Gi"
              cpu: "2"
        # 生产环境强烈建议使用反亲和性,将 Pod 分散到不同物理节点
        # For production, use anti-affinity to spread pods across different physical nodes.
        affinity:
          podAntiAffinity:
            preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    elasticsearch.k8s.elastic.co/cluster-name: "skywalking-es"
                topologyKey: "kubernetes.io/hostname"
    # 挂载 ILM 策略
    # ECK Operator will automatically detect the ConfigMap with the special label
    # and configure Elasticsearch to use it. No explicit mounting is needed in the CR.
    # The presence of the labeled ConfigMap is sufficient.
    
  http:
    tls:
      selfSignedCertificate:
        disabled: true # 简化示例,生产环境必须启用 TLS
  auth: {} # 禁用认证以简化,生产环境必须启用

通过 common.k8s.elastic.co/object-type: "ilm-policy" 这个标签,ECK Operator会自动将 ConfigMap 中的 JSON 内容配置为 ES 的 ILM 策略。当我们需要调整索引保留时间时,只需修改 policy.json 并提交到 Git,ArgoCD 会自动更新 ConfigMap,ECK Operator 随之更新 ES 内部的策略,整个过程无需手动干预。

第三步:声明式管理 SkyWalking 及其核心配置

部署 SkyWalking Operator 之后,我们可以定义 OAPServerUI 资源。最大的挑战在于如何声明式地管理 OAP Server 的配置文件,特别是 alarm-settings.yml。一个常见的错误是尝试通过修改 Operator 的代码或构建自定义的 Docker 镜像来注入配置,这违背了 GitOps 的原则。

正确的做法是,将配置文件作为 ConfigMap 进行管理,并通过 volumeMounts 挂载到 OAP Pod 中,覆盖默认的配置。

文件 tenants/overlays/dev/skywalking-configs.yaml

# tenants/overlays/dev/skywalking-configs.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: skywalking-oap-config
  namespace: observability
data:
  # 覆盖 OAP Server 的核心配置文件
  # 注意:这里的配置需要与 SkyWalking 版本兼容
  # The key must match the filename expected by SkyWalking OAP Server
  alarm-settings.yml: |
    rules:
      # Rule unique name, must be ended with `_rule`.
      service_resp_time_rule:
        metrics-name: service_sla
        op: ">"
        threshold: 1000 # in milliseconds
        period: 10
        count: 3
        silence-period: 5
        message: Response time of service {name} is more than 1000ms in 3 minutes of last 10 minutes.
      service_slow_endpoint_rule:
        metrics-name: endpoint_avg
        op: ">"
        threshold: 1000
        period: 10
        count: 2
        silence-period: 2
        message: Response time of endpoint {name} is more than 1000ms in 2 minutes of last 10 minutes.
    webhooks:
      # - url: http://your-webhook-host/alarm
      #   secret: your-secret

文件 tenants/base/skywalking/oap-server.yaml

# tenants/base/skywalking/oap-server.yaml
apiVersion: oap.skywalking.apache.org/v1alpha1
kind: OAPServer
metadata:
  name: skywalking-oap
  namespace: observability
spec:
  version: "9.5.0"
  instances: 2
  # 这里的配置将覆盖 OAP 容器的启动参数
  config:
    - name: SW_STORAGE
      value: "elasticsearch" # 使用 ES 作为存储
    - name: SW_STORAGE_ES_CLUSTER_NODES
      # 这是与 ECK 创建的 ES Service 的通信地址
      value: "skywalking-es-es-http.observability.svc:9200"
    - name: SW_HEALTH_CHECKER
      value: "default"
    - name: SW_TELEMETRY
      value: "prometheus" # 暴露 Prometheus 指标
    - name: SW_CORE_LOG_LEVEL
      value: "INFO"
    # ILM 相关配置,确保 SkyWalking 创建的索引能匹配上我们定义的 ILM Policy
    - name: SW_STORAGE_ES_INDEX_SHARDS_NUMBER
      value: "2"
    - name: SW_STORAGE_ES_SUPER_DATASET_SHARDS_NUMBER
      value: "2"
    - name: SW_STORAGE_ES_REPLICA_NUMBER
      value: "1"
    # IMPORTANT: SkyWalking will create indices with a `-yyyyMMdd` suffix.
    # The ILM policy must be applied to these indices via an index template.
    # The ECK ILM management is for indices it creates, for SkyWalking, we need to
    # instruct SkyWalking itself on how to behave.
    # SkyWalking 9+ has built-in support for ILM.
    - name: SW_STORAGE_ES_DAY_STEP
      value: "1" # Rollover daily
    - name: SW_STORAGE_ES_LOG_TTL
      value: "30" # Log data TTL in days
    - name: SW_STORAGE_ES_RECORD_TTL
      value: "7"  # Record data TTL in days
  
  # 关键部分:挂载包含告警规则的 ConfigMap
  # This is the core of declarative configuration management.
  extraVolumeMounts:
    - name: oap-config-volume
      mountPath: /skywalking/config/alarm-settings.yml
      subPath: alarm-settings.yml # 精确挂载文件,而不是整个目录
      readOnly: true
  
  extraVolumes:
    - name: oap-config-volume
      configMap:
        name: skywalking-oap-config # 对应上面创建的 ConfigMap
        items:
          - key: alarm-settings.yml
            path: alarm-settings.yml

现在,当告警阈值需要从 1000ms 调整为 800ms 时,工程师只需提交一个 PR 来修改 skywalking-configs.yaml。一旦合并,ArgoCD 会更新 ConfigMap,随后 Kubernetes 会触发对 OAP Server StatefulSet 的滚动更新,新的 Pod 启动后将加载新的告警规则。整个过程透明、可审计。

第四步:为 Web API 自动注入 SkyWalking Agent

要观察一个应用,必须为其注入探针。SkyWalking Operator 提供了自动注入的能力,这比手动修改 Dockerfile 或启动脚本要优雅得多。我们只需要在被监控应用的 Deployment 上添加特定的注解。

文件 tenants/base/web-api/deployment.yaml

# tenants/base/web-api/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-web-api
  namespace: default
  labels:
    app: my-web-api
spec:
  replicas: 1 # 副本数将在 overlay 中被覆盖
  selector:
    matchLabels:
      app: my-web-api
  template:
    metadata:
      labels:
        app: my-web-api
      # 关键注解,触发 SkyWalking Operator 的注入逻辑
      annotations:
        # 启用自动注入
        swck-injection: "true"
        # SkyWalking Agent 的配置
        agent.skywalking.apache.org/agent-name: "my-web-api"
        agent.skywalking.apache.org/collector-backend-services: "skywalking-oap.observability.svc:11800"
        agent.skywalking.apache.org/logging-level: "INFO"
    spec:
      containers:
      - name: app
        # 一个标准的 Java Web API 镜像
        image: your-repo/my-java-web-api:1.0.0
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: "500m"
            memory: "1Gi"
          requests:
            cpu: "250m"
            memory: "512Mi"
        # 启动命令中不需要包含 -javaagent 参数,Operator 会自动添加
        # The command should NOT include the -javaagent parameter.
        # The operator's admission webhook will patch the pod spec to add it.
        # The JAVA_TOOL_OPTIONS environment variable will be set.
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name

当这个 Deployment 被应用到集群时,SkyWalking Operator 的 MutatingAdmissionWebhook 会拦截 Pod 的创建请求,动态修改其定义,添加一个 initContainer 来下载 SkyWalking agent,并设置 JAVA_TOOL_OPTIONS 环境变量来加载 agent。这一切对应用开发者是透明的。

最终的 GitOps 工作流

至此,我们已经将整个可观测性技术栈及其配置纳入了 GitOps 的管理范畴。一个典型的变更流程如下:

  1. 场景: 生产环境的 my-web-api 服务需要将响应时间告警阈值从 1000ms 调整为 800ms。
  2. 操作: 工程师在本地克隆 a-gitops-observability-repo 仓库,创建一个新分支。
  3. 修改: 修改 tenants/overlays/staging/skywalking-configs.yaml 文件中的 threshold 值为 800
  4. 提交与PR: 提交修改,并向 main 分支发起一个 Pull Request,在描述中说明变更原因。
  5. 评审与合并: SRE 团队或相关负责人评审该 PR。确认无误后,合并到 main 分支。
  6. 自动同步: ArgoCD 监测到 main 分支的变更,自动将更新后的 ConfigMap 同步到 staging 集群。
  7. 生效: SkyWalking OAP Pod 滚动更新,加载新的告警规则。
  8. 推广到生产: 验证无误后,通过 cherry-pick 或合并分支等方式将此变更应用到 tenants/overlays/production/ 目录,重复上述流程。

方案的局限性与未来展望

此方案实现了彻底的声明式管理,但并非没有权衡。每次修改 ConfigMap 都会导致 SkyWalking OAP Server 的 Pod 重启,对于大规模、高流量的实例,这可能会导致短暂的监控数据接收中断。虽然滚动更新可以缓解这个问题,但并非零中断。SkyWalking 提供了 Dynamic Configuration API,可以在运行时更新配置而无需重启,但将其与 GitOps 模型结合会更复杂,可能需要一个中间控制器来监听 Git 变更并调用 API,这破坏了 K8s 原生资源模型的简洁性。

其次,对于 Elasticsearch 的深度性能调优,例如分片策略、JVM 配置等,虽然也可以通过 ECK Operator 的 CRD 进行声明式管理,但这需要运维人员对 ES 有非常深入的理解。本方案解决了配置管理的一致性和可追溯性问题,但并未降低技术栈本身的复杂性。

未来的迭代方向可能包括:

  1. 探索一种混合模式,即大部分静态配置通过 GitOps 管理,而少数需要频繁动态调整的配置(如告警规则的启用/禁用)通过一个与 Git 同步的“配置服务”暴露 API 来实现,寻找声明式模型与运行时灵活性之间的平衡点。
  2. 将 ES 的容量规划与成本管理也纳入 GitOps 流程,例如通过自定义的控制器,根据 Git 中定义的 SLO 目标和历史用量数据,自动调整 ES NodeSet 的 countstorage 大小,实现真正的 FinOps。

  目录