--- type: runbook tags: [istio, install, helm, runbook] created: 2026-06-01 --- # Istio 1.30.0 — 기존 설치 teardown + Helm 클린 재설치 > [!abstract] > 홈랩 클러스터의 깨진 Istio 1.30.0 설치를 전부 제거하고 **순수 Helm chart**(base → istiod → ingress/egress)로 클린 재설치한 런북. 흐름 0단계(설치·구조)의 실제 실행 기록 — 본 아카이브 전체가 이 설치 위에서 동작한다. > **하나의 그림**: Istio Helm 설치는 *의존 스택*이다 — `base(CRD)` → `istiod(control plane)` → `gateways(data plane)`, 각 release가 앞 layer를 전제로 한다. 그리고 Helm은 자기가 만든 *선언적* 리소스만 소유하고, istiod가 런타임에 *동적으로* 만드는 리소스(CA cert, leader-election cm)는 소유 밖이다. 이 **의존 방향 + ownership 경계** 두 축이 install 순서·teardown 역순·CRD 잔여물·configmap 수동삭제를 *전부* 설명한다. **Date:** 2026-06-01 **호스트:** homelab (kubespray bare-metal, k8s v1.30.6, 3노드) **도메인:** Kubernetes / Istio **상태:** ✅ 완료 — base/istiod/ingress/egress 4개 release 1.30.0 deployed, 전 pod 1/1 Running, analyze 경고 0 **대상독자:** Istio를 IaC로 굴리려는 DevOps/SRE. **선행개념:** Helm release/values, CRD, control plane vs data plane. > ⚠️ 선행: 이 작업 전 **클러스터 control-plane 장애**를 먼저 수리해야 했음. > 상세 → [control-plane 장애 수리 런북](arch__runbook-controlplane-outage.html) > (그 수리 없이는 istiod가 `0/1`로 안 뜸 — Deployment가 Pod를 못 만드는 상태였음) --- ## 1. 배경 지식 — 왜 "순수 Helm 스택"으로 까는가 Istio를 설치하는 길은 둘이다. `istioctl install`은 하나의 `IstioOperator` CR을 받아 control plane과 gateway를 *한 번에* 깔아주는 편의 경로다. 반면 **순수 Helm chart**는 `base`/`istiod`/`gateway`를 **각각 별도 release로** 깐다. 편해 보이는 전자를 두고 굳이 후자를 택하는 이유는 *소유권(ownership)* 에 있다. 쿠버네티스에서 어떤 도구가 리소스를 "관리한다"는 건 결국 그 리소스에 **소유 라벨/애너테이션을 박는 것**이다. `istioctl`은 operator 라벨로, Helm은 release 라벨(`app.kubernetes.io/managed-by: Helm` + release name)로 소유를 표시한다. **두 도구는 서로의 라벨을 모른다.** 그래서 operator로 깐 위에 Helm으로 덮거나 그 반대를 하면, 같은 Deployment를 두 도구가 서로 자기 것이라 주장하며 충돌한다. 프로덕션처럼 GitOps로 굴릴 환경에선 "release/values 파일 = 형상의 단일 소스, 버전은 `--version 1.30.0`으로 핀"이라는 재현성이 핵심이라, ownership이 명확한 순수 Helm을 택한다. 이 문서가 푸는 문제는 두 가지다. (1) **깨진 기존 설치를 어떻게 *깨끗이* 들어내는가** — Helm `uninstall` 한 방으로 안 되는 이유가 있다. (2) **어떤 순서로 다시 까는가** — base→istiod→gw 순서가 단순 관례가 아니라 *의존성*에서 강제된다는 것. 이 둘을 이해하려면 먼저 Istio 설치가 **3개 layer의 의존 스택**이라는 그림을 세워야 한다. | layer (release) | 무엇 | 의존 | |---|---|---| | `base` | CRD 15개 + cluster-scope 리소스(clusterrole 등) | 없음 (맨 아래) | | `istiod` | control plane: deploy/svc/sa + webhook(inject/validate) | CRD가 있어야 자기 webhook·config를 등록 | | `gateway` | data plane: ingress(NodePort)/egress(ClusterIP) Pod | istiod가 떠야 xDS로 설정받음 | `base`가 CRD(=Gateway/VirtualService/DestinationRule 등의 *타입 정의*)를 먼저 깔지 않으면 istiod는 자기가 watch할 타입조차 모른다. gateway Pod는 빈 Envoy로 떠봤자 istiod가 없으면 라우팅 설정을 못 받아 무의미하다. **아래 layer가 위 layer의 전제** — 이게 install이 아래→위로 가는 이유 전부다. --- ## 2. 핵심 — 의존 방향 + ownership 경계 (한 장의 그림) > **앵커 한 문장:** install은 *의존 스택을 아래→위로 쌓고*, teardown은 *정확히 역순으로 허문다* — 단 Helm이 소유한 박스만. 박스 밖(CRD keep + runtime cm)은 손이 안 닿아 수동 삭제가 필요하다. 이 한 문장에서 이후 모든 절차가 따라 나온다. 그림으로 고정하자. ```mermaid flowchart TB subgraph helm["Helm-owned (release/values로 선언적 관리)"] base["base chart\nCRD 15 + cluster role\nresource-policy: keep"] istiod["istiod chart\ncontrol plane: deploy/svc/sa\nwebhook inject/validate"] gw["gateway charts\ningress NodePort / egress ClusterIP\ndata plane"] base --> istiod --> gw end subgraph runtime["istiod runtime-created (Helm 소유 밖)"] cm["istio-ca-root-cert / istio-leader\nip-autoallocate / namespace-controller"] end istiod -. 동적 생성 .-> cm install["install: 아래→위"] -.->|base then istiod then gw| helm teardown["teardown: 위→아래"] -.->|istiod uninstall, base uninstall, CRD/cm manual| helm ``` **왜 teardown은 역순인가 (참조 순서).** base를 먼저 지우면 istiod가 *참조하던* CRD가 사라진다. 그러면 istiod release를 정리할 때 finalizer/cleanup 로직이 참조 대상을 못 찾아 순서가 꼬일 수 있다. DB에서 외래키 걸린 행을 부모부터 지우면 깨지는 것과 같은 원리 — 그래서 자식(istiod)부터, 부모(base)는 나중에. (비유의 한계: Istio엔 진짜 FK 제약이 강제돼 있진 않다. "참조가 끊겨도 *대개* 견디지만 cleanup 경로에서 비결정적으로 꼬일 수 있다"가 정확한 표현.) **왜 `helm uninstall` 한 방으로 안 끝나는가 (ownership 경계).** 위 그림의 두 박스가 답이다. Helm은 **자기가 chart로 렌더해 apply한 선언적 리소스만** 소유한다. 그런데 teardown을 막는 두 부류는 그 소유 밖에 있다. - **CRD — keep-policy로 *일부러* 남긴다.** base chart는 CRD에 `helm.sh/resource-policy: keep` 애너테이션을 박는다. 그래서 `helm uninstall istio-base`를 해도 CRD 15개는 *살아남는다*. 이건 버그가 아니라 **데이터 보호 장치**다: CRD가 지워지면 그 타입의 *모든 CR 인스턴스가 함께 cascade 삭제*된다(Gateway/VirtualService 등 사용자 설정 전멸). 업그레이드 때 차트만 갈아끼우면서 사용자 리소스를 지키려는 의도. 완전 초기화를 원하면 **명시적으로** `kubectl delete crd` 해야 한다. - **runtime configmap — istiod가 *런타임에* 만든다.** `istio-ca-root-cert`(메시 CA 신뢰 앵커, 각 ns에 배포), `istio-leader`(leader-election), `istio-ip-autoallocate`, `istio-namespace-controller-election` 등은 *chart에 없다*. istiod 프로세스가 살아 돌면서 동적으로 생성한 것이라 Helm 소유 라벨이 없다 → `uninstall`이 못 건드린다 → 수동 삭제. 이 경계가 곧 **teardown이 2단계로 쪼개지는 이유**다: ① Helm-owned 박스는 `helm uninstall`이 깔끔히 → ② 박스 밖(CRD + cm)은 `kubectl delete`로 손수. --- ## 3. 예시 — 실제 teardown → 재설치 → 검증 ### 3.1 사전 상태 (teardown 직전) ``` $ helm -n istio-system list istio-base base-1.30.0 deployed # CRD만 istiod istiod-1.30.0 deployed # 0/1, RS/Pod 없음(= control-plane 장애 탓) # gateway chart 미설치 $ kubectl get crd | grep -c istio.io # 15 $ webhook: mutating istio-sidecar-injector, validating istio-validator/istiod-default-validator # 전 네임스페이스 istio CR(Gateway/VS/DR/SE/PA/AP 등): 0개 ← teardown이 사용자 리소스에 영향 없음 ``` 보존 대상: `service-a/backend`(2/2, plain Deployment — CRD 삭제 무관), `istioinaction` ns(injection 라벨만). CR이 0개라 CRD를 통째로 지워도 cascade로 사라질 사용자 설정이 없다 — 그래서 완전 초기화가 *안전한* 타이밍이다. ### 3.2 Teardown — 2단계(Helm 역순 → 수동) ```bash CTX=homelab; NS=istio-system # (1) helm release 제거 — istiod 먼저, 그 다음 base (의존 역순) helm --kube-context $CTX uninstall istiod -n $NS helm --kube-context $CTX uninstall istio-base -n $NS # → base 차트는 CRD를 'resource-policy: keep'로 남김(데이터 보호). 아래서 수동 삭제. # → istiod 차트가 소유한 webhook/clusterrole/sa/deploy/svc 는 함께 제거됨. # (2) CRD 15개 수동 삭제 (완전 초기화. CR 0개라 안전) kubectl --context $CTX get crd -o name | grep 'istio.io' | xargs -r kubectl --context $CTX delete # (3) istiod 런타임 생성 잔여 configmap 정리 (helm 미소유 → 수동) kubectl --context $CTX -n $NS delete cm \ istio-ca-crl istio-ca-root-cert istio-ip-autoallocate \ istio-leader istio-namespace-controller-election ``` `(1)`은 그림의 Helm-owned 박스를, `(2)`는 keep-policy로 남은 CRD를, `(3)`은 박스 밖 runtime cm을 처리한다 — §2의 경계가 명령 3줄에 1:1로 대응한다. **teardown 검증:** ``` istio CRD: 0 / helm release: 0 / istio webhook: 0 / clusterrole(istio): 0 istio-system: No resources found service-a/backend: 2/2 (보존 OK) ``` ### 3.3 재설치 — Helm, base → istiod → gateways (의존 정순) repo IaC: `install/helm/values-*.yaml`. 동일 명령이 `Makefile`(`make install`) 및 `install/helm/README.md`에도 정리돼 있음. 버전 핀: `--version 1.30.0`. ```bash CTX=homelab; NS=istio-system; VER=1.30.0 # repo 등록(최초 1회) helm repo add istio https://istio-release.storage.googleapis.com/charts && helm repo update # [1] base — CRD + cluster 리소스 helm --kube-context $CTX upgrade --install istio-base istio/base \ -n $NS --create-namespace --version $VER --wait --timeout 3m # [2] istiod — control plane (values: access log on, 작은 리소스, autoscale off) helm --kube-context $CTX upgrade --install istiod istio/istiod \ -n $NS --version $VER -f install/helm/values-istiod.yaml --wait --timeout 4m # [3] gateways — ingress(NodePort) + egress(ClusterIP) helm --kube-context $CTX upgrade --install istio-ingressgateway istio/gateway \ -n $NS --version $VER -f install/helm/values-ingress-gateway.yaml --wait --timeout 3m helm --kube-context $CTX upgrade --install istio-egressgateway istio/gateway \ -n $NS --version $VER -f install/helm/values-egress-gateway.yaml --wait --timeout 3m ``` 각 단계의 `--wait`는 단순 옵션이 아니라 **의존 게이트**다: 앞 release의 Pod가 Ready 될 때까지 블록하므로, base가 떠야 istiod로, istiod가 떠야 gw로 넘어간다. `--timeout`(3m/4m/3m)이 그 게이트의 상한 — 넘으면 다음 단계로 안 가고 실패한다. 즉 명령 나열 순서가 곧 의존 스택을 *강제*한다. **values 핵심 근거 (왜 이 값인가):** | chart | 파일 | 핵심 설정 | 왜 | |---|---|---|---| | istiod | `values-istiod.yaml` | `meshConfig.accessLogFile=/dev/stdout`(TEXT), `enablePrometheusMerge`, `pilot.autoscaleEnabled=false replicaCount=1`, proxy 리소스 축소 | 관측성 시나리오 전제(access log on), 홈랩 단일 규모. `outboundTrafficPolicy: REGISTRY_ONLY`는 주석처리 → egress 시나리오에서 전환 | | ingress gw | `values-ingress-gateway.yaml` | `service.type=NodePort` (80→31080, 443→31443), `istio: ingressgateway` 라벨 | bare-metal·LB 없음(MetalLB 도입 전). Gateway 리소스 selector와 라벨 일치 필요 | | egress gw | `values-egress-gateway.yaml` | `service.type=ClusterIP`, 포트 15443(tls) 포함 | egress는 외부 노출 불필요, 메시 내부 트래픽만 경유 | ### 3.4 결과 검증 ``` $ helm -n istio-system list istio-base base-1.30.0 deployed istiod istiod-1.30.0 deployed istio-ingressgateway gateway-1.30.0 deployed istio-egressgateway gateway-1.30.0 deployed $ kubectl -n istio-system get deploy istiod 1/1 (registry.istio.io/release/pilot:1.30.0) istio-ingressgateway 1/1 (worker1) istio-egressgateway 1/1 (worker2) $ kubectl -n istio-system get svc istio-ingressgateway NodePort 15021:32391, 80:31080, 443:31443 istio-egressgateway ClusterIP 15021,80,443,15443 istiod ClusterIP 15010,15012,443,15014 $ istioctl proxy-status # 데이터플레인 ↔ istiod xDS sync istio-egressgateway ... 1.30.0 SYNCED(CDS,LDS,EDS) istio-ingressgateway ... 1.30.0 SYNCED(CDS,LDS,EDS) $ istioctl analyze -A Info [IST0102] (Namespace default) ... not enabled for injection # 무해 Info, 경고 0 ``` 검증 합격선 4종: **helm 4 release deployed / 전 pod 1/1 / proxy-status SYNCED / analyze 경고 0** — 전부 충족. `proxy-status`가 SYNCED라는 건 gateway(data plane)가 istiod(control plane)로부터 CDS/LDS/EDS를 정상 수신했다는 뜻 — §2의 스택이 위까지 살아 동작한다는 *최종 증거*다. > 부수 효과: control-plane 수리 후 `istioctl proxy-status`·`kubectl logs/exec`의 기존 `nodes/proxy` 인증 오류도 해소됨(같은 뿌리였음). 데이터플레인 진단이 정상 동작. --- ## 4. 정리 **멘탈모델 한 줄:** Istio Helm 설치 = *의존 스택(base→istiod→gw)* × *ownership 경계(Helm-owned 박스 vs runtime 박스 밖)*. 이 2×2 좌표 하나로 install 순서·teardown 역순·CRD 잔여·cm 수동삭제가 전부 연역된다. ### 다음 작업 - `scenarios/00-sample-apps/` 배포(`make apps`) → 시나리오 10(ingress)부터 트래픽 검증. - 프로덕션 적용: 프로덕션은 NodePort 대신 LB/MetalLB, istiod HA(replica↑/PodDisruptionBudget), `revision` 기반 카나리 업그레이드 고려. 본 재설치는 `revision: ""`(default) 단일. ### 관련 개념 / 파일 이 설치의 설계 원리는 → [컨트롤·데이터 플레인 설치 분리](arch__note-install-cp-dp-decoupling.html) · [istiod 성능 4요인](arch__note-control-plane-performance-factors.html) 참고. - 📎 [values-istiod.yaml](attachment/install/helm/values-istiod.yaml) — control plane (access log on, 단일 replica) - 📎 [values-ingress-gateway.yaml](attachment/install/helm/values-ingress-gateway.yaml) — NodePort 80→31080 / 443→31443 - 📎 [values-egress-gateway.yaml](attachment/install/helm/values-egress-gateway.yaml) — ClusterIP, 15443(tls) - 📎 [install/helm/README.md](attachment/install/helm/README.md) — 설치 순서 문서 - 📎 [install/verify.sh](attachment/install/verify.sh) — 설치 후 헬스체크 스크립트 - 선행: [control-plane 장애 수리 런북](arch__runbook-controlplane-outage.html) ## 핵심 정리 - **install은 의존 스택을 아래→위로, teardown은 위→아래로**. base(CRD) → istiod(control plane) → gateway(data plane). istiod를 base보다 먼저 지워야 *참조 순서*가 안 꼬인다(DB 외래키 삭제 순서와 동형). - **Helm은 자기가 만든 선언적 리소스만 소유**한다. CRD는 base 차트의 `resource-policy: keep`으로, runtime configmap은 istiod가 동적 생성한 것이라 — 둘 다 `uninstall` 밖. 완전 초기화엔 수동 `kubectl delete`가 필요(teardown이 2단계인 이유). - **순수 Helm = release/values 단위 선언적·버전핀·재현 가능 IaC**. `istioctl install`(operator)과는 ownership 라벨이 달라 혼용하면 충돌 위험. - **`--wait --timeout`이 검증의 일부**: 각 helm 단계가 Pod readiness까지 블록 → base→istiod→gw가 명령 나열이 아니라 "앞 layer Ready여야 진행"하는 게이트(상한 3m/4m/3m). - **gateway 라벨 `istio: ingressgateway`는 이후 `Gateway` 리소스 `selector`의 연결고리**다. 일치해야 트래픽이 흐른다(시나리오 10). - 검증 합격선 4종: helm 4 release deployed / 전 pod 1/1 / proxy-status SYNCED / analyze 경고 0. ## What you might be missing - **CRD keep-policy는 양날**: teardown 때는 "왜 안 지워져?"의 함정이지만, *업그레이드* 때는 CRD 유실(=모든 CR cascade 삭제) 없이 차트만 교체하게 해주는 데이터 보호 장치다. `helm uninstall`이 CRD를 남기는 건 버그가 아니라 의도. - **runtime configmap을 안 지우면**: `istio-ca-root-cert`는 각 ns에 뿌려지는 CA 신뢰 앵커다. 새 istiod가 재생성하므로 보통 무해하지만, 구 CA 잔재가 남으면 mTLS 신뢰 체인이 꼬일 수 있어 완전 초기화 시 정리한다. - **base를 먼저 지우면 안 되는 이유는 "데이터" 순서가 아니라 "참조" 순서**다. istiod가 참조하는 CRD가 먼저 사라지면 정리 로직이 참조 대상을 못 찾는다 — DB의 외래키 삭제 순서와 같은 원리. - **control-plane 장애와 같은 뿌리**: 본 재설치 후 `istioctl proxy-status`·`kubectl logs/exec`의 `nodes/proxy` 인증 오류가 동시에 풀린 건 우연이 아니다 — control-plane이 Pod/RS를 못 만들던 동일 원인이 데이터플레인 진단까지 막고 있었다. 선행 [control-plane 장애 수리 런북](arch__runbook-controlplane-outage.html) 참고. - **operator와 Helm은 라벨로만 충돌을 안다**: 두 설치 경로가 같은 Deployment를 서로 자기 것이라 주장하면 reconcile 루프가 싸운다. 한 메시는 한 설치 도구로 — 섞지 말 것.