iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Kubernetes

從看得到到看得懂:30 天在自架 K8s 上實踐可觀測性與告警系列 第 28 篇

Day 28:用 Terraform 管叢集外資源,並把 Grafana 設定納入版控

  • 分享至 

  • xImage
  •  

昨天盤點的結論是三個洞:Loki 的設定、Tempo 的顯式設定、五個版本號。今天先處理另一件事——Grafana 裡面的東西。

它跟前面所有東西都不一樣:儀表板、資料夾、使用者不是 Kubernetes 資源,它們存在 Grafana 自己的資料庫裡。kubectl get dashboard 這種指令不存在。Helm 裝得了 Grafana 這個程式,管不到它裡面裝了什麼。

這就是 Terraform 的位置:Helm 管「裝什麼」,Terraform 管「裝完之後裡面長什麼樣」。

今天的目標,以及它一開始就是錯的

我原本規劃的今天是:把儀表板從網頁拖出來的狀態,變成 Terraform 管理的檔案。

但這個目標在動手前就已經過時了。第二十天 Grafana 被 helm upgrade 洗掉之後,我就把兩張儀表板改成 ConfigMap 了,sidecar 會自動載入。它們早就是檔案。

所以今天真正的問題變成:同一張儀表板,ConfigMap 和 Terraform 兩種管法,該選哪個?

我決定兩個都寫,讓它們撞一次,看看會發生什麼。結果比我想的乾脆很多。

Terraform 是什麼

Terraform 是一個管理基礎設施的工具,核心是宣告式:

  1. 你寫一個檔案,描述你要的最終狀態
  2. 它去查現在的狀態
  3. 算出兩者的差異
  4. 只執行需要的動作

你不用寫「如果不存在就建立、如果存在就更新」這種判斷。這個概念前面其實一直在用——kubectl apply -f k8s/ 就是宣告式的,同一個指令跑十次跟跑一次結果一樣。

三個名詞:

是什麼
Provider 讓它知道怎麼跟某個服務溝通的外掛。有 AWS 的、Kubernetes 的,也有 Grafana 的
Resource 你要管理的一個東西。一張儀表板是一個、一個資料夾是一個
State 它記錄「我上次建了哪些東西」的檔案

為什麼我用 OpenTofu

Terraform 在 2023 年把授權從 MPL 改成 BUSL(Business Source License),那是一種有使用限制的授權。社群隨即分叉出 OpenTofu,維持 MPL,後來交給 Linux 基金會。

兩者的語法、指令、設定檔完全共用,Grafana 的 provider 也是 MPL,兩邊都能用。我這篇的指令是 tofu 開頭,你換成 terraform 一樣會動。

我選 OpenTofu 只是因為授權單純。但這件事值得知道,不然你照著別人 2023 年以前的文章做,會以為它們是同一個東西。

裝的時候踩了一個坑:brew install opentofu 直接失敗,因為 Homebrew 要從 ghcr.io 抓東西而我連不上。改抓官方的二進位檔,28 MB 的壓縮檔斷了兩次,最後用 curl -C - 續傳才完成。這種事寫出來不是抱怨,是因為它會發生在別人身上。

state 檔

state 是新手最容易踩的地方。 Terraform 靠它把「你寫的設定」和「線上實際的東西」對應起來。

如果你把 state 刪掉,它會以為什麼都還沒建,於是想再建一次——然後撞到已存在的東西報錯。反過來,如果線上的東西被別人刪了而 state 還記著,它會以為那個東西還在。

這句話我本來是當理論寫的,後來自己撞到了。為了重跑一次乾淨的 plan,我把資料夾從 state 移掉:

tofu state rm grafana_folder.observability

再 apply,第一件事就爆:

Error: failed to create folder: [POST /folders] createFolder (status 412): {}

412 是「前置條件不符」——資料夾已經存在。Grafana 上它好好的,只是 Terraform 不記得了。設定沒改、線上沒動,只有 state 被我弄丟,整個 apply 就停在第一步。

https://ithelp.ithome.com.tw/upload/images/20260928/201805705qWibQ5XBl.png

正確的處理不是去 Grafana 把資料夾刪掉重來,是把既有的東西接回 state:

tofu import grafana_folder.observability 1:observability

1:observability 就是前面那個「組織 ID 加 uid」。接回來之後 plan 就正常了。

這是 state 這個概念最實際的一課:Terraform 的世界觀完全建立在那個檔案上,它不會主動去線上核對有沒有漏掉什麼。

