콘텐츠로 이동

M7. Helm chart와 release lifecycle

NKS로 옮길 web-app, api-service, ai-service의 YAML을 환경마다 복사하면 같은 수정이 서로 다르게 누락되기 쉽습니다. Helm의 입력과 렌더링 결과, cluster에 저장된 release revision을 분리해서 읽어야 AI가 만든 values 변경을 검토하고 실패한 배포를 되돌릴 수 있습니다.

개념 정의의 정본은 GitOps Basics입니다. 원본 경로는 content/topics/L5/gitops-basics.mdx이며, 이 모듈은 Helm CLI로 chart를 렌더링하고 release lifecycle을 실행하는 역할만 맡습니다. Argo CD가 Helm을 release manager가 아니라 render 도구로 사용하는 경계도 정본의 설명을 따릅니다.

Helm 입력에서 cluster 상태까지
flowchart LR
Chart["Chart.yaml: package metadata"] --> Render["Helm render"]
Templates["templates/: Kubernetes YAML + Go template"] --> Render
Defaults["values.yaml: chart defaults"] --> Merge["values merge"]
File["-f env values"] --> Merge
Set["--set: one-off override"] --> Merge
Merge --> Render
Render --> Manifest["rendered manifest"]
Manifest --> Release["install / upgrade: named release"]
Release --> Cluster["Kubernetes resources"]
Release --> History["revision history"]
대상질문이 모듈의 확인 방법
chart어떤 template과 기본값을 묶어 배포하는가?Chart.yaml, values.yaml, templates/
values기본값 중 무엇을 이번 환경에서 덮어쓰는가?helm template, helm get values
rendered manifestKubernetes API에 전달될 최종 YAML은 무엇인가?helm linthelm template
release어느 namespace에 어떤 이름으로 설치했는가?helm status, helm list
revisioninstall·upgrade·rollback 이력이 몇 번째인가?helm history
actual resource실제 Deployment와 Pod가 원하는 상태에 도달했는가?kubectl get, readiness와 image digest

하나의 값이 실제 Kubernetes 필드로 이어지는 경로는 다음처럼 읽습니다.

values 입력template이 읽는 표현식rendered manifest
replicaCount: 1 또는 --set replicaCount=2replicas: {{ .Values.replicaCount }}Deployment.spec.replicas: 1 또는 2
image.repository: nginx.Values.image.repositorycontainer image의 repository
image.tag: 1.27-alpine@sha256:....Values.image.tag, 없으면 Chart.AppVersioncontainer image의 tag와 digest

