DevOps

【Grafana Foundation SDK】 부제: Declarative Observability

라즐리 2026. 9. 27. 19:57

개요

K8S에는 3가지 철학이 존재한다.

Immutable, Declarative, Self healing으로 각각 뜻은 다음과 같다.

요즘엔 이런 참고자료를 AI가 만들어주니 참 좋다 ㅋㅋ

인프라의 불변성, 선언성, 자가회복성 이렇게 3가지이다.

사실 인프라에서는 이 3가지만 잘 가지고 있어도 현대적인 인프라 (대기업에서는 DX라고들 하죠?)를 구성하는데 큰 지장이 없다.

 

하지만, 이러한 철학은 인프라 전반에 녹이게 된다면 정말 아름다운 아키텍처를 만들 수 있게 된다.

복잡한 기술이 아닌 이미 상용에있는 기술을 통해서도 충분히 구현이 가능하다.

 

주제랑 맞지 않게 왜 갑자기 K8S의 철학을 들이미는지는 해당 포스트를 천천히 읽다보면 이해가 갈 것이다.

Declarative

개인적인 생각이지만 K8S 운영/설계시 고려해야하는 순서는 Self Healing → Immutable → Declarative 라고 생각은 한다.

하지만 이번에 다뤄볼 주제는 Observability이기 때문에 Declarative를 우선으로 뽑아보았다.

 

많은 기업에서 Observability Tool을 도입하고 운영할때 다음과 같은 순서로 운영이 되게된다. (나도 솔직히 찔린다.)

  • Observability 확보라는 명분하에 Prometheus, Grafana를 도입한다.
  • Log 수집을 위해 Loki&Tempo나 ELK 스택을 고민하여 선정한다.
  • LGTM 스택도 아니고 그렇다고 완전한 ELK스택도 아닌 스택을 K8S내부에 구성한다.
  • 이후 개발자가 원하는 대시보드를 Grafana에서 만들어서 본다.

나쁜 방법은 아니다. 다만 여기서 가장 큰 문제가 있다.

만약 해킹등의 사유로 AWS 계정이 잠기게 되어 기존 인프라를 복원해야한다면?

요즘엔 다들 GitOps, Terraform등을 통해 많이들 큼직한 인프라를 구성을 해둔다.

하지만 정작 중요한 Observability는 선언하는 형태로 사용하지 않는다.

 

해킹은 먼 나라 이야기라고 생각된다면 최근에 올라온 하나의 뉴스기사를 보면 알 수 있다.

https://www.boannews.com/news/articleView.html?idxno=142912

 

유럽연합 집행위원회 AWS 계정 뚫렸다... 공공 플랫폼 데이터 유출 비상 - 보안뉴스

유럽연합 집행위원회(EC)가 아마존웹서비스(AWS) 클라우드 계정 해킹으로 공공 웹 플랫폼 데이터가 유출되는 초유의 사태를 겪었으나, 철저한 망 분리 전략을 통해

www.boannews.com

유럽 연합의 집행 위원회의 AWS가 털리는 사태도 발생했다.

큰 곳이기 때문에 화두가 된 것이지 작은 곳들은 지금 이 글을 보는 중에도 털리고 있다고 생각해야한다.

 

결국 인프라를 담당한다면 보안이 먼 이야기가 아니라는 가정을 해야한다.

당연히 보안관리를 잘해야하지만 여기서 다루는 내용은 그렇게 시시콜콜한 철학을 전파하는 이야기는 아니다.

 

해킹이 아니더라도 ISMS, 전자금융감독규정 등의 이유로 인해 기존 계정에서 마이그레이션을 해야할때도 있다.

적어도 데이터는 3-2-1 백업 전략등이 보편적으로 알려져있는 만큼 데이터는 백업을 하지만 정작 자주 보는 대시보드는 선언형으로 관리가 되고 있지 않다.

 

그래서 이번에는 유실되기 너무나 쉽고, 다시 만들기에는 공수가 들어가는 계륵같은 존재를 마이그레이션 하기 보다 새롭게 편하게 만드는 방법에 대해서 작성해보려고 한다.

 

전제

스택은 다음으로 고정하도록 하겠다.

  • IaC Tool = Terraform
  • Observability Tool = Loki + Prometheus + Grafana
  • Environment = Private Local HomeLab

