Overview
Kubernetes 클러스터를 운영하다 보면 Git에 정의된 manifest와 실제 클러스터 상태가 달라지는 현상, 이른바 Cluster Drift가 발생한다. 누군가 `kubectl edit` 으로 replicas를 수동 변경하거나, `kubectl scale` 로 급하게 스케일링했을 때, Git 소스와 클러스터 상태 사이에 괴리가 생긴다.
기존 `kubectl diff` 로도 비교가 가능하지만, YAML 파일만 지원하고, Helm/Kustomize를 직접 다룰 수 없으며, 출력이 raw unified diff라 가독성이 떨어진다.
이 글에서는 이런 문제를 해결하기 위해 직접 만든 두 가지 도구를 소개한다.
- kube-diff — Go로 작성한 CLI 도구. Plain YAML, Helm chart, Kustomize overlay를 클러스터와 비교
- kube-diff-action — GitHub Actions에서 kube-diff를 사용할 수 있는 Composite Action
| `kubectl diff` | `kube-diff` | |
| Input | YAML files only | Helm / Kustomize / plain YAML |
| Output | Raw unified diff | Per-resource colorized diff + summary |
| New resources | Full content dump | NEW label |
| Deleted detection | Not supported | Detects resources only in cluster |
| CI integration | Exit code only | JSON / Markdown report output |
| Filtering | None | Namespace, kind, label selector filter |

