Container Orchestration/Kubernetes

누가 kubectl edit 했어? — Kubernetes Cluster Drift 감지 도구 직접 만들기

Somaz 2026. 3. 23. 00:00
728x90
반응형

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가 자동으로

  1. 6개 플랫폼(linux/darwin/windows x amd64/arm64) 바이너리 빌드
  2. GitHub Release 생성 및 바이너리 첨부
  3. Homebrew tap 업데이트 (somaz94/homebrew-tap)
  4. 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를 만들면서 몇 가지 교훈을 얻었다.

  1. Kubernetes 기본값 정규화가 핵심이다 — 단순히 로컬과 클러스터를 비교하면 false positive가 대량 발생한다. `progressDeadlineSeconds`, `revisionHistoryLimit`, `dnsPolicy` 같은 기본값을 Kind별로 정확히 제거해야 의미 있는 diff가 나온다.
  2. Exit code 설계가 중요하다 — CI에서 "변경 감지"와 "에러"를 구분하지 않으면 워크플로우가 매번 실패로 처리된다. `exit code` 0/1/2 체계로 이를 분리했다.
  3. Composite Action은 가볍고 빠르다 — 기존 CLI 바이너리를 활용하는 경우, Docker Action보다 Composite Action이 훨씬 실용적이다. 빌드 시간 없이 바이너리만 다운로드하면 끝난다.
  4. 런타임 최신 버전 다운로드 전략 — kube-diff-action 은 kube-diff 바이너리를 런타임에 다운로드하므로, CLI만 업그레이드하면 Action 사용자도 자동으로 최신 기능을 사용하게 된다.

 

 

현재 v0.2.1 기준으로 `file/helm/kustomize` 비교, `namespace/kind/label` 필터링, `color/plain/json/markdown` 출력을 지원한다.

 

Kubernetes 클러스터의 drift를 빠르게 감지하고, CI에서 자동으로 모니터링하고 싶다면 한번 사용해보길 바란다.

 

 

 

 

 

 

 

 

 


Reference

 

 

 

 

 

 

Somaz | DevOps Engineer | Kubernetes & Cloud Infrastructure Specialist

728x90
반응형