Declarative Observability IaC

DashBoard

Grafana에서 가장 많이 사용하는 대시보드 기능이다.

이를 기업 거버넌스, 규정하에 비슷한 포맷, 우리의 Metric등을 이용해서 편하게 구성하기 위해서

https://grafana.com/docs/grafana/latest/as-code/observability-as-code/foundation-sdk/ 

 

Foundation SDK | Grafana documentation

Get started with the Grafana Foundation SDK The Grafana Foundation SDK is a set of tools, types, and libraries that let you define Grafana dashboards and resources using strongly typed code. By writing your dashboards as code, you can: Leverage strong typi

grafana.com

Grafana의 공식 SDK인 Foundation-SDK라는 것을 사용해볼 것 이다.

Go, Python, TS 등등 다양한 언어를 지원하지만 일반적으로는 Go, TS를 사용한다.

뭐...다들 둘 중 하나는 쓸 줄 알거라는 전제하에 진행해볼 것이다.

둘다 못해도 AI가 있다 ㅎㅎ

 

우선 이게 뭔지 간략하게 설명하면, Grafana 대시보드를 생성하기 쉽게해주는 Json코드 발사대 SDK라고 이해하면 된다.

그러면 이걸 굳이 써야하나? AI 딸깍으로 걍 Json 발사하면 안되나?

대부분의 기업에서는 기업에 거버넌스라는 것이 존재하고 Custom Metric들도 존재할 수 있다.

이런 값들은 공통 옵션으로 취급하여 헬퍼 함수로 빼둔다면 기업 거버넌스에 맞게끔 동작시킬 수 있다.

 

리뷰를 통해 기업 공통 옵션에 대한 헬퍼 함수 사용 여부를 CI레벨에서 검증하고 Terraform으로 이식할 수 있게 Workflow화 하는 것도 방법이다.

https://github.com/High-PO/Grafana-Dashboard-Examples/tree/main/60-posted

 

Grafana-Dashboard-Examples/60-posted at main · High-PO/Grafana-Dashboard-Examples

Contribute to High-PO/Grafana-Dashboard-Examples development by creating an account on GitHub.

github.com

우선 예제는 여기에 올려두었다.

 

아래는 동작 순서이다.

Go 코드 작성 → go run → 대시보드 JSON 생성 → Grafana에 반영 (지금은 수동 Import, 이후 Terraform)

 

사용하기 위한 조건이다.

도구 버전 용도
Go 1.24 이상 대시보드 코드 작성 / JSON 생성
Grafana 12.x (필자 홈랩은 12.0.1) 결과 확인
(선택) Docker - 로컬에 Grafana를 띄워서 테스트

 

우선 Hand-On Lab 처럼 코드블럭을 작성해두었다.

 

Go를 먼저 설치해야하는데 공식 사이트에서 확인하기 귀찮으면 아래 적어둔 스크립트대로 하면 된다.

https://go.dev/doc/install

 

Download and install - The Go Programming Language

Documentation Download and install Download and install Download and install Go quickly with the steps described here. For other content on installing, you might be interested in: Download Go installation Select the tab for your computer's operating system

go.dev

# macOS
brew install go

# Linux (공식 바이너리)
curl -LO https://go.dev/dl/go1.24.7.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.24.7.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrc

go version

 

그리고 이제 디렉토리를 만들고 SDK를 설치해보자

mkdir grafana-dashboards && cd grafana-dashboards
go mod init github.com/<계정>/grafana-dashboards

go get github.com/grafana/grafana-foundation-sdk/go@v0.0.20

이제 간단하게 CPU 사용률을 보여주는 대시보드를 만들어 보자

package main

import (
	"encoding/json"
	"fmt"

	"github.com/grafana/grafana-foundation-sdk/go/cog"
	"github.com/grafana/grafana-foundation-sdk/go/common"
	"github.com/grafana/grafana-foundation-sdk/go/dashboard"
	"github.com/grafana/grafana-foundation-sdk/go/prometheus"
	"github.com/grafana/grafana-foundation-sdk/go/timeseries"
	"github.com/grafana/grafana-foundation-sdk/go/units"
)