실제 scaffold의 image template은 {{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}처럼 두 key를 조합합니다.

.Valuesvalues.yaml, -f, --set에서 합쳐진 입력 객체입니다. key를 넘겼다고 Kubernetes 필드가 자동으로 생기지는 않습니다. template이 그 key를 읽어 출력에 사용해야 rendered manifest가 바뀝니다. chart에 values.schema.json이 없다면 사용되지 않는 key가 오류 없이 무시될 수도 있으므로 최종 render diff를 확인해야 합니다.

values의 대표 우선순위는 values.yaml < -f values file < --set입니다. 같은 종류의 -f--set을 여러 번 주면 오른쪽 값이 우선합니다. helm template은 이 입력을 합쳐 로컬에서 manifest를 만들지만 API server가 실제로 그 resource를 받아들이는지, Pod가 Ready가 되는지까지 보장하지 않습니다.

upgrade 실패를 복구할 때 읽는 두 상태
flowchart TD
Upgrade["helm upgrade"] --> Revision3["release revision 3: failed"]
Upgrade --> Deployment["Deployment: bad image를 시도"]
Revision3 --> Diagnose["history + status"]
Deployment --> Diagnose
Diagnose --> Target["마지막 성공 revision 2 선택"]
Target --> Rollback["helm rollback release 2"]
Rollback --> Revision4["새 revision 4: deployed"]
Revision4 --> Ready["Deployment 2/2 Ready + 이전 image digest"]

rollback은 history를 지우거나 revision 번호를 뒤로 돌리는 명령이 아닙니다. 과거 revision의 chart와 values를 다시 적용한 새 revision을 만듭니다. 따라서 “revision 2로 rollback했으니 현재 revision도 2”라고 판단하면 안 됩니다.

시점 의존 설명은 2026-07-15에 다음 공식 1차 자료로 확인했습니다.

실제 NKS release 설치·upgrade·rollback은 cluster 쓰기와 workload 변경을 일으키므로 이 모듈에서는 excluded입니다. NKS kubeconfig·IAM 권한·private registry 접근은 승인된 non-production 환경에서 별도로 검증해야 하며, 여기의 k3d 결과는 NKS 실측값이 아닙니다. production NKS 자동 변경, chart repository 운영, OCI signing, Helm plugin 개발은 범위 밖입니다.

개념비유비유의 경계
chart같은 제품을 만드는 조립 설명서와 부품 목록실행 결과가 아니라 렌더링 가능한 package입니다.
values설명서의 선택 항목에 넣는 주문 사양모든 Kubernetes 필드를 무제한으로 바꾸는 장치는 아닙니다.
release특정 namespace에 접수된 이름 있는 주문chart 파일 자체와 cluster의 release는 같은 대상이 아닙니다.
revision주문 변경 이력의 순번Git commit이나 Deployment revision과 같은 번호가 아닙니다.
rollback과거 주문 사양으로 새 변경을 접수하는 작업과거 기록을 삭제하거나 시간을 되감지 않습니다.
항목검증값
기준일2026-07-15
hostmacOS arm64
Docker client / engine29.6.1 / 29.6.1
k3d5.9.0
K3s image / API serverrancher/k3s:v1.36.2-k3s1 / v1.36.2+k3s1
kubectl1.36.1
Helm4.2.3
workload imagenginx:1.27-alpine + manifest에 고정한 multi-platform digest
evidence 환경local-cluster; 고정 context + k8s-wb-m7-<UTC run ID>

Setup·install·upgrade·failure·rollback·cleanup은 executed-local, helm template만 cluster에 쓰지 않는 검증은 rendered입니다. 실제 NKS 명령은 excluded이며 예상 출력을 두지 않습니다.

  • 저장소 root에서 실행하며 M0~M6를 완료한 상태여야 합니다.
  • Docker Desktop과 M2의 cs-study-workbook cluster가 실행 중이어야 합니다.
  • Docker Hub에서 고정 digest image를 처음 받는 환경은 네트워크가 필요합니다.
  • 모든 Helm cluster 명령은 --kube-context k3d-cs-study-workbook과 고유 namespace를 명시합니다.
  • 예상 소요 시간은 100분이며 실패 upgrade의 timeout 약 10초를 포함합니다.

격리 namespace와 chart scaffold 만들기

섹션 제목: “격리 namespace와 chart scaffold 만들기”

helm create가 현재 Helm 4.2.3의 기본 chart 구조를 만듭니다. 성공 전 오류가 나면 EXIT trap이 namespace를 지우며, rollback 자체가 실패하면 상태 디렉터리를 보존하고 exit 70으로 중단합니다.

Terminal window
set -euo pipefail
rm -rf /tmp/k8s-wb-m7
mkdir -p /tmp/k8s-wb-m7
chmod 700 /tmp/k8s-wb-m7
node automation/k8s-workbook/preflight.mjs --require-cluster
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="k8s-wb-m7-$(date -u +%Y%m%d%H%M%S)"
RELEASE="api-service"
CHART="/tmp/k8s-wb-m7/sample-service"
printf '%s' "$NS" > /tmp/k8s-wb-m7/namespace
printf '%s' "$RELEASE" > /tmp/k8s-wb-m7/release
printf '%s' "$CHART" > /tmp/k8s-wb-m7/chart
if kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then
echo "namespace collision: $NS" >&2
exit 1
fi
rollback() {
STATUS=$?
trap - EXIT
set +e
ROLLBACK_FAILED=0
helm uninstall "$RELEASE" --kube-context k3d-cs-study-workbook \
-n "$NS" >/dev/null 2>&1
kubectl --context k3d-cs-study-workbook delete namespace "$NS" \
--ignore-not-found --wait=true >/dev/null 2>&1
test $? = 0 || ROLLBACK_FAILED=1
REMAINING_NAMESPACE="$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \
--ignore-not-found -o name 2>/dev/null)"
test $? = 0 || ROLLBACK_FAILED=1
test -z "$REMAINING_NAMESPACE" || ROLLBACK_FAILED=1
if test "$ROLLBACK_FAILED" != 0; then
echo 'rollback incomplete: preserve /tmp/k8s-wb-m7 and inspect namespace' >&2
exit 70
fi
rm -rf /tmp/k8s-wb-m7
exit "$STATUS"
}
trap rollback EXIT
kubectl --context k3d-cs-study-workbook create namespace "$NS" >/dev/null
helm create "$CHART" >/dev/null
test -f "$CHART/Chart.yaml"
test -f "$CHART/values.yaml"
test -f "$CHART/templates/deployment.yaml"
trap - EXIT
printf 'setup namespace=%s chart=sample-service release=%s\n' "$NS" "$RELEASE"

예상 출력:

PASS platform: darwin/arm64
PASS architecture: arm64
PASS docker-cli: v29.6.1
PASS docker-engine: v29.6.1
PASS k3d: v5.9.0
PASS kubectl-skew: v1.36.1
PASS helm: v4.2.3
PASS context: k3d-cs-study-workbook
PASS kubernetes-server: v1.36.2+k3s1
setup namespace=k8s-wb-m7-<UTC 14자리 run ID> chart=sample-service release=api-service

host architecture, Docker patch version과 namespace run ID는 변동 필드입니다.

Lab 1 — 기본 values를 덮어쓰고 최종 YAML 렌더링하기

섹션 제목: “Lab 1 — 기본 values를 덮어쓰고 최종 YAML 렌더링하기”

values.yamlreplicaCount: 1--set replicaCount=2로 덮어씁니다. image는 tag만 고정하지 않고 digest까지 manifest에 남깁니다. helm lint 성공과 원하는 최종 필드를 모두 확인해야 render 검토가 끝납니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m7/namespace)"
RELEASE="$(cat /tmp/k8s-wb-m7/release)"
CHART="$(cat /tmp/k8s-wb-m7/chart)"
DIGEST="sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"
IMAGE="nginx:1.27-alpine@$DIGEST"
helm lint "$CHART" > /tmp/k8s-wb-m7/lint.log
grep -q '1 chart(s) linted, 0 chart(s) failed' /tmp/k8s-wb-m7/lint.log
grep -Fq 'replicas: {{ .Values.replicaCount }}' "$CHART/templates/deployment.yaml"
grep -Fq '{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}' \
"$CHART/templates/deployment.yaml"
helm template "$RELEASE" "$CHART" --namespace "$NS" \
--set image.repository=nginx \
--set-string "image.tag=1.27-alpine@$DIGEST" \
--set replicaCount=2 \
--set-string podLabels.stage=rendered \
--set-string unusedFlag=ignored \
> /tmp/k8s-wb-m7/rendered.yaml
DEFAULT_REPLICAS="$(awk '$1 == "replicaCount:" {print $2; exit}' "$CHART/values.yaml")"
CHART_NAME="$(awk '$1 == "name:" {print $2; exit}' "$CHART/Chart.yaml")"
grep -q '^ replicas: 2$' /tmp/k8s-wb-m7/rendered.yaml
grep -q "image: \"$IMAGE\"" /tmp/k8s-wb-m7/rendered.yaml
grep -q '^ stage: rendered$' /tmp/k8s-wb-m7/rendered.yaml
if grep -q 'unusedFlag' /tmp/k8s-wb-m7/rendered.yaml; then
echo 'unused value unexpectedly reached rendered manifest' >&2
exit 2
fi
test "$DEFAULT_REPLICAS" = "1"
test "$CHART_NAME" = "sample-service"
printf 'chart=%s default_replicas=%s lint=pass\n' "$CHART_NAME" "$DEFAULT_REPLICAS"
printf 'template replicas=.Values.replicaCount image=.Values.image.repository+.Values.image.tag\n'
printf 'render release=%s replicas=2 image=%s stage=rendered\n' "$RELEASE" "$IMAGE"
printf 'unused_value key=unusedFlag rendered=false\n'