1. 프로젝트 구조
kube-diff (CLI)
kube-diff/
├── cmd/
│ ├── main.go # 엔트리포인트
│ └── cli/
│ ├── root.go # Cobra root command
│ ├── file.go # file subcommand
│ ├── helm.go # helm subcommand
│ ├── kustomize.go # kustomize subcommand
│ ├── version.go # version subcommand
│ └── run.go # 공유 비교 로직
├── internal/
│ ├── source/ # Manifest loaders (file, helm, kustomize)
│ ├── cluster/ # K8s dynamic client fetcher
│ ├── diff/ # Normalization & unified diff
│ └── report/ # Color/JSON/Markdown output
├── examples/
│ ├── file/ # Plain YAML 예제
│ ├── helm/ # Helm chart 예제
│ └── kustomize/ # Kustomize overlay 예제
├── scripts/
│ ├── demo.sh # 데모 스크립트
│ └── demo-clean.sh # 데모 정리
├── .goreleaser.yml # GoReleaser 설정
└── Makefile
kube-diff-action (GitHub Action)
kube-diff-action/
├── action.yml # Composite Action 정의
├── scripts/
│ ├── install.sh # kube-diff 바이너리 설치
│ ├── run.sh # kube-diff 실행 & output 설정
│ └── comment.sh # PR 코멘트 생성/업데이트
└── .github/workflows/
├── ci.yml # CI (ShellCheck, kind cluster 테스트)
├── release.yml # 릴리스 자동화
└── use-action.yml # Smoke Test (릴리스된 액션 검증)
2. 핵심 설계 포인트
2.1 Kubernetes Dynamic Client
특정 리소스 타입에 의존하지 않고, `unstructured.Unstructured` 를 사용하는 dynamic client로 모든 종류의 Kubernetes 리소스를 다룬다.
// internal/cluster/fetcher.go
func (f *Fetcher) Get(ctx context.Context, apiVersion, kind, namespace, name string) (*unstructured.Unstructured, error) {
gvr, err := f.resolveGVR(apiVersion, kind)
if err != nil {
return nil, err
}
var resource *unstructured.Unstructured
if namespace != "" {
resource, err = f.client.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
} else {
resource, err = f.client.Resource(gvr).Get(ctx, name, metav1.GetOptions{})
}
return resource, err
}
2.2 공유 비교 로직
file, helm, kustomize 세 가지 서브커맨드가 모두 동일한 `runDiff()` 함수를 공유한다. Source 인터페이스만 다르게 주입하면 된다.
// cmd/cli/run.go — 핵심 흐름
func runDiff(cmd *cobra.Command, src source.Source) error {
// 1. 로컬 리소스 로드
resources, err := src.Load()
// 2. 필터링 (namespace, kind, label selector)
// ...
// 3. 클러스터에서 대응하는 리소스 조회
fetcher, _ := cluster.NewFetcher(kubeconfig, kubeContext)
for _, r := range resources {
clusterObj, err := fetcher.Get(ctx, r.APIVersion, r.Kind, r.Namespace, r.Name)
// 4. diff 비교
result, _ := diff.Compare(r.Object, clusterObj)
results = append(results, result)
}
// 5. 리포트 출력 (color, plain, json, markdown)
summary := report.NewSummary(results)
// ...
}
2.3 Kubernetes 기본값 정규화
클러스터는 리소스에 다양한 기본값을 자동 추가한다. 이를 그대로 비교하면 false positive가 대량 발생한다.
예를 들어, Deployment를 apply하면 클러스터가 자동으로 추가하는 필드들:
- `spec.progressDeadlineSeconds: 600`
- `spec.revisionHistoryLimit: 10`
- `spec.strategy.type: RollingUpdate`
- `spec.template.spec.dnsPolicy: ClusterFirst`
- `spec.template.spec.restartPolicy: Always`
- Container의 `terminationMessagePath`, `terminationMessagePolicy`
- Container port의 `protocol: TCP`
- 등등...
이런 기본값들을 Kind별로 정규화하여 제거한다.
// internal/diff/normalize.go
func Normalize(obj *unstructured.Unstructured) *unstructured.Unstructured {
// 공통 메타데이터 제거 (managedFields, uid, resourceVersion 등)
// ...
// Kind별 기본값 제거
switch kind {
case "Deployment", "StatefulSet":
normalizeDeploymentSpec(spec) // progressDeadlineSeconds, strategy 등
case "Service":
normalizeServiceSpec(spec) // clusterIP, sessionAffinity 등
case "Namespace":
normalizeNamespaceSpec(obj) // spec.finalizers 등
case "Pod":
normalizePodSpec(spec)
case "Job":
normalizeJobSpec(spec)
case "DaemonSet":
normalizeDaemonSetSpec(spec)
}
return normalized
}
- 이 정규화 덕분에 실제로 의미 있는 차이만 diff에 표시된다.
2.4 Exit Code 설계
| Code | Meaning |
| 0 | No changes detected |
| 1 | Changes detected |
| 2 | Error occurred |
- CI에서 exit code 1은 "drift 있음"이지 에러가 아니므로, GitHub Action에서는 exit code 0과 1 모두 성공으로 처리하고, 2만 실패로 간주한다.
3. 샘플 애플리케이션
데모에 사용한 샘플 리소스들이다.
3.1 Plain YAML (file mode)
Namespace
# examples/file/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: kube-diff-demo
ConfigMap
# examples/file/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-config
namespace: kube-diff-demo
data:
APP_ENV: production
LOG_LEVEL: info
MAX_CONNECTIONS: "100"
Deployment
# examples/file/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
namespace: kube-diff-demo
labels:
app: demo-app
spec:
replicas: 2
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: app
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
envFrom:
- configMapRef:
name: demo-config
Service
# examples/file/service.yaml
apiVersion: v1
kind: Service
metadata:
name: demo-app
namespace: kube-diff-demo
spec:
selector:
app: demo-app
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP
3.2 Helm Chart (helm mode)
Chart.yaml
# examples/helm/demo-chart/Chart.yaml
apiVersion: v2
name: demo-chart
description: Demo Helm chart for kube-diff examples
version: 0.1.0
type: application
values.yaml
# examples/helm/demo-chart/values.yaml
replicaCount: 2
image: nginx:1.25
namespace: kube-diff-demo
config:
APP_ENV: production
LOG_LEVEL: info
MAX_CONNECTIONS: "100"
templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-app
namespace: {{ .Values.namespace }}
labels:
app: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: app
image: {{ .Values.image }}
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
namespace: {{ .Values.namespace }}
data:
{{- range $key, $val := .Values.config }}
{{ $key }}: {{ $val | quote }}
{{- end }}
templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-svc
namespace: {{ .Values.namespace }}
spec:
selector:
app: {{ .Release.Name }}
ports:
- port: 80
targetPort: 80
type: ClusterIP
values-drift.yaml (의도적으로 다른 값)
# examples/helm/values-drift.yaml
replicaCount: 3
image: nginx:1.26
namespace: kube-diff-demo
config:
APP_ENV: staging
LOG_LEVEL: debug
MAX_CONNECTIONS: "200"
NEW_KEY: "added"
3.3 Kustomize (kustomize mode)
base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kube-diff-demo
resources:
- configmap.yaml
- deployment.yaml
- service.yaml
overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kube-diff-demo
resources:
- ../../base
patches:
- target:
kind: Deployment
name: demo-app
patch: |
- op: replace
path: /spec/replicas
value: 3
- op: replace
path: /spec/template/spec/containers/0/image
value: nginx:1.26
- target:
kind: ConfigMap
name: demo-config
patch: |
- op: replace
path: /data/LOG_LEVEL
value: debug
- op: add
path: /data/NEW_KEY
value: added
4. CLI 사용법과 데모
4.1 설치
# Homebrew
brew install somaz94/tap/kube-diff
# Krew (kubectl plugin)
kubectl krew install diff2
# Binary
curl -sL https://github.com/somaz94/kube-diff/releases/latest/download/kube-diff_linux_amd64.tar.gz | tar xz
sudo mv kube-diff /usr/local/bin/
# From source
go install github.com/somaz94/kube-diff/cmd@latest
4.2 기본 사용법
# Plain YAML 비교
kube-diff file ./manifests/ -n production
# Helm chart 비교
kube-diff helm ./my-chart --values values-prod.yaml --release my-release -n production
# Kustomize overlay 비교
kube-diff kustomize ./overlays/production -n production
4.3 필터링
# Kind 필터
kube-diff file ./manifests/ -n production -k Deployment,Service
# Label selector 필터
kube-diff file ./manifests/ -n production -l app=nginx,env=prod
# 조합
kube-diff file ./manifests/ -n production -k Deployment -l app=nginx
4.4 출력 형식
# Colorized (기본)
kube-diff file ./manifests/ -n production
# JSON
kube-diff file ./manifests/ -n production -o json
# Markdown
kube-diff file ./manifests/ -n production -o markdown
# Summary only
kube-diff file ./manifests/ -n production -s
4.5 데모 실행 결과
`make demo-all` 명령으로 전체 데모를 실행할 수 있다. 아래는 주요 Phase별 결과이다.
Phase 2 — Drift 없는 상태
클러스터에 manifest를 배포한 직후 비교하면 모든 리소스가 unchanged로 나온다.
✓ OK ConfigMap/demo-config (namespace: kube-diff-demo)
✓ OK Deployment/demo-app (namespace: kube-diff-demo)
✓ OK Namespace/kube-diff-demo
✓ OK Service/demo-app (namespace: kube-diff-demo)
Summary: 4 resources — 4 unchanged
Phase 3 — 클러스터에 수동 변경 적용
# 수동으로 replicas 변경
kubectl scale deploy/demo-app --replicas=5 -n kube-diff-demo
# ConfigMap에 새 키 추가
kubectl patch configmap demo-config -n kube-diff-demo --type merge -p '{"data":{"DEBUG_MODE":"true"}}'
Phase 4 — Drift 감지
~ CHANGED ConfigMap/demo-config (namespace: kube-diff-demo)
--- cluster
+++ local
@@ -1,7 +1,6 @@
apiVersion: v1
data:
APP_ENV: production
- DEBUG_MODE: "true"
LOG_LEVEL: info
MAX_CONNECTIONS: "100"
~ CHANGED Deployment/demo-app (namespace: kube-diff-demo)
--- cluster
+++ local
@@ -6,7 +6,7 @@
name: demo-app
namespace: kube-diff-demo
spec:
- replicas: 5
+ replicas: 2
selector:
✓ OK Namespace/kube-diff-demo
✓ OK Service/demo-app (namespace: kube-diff-demo)
Summary: 4 resources — 2 changed, 2 unchanged
- 클러스터에서 수동으로 추가한 `DEBUG_MODE` 와 변경한 `replicas: 5` 가 정확히 감지된다.
Phase 7 — JSON 출력
{
"total": 4,
"changed": 2,
"new": 0,
"deleted": 0,
"unchanged": 2,
"resources": [
{
"kind": "ConfigMap",
"name": "demo-config",
"namespace": "kube-diff-demo",
"status": "changed"
},
{
"kind": "Deployment",
"name": "demo-app",
"namespace": "kube-diff-demo",
"status": "changed"
},
{
"kind": "Namespace",
"name": "kube-diff-demo",
"status": "unchanged"
},
{
"kind": "Service",
"name": "demo-app",
"namespace": "kube-diff-demo",
"status": "unchanged"
}
]
}
Phase 7 — Markdown 출력
## kube-diff Report
**4** resources — **2** changed, 2 unchanged
| Status | Resource | Namespace |
|--------|----------|-----------|
| CHANGED | ConfigMap/demo-config | kube-diff-demo |
| CHANGED | Deployment/demo-app | kube-diff-demo |
| OK | Namespace/kube-diff-demo | - |
| OK | Service/demo-app | kube-diff-demo |
5. GitHub Action 구현
5.1 왜 Composite Action인가
kube-diff-action은 Docker Action이 아닌 Composite Action으로 구현했다. 이유는 단순하다.
- kube-diff 바이너리를 다운로드해서 실행하는 것이 전부
- Docker 이미지를 빌드하거나 유지할 필요 없음
- Runner의 kubeconfig과 kubectl을 직접 사용 가능
- 실행 속도가 빠름
5.2 action.yml
name: kube-diff Action
description: Compare local Kubernetes manifests against live cluster state and report drift
author: somaz94
inputs:
source:
description: 'Source type: file, helm, or kustomize'
required: true
path:
description: 'Path to manifests, Helm chart, or Kustomize overlay'
required: true
values:
description: 'Helm values files (comma-separated)'
required: false
default: ''
release:
description: 'Helm release name'
required: false
default: 'release'
namespace:
description: 'Filter by namespace'
required: false
default: ''
kind:
description: 'Filter by resource kind (comma-separated)'
required: false
default: ''
selector:
description: 'Filter by label selector (e.g., app=nginx,env=prod)'
required: false
default: ''
output:
description: 'Output format: color, plain, json, markdown'
required: false
default: 'markdown'
summary-only:
description: 'Show summary only without diff details'
required: false
default: 'false'
comment:
description: 'Post diff result as PR comment'
required: false
default: 'true'
version:
description: 'kube-diff version to install (e.g., v0.2.1). Default: latest'
required: false
default: 'latest'
token:
description: 'GitHub token for PR comments'
required: false
default: ${{ github.token }}
outputs:
result:
description: 'Diff output text'
value: ${{ steps.diff.outputs.result }}
exit-code:
description: 'Exit code from kube-diff (0=no changes, 1=changes detected)'
value: ${{ steps.diff.outputs.exit-code }}
has-changes:
description: 'Whether changes were detected (true/false)'
value: ${{ steps.diff.outputs.has-changes }}
runs:
using: composite
steps:
- name: Install kube-diff
shell: bash
env:
VERSION: ${{ inputs.version }}
run: ${{ github.action_path }}/scripts/install.sh
- name: Run kube-diff
id: diff
shell: bash
env:
INPUT_SOURCE: ${{ inputs.source }}
INPUT_PATH: ${{ inputs.path }}
# ... (환경변수로 input 전달)
run: ${{ github.action_path }}/scripts/run.sh
- name: Comment on PR
if: inputs.comment == 'true' && github.event_name == 'pull_request'
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
DIFF_RESULT: ${{ steps.diff.outputs.result }}
HAS_CHANGES: ${{ steps.diff.outputs.has-changes }}
run: ${{ github.action_path }}/scripts/comment.sh
5.3 install.sh — 바이너리 설치
OS/Arch를 자동 감지하고, GitHub Releases에서 최신 바이너리를 다운로드한다:
#!/usr/bin/env bash
set -euo pipefail
VERSION="${VERSION:-latest}"
# OS/Arch 자동 감지
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "${ARCH}" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
arm64) ARCH="arm64" ;;
*) echo "::error::Unsupported architecture: ${ARCH}"; exit 1 ;;
esac
# latest 버전 자동 해석
if [[ "${VERSION}" == "latest" ]]; then
VERSION=$(curl -sL https://api.github.com/repos/somaz94/kube-diff/releases/latest \
| grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
fi
# 다운로드 & 설치
VERSION_NUM="${VERSION#v}"
FILENAME="kube-diff_${VERSION_NUM}_${OS}_${ARCH}.tar.gz"
URL="https://github.com/somaz94/kube-diff/releases/download/${VERSION}/${FILENAME}"
TMPDIR=$(mktemp -d)
trap 'rm -rf "${TMPDIR}"' EXIT
curl -sL -o "${TMPDIR}/kube-diff.tar.gz" "${URL}"
tar -xzf "${TMPDIR}/kube-diff.tar.gz" -C "${TMPDIR}"
chmod +x "${TMPDIR}/kube-diff"
sudo mv "${TMPDIR}/kube-diff" /usr/local/bin/kube-diff
주의: `set -euo pipefail` 환경에서 `VERSION` 환경변수가 없으면 `unbound variable` 에러가 발생한다. `VERSION="${VERSION:-latest}"` 기본값 설정이 반드시 필요하다.
5.4 run.sh — 실행 & Output 설정
#!/usr/bin/env bash
set -euo pipefail
# 커맨드 조합
CMD="kube-diff ${INPUT_SOURCE} ${INPUT_PATH}"
# Helm 전용 플래그
if [[ "${INPUT_SOURCE}" == "helm" ]]; then
[[ -n "${INPUT_VALUES}" ]] && # values 파일 추가
[[ -n "${INPUT_RELEASE}" ]] && CMD+=" -r ${INPUT_RELEASE}"
fi
# 글로벌 플래그
[[ -n "${INPUT_NAMESPACE}" ]] && CMD+=" -n ${INPUT_NAMESPACE}"
[[ -n "${INPUT_KIND}" ]] && CMD+=" -k ${INPUT_KIND}"
[[ -n "${INPUT_SELECTOR}" ]] && CMD+=" -l ${INPUT_SELECTOR}"
# 실행 & 결과 캡처
set +e
RESULT=$(eval "${CMD}" 2>&1)
EXIT_CODE=$?
set -e
# GitHub Output 설정 (multiline)
{
echo "result<<KUBE_DIFF_EOF"
echo "${RESULT}"
echo "KUBE_DIFF_EOF"
} >> "${GITHUB_OUTPUT}"
# Exit code 0(변경 없음), 1(변경 있음) → 성공 / 2(에러) → 실패
if [[ ${EXIT_CODE} -eq 2 ]]; then
exit 1
fi
5.5 comment.sh — PR 코멘트
`<!-- kube-diff-action -->` 마커를 사용해 기존 코멘트가 있으면 업데이트, 없으면 새로 생성한다.
#!/usr/bin/env bash
set -euo pipefail
MARKER="<!-- kube-diff-action -->"
# 기존 코멘트 확인
EXISTING_COMMENT_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
--jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | head -1 || true)
if [[ -n "${EXISTING_COMMENT_ID}" ]]; then
# 업데이트
gh api "repos/${REPO}/issues/comments/${EXISTING_COMMENT_ID}" \
-X PATCH -f body="${BODY}" > /dev/null
else
# 새로 생성
gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
-f body="${BODY}" > /dev/null
fi
5.6 kube-diff-action 사용 예시
- name: Check drift
id: diff
uses: somaz94/kube-diff-action@v1
with:
source: file
path: ./manifests/
namespace: production
output: markdown
- name: Fail if drift
if: steps.diff.outputs.has-changes == 'true'
run: |
echo "::error::Drift detected — review the PR comment for details"
exit 1
6. CI/CD 파이프라인
6.1 kube-diff CI
# .github/workflows/ci.yml (발췌)
test-unit:
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: go test ./... -v -race -cover
e2e:
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
- uses: helm/kind-action@v1 # kind cluster 생성
- run: make build
- run: make demo-all # 전체 데모 실행
6.2 kube-diff-action CI
kind cluster를 활용해 실제 Kubernetes 환경에서 file/helm 모드를 테스트한다:
# .github/workflows/ci.yml (발췌)
test-action-file:
steps:
- uses: actions/checkout@v4
- name: Setup kind cluster
uses: helm/kind-action@v1
- name: Apply test manifests
run: kubectl apply -f /tmp/test-manifests/
- name: Run kube-diff action
id: diff
uses: ./ # 로컬 action 테스트
with:
source: file
path: /tmp/test-manifests/
namespace: default
comment: 'false'
6.3 Smoke Test (Released Action)
릴리스 후 실제 `somaz94/kube-diff-action@v1` 으로 동작을 검증하는 워크플로우이다.
# .github/workflows/use-action.yml
on:
workflow_dispatch:
workflow_run:
workflows: ["Create release"]
types: [completed]
jobs:
smoke-test-file:
steps:
- uses: somaz94/kube-diff-action@v1 # 릴리스된 버전 사용
with:
source: file
path: /tmp/test-manifests/
namespace: default
comment: 'false'
7. 배포 — GoReleaser, Homebrew, Krew
7.1 GoReleaser 설정
# .goreleaser.yml
version: 2
builds:
- main: ./cmd/main.go
binary: kube-diff
env:
- CGO_ENABLED=0
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
ldflags:
- -s -w
- -X github.com/somaz94/kube-diff/cmd/cli.version={{.Version}}
- -X github.com/somaz94/kube-diff/cmd/cli.commit={{.Commit}}
- -X github.com/somaz94/kube-diff/cmd/cli.date={{.Date}}
brews:
- repository:
owner: somaz94
name: homebrew-tap
description: "Compare local Kubernetes manifests against live cluster state"
krews:
- name: diff2
repository:
owner: somaz94
name: krew-index
short_description: "Diff local K8s manifests vs cluster"
tag를 push하면 GoReleaser가 자동으로
- 6개 플랫폼(linux/darwin/windows x amd64/arm64) 바이너리 빌드
- GitHub Release 생성 및 바이너리 첨부
- Homebrew tap 업데이트 (somaz94/homebrew-tap)
- Krew 플러그인 index 업데이트 (somaz94/krew-index)
7.2 kube-diff-action 버전 관리
`kube-diff-action` 은 `install.sh` 에서 런타임에 최신 kube-diff 바이너리를 다운로드하므로, kube-diff가 업그레이드되면 action 사용자들은 다음 실행 시 자동으로 최신 버전을 받게 된다.
`major-tag-action` 을 사용해 v1 태그를 항상 최신 릴리스 커밋으로 갱신한다.
# .github/workflows/release.yml (발췌)
- name: "Update major version tag"
uses: somaz94/major-tag-action@v1
with:
tag: ${{ github.ref_name }}
github_token: ${{ secrets.EXAMPLE_PAT_TOKEN }}
- 이렇게 하면 `somaz94/kube-diff-action@v1` 으로 항상 최신 패치 버전을 사용할 수 있다.
8. 실제 활용 시나리오
CLI 활용
| 시나리오 | 명령어 |
| 배포 전 drift 확인 | kube-diff file ./manifests/ -n production |
| 인시던트 후 감사 | kube-diff file ./manifests/ -n production -o json > drift-report.json |
| GitOps sync 검증 | kube-diff kustomize ./overlays/production -n production |
| 멀티 클러스터 비교 | kube-diff file ./manifests/ --context prod-cluster -n app |
| Helm 업그레이드 미리보기 | kube-diff helm ./chart/ -f values-prod.yaml -r my-release -n production |
GitHub Action 활용
PR Drift Gate — manifest 변경 PR에서 자동으로 drift 체크
name: Drift Check
on:
pull_request:
paths: ['manifests/**']
jobs:
check-drift:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
- name: Setup kubeconfig
run: echo "${{ secrets.EXAMPLE_KUBECONFIG }}" | base64 -d > /tmp/kubeconfig
env:
KUBECONFIG: /tmp/kubeconfig
- name: Check drift
uses: somaz94/kube-diff-action@v1
with:
source: file
path: ./manifests/
namespace: production
스케줄 기반 Drift 모니터링 — 6시간마다 drift 체크 후 Slack 알림
name: Drift Monitor
on:
schedule:
- cron: '0 */6 * * *'
jobs:
monitor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup kubeconfig
run: echo "${{ secrets.EXAMPLE_KUBECONFIG }}" | base64 -d > /tmp/kubeconfig
env:
KUBECONFIG: /tmp/kubeconfig
- name: Check drift
id: diff
uses: somaz94/kube-diff-action@v1
with:
source: file
path: ./manifests/
namespace: production
output: json
comment: 'false'
- name: Alert on drift
if: steps.diff.outputs.has-changes == 'true'
run: |
curl -X POST "${{ secrets.EXAMPLE_SLACK_WEBHOOK }}" \
-H 'Content-Type: application/json' \
-d '{"text": "Cluster drift detected in production!"}'
마무리
kube-diff를 만들면서 몇 가지 교훈을 얻었다.
- Kubernetes 기본값 정규화가 핵심이다 — 단순히 로컬과 클러스터를 비교하면 false positive가 대량 발생한다. `progressDeadlineSeconds`, `revisionHistoryLimit`, `dnsPolicy` 같은 기본값을 Kind별로 정확히 제거해야 의미 있는 diff가 나온다.
- Exit code 설계가 중요하다 — CI에서 "변경 감지"와 "에러"를 구분하지 않으면 워크플로우가 매번 실패로 처리된다. `exit code` 0/1/2 체계로 이를 분리했다.
- Composite Action은 가볍고 빠르다 — 기존 CLI 바이너리를 활용하는 경우, Docker Action보다 Composite Action이 훨씬 실용적이다. 빌드 시간 없이 바이너리만 다운로드하면 끝난다.
- 런타임 최신 버전 다운로드 전략 — kube-diff-action 은 kube-diff 바이너리를 런타임에 다운로드하므로, CLI만 업그레이드하면 Action 사용자도 자동으로 최신 기능을 사용하게 된다.
현재 v0.2.1 기준으로 `file/helm/kustomize` 비교, `namespace/kind/label` 필터링, `color/plain/json/markdown` 출력을 지원한다.
Kubernetes 클러스터의 drift를 빠르게 감지하고, CI에서 자동으로 모니터링하고 싶다면 한번 사용해보길 바란다.
Reference
- kube-diff GitHub Repository
- kube-diff-action GitHub Repository
- kube-diff Documentation
- Kubernetes Dynamic Client
- GoReleaser
- Cobra CLI Framework
- GitHub Composite Actions
Somaz | DevOps Engineer | Kubernetes & Cloud Infrastructure Specialist
'Container Orchestration > Kubernetes' 카테고리의 다른 글
| Kubernetes 환경에서 Redis Redlock 구현하기: 분산 락의 완전한 이해 (0) | 2025.12.24 |
|---|---|
| Kubernetes Local Storage Solutions: OpenEBS vs Longhorn vs Rook Ceph 완전 비교 가이드 (0) | 2025.12.17 |
| Containerd v3에서 Insecure Registry 설정 방식 변경 (0) | 2025.11.26 |
| Kubernetes 클러스터 구축하기(kubespray 2025v.) (4) | 2025.07.31 |
| Helmfile 완전 정복: 실무에서 Helm을 선언적으로 관리하는 방법 (0) | 2025.06.16 |