func main() {
	// 데이터소스는 UID를 박지 않고 ${datasource} 변수를 참조한다
	ds := common.DataSourceRef{
		Type: cog.ToPtr("prometheus"),
		Uid:  cog.ToPtr("${datasource}"),
	}

	builder := dashboard.NewDashboardBuilder("Hello Foundation SDK").
		Uid("hello-sdk").
		Tags([]string{"sdk", "example"}).
		Refresh("30s").
		Time("now-1h", "now").
		WithVariable(dashboard.NewDatasourceVariableBuilder("datasource").
			Label("Datasource").
			Type("prometheus")).
		WithRow(dashboard.NewRowBuilder("Node")).
		WithPanel(timeseries.NewPanelBuilder().
			Title("노드 CPU 사용률").
			Datasource(ds).
			Unit(units.PercentUnit).
			Min(0).
			Max(1).
			Span(24).
			Height(8).
			WithTarget(prometheus.NewDataqueryBuilder().
				Datasource(ds).
				Expr(`1 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))`).
				LegendFormat("{{instance}}").
				RefId("A")))

	dash, err := builder.Build()
	if err != nil {
		panic(err)
	}

	out, _ := json.MarshalIndent(dash, "", "  ")
	fmt.Println(string(out))
}
go run . > hello.json

이렇게 Go를 실행할때 나오는 json stdout값을 .json 확장자로 저장하게 된다면 대시보드가 뚝딱 나오게 된다.

위 코드를 Go를 모르는 사람이 보더라도 한가지 패턴이 보이게 될 것이다.

xxx.NewXxxBuilder(...).   // 1. 빌더 생성
    옵션A(...).            // 2. 메서드 체이닝으로 옵션 설정
    옵션B(...).
    Build()               // 3. 최종 객체 생성 (대시보드에서만 직접 호출)

죄다 이 패턴이다.

빌더라는 것을 만들고 거기에 옵션 옵션 옵션 이래 붙이고 마지막에 build 해버리는 말 그대로 사용자 친화적인 포맷이다.

 

그리고 좋은 점이 하나 더 있다.

builder.
	WithRow(dashboard.NewRowBuilder("CPU")).
	WithPanel(panelA.Span(8).Height(8)).
	WithPanel(panelB.Span(8).Height(8)).
	WithPanel(panelC.Span(8).Height(8)).  // 8+8+8 = 24 → 여기서 줄바꿈
	WithRow(dashboard.NewRowBuilder("Memory")).
	...
 

gridPos(x, y) 좌표를 직접 계산할 필요가 없다.
Grafana 그리드는 가로 24칸인데, Span(너비)과 Height(높이)만 지정하면 SDK가 왼쪽에서 오른쪽으로 채우고, 24칸이 차면 다음 줄로 넘긴다.
UI에서 패널을 드래그하며 맞추던 작업이 없어지는 게 체감상 크게 다가올 것이다.

또한 옵션 값이 전부 상수로 정의돼 있어서 IDE 자동완성만 따라가면 된다는 점에서 초기에 적응하기도 편할 것이다.

엥? 난 이미 쓰는 대시보드가 있는딩?
이건 우짬? 버려 걍?

나 같은 생각을 하는 사람이 있을 거 같아서 아래 내용도 해보았다.

바로 기존 대시보드 마이그레이션이다.

https://github.com/High-PO/Grafana-Dashboard-Examples/blob/main/60-posted/foundation-sdk/go/tools/convert/main.go

 

Grafana-Dashboard-Examples/60-posted/foundation-sdk/go/tools/convert/main.go at main · High-PO/Grafana-Dashboard-Examples

Contribute to High-PO/Grafana-Dashboard-Examples development by creating an account on GitHub.

github.com

이전에 올려두었던 FinOps 대시보드를 GO언어로 마이그레이션 해보았다.

아래는 예제이다.

package main

import (
	"encoding/json"
	"fmt"
	"os"

	"github.com/grafana/grafana-foundation-sdk/go/cog/plugins"
	"github.com/grafana/grafana-foundation-sdk/go/dashboard"
)