예상 출력:

chart=sample-service default_replicas=1 lint=pass
template replicas=.Values.replicaCount image=.Values.image.repository+.Values.image.tag
render release=api-service replicas=2 image=nginx:1.27-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10 stage=rendered
unused_value key=unusedFlag rendered=false

관찰 포인트는 chart 기본값이 바뀐 것이 아니라 이번 render 입력만 2개 replica로 계산되었다는 점입니다. rendered.yaml은 검토 대상이지 아직 cluster의 release가 아닙니다.

render에서 확인한 chart를 replicaCount=1로 설치합니다. Helm status만 믿지 않고 Deployment Ready 수, managed-by label과 실제 runtime image digest를 함께 확인합니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m7/namespace)"
RELEASE="$(cat /tmp/k8s-wb-m7/release)"
CHART="$(cat /tmp/k8s-wb-m7/chart)"
DIGEST="sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"
helm install "$RELEASE" "$CHART" \
--kube-context k3d-cs-study-workbook -n "$NS" \
--set image.repository=nginx \
--set-string "image.tag=1.27-alpine@$DIGEST" \
--set replicaCount=1 \
--wait=watcher --timeout=90s >/dev/null
STATUS_OUTPUT="$(helm status "$RELEASE" --kube-context k3d-cs-study-workbook -n "$NS")"
STATUS="$(printf '%s\n' "$STATUS_OUTPUT" | awk '$1 == "STATUS:" {print $2}')"
REVISION="$(printf '%s\n' "$STATUS_OUTPUT" | awk '$1 == "REVISION:" {print $2}')"
DEPLOYMENT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment \
-l app.kubernetes.io/instance="$RELEASE" -o jsonpath='{.items[0].metadata.name}')"
READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment "$DEPLOYMENT" \
-o jsonpath='{.status.readyReplicas}/{.spec.replicas}')"
MANAGED_BY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment "$DEPLOYMENT" \
-o jsonpath='{.metadata.labels.app\.kubernetes\.io/managed-by}')"
IMAGE_ID="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \
-l app.kubernetes.io/instance="$RELEASE" \
-o jsonpath='{.items[0].status.containerStatuses[0].imageID}')"
USER_VALUES="$(helm get values "$RELEASE" --kube-context k3d-cs-study-workbook \
-n "$NS" -o json)"
RECORDED_REPLICAS="$(printf '%s' "$USER_VALUES" | node -e '
let input = "";
process.stdin.on("data", chunk => input += chunk);
process.stdin.on("end", () => process.stdout.write(String(JSON.parse(input).replicaCount)));
')"
RECORDED_TAG="$(printf '%s' "$USER_VALUES" | node -e '
let input = "";
process.stdin.on("data", chunk => input += chunk);
process.stdin.on("end", () => process.stdout.write(JSON.parse(input).image?.tag ?? ""));
')"
test "$STATUS" = "deployed"
test "$REVISION" = "1"
test "$READY" = "1/1"
test "$MANAGED_BY" = "Helm"
test "$RECORDED_REPLICAS" = "1"
test "$RECORDED_TAG" = "1.27-alpine@$DIGEST"
case "$IMAGE_ID" in *"@$DIGEST") ;; *) echo 'unexpected runtime image digest' >&2; exit 2 ;; esac
printf 'release=%s revision=%s status=%s deployment_ready=%s managed_by=%s digest_verified=true\n' \
"$RELEASE" "$REVISION" "$STATUS" "$READY" "$MANAGED_BY"
printf 'recorded_values replicaCount=%s image_tag=%s\n' "$RECORDED_REPLICAS" "$RECORDED_TAG"

