control plane과 data plane의 설치·수명주기를 분리하면 istiod 업그레이드가 data plane에 투명해진다
“Istio를 업그레이드한다"가 왜 “mesh 전체를 한 번에 흔드는 일"이 아니라 “proxy를 하나씩 새 control plane으로 옮기는 일"이 될 수 있는지를 다룬다. 결론부터: istiod(control plane)와 Envoy(data plane)는 xDS라는 느슨한 gRPC 계약으로만 묶인 별개 컴포넌트라서, 새 istiod를 옆에 나란히 띄우고(revision canary) workload를 한 namespace씩 옮길 수 있고, 그래서 control plane 교체가 data plane에 투명해진다. 설치 단위(base/istiod/gateway 3 Helm chart)·revision 메커니즘·canary 흐름을 “왜 이렇게 설계됐나"의 원리로 풀고, 운영 detail(canary 명령·합격 기준)은 운영 플레이북 §06에 위임한다.
대상: Istio 1.30 / Helm 설치. 독자: control plane 업그레이드의 안전성 모델을 원리로 이해하려는 DevOps/SRE. 선행: xDS가 무엇인지 대략의 감(xDS API 계층).
01. 배경 — 왜 “분리"가 필요한가: monolithic mesh의 업그레이드 공포
먼저 분리가 없을 때의 세계를 그려야 분리의 가치가 보인다. Istio를 “하나의 덩어리"로 생각하면 — 즉 control plane과 그것이 제어하는 모든 proxy를 한 운명으로 묶으면 — 업그레이드는 다음 딜레마에 빠진다.
- istiod를 in-place로 새 버전으로 갈아치우면, 그 순간 mesh 안 모든 Envoy가 동시에 새 control plane의 설정을 받는다. 새 버전이 한 군데라도 호환성 문제를 일으키면 blast radius = mesh 전체다. 롤백하려면 또 한 번 전체를 흔들어야 한다.
- 반대로 무서워서 안 올리면 버전이 고여 EOL·CVE가 쌓인다.
이 딜레마의 근원은 **결합(coupling)**이다. “control plane 버전"과 “내 워크로드가 받는 설정"이 1:1로 묶여 있으면, 버전을 바꾸는 행위가 곧 전체 워크로드의 설정을 바꾸는 행위가 된다. 그래서 부분적으로·되돌릴 수 있게·blast radius를 작게 업그레이드하려면, 먼저 이 둘을 떼어내야 한다. 떼어낼 수 있다는 사실 자체가 Istio 아키텍처의 핵심 자산이고, 이 문서 전체가 그 한 가지 사실의 전개다.
떼어내는 데 필요한 전제 두 가지: ① control plane과 data plane이 느슨하게 연결돼 있어야 하고(그래야 control plane을 통째로 바꾸지 않고 일부만 새것에 붙일 수 있다), ② 새 control plane을 구 것을 죽이지 않고 옆에 띄울 수 있어야 한다. 1번을 §02가, 2번을 §03~04가 책임진다.
02. 핵심 — 두 컴포넌트, 하나의 느슨한 계약 (멘탈모델 ANCHOR)
머릿속에 박을 그림 하나: istiod와 Envoy는 “전선 한 가닥(xDS gRPC 스트림)“으로만 연결된 독립 박스 두 개다. 전선을 뽑아도 Envoy는 마지막에 받은 설정으로 계속 굴러가고, 전선의 반대쪽 끝(어느 istiod)은 갈아 끼울 수 있다. 이 그림에서 분리·canary·투명한 업그레이드가 모두 따라 나온다.
두 박스의 역할부터 명확히 가르자.
- control plane = istiod — K8s 상태(Service/Endpoint/Pod)와 Istio CRD(VirtualService 등)를 Envoy 설정으로 컴파일해서 proxy에 push하는 두뇌. 트래픽 데이터 경로에 직접 들어가 있지 않다 — 패킷은 istiod를 거치지 않는다.
- data plane = Envoy proxy — sidecar(Pod 옆 컨테이너) 또는 gateway(독립 Deployment). 실제 패킷을 받아 라우팅·mTLS·정책 enforcement를 직접 수행한다.
둘을 잇는 유일한 연결이 xDS(LDS/RDS/CDS/EDS/SDS) gRPC 스트림이다. “느슨한 계약"이라는 말의 구체적 의미는 두 가지 성질로 드러난다 — 그리고 이 두 성질이 §01의 전제 ①②를 정확히 충족한다.
- fail static (전제 ① 충족). istiod가 잠깐 죽어도 Envoy는 마지막으로 받은 설정으로 계속 트래픽을 처리한다. 멈추는 것은 새 설정·새 endpoint·cert 갱신뿐이고, 이미 맺어진 연결과 기존 라우팅은 끊기지 않는다. → control plane을 통째로 바꾸지 않고도 data plane이 살아 있다.
- 다중 control plane 공존 (전제 ② 충족). istiod를 여러 버전 동시에 띄워도, 각 Envoy는 자기가 부트스트랩 때 지정받은 한 istiod로부터만 push를 받는다. proxy 입장에선 “내 두뇌는 정확히 하나"이고, 그 하나가 누구인지가 proxy마다 다를 수 있다. → 새 istiod를 옆에 띄우는 게 가능하다.
flowchart LR
subgraph CP["control plane (istiod) - not on data path"]
D1["istiod rev=1-27"]
D2["istiod rev=1-30 (canary)"]
end
subgraph DP["data plane (Envoy) - serves traffic"]
P1["sidecar A rev=1-27"]
P2["sidecar B rev=1-30"]
GW["ingress gateway rev=1-27"]
end
D1 -- xDS push --> P1
D1 -- xDS push --> GW
D2 -- xDS push --> P2
P1 -. pod-to-pod traffic .- P2이 그림이 문서의 결론이다. 같은 mesh 안에서 rev=1-27과 rev=1-30 istiod가 공존하고, 각 proxy는 자기 istio.io/rev가 가리키는 istiod 하나에만 붙는다. 아래쪽 점선(pod-to-pod traffic)이 중요하다 — rev이 다른 sidecar끼리도 서로 트래픽을 주고받는다. data plane은 통일된 하나의 mesh이고, 그 위에서 control plane만 두 버전이 굴러간다. 그러니 “업그레이드"란 control plane을 한 번에 교체하는 게 아니라, proxy를 어느 istiod에 붙일지 하나씩 바꾸는 일이다.
설치를 별 chart로 쪼개는 것은 수단이고, 수명주기 분리(istiod를 다른 컴포넌트와 무관하게 추가/교체)가 목적이다. xDS가 느슨한 계약이라서 이 분리가 물리적으로 가능하다.
03. 그 분리가 설치 형태로 굳은 모습 — base / istiod / gateway 3 Helm chart
§02의 “수명주기가 다르면 따로 다뤄야 한다"는 원리를 설치 단계에서 그대로 구현한 것이 3개의 독립 Helm release다. 1.30 기준 공식 문서는 이 3-chart Helm 설치와 istioctl install -f IstioOperator.yaml을 동등하게 지원되는 두 설치 방법으로 나란히 안내한다 — 어느 쪽도 deprecated가 아니다(deprecated된 것은 IstioOperator CR을 클러스터 내부에서 자동으로 reconcile하던 in-cluster operator 패턴뿐이며, 1.23에서 deprecated 공지·1.24에서 제거됐다). 이 문서는 그중 3-chart Helm 경로를 다룬다. 각 chart의 책임(=변경 주기, =blast radius)이 다르고, 다르기 때문에 따로 설치·업그레이드된다.
| chart | 설치 대상 | 수명주기(변경 주기) | blast radius |
|---|---|---|---|
istio/base |
CRD + cluster-wide RBAC/webhook 골격 | mesh당 1번, 거의 안 바뀜 | cluster 전역 (모든 revision 공유) |
istio/istiod |
control plane Deployment(istiod) | revision마다 별도 release | 그 revision에 붙은 proxy만 |
istio/gateway |
data plane gateway Deployment+Service | gateway마다 별도, app처럼 자주 | 그 gateway가 처리하는 edge 트래픽 |
# 설치 순서 = 의존 순서 (base의 CRD가 있어야 istiod가 뜨고, istiod가 있어야 gateway가 주입됨)
helm install istio-base istio/base -n istio-system --create-namespace
helm install istiod istio/istiod -n istio-system --wait
helm install istio-ingress istio/gateway -n istio-ingress --create-namespace
이 표를 읽는 핵심은 세로 줄(수명주기)의 비대칭이다.
- base ↔ istiod 비대칭이 canary의 토대다. CRD/webhook(base)은 mesh 전체가 공유하는 고정 토대라 revision과 무관하게 한 벌만 둔다. 반면 istiod는 버전마다 따로 깔 수 있어야 §02의 “여러 버전 공존"이 성립한다. 그래서 base=release 1개, istiod=revision N개라는 비대칭이 나온다. 이 비대칭이 우연이 아니라 설계 의도다.
- gateway가 별 chart인 이유 = 그것이 data plane이라서. gateway proxy는 control plane이 아니라 data plane이다(§02의 분류). istiod와 lifecycle을 묶을 이유가 없고, app과 가까운 namespace에 두어 ingress/egress를 독립적으로 scale·배포·롤백한다. “control plane을 안 건드리고 gateway만 재배포"가 가능한 게 이 분리의 직접적 이득이다.
1.30 기준 deprecated·제거된 것은 클러스터 내부에서 IstioOperator CR을 감시해 자동으로 reconcile하던 in-cluster operator 컨트롤러뿐이다(1.23에서 deprecated 공지, 1.24에서 제거). IstioOperator API(CRD) 자체는 istioctl install -f IstioOperator.yaml 형태로 지금도 정식 지원되며 deprecated가 아니다 — 공식 발표문도 “istioctl install 명령과 IstioOperator YAML 파일로 Istio를 설치하는 사용자는 영향받지 않는다"고 명시한다. 과거 “empty 프로파일로 gateway만 켜기” 같은 IstioOperator 2개 분리 패턴도, 이제는 위처럼 istiod chart와 gateway chart 분리로 자연히 달성된다. 기존 operator-owned 설치가 있으면 Helm으로 재정렬할 때 ownership(label/annotation) 충돌을 먼저 확인할 것.
04. 분리를 “동시 존재"로 구현하는 구체 장치 — revision
§02~03이 “왜·무엇을"이라면, revision은 그것을 실제 클러스터에 어떻게 박는가다. revision 문자열(예: 1-30-1)은 다음 모든 곳에 동일하게 박혀, “이 proxy는 어느 istiod의 것인가"라는 단 하나의 질문에 답한다. 이 문자열이 매개라서, 같은 클러스터에 두 revision이 충돌 없이 공존한다.
revision = "1-30-1" 이 박히는 곳 (모두 같은 문자열이라 서로를 가리킨다)
- istiod Deployment/Service 이름 : istiod-1-30-1
- injection MutatingWebhookConfig : istio-revision-tag / istio-sidecar-injector-1-30-1
- namespace label : istio.io/rev=1-30-1
- 주입된 sidecar의 xDS 연결 대상 : istiod-1-30-1.istio-system.svc
마지막 줄이 메커니즘의 심장이다. 주입 webhook은 namespace의 istio.io/rev label을 보고, 새로 뜨는 Pod에 “네 두뇌는 istiod-1-30-1.istio-system.svc다"라는 bootstrap 설정을 박는다. 즉 proxy가 어느 istiod에 붙을지는 “주입되는 순간” 고정된다. 여기서 가장 비직관적이지만 가장 중요한 결론이 나온다.
label을 바꾼다고 기존 Pod가 옮겨가지 않는다. 이미 주입이 끝난 sidecar는 옛 bootstrap을 들고 옛 istiod에 그대로 붙어 있다. label은 “앞으로 뜰 Pod의 두뇌"만 정한다.
그래서 revision 전환은 반드시 두 단계다 — label로 미래를 정하고, restart로 현재를 미래로 끌어온다.
# 1) namespace를 새 revision으로 표시 → 이후 주입되는 Pod의 bootstrap이 바뀐다
kubectl label ns app-a istio.io/rev=1-30-1 --overwrite
# 2) Pod를 재시작 → 재주입이 일어나 새 sidecar가 새 istiod에 붙는다
kubectl rollout restart deployment -n app-a
label만 바꾸고 rollout restart를 빼먹으면, 기존 sidecar가 구 istiod에 계속 붙어 있어 “업그레이드한 줄 알았는데 안 된” 상태가 된다. 메커니즘상 당연한 결과지만(주입은 Pod 생성 시 1회), 증상은 “label은 새건데 동작은 옛것"이라 헷갈린다. 전환 검증은 label이 아니라 항상 istioctl proxy-status로 실제 연결 대상을 확인한다.
revision 문자열이 1.30.1이 아니라 1-30-1인 이유: 이 값이 istiod Deployment/Service 같은 오브젝트 이름(istiod-1-30-1)으로도 동시에 쓰이고, 오브젝트 이름은 DNS-1123 label 규칙을 따라야 하는데 이 규칙이 .을 허용하지 않기 때문이다 — K8s label 값 자체는 사실 .을 허용한다(예: app.kubernetes.io/name처럼 label 값에 점이 흔히 쓰인다). 점을 못 쓰게 만드는 제약은 label이 아니라 오브젝트 이름 규칙 하나뿐이다. 그래서 점을 대시로 바꾼다 — 사소해 보이지만 §04 표의 “같은 문자열이 여러 곳에 박힌다"는 제약이 만든 필연이다.
05. 예시 — canary 한 번 돌리고 “정말 옮겨졌나” 눈으로 확인하기
세 가지(xDS 느슨한 계약 §02 + chart 분리 §03 + revision §04)가 합쳐지면 in-place 교체가 아닌 나란히 띄우고 옮기기가 된다. 한 namespace를 옮기는 전체 흐름과, 각 단계에서 무엇이 보여야 성공인지를 같이 본다.
sequenceDiagram participant Op as operator participant New as istiod rev=1-30 participant NS as namespace app-a participant Pod as sidecar(app-a) Op->>New: helm install (canary, 기존 1-27 유지) Note over New: 구 control plane 그대로 가동 Op->>NS: label istio.io/rev=1-30 Op->>Pod: rollout restart Pod->>New: 새 sidecar가 rev=1-30 istiod에 xDS 연결 Note over Op: proxy-status / 503·latency 검증 Op->>NS: (전체 정상) 나머지 ns도 동일 전환 Op->>New: 구 rev=1-27 istiod 제거
검증의 핵심 명령은 istioctl proxy-status다. 이 명령은 각 proxy가 실제로 어느 istiod에 붙어 있는지와 그 동기화 상태를 보여준다 — label이 아니라 진실을 본다. 전환 전후 출력 대조가 “정말 옮겨졌나"의 유일한 신뢰 가능한 근거다.
# 전환 전: app-a의 sidecar가 구 istiod(1-27)에 붙어 있음
$ istioctl proxy-status
NAME CLUSTER ... ISTIOD VERSION
app-a-7c9...istio-proxy Kubernetes ... istiod-1-27-3-xxxx 1.27.3
ingressgateway-... Kubernetes ... istiod-1-27-3-xxxx 1.27.3
# label + rollout restart 후: app-a만 새 istiod(1-30)로 이동, gateway는 아직 구버전
$ istioctl proxy-status
NAME CLUSTER ... ISTIOD VERSION
app-a-9f2...istio-proxy Kubernetes ... istiod-1-30-1-yyyy 1.30.1
ingressgateway-... Kubernetes ... istiod-1-27-3-xxxx 1.27.3
읽는 법: ISTIOD 열이 istiod-1-30-1-...로 바뀌고 동기화 상태(CDS/LDS/EDS/RDS)가 모두 SYNCED면 그 proxy는 정상적으로 새 control plane으로 옮겨진 것이다. 위 출력은 §02 anchor 그림을 그대로 증명한다 — app-a sidecar(rev=1-30)와 ingressgateway(rev=1-27)가 서로 다른 istiod에 붙어 공존하고, 그래도 mesh는 한 덩어리로 트래픽을 흘린다.
이 흐름에서 data plane이 받는 충격은 “Pod 한 번 재시작"뿐이고, 그것도 namespace 단위로 점진 적용되며, 언제든 label을 되돌리고 다시 restart하면 즉시 rollback된다(구 istiod를 아직 안 지웠으므로 붙을 두뇌가 살아 있다). control plane 버전 교체 자체는 트래픽 경로에 끼어들지 않는다 — 이것이 “투명하다"의 정확한 의미다.
구체적 canary 설치 명령, DNS-1123 규칙, 전환 합격 기준(proxy-status SYNCED / 503·504 증가 없음 / p95·p99 변화 없음 / istiod push error 없음)은 중복하지 않고 운영 플레이북 §06 — revision canary upgrade에 정리돼 있다. 업그레이드가 push 부하·warmup에 미치는 영향과 그 진단은 istiod 성능 4요인을 참조.
핵심 정리
- 하나의 anchor: istiod와 Envoy는 xDS gRPC 전선 한 가닥으로만 묶인 별개 박스다. 전선을 뽑아도 Envoy는 마지막 설정으로 굴러가고(fail static), 전선의 반대쪽 끝(어느 istiod)은 proxy마다 다르게 갈아 끼울 수 있다.
- 분리의 동기: control plane 버전과 워크로드 설정이 결합돼 있으면 업그레이드 blast radius=mesh 전체. 떼어내야 부분적·되돌릴 수 있는 업그레이드가 가능하다.
- 설치 형태: base(CRD/webhook, 1벌) + istiod(revision마다 N벌) + gateway(별 Deployment). base↔istiod의 수명주기 비대칭이 canary의 토대. deprecated된 건 IstioOperator API가 아니라 in-cluster operator 컨트롤러뿐 — istioctl install과 Helm 3-chart는 1.30 기준 둘 다 유효한 정식 설치 방법이다.
- revision 메커니즘:
1-30-1한 문자열이 istiod 이름·webhook·namespace label·sidecar 연결 대상에 동일하게 박혀 여러 버전 공존을 성립시킨다. 점(.)을 못 쓰는 이유는 label 제약이 아니라 오브젝트 이름(DNS-1123) 제약 하나뿐이다. - 업그레이드 = ns 단위 이주: label 변경 + rollout restart 2단계. label만 바꾸고 restart 빼먹으면 안 옮겨진다 —
istioctl proxy-status로 실제 연결 대상을 검증. 언제든 label 되돌려 rollback.
What you might be missing
- 분리의 본질은 chart 개수가 아니라 lifecycle 독립. “왜 chart를 3개로 쪼개나"의 답은 “각 컴포넌트의 변경 주기와 blast radius가 다르기 때문"이다(§03 표 세로 줄). CRD는 거의 안 바뀌고, istiod는 분기마다 canary로 바뀌고, gateway는 app처럼 자주 배포된다 — 묶으면 한쪽 변경이 다른 쪽을 강제로 끌고 간다.
- base(CRD)는 revision으로 canary 되지 않는다. istiod는 여러 버전 공존하지만 CRD 스키마는 cluster에 한 벌뿐이다. 그래서 CRD가 깨지는 major 변경은 진짜 위험 — canary로 격리되지 않는 유일한 부분이다. 업그레이드 전
istioctl x precheck로 CRD 호환성을 먼저 본다. - gateway도 자기 revision이 있다. sidecar만 revision label로 옮긴다고 생각하기 쉽지만, gateway Deployment도 어느 istiod에 붙는지 revision 지정이 필요하다(§05 출력에서 ingressgateway가 구버전에 남아 있던 것이 바로 이 함정의 모습). ingress/egress gateway를 빼먹으면 control plane만 새 버전이고 edge proxy는 구 버전에 붙어 있는 비대칭이 생긴다.
- fail static의 한계 = cert 갱신. istiod가 길게 죽으면 새 설정뿐 아니라 SPIFFE workload cert 갱신도 멈춘다(기본 cert TTL은 24h, 갱신은 그 절반인 12h 시점). 짧은 장애엔 투명하지만 장기 control plane 장애는 cert 만료로 mTLS가 무너질 수 있다 — “istiod 없어도 영원히 괜찮다"는 아니다. cert와 SPIFFE identity의 동작은 mTLS·SPIFFE identity 참조.
검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)
검증 방법: Istio/Envoy/Kubernetes 공식 문서·소스코드 대조 + homelab 클러스터 실측 2건(T39는 격리 전제가 성립하지 않아 판단 불가, T40은 통과).
| 주장 | 판정 | 근거 |
|---|---|---|
| C1. istiod-Envoy는 xDS gRPC 느슨한 계약으로만 연결된 별개 컴포넌트 | ✅ 문헌 확인 | istio.io/latest/docs/ops/deployment/architecture/ |
| C2. istiod는 트래픽 데이터 경로에 없다(패킷은 istiod를 거치지 않음) | ✅ 문헌 확인 | istio.io/latest/docs/ops/deployment/architecture/ |
| C3. fail static — istiod 다운 시에도 마지막 설정으로 계속 처리 | 실측 불가 | envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol (문헌) · T39 실측 (NetworkPolicy가 클러스터 Calico에서 enforce 안 돼 격리 자체가 실패 — 인프라 결함이며 반증 아님) |
| C4. 여러 istiod 공존 시 proxy는 부트스트랩 때 지정된 하나에만 연결 | ✅ 실측 확인 | T88 실측 — verify-rev 병행 설치: CA_ADDR/proxy-status가 revision별 istiod 고정, cross-revision 트래픽 mTLS 정상 · istio.io canary upgrade |
| C5. “현행 권장은 3-chart Helm, istioctl install은 deprecated”(원문 주장) | ❌ 오류 — 본문 교정 | istio.io/latest/docs/setup/install/ |
| C6. “IstioOperator API와 in-cluster operator 둘 다 deprecated”(원문 주장) | ❌ 오류 — 본문 교정 | istio.io/latest/blog/2024/in-cluster-operator-deprecation-announcement/ |
| C7. base chart = CRD+cluster-wide RBAC/webhook, mesh당 1회 설치 | ✅ 문헌 확인 | istio.io/latest/docs/setup/install/helm/ |
| C8. istiod chart는 revision마다 독립 Helm release | ✅ 문헌 확인 | istio.io/latest/docs/setup/upgrade/canary/ |
| C9. revision 문자열이 이름·webhook·namespace label·연결대상에 동일하게 박힘 | ✅ 문헌 확인 | istio.io/latest/docs/setup/upgrade/canary/ |
| C10. namespace label 변경은 기존 Pod에 소급 적용 안 되고 restart로만 재주입 | ✅ 실측 확인 | istio.io/latest/docs/setup/upgrade/canary/ · T40 실측 |
| C11. “점 금지 이유가 label 값과 DNS-1123 둘 다”(원문 주장) | ❌ 오류 — 본문 교정 | kubernetes.io/docs/concepts/overview/working-with-objects/labels/ |
| C12. istioctl proxy-status는 ISTIOD 연결 대상·CDS/LDS/EDS/RDS 동기화 상태를 보여줌 | ✅ 문헌 확인 | istio.io/latest/docs/ops/diagnostic-tools/proxy-cmd/ |
| C13. SPIFFE workload cert 기본 TTL은 24h | ✅ 문헌 확인 | github.com/istio/istio (pilot/cmd/pilot-agent/options/options.go) |
| C14. cert 갱신은 TTL의 절반인 12h 시점 | ✅ 문헌 확인 | github.com/istio/istio (pilot/cmd/pilot-agent/options/options.go) |
| C15. label 되돌리고 재시작하면 구 istiod가 살아있는 한 즉시 rollback | ✅ 문헌 확인 | istio.io/latest/docs/setup/upgrade/canary/ (C10과 동일 메커니즘, T40 실측으로 간접 확인) |