在本機它就是一個檔案 terraform.tfstate。多人協作時要放在共用的地方,不然兩份 state 會打架。

實際打開來看,我那個資料夾在裡面長這樣:

{
  "type": "grafana_folder",
  "name": "observability",
  "attributes": {
    "id": "1:observability",
    "org_id": "1",
    "title": "可觀測性",
    "uid": "observability",
    "url": "http://localhost:3000/dashboards/f/observability/..."
  }
}

注意 id 是 1:observability——組織 ID 加資料夾 uid。這串東西是 Terraform 認人的依據,不是我寫在 .tf 裡的名字。state 沒了,這個對應就沒了。

這個例子裡沒有敏感資料,但如果你管的是資料源,連線字串和密碼都會原封不動躺在裡面。所以第一件事是把它加進 .gitignore。

*.tfstate
*.tfstate.*
.terraform/

寫設定

terraform {
  required_version = ">= 1.6"
  required_providers {
    grafana = {
      source  = "grafana/grafana"
      version = "~> 4.0"   # 4.x 可以,5.0 不行
    }
  }
}

provider "grafana" {
  url = "http://localhost:3000"
}

~> 4.0 這個寫法的意思是「4.x 可以,5.0 不行」。init 之後實際裝到的是 v4.46.0——大版本鎖住、小版本讓它自己跟。

版本鎖定是 IaC 的基本功。 不鎖的話,同一份檔案今天和三個月後可能建出不同的東西,那就失去「可重現」的意義了。這跟昨天盤點出來「五個 chart 只有一個記了版本」是同一件事,只是這次我在寫的時候就記了。

init 還會產生一個 .terraform.lock.hcl,記下實際解析到的精確版本和雜湊值。那個檔案應該進 git——它是「這份設定被驗證過的環境」的證據。我的 .gitignore 一開始把它一起擋掉了,那是錯的,後來拿掉。

url 指向 kubectl port-forward 開在本機的埠,跟前面那隻 agent 一樣不進叢集。

帳密不寫在檔案裡,provider 會讀環境變數 GRAFANA_AUTH,格式是 帳號:密碼:

export GRAFANA_AUTH="admin:$(kubectl -n monitoring get secret kps-grafana \
  -o jsonpath='{.data.admin-password}' | base64 -d)"

然後是三個資源——一個資料夾、兩張儀表板:

resource "grafana_folder" "observability" {
  title = "可觀測性"
  uid   = "observability"
}

resource "grafana_dashboard" "checkout_sli" {
  folder      = grafana_folder.observability.uid
  config_json = file("${path.module}/dashboards/checkout-sli.json")
  overwrite   = true
}

這裡有一個小地方要注意:folder 給的是 uid 不是 id。我參考的舊範例寫的是 .id,現在官方範例是 .uid。這種欄位改名在 provider 裡很常見,照著舊文章抄會卡住。

overwrite = true 的意思是「已經有同 uid 的儀表板時覆蓋它,不要報錯」。我特地加了這行,因為那兩張圖現在確實已經存在。

init 和 plan

tofu init      # 下載 provider,每個目錄第一次跑
tofu plan      # 顯示「如果執行會做什麼」,不動手
tofu apply     # 真的做

plan 的輸出:

Plan: 3 to add, 0 to change, 0 to destroy.

三個都是 add。 但那兩張儀表板明明已經在 Grafana 上了。

原因是 state 是空的。Terraform 不是去問 Grafana「有沒有這張圖」,它是問自己的 state「我建過這張圖嗎」。沒建過,所以它要建。

https://ithelp.ithome.com.tw/upload/images/20260928/20180570H2RkcJcrhM.png

plan 最該養成習慣的用法是看有沒有意外的 destroy。 那代表你的設定跟線上差太多,Terraform 打算砍掉重建。在正式環境這可能是災難。今天沒有,因為我還沒建過任何東西。

apply 撞牆

Error: [POST /dashboards/db][400] postDashboardBadRequest
{"message":"Cannot save provisioned dashboard"}

https://ithelp.ithome.com.tw/upload/images/20260928/20180570dEiDLgCUS1.png

兩張儀表板都失敗,只有資料夾建起來了。

provisioned 這個字是線索。去翻 Grafana 那份 sidecar 產生的 provider 設定:

providers:
  - name: 'sidecarProvider'
    type: file
    allowUiUpdates: false
    options:
      path: /tmp/dashboards

allowUiUpdates: false。

一個布林值。從 ConfigMap provision 進去的儀表板是唯讀的——網頁上改不了、API 改不了、Terraform 也改不了。Grafana 認得出那張圖是誰送的,不讓第二個人動它。