예상 출력:

release=api-service revision=1 status=deployed deployment_ready=1/1 managed_by=Helm digest_verified=true
recorded_values replicaCount=1 image_tag=1.27-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10

Pod 이름, Node, pull·readiness 시간은 변동 필드입니다. release 이름은 같은 chart를 어느 설치 인스턴스로 관리하는지 식별하고, namespace와 함께 release의 범위를 정합니다.

Lab 3 — values 변경으로 revision 2 upgrade하기

섹션 제목: “Lab 3 — values 변경으로 revision 2 upgrade하기”

같은 chart의 replica 수를 2로 바꾸고 Pod template에 track=stable label을 추가합니다. 성공한 upgrade는 revision 1을 superseded, revision 2를 deployed로 남깁니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m7/namespace)"
RELEASE="$(cat /tmp/k8s-wb-m7/release)"
CHART="$(cat /tmp/k8s-wb-m7/chart)"
DIGEST="sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"
helm upgrade "$RELEASE" "$CHART" \
--kube-context k3d-cs-study-workbook -n "$NS" \
--set image.repository=nginx \
--set-string "image.tag=1.27-alpine@$DIGEST" \
--set replicaCount=2 \
--set-string podLabels.track=stable \
--wait=watcher --timeout=90s >/dev/null
DEPLOYMENT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment \
-l app.kubernetes.io/instance="$RELEASE" -o jsonpath='{.items[0].metadata.name}')"
READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment "$DEPLOYMENT" \
-o jsonpath='{.status.readyReplicas}/{.spec.replicas}')"
TRACK="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment "$DEPLOYMENT" \
-o jsonpath='{.spec.template.metadata.labels.track}')"
HISTORY_JSON="$(helm history "$RELEASE" --kube-context k3d-cs-study-workbook \
-n "$NS" -o json)"
REV1_STATUS="$(printf '%s' "$HISTORY_JSON" | node -e '
let input = "";
process.stdin.on("data", chunk => input += chunk);
process.stdin.on("end", () => process.stdout.write(
JSON.parse(input).find(item => item.revision === 1)?.status ?? ""
));')"
REV2_STATUS="$(printf '%s' "$HISTORY_JSON" | node -e '
let input = "";
process.stdin.on("data", chunk => input += chunk);
process.stdin.on("end", () => process.stdout.write(
JSON.parse(input).find(item => item.revision === 2)?.status ?? ""
));')"
test "$READY" = "2/2"
test "$TRACK" = "stable"
test "$REV1_STATUS" = "superseded"
test "$REV2_STATUS" = "deployed"
printf 'upgrade revision=2 status=%s previous=%s replicas=%s track=%s\n' \
"$REV2_STATUS" "$REV1_STATUS" "$READY" "$TRACK"