func main() {
	plugins.RegisterDefaultPlugins() // 패널/쿼리 타입을 해석하기 위해 필수

	raw, _ := os.ReadFile(os.Args[1])
	dash := dashboard.Dashboard{}
	if err := json.Unmarshal(raw, &dash); err != nil {
		panic(err)
	}
	fmt.Println(dashboard.DashboardConverter(dash))
}
go run . "FinOps.json" > draft.go.txt

위 코드를 사용하여 기존 대시보드의 json파일을 go언어로 변경해두는게 가능하다..!

쉘 스크립트를 사용하여 여러번 사용해 기존 대시보드를 모오두 코드화 해버리면? 이제는 대시보드를 잃어버리는 것에 대한 두려움이 0에 수렴하게 될 것이다.

 

조금 성숙한 조직이라면  데이터소스, Stat 기본값, 범례 스타일 같은 걸 common.Stat() 같은 함수로 한 번만 정의해두면 좋다.

// internal/common/common.go — "우리 팀 기본값"은 여기서 한 번만 정의한다

// 데이터소스는 UID를 박지 않고 ${datasource} 변수를 참조 → 로컬/홈랩/운영 어디서든 동작
func Datasource() sdkcommon.DataSourceRef {
	return sdkcommon.DataSourceRef{
		Type: cog.ToPtr("prometheus"),
		Uid:  cog.ToPtr("${datasource}"),
	}
}

// Stat 패널 기본형: lastNotNull, threshold 색상, area 그래프 — 팀 표준
func Stat(title, unit string) *stat.PanelBuilder {
	return stat.NewPanelBuilder().
		Title(title).
		Datasource(Datasource()).
		Unit(unit).
		ColorScheme(ColorMode(dashboard.FieldColorModeIdThresholds)).
		ColorMode(sdkcommon.BigValueColorModeValue).
		GraphMode(sdkcommon.BigValueGraphModeArea).
		ReduceOptions(sdkcommon.NewReduceDataOptionsBuilder().
			Values(false).
			Calcs([]string{"lastNotNull"}))
}

이게 곧 앞에서 언급하였던 "기업 거버넌스에 맞는 비슷한 포맷"의 기본이 되도록 구성이 가능한 형태라고 볼 수 있다.

팀 표준이 코드로 강제되는 셈으로 이후 AI를 통해서 대시보드를 찍어내는 환경이 되어도 팀의 컨벤션이 강제로 유지가 될 수 있다.

 

사용할때는? 아래와 같이 사용하면 된다.

// Before: 패널마다 옵션 12줄
// After:
common.Stat("월 비용", "currencyKRW").WithTarget(common.Query(`sum(...)`))

stat.NewPanelBuilder()를 직접 부르는 PR은 CI에서 막으면, 팀 표준이 리뷰어 눈이 아니라 코드로 강제될 수도 있다.

Terraform

이제는 이걸 Terraform으로 배포를 해보도록 하자.

아니 코드로 관리하면 된거지 뭘 또 Terraform을 쓰는지 이해가 안간다면 Terraform을 통한 IaC의 뽕을 아직 느끼지 못했을 가능성이 높다.

사실 장난스러운 말이지만 이걸 업무적으로 치환할 경우 배포를 Engineer가 직접하지 않음으로서 Infra에 대한 CI/CD 구성과 선언형 아키텍처가 완전하게 만들어진다고 보면 된다.

 

우선 테라폼 예제를 올려두긴 했으나, 해당 Terraform은 조건이 좀 많아서 코드 블럭만 참고를 하면 좋다.

https://github.com/High-PO/Grafana-Dashboard-Examples/tree/main/60-posted/terraform

 

Grafana-Dashboard-Examples/60-posted/terraform at main · High-PO/Grafana-Dashboard-Examples

Contribute to High-PO/Grafana-Dashboard-Examples development by creating an account on GitHub.

github.com

locals {
  # dist/*.json 을 전부 읽어서 파일명 = 리소스 키로 사용
  dashboards = {
    for file in fileset(var.dashboards_dir, "*.json") :
    trimsuffix(file, ".json") => "${var.dashboards_dir}/${file}"
  }
}

resource "grafana_folder" "sdk" {
  uid   = "foundation-sdk"
  title = "Foundation SDK"
}

