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를 옆에 띄우는 게 가능하다.
이 그림이 문서의 결론이다. 같은 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) 권장 설치는 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 트래픽 |
# 설치 순서 = 의존 순서 (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 기준 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이 충돌 없이 공존한다.
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인 이유: 이 값이 K8s label 값이자 DNS-1123 label(Service 이름 istiod-1-30-1)로 동시에 쓰이는데, 둘 다 .을 허용하지 않기 때문이다. 그래서 점을 대시로 바꾼다 — 사소해 보이지만 §04 표의 "같은 문자열이 여러 곳에 박힌다"는 제약이 만든 필연이다.
05. 예시 — canary 한 번 돌리고 "정말 옮겨졌나" 눈으로 확인하기
세 가지(xDS 느슨한 계약 §02 + chart 분리 §03 + revision §04)가 합쳐지면 in-place 교체가 아닌 나란히 띄우고 옮기기가 된다. 한 namespace를 옮기는 전체 흐름과, 각 단계에서 무엇이 보여야 성공인지를 같이 본다.
검증의 핵심 명령은 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의 토대. 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 참조.