예상 출력:

upgrade revision=2 status=deployed previous=superseded replicas=2/2 track=stable

관찰 포인트는 helm upgrade가 release revision과 Deployment의 새 rollout을 함께 만들지만, Helm revision과 Deployment revision 번호는 서로 다른 이력이라는 점입니다.

Lab 4 — 잘못된 image upgrade 실패 재현

섹션 제목: “Lab 4 — 잘못된 image upgrade 실패 재현”

존재하지 않는 image tag를 넣어 revision 3을 실패시킵니다. --wait=watcher가 readiness를 기다리다 timeout되고, release status와 Deployment의 desired image를 모두 확인한 뒤 의도적으로 exit 1을 반환합니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m7/namespace)"
RELEASE="$(cat /tmp/k8s-wb-m7/release)"
CHART="$(cat /tmp/k8s-wb-m7/chart)"
set +e
BAD_OUTPUT="$(helm upgrade "$RELEASE" "$CHART" \
--kube-context k3d-cs-study-workbook -n "$NS" \
--set image.repository=nginx \
--set-string image.tag=missing-m7 \
--set replicaCount=2 \
--wait=watcher --timeout=10s 2>&1)"
BAD_EXIT=$?
set -e
test "$BAD_EXIT" = "1"
case "$BAD_OUTPUT" in *"UPGRADE FAILED"*) ;; *) echo 'expected Helm upgrade failure' >&2; exit 2 ;; esac
STATUS_OUTPUT="$(helm status "$RELEASE" --kube-context k3d-cs-study-workbook -n "$NS")"
STATUS="$(printf '%s\n' "$STATUS_OUTPUT" | awk '$1 == "STATUS:" {print $2}')"
REVISION="$(printf '%s\n' "$STATUS_OUTPUT" | awk '$1 == "REVISION:" {print $2}')"
DEPLOYMENT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment \
-l app.kubernetes.io/instance="$RELEASE" -o jsonpath='{.items[0].metadata.name}')"
DESIRED_IMAGE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment "$DEPLOYMENT" \
-o jsonpath='{.spec.template.spec.containers[0].image}')"
PULL_REASON=""
for _ in $(seq 1 30); do
PULL_REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \
-l app.kubernetes.io/instance="$RELEASE" \
-o jsonpath='{range .items[*]}{.spec.containers[0].image}{"="}{.status.containerStatuses[0].state.waiting.reason}{"\n"}{end}' \
| awk -F= '$1 == "nginx:missing-m7" {print $2; exit}')"
case "$PULL_REASON" in ErrImagePull|ImagePullBackOff) break ;; esac
sleep 1
done
test "$STATUS" = "failed"
test "$REVISION" = "3"
test "$DESIRED_IMAGE" = "nginx:missing-m7"
case "$PULL_REASON" in ErrImagePull|ImagePullBackOff) ;; *) echo 'expected image pull failure' >&2; exit 2 ;; esac
printf 'upgrade revision=%s status=%s exit=%s desired_image=%s pull_reason=%s\n' \
"$REVISION" "$STATUS" "$BAD_EXIT" "$DESIRED_IMAGE" "$PULL_REASON"
exit 1

