Elastic 9.4 带来了一个专为 Kibana 仪表板设计的类型化 API,以及原生的 Terraform 资源。
这意味着,你的仪表板终于可以像 Kubernetes 配置或数据库 Schema 一样,纳入 GitOps 工作流------支持漂移检测、PR 可审查的 Diff、以及基于 Git 的版本回滚。
一、为什么仪表板需要 API?
如果你曾经尝试过把 Kibana 仪表板纳入版本控制,你一定会遇到一个令人绝望的事实:
Elastic 原有的 Saved Object API 在导出仪表板时,会把它打包成一个巨大的、字符串化的 JSON blob 。所有的可视化状态、内部引用关系、UUID、元数据,像一团乱麻一样被塞进一个叫做 panelsJSON 的字段里。
你想修改一个 Lens 图表的颜色配置。你导出的 JSON 里,panelsJSON 的值是一条长达数千字符的单行字符串。里面嵌套着随机生成的 ID、并行数组、UI 内部的状态快照------这些东西对 Kibana 前端恢复画面有用,但对人类阅读来说,无异于天书。
这直接导致了三个致命伤:
- GitOps 不可能:没有清晰的 Diff,代码审查无从谈起。Reviewer 看到一行 2000 字符的变更,根本不可能知道改了什么。
- 环境晋升靠运气:你无法用自动化的方式把"测试环境的仪表板"可靠地推送到生产环境,因为里面硬编码着环境特定的 UUID 和内部引用。
- 回滚是噩梦:想回滚到上周的稳定版本?你只能手动覆盖整个文档,祈祷别把生产环境搞砸。
甚至对于大语言模型(LLM)来说,这种格式也太复杂、太容易出错。如果未来你想用自然语言生成一个仪表板("给我做一个展示支付服务错误率的仪表板"),LLM 必须先能可靠地读写这种结构------而 Saved Object 格式显然不是为这种场景设计的。
二、新 Dashboards API:把"黑盒"变成"白盒"
Elastic 9.4 引入的 Kibana Dashboards API 不是对旧 API 的简单修补,而是一次从存储层到接口层的彻底重构。
它提供 5 个端点,覆盖仪表板的全生命周期:
#mermaid-svg-wkKNq6MQ4ZCDvhPV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .error-icon{fill:#552222;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .marker.cross{stroke:#333333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wkKNq6MQ4ZCDvhPV p{margin:0;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .cluster-label text{fill:#333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .cluster-label span{color:#333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .cluster-label span p{background-color:transparent;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .label text,#mermaid-svg-wkKNq6MQ4ZCDvhPV span{fill:#333;color:#333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .node rect,#mermaid-svg-wkKNq6MQ4ZCDvhPV .node circle,#mermaid-svg-wkKNq6MQ4ZCDvhPV .node ellipse,#mermaid-svg-wkKNq6MQ4ZCDvhPV .node polygon,#mermaid-svg-wkKNq6MQ4ZCDvhPV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .rough-node .label text,#mermaid-svg-wkKNq6MQ4ZCDvhPV .node .label text,#mermaid-svg-wkKNq6MQ4ZCDvhPV .image-shape .label,#mermaid-svg-wkKNq6MQ4ZCDvhPV .icon-shape .label{text-anchor:middle;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .rough-node .label,#mermaid-svg-wkKNq6MQ4ZCDvhPV .node .label,#mermaid-svg-wkKNq6MQ4ZCDvhPV .image-shape .label,#mermaid-svg-wkKNq6MQ4ZCDvhPV .icon-shape .label{text-align:center;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .node.clickable{cursor:pointer;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .arrowheadPath{fill:#333333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wkKNq6MQ4ZCDvhPV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wkKNq6MQ4ZCDvhPV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wkKNq6MQ4ZCDvhPV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .cluster text{fill:#333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .cluster span{color:#333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wkKNq6MQ4ZCDvhPV rect.text{fill:none;stroke-width:0;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .icon-shape,#mermaid-svg-wkKNq6MQ4ZCDvhPV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .icon-shape p,#mermaid-svg-wkKNq6MQ4ZCDvhPV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .icon-shape .label rect,#mermaid-svg-wkKNq6MQ4ZCDvhPV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wkKNq6MQ4ZCDvhPV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wkKNq6MQ4ZCDvhPV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wkKNq6MQ4ZCDvhPV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建
读取
Upsert
更新或创建
删除
分页列表
🆕 POST /api/dashboards
Dashboard
📖 GET /api/dashboards/{id}
🔄 PUT /api/dashboards/{id}
❌ DELETE /api/dashboards/{id}
📋 GET /api/dashboards
所有 Dashboards
更重要的是,它与旧 API 有着本质区别:
| 特性 | 旧 Saved Object API | 新 Dashboards API |
|---|---|---|
| 数据结构 | panelsJSON 字符串黑盒 |
每个面板独立的类型化 Schema |
| 可视化类型 | 统一吞进字符串 | 12 种类型分别验证(XY、Metric、Pie、Gauge、Heatmap、Data Table、Treemap 等) |
| 查询语言 | 混杂在内部状态里 | 清晰区分 Data View 与 ES|QL 两种模式 |
| 过滤器 | 嵌套在 UI 状态深处 | Dashboard 级别的类型化过滤器(is、range、exists、and/or 组合) |
| 布局 | 不可控 | 48 列网格系统 + 可折叠分组 |
| 写入验证 | 保存时不管,渲染时报错 | 写入时即验证,错误在 API 调用阶段就暴露 |
| 面板来源 | 仅限内嵌 | 支持内嵌面板 + Library 面板(跨仪表板复用) |
这意味着,当你导出一个仪表板时,你得到的是一份人类可读、机器可验证、版本控制友好的结构化 JSON。
三、幕后功臣:Transforms Layer(转换层)
新 API 的诞生并非一蹴而就。Kibana 从第一天起就把用户创建的 UI 内容直接快照化存进 Elasticsearch。当时的假设很简单:只要能被 UI 读回来还原画面,存储格式长什么样根本不重要。
于是,前端把当时的 React 组件状态、Redux store 切片、甚至一些临时性的内部标记,原封不动地塞进了 panelsJSON、uiStateJSON、visState 等字段里。这些字段被字符串化后存入 Elasticsearch,团队内部给它们起了个贴切的外号:"JSON Bags"(JSON 垃圾袋)。
当你解开这些垃圾袋,看到的不是整洁的数据结构,而是深层次的嵌套、随机生成的 ID、并行数组、以及不同状态块之间的隐式引用。这些东西在前端代码里或许合理,但作为公共 API 的契约,它们是灾难。
3.1 面临的选择
走到了一个岔路口:
- 选项 A:保留现有 Kibana 仪表板系统的所有代码,在旁边另起炉灶建一个全新的 API,绕过旧系统,只支持最常用的功能。
- 选项 B :把新的类型化 Schema 注入现有系统,在旧 UI 和新 API 之间架设一层中间件,让同一套 Schema 同时驱动 UI 和公共端点。
选项 B------因为这意味着用户现有的所有仪表板都能无缝迁移,不需要重建任何东西。
3.2 Transforms Layer 如何工作
这个中间件就是 Transforms Layer 。它的职责只有一个:在" legacy UI 状态形状"和"干净的 API 形状"之间双向翻译。
#mermaid-svg-LL0fKmNQwcfWNzjA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LL0fKmNQwcfWNzjA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LL0fKmNQwcfWNzjA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LL0fKmNQwcfWNzjA .error-icon{fill:#552222;}#mermaid-svg-LL0fKmNQwcfWNzjA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LL0fKmNQwcfWNzjA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LL0fKmNQwcfWNzjA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LL0fKmNQwcfWNzjA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LL0fKmNQwcfWNzjA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LL0fKmNQwcfWNzjA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LL0fKmNQwcfWNzjA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LL0fKmNQwcfWNzjA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LL0fKmNQwcfWNzjA .marker.cross{stroke:#333333;}#mermaid-svg-LL0fKmNQwcfWNzjA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LL0fKmNQwcfWNzjA p{margin:0;}#mermaid-svg-LL0fKmNQwcfWNzjA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LL0fKmNQwcfWNzjA .cluster-label text{fill:#333;}#mermaid-svg-LL0fKmNQwcfWNzjA .cluster-label span{color:#333;}#mermaid-svg-LL0fKmNQwcfWNzjA .cluster-label span p{background-color:transparent;}#mermaid-svg-LL0fKmNQwcfWNzjA .label text,#mermaid-svg-LL0fKmNQwcfWNzjA span{fill:#333;color:#333;}#mermaid-svg-LL0fKmNQwcfWNzjA .node rect,#mermaid-svg-LL0fKmNQwcfWNzjA .node circle,#mermaid-svg-LL0fKmNQwcfWNzjA .node ellipse,#mermaid-svg-LL0fKmNQwcfWNzjA .node polygon,#mermaid-svg-LL0fKmNQwcfWNzjA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LL0fKmNQwcfWNzjA .rough-node .label text,#mermaid-svg-LL0fKmNQwcfWNzjA .node .label text,#mermaid-svg-LL0fKmNQwcfWNzjA .image-shape .label,#mermaid-svg-LL0fKmNQwcfWNzjA .icon-shape .label{text-anchor:middle;}#mermaid-svg-LL0fKmNQwcfWNzjA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LL0fKmNQwcfWNzjA .rough-node .label,#mermaid-svg-LL0fKmNQwcfWNzjA .node .label,#mermaid-svg-LL0fKmNQwcfWNzjA .image-shape .label,#mermaid-svg-LL0fKmNQwcfWNzjA .icon-shape .label{text-align:center;}#mermaid-svg-LL0fKmNQwcfWNzjA .node.clickable{cursor:pointer;}#mermaid-svg-LL0fKmNQwcfWNzjA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LL0fKmNQwcfWNzjA .arrowheadPath{fill:#333333;}#mermaid-svg-LL0fKmNQwcfWNzjA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LL0fKmNQwcfWNzjA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LL0fKmNQwcfWNzjA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LL0fKmNQwcfWNzjA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LL0fKmNQwcfWNzjA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LL0fKmNQwcfWNzjA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LL0fKmNQwcfWNzjA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LL0fKmNQwcfWNzjA .cluster text{fill:#333;}#mermaid-svg-LL0fKmNQwcfWNzjA .cluster span{color:#333;}#mermaid-svg-LL0fKmNQwcfWNzjA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LL0fKmNQwcfWNzjA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LL0fKmNQwcfWNzjA rect.text{fill:none;stroke-width:0;}#mermaid-svg-LL0fKmNQwcfWNzjA .icon-shape,#mermaid-svg-LL0fKmNQwcfWNzjA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LL0fKmNQwcfWNzjA .icon-shape p,#mermaid-svg-LL0fKmNQwcfWNzjA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LL0fKmNQwcfWNzjA .icon-shape .label rect,#mermaid-svg-LL0fKmNQwcfWNzjA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LL0fKmNQwcfWNzjA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LL0fKmNQwcfWNzjA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LL0fKmNQwcfWNzjA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 消费者
服务端:Transforms Layer
存储层(Elasticsearch)
各面板独立 Transform
写入 UI State
写入 Typed Schema
序列化为 JSON Bags
反序列化
返回 Typed Schema
返回 Typed Schema
运行时验证
内部 UI State
JSON Bags
panelsJSON / uiStateJSON
Panel Transform
Registry
面板转换注册表
Schema Registry
公共 API 契约库
XY Chart
Transform
序列化 ↔ 反序列化
Metric Panel
Transform
Markdown
Transform
Controls
Transform
...更多面板
Kibana UI
原有代码
无感知
API Client
Terraform / curl / LLM
关键机制:
- 逐面板注册:每个面板类型(XY Chart、Metric、Markdown 等)都向仪表板注册自己的 Transform 函数。负责该面板的团队会迭代优化 Schema,并同步修改 UI 代码,确保 UI 和新 API 看到的是同一套逻辑结构。
- Schema 即契约 :每个面板在注册 Transform 时,必须同时注册一个完整、严格的 Schema。这个 Schema 成为公共 API 契约的一部分。如果一个面板类型还没准备好(比如 Links 面板、ML 面板),它就不会出现在公共 API 的响应里,从而避免未来发生破坏性变更。
- 向后兼容是底线:整个重构是在"用户每天仍在保存和加载仪表板"的线上环境中完成的。任何时候都不能破坏现有用户的体验。
四、实战:从 UI 到 API 的完整 Walkthrough
新 API 在 Elastic 9.4 中默认可用,并且兼容你现有的所有仪表板。下面是一个从 UI 出发,快速体验 API 的完整流程。
步骤 1:在 Kibana 中打开一个现有仪表板
假设你打开了 "Logs Web Traffic" 之类的示例仪表板。顶部菜单点击 Share → Export JSON。
步骤 2:查看导出的结构化 JSON
你会看到一个 Flyout 弹出层,里面不再是密密麻麻的单行字符串,而是格式清晰的 JSON。它包含:
title、description、tagstime_range、refresh_intervalquery(KQL 或 Lucene)panels数组,每个面板有明确的type、grid位置、xy_chart_config或metric_chart_config等
步骤 3:一键导入到 Console
点击 Open in Console ,Kibana 会自动在 Dev Tools 中为你生成一个预填充的 POST 请求:
http
POST kbn:/s/production/api/dashboards
{
"title": "Operations overview",
"description": "Production traffic metrics",
"tags": ["observability", "production"],
"time_range": { "from": "now-1h", "to": "now" },
"query": { "language": "kql", "text": "service.name:payments" },
"panels": [ ... ]
}
注意 URL 中的 /s/production/------这是 Kibana Space ID。新 API 完全支持 Space 级别的多租户隔离。
步骤 4:验证
切换到 production Space,你会看到仪表板已经原样创建,包含原来的 Controls、Metrics 和 Time Series 图表。
五、Terraform 原生支持:把 HCL 变成仪表板的"源代码"
如果说 Dashboards API 解决了数据结构的问题,那么 Elastic Stack Terraform Provider 则解决了基础设施编排的问题。
Provider 中新增的 elasticstack_kibana_dashboard 资源,把 API 的每一个类型化字段都映射成了原生的 HCL(HashiCorp Configuration Language)。这意味着你可以:
- 用
terraform plan预览变更 - 用
terraform apply部署 - 用原生机制做 Drift Detection(漂移检测)
- 用
terraform import把现有仪表板纳入管理
5.1 一个完整的 Terraform 示例
下面是一个支付服务的监控仪表板,包含一个时序面积图和一个核心指标 KPI:
hcl
terraform {
required_providers {
elasticstack = {
source = "elastic/elasticstack"
version = "~> 0.14"
}
}
}
provider "elasticstack" {
kibana {}
}
resource "elasticstack_kibana_dashboard" "service_overview" {
title = "Service Overview"
description = "Key metrics for the payments service"
tags = ["production", "payments"]
time_range = { from = "now-1h", to = "now" }
refresh_interval = { pause = false, value = 30000 }
query = { language = "kql", text = "service.name:payments" }
panels = [
{
type = "vis"
grid = { x = 0, y = 0, w = 32, h = 15 }
xy_chart_config = {
axis = {
x = { title = { visible = true } }
y = {
title = { visible = true }
scale = "linear"
domain_json = jsonencode({ type = "fit" })
}
}
legend = { visibility = "visible", inside = false, position = "right" }
fitting = { type = "none" }
decorations = { fill_opacity = 0.3 }
query = { language = "kql", expression = "" }
layers = [{
type = "area"
data_layer = {
ignore_global_filters = false
sampling = 1
data_source_json = jsonencode({
type = "data_view_spec",
index_pattern = "logs-*"
})
y = [{
config_json = jsonencode({
operation = "count",
empty_as_null = true,
color = { type = "auto" }
})
}]
}
}]
}
},
{
type = "vis"
grid = { x = 32, y = 0, w = 16, h = 15 }
metric_chart_config = {
ignore_global_filters = false
sampling = 1
data_source_json = jsonencode({
type = "data_view_spec",
index_pattern = "logs-*",
time_field = "@timestamp"
})
query = { language = "kql", expression = "" }
metrics = [{
config_json = jsonencode({
type = "primary"
operation = "count"
empty_as_null = false
color = { type = "auto" }
format = { type = "number", decimals = 2, compact = false }
})
}]
}
}
]
}
💡 关于
jsonencode的说明 :你可能会注意到某些字段(如data_source_json、config_json、domain_json)仍然使用了jsonencode()。这不是设计倒退,而是 Terraform HCL 类型系统与某些高度动态的面板配置之间的务实妥协。核心结构(面板类型、网格位置、查询、过滤器)已经是完全类型化的 HCL 块,而这些内部配置对象由于各面板差异极大,暂时以 JSON 字符串形式承载。未来 Provider 的迭代会把更多常用配置展开为原生 HCL 属性。
5.2 访问控制:防止"绕过 Terraform 的手动修改"
生产环境中最怕什么?最怕有人登录 Kibana UI,随手把仪表板改了,导致 Terraform 状态与实际环境不一致。
Provider 支持仪表板的访问控制模型:
hcl
resource "elasticstack_kibana_dashboard" "protected" {
title = "Production SLOs"
# ... 其他配置 ...
access_control = {
access_mode = "write_restricted"
}
}
当 access_mode 设为 "write_restricted" 时,只有创建者(即 Terraform 服务账号)拥有写入权限。这相当于给仪表板加了一把锁:所有变更必须流经 Terraform Pipeline,任何人直接在 UI 里点击"保存"都会被拒绝。
六、GitOps 工作流:仪表板终于成为一等基础设施公民
有了类型化 API 和 Terraform 支持,Kibana 仪表板可以像 Kubernetes Deployment 或 AWS VPC 一样,被完全纳入 GitOps 工作流。
#mermaid-svg-zBxF0n9YhH0E3L2g{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zBxF0n9YhH0E3L2g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zBxF0n9YhH0E3L2g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zBxF0n9YhH0E3L2g .error-icon{fill:#552222;}#mermaid-svg-zBxF0n9YhH0E3L2g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zBxF0n9YhH0E3L2g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zBxF0n9YhH0E3L2g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zBxF0n9YhH0E3L2g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zBxF0n9YhH0E3L2g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zBxF0n9YhH0E3L2g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zBxF0n9YhH0E3L2g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zBxF0n9YhH0E3L2g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zBxF0n9YhH0E3L2g .marker.cross{stroke:#333333;}#mermaid-svg-zBxF0n9YhH0E3L2g svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zBxF0n9YhH0E3L2g p{margin:0;}#mermaid-svg-zBxF0n9YhH0E3L2g .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zBxF0n9YhH0E3L2g .cluster-label text{fill:#333;}#mermaid-svg-zBxF0n9YhH0E3L2g .cluster-label span{color:#333;}#mermaid-svg-zBxF0n9YhH0E3L2g .cluster-label span p{background-color:transparent;}#mermaid-svg-zBxF0n9YhH0E3L2g .label text,#mermaid-svg-zBxF0n9YhH0E3L2g span{fill:#333;color:#333;}#mermaid-svg-zBxF0n9YhH0E3L2g .node rect,#mermaid-svg-zBxF0n9YhH0E3L2g .node circle,#mermaid-svg-zBxF0n9YhH0E3L2g .node ellipse,#mermaid-svg-zBxF0n9YhH0E3L2g .node polygon,#mermaid-svg-zBxF0n9YhH0E3L2g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zBxF0n9YhH0E3L2g .rough-node .label text,#mermaid-svg-zBxF0n9YhH0E3L2g .node .label text,#mermaid-svg-zBxF0n9YhH0E3L2g .image-shape .label,#mermaid-svg-zBxF0n9YhH0E3L2g .icon-shape .label{text-anchor:middle;}#mermaid-svg-zBxF0n9YhH0E3L2g .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zBxF0n9YhH0E3L2g .rough-node .label,#mermaid-svg-zBxF0n9YhH0E3L2g .node .label,#mermaid-svg-zBxF0n9YhH0E3L2g .image-shape .label,#mermaid-svg-zBxF0n9YhH0E3L2g .icon-shape .label{text-align:center;}#mermaid-svg-zBxF0n9YhH0E3L2g .node.clickable{cursor:pointer;}#mermaid-svg-zBxF0n9YhH0E3L2g .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zBxF0n9YhH0E3L2g .arrowheadPath{fill:#333333;}#mermaid-svg-zBxF0n9YhH0E3L2g .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zBxF0n9YhH0E3L2g .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zBxF0n9YhH0E3L2g .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zBxF0n9YhH0E3L2g .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zBxF0n9YhH0E3L2g .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zBxF0n9YhH0E3L2g .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zBxF0n9YhH0E3L2g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zBxF0n9YhH0E3L2g .cluster text{fill:#333;}#mermaid-svg-zBxF0n9YhH0E3L2g .cluster span{color:#333;}#mermaid-svg-zBxF0n9YhH0E3L2g div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zBxF0n9YhH0E3L2g .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zBxF0n9YhH0E3L2g rect.text{fill:none;stroke-width:0;}#mermaid-svg-zBxF0n9YhH0E3L2g .icon-shape,#mermaid-svg-zBxF0n9YhH0E3L2g .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zBxF0n9YhH0E3L2g .icon-shape p,#mermaid-svg-zBxF0n9YhH0E3L2g .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zBxF0n9YhH0E3L2g .icon-shape .label rect,#mermaid-svg-zBxF0n9YhH0E3L2g .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zBxF0n9YhH0E3L2g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zBxF0n9YhH0E3L2g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zBxF0n9YhH0E3L2g :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🛡️ 运维与治理
🚀 部署阶段
🔧 开发阶段
git push
CI 运行
terraform plan
合并到 main
CD 触发
terraform apply
自动化测试通过
产生
定时/手动
terraform plan
git revert
terraform apply
持续监控
恢复一致
开发者在 HCL 中
定义/修改仪表板
创建 Pull Request
PR 中展示清晰 Diff
Reviewer 逐行审查
主分支代码
测试环境
Staging
生产环境
Production
某人登录 Kibana UI
试图手动修改
配置漂移 Drift
CI 报警:
发现与代码不一致
秒级回滚到
已知良好状态
这个工作流带来的好处是实实在在的:
- 定义即代码:仪表板与 Elasticsearch 索引、数据视图(Data View)、告警规则(Alerting Rules)放在同一个代码仓库里,上下文完整。
- 审查即安全 :
terraform plan的输出会精确告诉你------"这个 Metric 面板的小数位数从 2 改成了 0"、"时间范围从now-1h改成了now-15m"。 - 环境一致性 :通过 Terraform Workspace 或变量文件(
terraform.tfvars)管理不同环境的差异,比如测试环境用logs-staging-*,生产环境用logs-production-*。 - 漂移检测 :当有人(无论是有意还是无意)绕过了流程,在 UI 里改了仪表板,下一次
terraform plan会立即发现差异。 - 基于 Git 的回滚 :生产出问题?
git revert上一次提交,然后terraform apply。30 秒内回到上一个稳定版本,不需要在 Kibana 里手忙脚乱地找"上一个版本"的导出文件。
七、快速开始:三步走
1. 配置 Provider
确保你的 Elastic Stack 版本 ≥ 9.4,然后配置 Provider:
hcl
terraform {
required_providers {
elasticstack = {
source = "elastic/elasticstack"
version = "~> 0.14"
}
}
}
provider "elasticstack" {
kibana {}
}
2. 定义并部署
参考上面的 elasticstack_kibana_dashboard 示例,运行:
bash
terraform plan # 预览变更
terraform apply # 创建或更新
3. 导入存量仪表板
如果你已经有大量手动创建的仪表板,可以用一行命令把它们纳入 Terraform 管理:
bash
terraform import elasticstack_kibana_dashboard.my_dashboard <space_id>/<dashboard_id>
📖 完整的资源 Schema 和文档请查阅 Terraform Registry - Elastic Stack Provider。
八、Roadmap:还在快速进化
Dashboards API 和 Terraform Provider 在 Elastic 9.4 中处于 Technical Preview(技术预览) 阶段,两者都在积极迭代中。
短期内即将补齐的能力:
| 领域 | 当前状态 | 路线图 |
|---|---|---|
| API 面板覆盖 | 支持 12 种核心可视化 + Markdown + Controls | Links、Machine Learning、Alerts、Log analysis、Vega、Maps 等面板即将加入 |
| Terraform 面板类型 | XY Chart、Metric、Markdown 等 | Image、Links、SLO(服务级别目标)、Synthetics 等已支持 API 的类型将展开为原生 HCL |
| Terraform 过滤器 | 基础 Dashboard 查询 | Dashboard-level 的 Filter Pills、Controls、Drilldowns 将在 HCL 中完整暴露 |
结语
Elastic 9.4 的 Dashboards API 和 Terraform 支持,本质上是在解决一个被忽视已久的问题:可视化不应该只是 UI 的附庸,它应该是一种可版本化、可审查、可自动化的基础设施资产。
从 panelsJSON 垃圾袋到类型化 Schema,从手动导出导入到 terraform plan 的清晰 Diff,从"谁改了生产仪表板"的悬案到 write_restricted 的访问控制------这套组合拳让 Kibana 仪表板第一次真正拥有了"基础设施即代码"的尊严。
如果你正在管理超过三个环境的 Elastic Stack,或者你的团队已经开始用 Terraform 管理索引和告警规则,那么现在就是把仪表板也纳入同一条流水线的最佳时机。