--- type: note tags: [istio, install, upgrade, helm, revision] created: 2026-06-07 --- # control plane과 data plane의 설치·수명주기를 분리하면 istiod 업그레이드가 data plane에 투명해진다 > [!abstract] 이 문서가 다루는 것 > "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](arch__src-operations-playbook.html)에 위임한다. > > 대상: Istio 1.30 / Helm 설치. 독자: control plane 업그레이드의 안전성 모델을 원리로 이해하려는 DevOps/SRE. 선행: xDS가 무엇인지 대략의 감([xDS API 계층](xds__note-xds-api-layers.html)). --- ## 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를 옆에 띄우는 게 가능하다. ```mermaid 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에 붙일지 하나씩 바꾸는 일**이다. > [!key] 한 문장 > 설치를 별 chart로 쪼개는 것은 *수단*이고, **수명주기 분리**(istiod를 다른 컴포넌트와 무관하게 추가/교체)가 *목적*이다. xDS가 느슨한 계약이라서 이 분리가 물리적으로 가능하다. --- ## 03. 그 분리가 설치 형태로 굳은 모습 — base / istiod / gateway 3 Helm chart §02의 "수명주기가 다르면 따로 다뤄야 한다"는 원리를 설치 단계에서 그대로 구현한 것이 **3개의 독립 Helm release**다. 현행(1.30) 권장 설치는 deprecated된 `istioctl install -f IstioOperator.yaml`이 아니라 이 3-chart다. 각 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 트래픽 | ```bash # 설치 순서 = 의존 순서 (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만 재배포"가 가능한 게 이 분리의 직접적 이득이다. > [!warning] IstioOperator / in-cluster operator는 deprecated > 1.30 기준 `IstioOperator` API와 in-cluster operator(operator pattern으로 IstioOperator CR을 reconcile하던 방식)는 deprecated다. 과거 "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이 충돌 없이 공존한다. ```text 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로 *현재*를 미래로 끌어온다. ```bash # 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 ``` > [!warning] 진짜 함정은 label이 아니라 재시작 누락 > label만 바꾸고 `rollout restart`를 빼먹으면, 기존 sidecar가 구 istiod에 계속 붙어 있어 "업그레이드한 줄 알았는데 안 된" 상태가 된다. 메커니즘상 당연한 결과지만(주입은 Pod 생성 시 1회), 증상은 "label은 새건데 동작은 옛것"이라 헷갈린다. 전환 검증은 label이 아니라 항상 `istioctl proxy-status`로 *실제 연결 대상*을 확인한다. revision 문자열이 `1.30.1`이 아니라 `1-30-1`인 이유: 이 값이 K8s **label 값**이자 **DNS-1123 label**(Service 이름 `istiod-1-30-1`)로 동시에 쓰이는데, 둘 다 `.`을 허용하지 않기 때문이다. 그래서 점을 대시로 바꾼다 — 사소해 보이지만 §04 표의 "같은 문자열이 여러 곳에 박힌다"는 제약이 만든 필연이다. --- ## 05. 예시 — canary 한 번 돌리고 "정말 옮겨졌나" 눈으로 확인하기 세 가지(xDS 느슨한 계약 §02 + chart 분리 §03 + revision §04)가 합쳐지면 in-place 교체가 아닌 **나란히 띄우고 옮기기**가 된다. 한 namespace를 옮기는 전체 흐름과, 각 단계에서 *무엇이 보여야 성공인지*를 같이 본다. ```mermaid 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이 아니라 진실을 본다. 전환 *전후* 출력 대조가 "정말 옮겨졌나"의 유일한 신뢰 가능한 근거다. ```text # 전환 전: 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](arch__src-operations-playbook.html)에 정리돼 있다. 업그레이드가 push 부하·warmup에 미치는 영향과 그 진단은 [istiod 성능 4요인](arch__note-control-plane-performance-factors.html)을 참조. --- ## 핵심 정리 - **하나의 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의 토대. IstioOperator/in-cluster operator는 1.30에서 deprecated → Helm 3-chart 권장. - **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](sec__note-mtls-spiffe-identity.html) 참조.