예상 출력:

upgrade revision=3 status=failed exit=1 desired_image=nginx:missing-m7 pull_reason=<ErrImagePull|ImagePullBackOff>

timeout 상세 문구, 실패 Pod 이름과 ErrImagePull에서 ImagePullBackOff로 바뀌는 시점은 변동 필드입니다. upgrade가 실패해도 이전 Ready Pod가 일부 남을 수 있으므로 “서비스 Pod가 하나 보인다”만으로 release 성공을 판정하지 않습니다.

Lab 5 — 마지막 성공 revision으로 rollback하기

섹션 제목: “Lab 5 — 마지막 성공 revision으로 rollback하기”

history에서 마지막 성공 상태였던 revision 2를 명시합니다. rollback 뒤 현재 revision은 2가 아니라 4이며, replica·label·runtime digest까지 되돌아왔는지 확인합니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m7/namespace)"
RELEASE="$(cat /tmp/k8s-wb-m7/release)"
DIGEST="sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"
helm rollback "$RELEASE" 2 \
--kube-context k3d-cs-study-workbook -n "$NS" \
--wait=watcher --timeout=90s >/dev/null
STATUS_OUTPUT="$(helm status "$RELEASE" --kube-context k3d-cs-study-workbook -n "$NS")"
STATUS="$(printf '%s\n' "$STATUS_OUTPUT" | awk '$1 == "STATUS:" {print $2}')"
REVISION="$(printf '%s\n' "$STATUS_OUTPUT" | awk '$1 == "REVISION:" {print $2}')"
DEPLOYMENT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment \
-l app.kubernetes.io/instance="$RELEASE" -o jsonpath='{.items[0].metadata.name}')"
READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment "$DEPLOYMENT" \
-o jsonpath='{.status.readyReplicas}/{.spec.replicas}')"
TRACK="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment "$DEPLOYMENT" \
-o jsonpath='{.spec.template.metadata.labels.track}')"
DESIRED_IMAGE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get deployment "$DEPLOYMENT" \
-o jsonpath='{.spec.template.spec.containers[0].image}')"
DIGEST_COUNT="-1"
for _ in $(seq 1 30); do
DIGEST_COUNT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \
-l app.kubernetes.io/instance="$RELEASE" \
-o jsonpath='{range .items[*]}{.status.containerStatuses[0].imageID}{"\n"}{end}' \
| awk -v digest="$DIGEST" 'index($0, digest) {count++} END {print count+0}')"
test "$DIGEST_COUNT" = "2" && break
sleep 1
done
test "$STATUS" = "deployed"
test "$REVISION" = "4"
test "$READY" = "2/2"
test "$TRACK" = "stable"
test "$DESIRED_IMAGE" = "nginx:1.27-alpine@$DIGEST"
test "$DIGEST_COUNT" = "2"
printf 'rollback failed_revision=3 target=2 new_revision=%s status=%s replicas=%s digest_pods=%s\n' \
"$REVISION" "$STATUS" "$READY" "$DIGEST_COUNT"

예상 출력:

rollback failed_revision=3 target=2 new_revision=4 status=deployed replicas=2/2 digest_pods=2