resource "grafana_dashboard" "this" {
  for_each    = local.dashboards
  folder      = grafana_folder.sdk.uid
  config_json = file(each.value)
  overwrite   = var.overwrite   # 기본 false
}
go run . > dist/hello.json 으로 떨어뜨린 JSON을 fileset으로 전부 읽으니까, 대시보드가 늘어나도 Terraform 코드는 안 건드린다.
결국 구조상 dist/에 파일 하나 추가 = 리소스 하나 추가가 되는 형태가된다.

흐름은 코드 수정 → make build → PR → merge → terraform plan → apply 순으로 진행이 된다.

UI에서 손으로 고치면 다음 plan에 되돌리는 diff로 잡히기 때문에 IaC를 강요하는 가드레일의 목적이 어느정도는 생기게 된다.

make build는 cd foundation-sdk/go && go run . -out ../../dist 와 동일하다. (Makefile은 레포에 있으니 참고)

 

해당 방식으로 실제로 배포한 모습은 다음과 같다.

배포가 완료되면 이제 확인을 위해 Grafana로 접속한다.

그러면 Foundation SDK라는 폴더가 생기게 되고 여기에 이제 아까 Go로 만든 대시보드와 마이그레이션한 대시보드 두개가 존재한다.

그러면 정말 아무런 정보없이 시작한 Go언어 포맷이지만? 이렇게 깔끔하게 대시보드가 만들어지는 것을 볼 수 있다.

마이그레이션한 대시보드는?

정말 놀라울 정도로 똑같은것을 알 수 있다.

근데? 마이그레이션이 그대로 되지는 않는 영역도 분명히 존재한다.

  • 컨버터 출력은 초안이다. draft.go.txt를 그대로 go run 하는 게 아니라, 반복되는 데이터소스·Stat 옵션을 헬퍼로 빼고 gridPos를 지워 Span/Height만 남기는 정리가 들어간다.
  • 패널 ID가 원본과 달라진다. SDK는 ID를 안 붙여서 common.Build()에서 1, 2, 3… 순서로 부여하기 때문에 그렇다.
  • schemaVersion: SDK v0.0.20 기본값 42, 홈랩 12.0.1은 41. 안 맞추면 매 plan마다 의미 없는 diff가 뜬다.

그래서 이를 해결하기 위해서 "똑같다"는 것을 눈으로 안 보고 tools/compare로 확인했다.

UI export JSON과 SDK JSON은 키 순서·기본값 표기·pluginVersion이 달라 그냥 diff하면 노이즈라, 쿼리·단위·임계값·배치·변수만 뽑아 비교하는 도구다.

Git Sync는? grafanalib/Grafonnet은?

사실 이 글을 쓰기전 이미 이전 회사에서 Grafana 12에 Git Sync가 들어왔다는 사실을 알고 도입을 검토하긴 했었다.

그렇기에 Terraform과 Git Sync 중에 고민을 많이하였었다.

그런데 UI에서 만든 대시보드를 Git 레포에 JSON으로 동기화해주는 기능이라, "유실 방지"만 목적이면 그걸로 충분하다고 생각한다.

다만 Git Sync는 JSON을 저장하는 거지 JSON을 만드는 방법을 바꾸진 않는다.

 

패널 옵션 표준화, 커스텀 메트릭 헬퍼, PromQL 문법 검사 같은 건 여전히 코드 레벨이 필요하다.

그래서 이 글은 "만드는 쪽"인 SDK를 다루고, 저장/배포는 Terraform으로 한다.

Git Sync를 배포 경로로 쓰고 SDK로 생성만 하는 조합도 당연히 된다.


이전에 쓰던 grafanalib(Python)이나 Grafonnet(Jsonnet)도 같은 목적의 도구인데, Foundation SDK는 Grafana 공식이고 스키마가 Grafana 버전이랑 같이 올라간다는 게 차이다.

 

grafanalib에서 옮기는 얘기는 천천히....풀어볼 수 있도록 하겠다.

정리

  1. Terraform을 통해서 선언형 인프라를 구성하는 습관을 들이자
  2. Observability도 예외는 아니다.
  3. Grafana HelmChart만을 GitOps나 Helm Values파일로 관리하지 말고 SDK를 이용해 IaC로 관리하자

끝맺음

솔직히 난잡한 글인 것은 확실하다.

주기적으로 조정하면서 글의 가독성을 올리도록 하겠다.