這個錯誤比成功有價值

我原本準備寫的是另一件事。

Terraform 管理儀表板有一個很有名的麻煩:JSON 會漂移。你用檔案建好圖,然後有人在網頁上調一下,線上的 JSON 就跟檔案不一樣了;下次 plan 一堆差異。而且 Grafana 自己也會塞 version 之類的欄位進去,你什麼都沒改 plan 還是有東西。

常見的處理方式是用 ignore_changes 忽略某些欄位,或是接受「檔案是唯一事實來源、網頁上的修改隨時會被覆蓋」。

但 Grafana 的答案比這兩個都乾脆:不讓它漂移。

被 provision 的東西直接鎖成唯讀,於是「兩個權威來源」這個問題在源頭就不存在。我花了半天想解決的問題,這個生態系已經用一個布林值擋掉了。

代價是你得挑一邊——而挑哪一邊,現在有實際的判準了。

分工怎麼切

ConfigMap + sidecar OpenTofu
叢集重建後自動回來 ✅ ❌ 要另外跑一次
需要額外工具與 state 不用 要
防止有人在網頁上亂改 ✅ 直接鎖成唯讀 ❌ 只能事後發現
分資料夾 ❌ 預設全丟 General ✅
管使用者、權限、組織 ❌ ✅

我選 ConfigMap 管儀表板,理由是第一列:它跟叢集一起回來,不用記得多跑一個指令。 最後一天要砍掉叢集重建,少一個步驟就少一個會忘的地方。

而且那 26 張 chart 自己帶的儀表板本來就是用 ConfigMap 送的——這不是我發明的做法,是這個生態系的預設。

那 Terraform 管什麼?管 sidecar 做不到的事。

順帶一提資料源也適用同一條規則。Prometheus、Loki、Tempo 這三個資料源是在 kps 的 values 裡宣告的,chart 把它們變成一個帶 grafana_datasource=1 標籤的 ConfigMap,一樣由 sidecar 送進去、一樣被鎖成唯讀。所以我沒有把它們搬到 Terraform——已經在檔案裡的東西,換一個工具管不會變得更在檔案裡。

那 exemplar 那段設定呢?就是查指標時可以直接跳到 trace 的那個關聯。它現在是 kps values 裡的三行。如果那台 Grafana 是別人裝的、我只有 API 權限,那它就該由 Terraform 管。判準不是「哪個工具比較好」,是「這台 Grafana 是不是我用 Helm 裝的」。

所以今天 Terraform 實際管的只有一個東西:資料夾。

兩個工具靠一個名字接起來

Grafana 上現在有 28 張儀表板,26 張來自 chart,我的兩張混在 General 裡。sidecar 預設把所有圖丟進同一個地方,沒有分類。

分資料夾要兩邊配合。Helm values 這側打開註記支援:

grafana:
  sidecar:
    dashboards:
      folderAnnotation: grafana_folder
      provider:
        foldersFromFilesStructure: true

ConfigMap 那側加一個註記:

metadata:
  labels:
    grafana_dashboard: "1"
  annotations:
    grafana_folder: "可觀測性"

而那個資料夾是 Terraform 建的。升級完 Grafana 再看一次:

uid=agent-observability    folder=可觀測性
uid=checkout-sli           folder=可觀測性

https://ithelp.ithome.com.tw/upload/images/20260928/20180570kujOtFE91W.png

兩張圖進去了。再跑一次 plan 確認 Terraform 還認得那個資料夾:

No changes. Your infrastructure matches the configuration.

https://ithelp.ithome.com.tw/upload/images/20260928/20180570EwcYOb7Rop.png

它們不是搶同一個東西,是各管一段,靠「可觀測性」這個名字對上。 這比我原本想的「二選一」好,也比「兩個都管同一張圖」實際。

小結

  • OpenTofu 是 Terraform 的 MPL 分支,語法指令通用,選它只是授權單純
  • 被 provision 的儀表板是唯讀的,Grafana 在源頭就禁止兩個權威來源
  • 儀表板留給 ConfigMap(跟叢集一起回來),Terraform 管它做不到的事

明天處理叢集裡面的東西:把五個 helm install 收成一個檔案,順便把昨天那三個洞補起來。


上一篇
Day 27:手動裝完就該砍掉重練:IaC 對可觀測性系統的意義
下一篇
Day 29:用 Helm / Helmfile 把整套 stack 版本化
系列文
從看得到到看得懂:30 天在自架 K8s 上實踐可觀測性與告警 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言