rollout 수렴 시간과 종료 중인 과거 Pod의 일시적 존재는 변동 필드입니다. 복구 완료 기준은 history만이 아니라 원하는 replica 전부 Ready와 고정 digest 확인입니다.

Lab 6 — 하나의 chart를 세 release 입력으로 렌더링하기

섹션 제목: “Lab 6 — 하나의 chart를 세 release 입력으로 렌더링하기”

세 서비스가 구조까지 항상 같다는 뜻은 아닙니다. 다만 같은 chart contract를 공유할 수 있다면 release 이름과 values를 분리해 최종 manifest가 서로 다른 instance label을 갖는지 먼저 render로 검토할 수 있습니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m7/namespace)"
CHART="$(cat /tmp/k8s-wb-m7/chart)"
DIGEST="sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"
for SERVICE in web-app api-service ai-service; do
OUTPUT="/tmp/k8s-wb-m7/${SERVICE}.yaml"
helm template "$SERVICE" "$CHART" --namespace "$NS" \
--set image.repository=nginx \
--set-string "image.tag=1.27-alpine@$DIGEST" \
--set replicaCount=1 > "$OUTPUT"
grep -q "app.kubernetes.io/instance: $SERVICE" "$OUTPUT"
done
printf 'rendered_instances web-app=pass api-service=pass ai-service=pass shared_chart=sample-service\n'

예상 출력:

rendered_instances web-app=pass api-service=pass ai-service=pass shared_chart=sample-service

이 결과는 세 release를 설치하거나 하나의 chart로 통합하라는 운영 권고가 아닙니다. 공통 template의 이득보다 서비스별 probe·port·resource 차이를 숨기는 비용이 크면 chart를 분리합니다.

release, namespace와 로컬 fixture 제거하기

섹션 제목: “release, namespace와 로컬 fixture 제거하기”

Helm release를 먼저 제거하고 namespace를 삭제합니다. release metadata, workload와 임시 chart가 모두 사라지고 공유 cluster는 유지되어야 합니다.

Terminal window
set -euo pipefail
test "$(kubectl config current-context)" = "k3d-cs-study-workbook"
NS="$(cat /tmp/k8s-wb-m7/namespace)"
RELEASE="$(cat /tmp/k8s-wb-m7/release)"
helm uninstall "$RELEASE" --kube-context k3d-cs-study-workbook -n "$NS" >/dev/null
RELEASE_COUNT="-1"
RESOURCE_COUNT="-1"
SECRET_COUNT="-1"
for _ in $(seq 1 60); do
RELEASE_COUNT="$(helm list --kube-context k3d-cs-study-workbook -n "$NS" -q \
| awk 'END {print NR+0}')"
RESOURCE_COUNT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get all -o name \
| awk 'END {print NR+0}')"
SECRET_COUNT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get secret \
-l owner=helm -o name | awk 'END {print NR+0}')"
test "$RELEASE_COUNT" = "0" && test "$RESOURCE_COUNT" = "0" \
&& test "$SECRET_COUNT" = "0" && break
sleep 1
done
test "$RELEASE_COUNT" = "0"
test "$RESOURCE_COUNT" = "0"
test "$SECRET_COUNT" = "0"
kubectl --context k3d-cs-study-workbook delete namespace "$NS" --wait=false >/dev/null
kubectl --context k3d-cs-study-workbook wait --for=delete \
"namespace/$NS" --timeout=120s >/dev/null
rm -rf /tmp/k8s-wb-m7
test ! -e /tmp/k8s-wb-m7
if kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then
echo 'cleanup failed: namespace remains' >&2
exit 1
fi
k3d cluster list | awk '$1 == "cs-study-workbook" && $2 == "1/1" && $3 == "1/1" \
{found=1} END {exit found ? 0 : 1}'
printf 'cleanup namespace=0 releases=0 workloads=0 helm_secrets=0 fixture=0 cluster_retained=true\n'

예상 출력:

cleanup namespace=0 releases=0 workloads=0 helm_secrets=0 fixture=0 cluster_retained=true

namespace와 resource 삭제 시간은 변동 필드입니다. node에 남은 container image cache는 Kubernetes workload나 release가 아니며 이 cleanup의 삭제 대상이 아닙니다.

명령 뒤보이는 변화판단
helm template.Values로 replica·image·label이 합쳐진 최종 YAML사용되지 않는 key와 cluster readiness를 구분합니다.
helm installrelease revision 1과 Helm 관리 Deploymentstatus와 actual Pod digest를 함께 봅니다.
정상 helm upgraderevision 2, replica 2개와 새 Pod labelvalues 변경이 rollout으로 이어졌습니다.
실패 helm upgraderevision 3 failed, bad desired image와 pull failure일부 이전 Pod가 Ready여도 upgrade는 실패입니다.
helm rollback ... 2revision 4 deployed, revision 2의 values로 복구rollback도 새 이력을 추가합니다.
helm uninstall + cleanuprelease metadata·workload·namespace가 모두 0공유 cluster 외 모듈 소유 자원이 남지 않아야 합니다.

Q1. AI가 helm template이 성공했으니 NKS 배포도 안전하다고 말합니다. 승인하시겠습니까?

섹션 제목: “Q1. AI가 helm template이 성공했으니 NKS 배포도 안전하다고 말합니다. 승인하시겠습니까?”

아니요. render는 template 문법과 계산된 YAML을 보는 단계이며 NKS의 API 지원, RBAC, admission 정책, image pull과 readiness를 보장하지 않습니다. 렌더링 diff를 리뷰한 뒤 승인된 non-production NKS에서 server-side 검증과 rollout 결과를 별도로 확인해야 합니다.

Q2. revision 3 upgrade가 실패했지만 기존 Pod 1개가 Ready입니다. 성공으로 바꿔 기록해도 될까요?

섹션 제목: “Q2. revision 3 upgrade가 실패했지만 기존 Pod 1개가 Ready입니다. 성공으로 바꿔 기록해도 될까요?”

안 됩니다. release status가 failed이고 Deployment가 bad image를 desired state로 가진다면 이전 Pod는 rolling update 중 서비스를 일부 지탱할 뿐입니다. history에서 마지막 성공 revision을 고르고 rollback한 뒤 전체 desired replica와 image digest가 복구됐는지 확인합니다.

Q3. 세 서비스에 같은 chart를 쓰자는 제안을 어떤 기준으로 리뷰하시겠습니까?

섹션 제목: “Q3. 세 서비스에 같은 chart를 쓰자는 제안을 어떤 기준으로 리뷰하시겠습니까?”

공통 Deployment·Service contract가 실제로 같은지, values가 서비스 차이를 명시적으로 표현하는지, probe·port·resource·배치 정책 차이가 조건문에 숨지 않는지 봅니다. 공통화가 rendered manifest 이해를 어렵게 만들면 서비스별 chart 또는 더 작은 공통 helper 경계를 선택합니다.

함정 1 — --set만 배포 기록으로 남기기

섹션 제목: “함정 1 — --set만 배포 기록으로 남기기”

--set을 운영의 유일한 기록으로 사용하면 어떤 값이 배포되었는지 Git review가 어렵습니다. 환경별 지속 값은 검토 가능한 values file에 두고 일회성 override도 배포 기록에 남깁니다.

함정 2 — dry-run 출력에 Secret이 없다고 가정하기

섹션 제목: “함정 2 — dry-run 출력에 Secret이 없다고 가정하기”

helm upgrade --dry-run 출력에는 Secret manifest가 포함될 수 있습니다. 공식 CLI가 제공하는 --hide-secret 경계를 확인하고 실제 Secret이나 전체 dry-run 출력을 공개 로그에 복사하지 않습니다.

함정 3 — rollback 대상과 현재 revision을 같은 번호로 읽기

섹션 제목: “함정 3 — rollback 대상과 현재 revision을 같은 번호로 읽기”

helm rollback RELEASE 2 뒤 현재 revision을 2라고 가정하면 history를 잘못 읽습니다. rollback은 과거 값을 사용한 새 revision을 만들며, application readiness 검증도 따로 필요합니다.

다음 M8에서는 Helm으로 설치할 수 있는 KEDA를 포함해 HPA·KEDA의 Pod층 scaling과 Cluster Autoscaler의 Node층 scaling을 분리하고, 무엇이 어느 계층의 replica·capacity를 바꾸는지 관찰합니다.