{
  "tier": "public",
  "generated": "2026-07-03T13:42:19",
  "docs": [
    {
      "title": "Neovim + LazyVim DevOps 환경 구성",
      "desc": "날짜: 2026-02-21 목적: DevOps/SRE 작업(YAML, Bash, Dockerfile, Terraform, JSON)에 최적화된 nvim IDE 환경",
      "url": "/public/devtools/nvim__guide-lazyvim-devops-setup.html",
      "domain": "devtools",
      "text": "Neovim + LazyVim DevOps 환경 구성 날짜 : 2026-02-21 목적 : DevOps/SRE 작업(YAML, Bash, Dockerfile, Terraform, JSON)에 최적화된 nvim IDE 환경 설치 요약 항목 내용 Neovim v0.11.6 (appimage, /usr/local/bin/nvim ) 배포판 LazyVim (starter template) LSP yaml, bash, dockerfile, terraform, json (extras로 자동 구성) 파일 탐색 snacks.nvim (LazyVim 기본 picker) + ripgrep + fd 구문 강조 treesitter 아이콘 JetBrainsMono Nerd Font 설치된 구성 요소 시스템 패키지 sudo apt install -y ripgrep fd-find cmake Neovim (appimage) curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.appimage chmod +x nvim-linux-x86_64.appimage sudo mv nvim-linux-x86_64.appimage /usr/local/bin/nvim Nerd Font JetBrainsMono Nerd Font → ~/.local/share/fonts/ 터미널 에뮬레이터에서 \"JetBrainsMono Nerd Font\" 선택 필요 LazyVim git clone https://github.com/LazyVim/starter ~/.config/nvim rm -rf ~/.config/nvim/.git 커스텀 설정 파일 ~/.config/nvim/lua/plugins/devops-extras.lua DevOps 관련 LazyVim extras 활성화: - lang.yaml — yamlls LSP, YAML treesitter - lang.json — jsonls LSP, JSON treesitter - lang.docker — dockerls LSP, Dockerfile treesitter - lang.terraform — terraform-ls LSP, HCL treesitter ~/.config/nvim/lua/config/options.lua 줄번호 + 상대 줄번호 탭/들여쓰기 2칸 (YAML 기본) 시스템 클립보드 연동 ( unnamedplus ) 주요 키바인딩 (LazyVim 기본) 키 동작 <Space> Leader key <leader>ff 파일 검색 (picker) <leader>fg 라이브 grep <leader>e 파일 탐색기 (neo-tree) <leader>l Lazy 플러그인 매니저 K LSP hover 문서 gd 정의로 이동 <leader>ca 코드 액션 <leader>cf 포맷팅 사후 작업 (수동) 터미널 폰트 설정 : 터미널 에뮬레이터에서 \"JetBrainsMono Nerd Font\"로 변경 LSP 서버 설치 : 첫 nvim 실행 시 Mason이 자동으로 LSP 서버 설치 ( :Mason 으로 확인) treesitter 파서 설치 : 파일을 열면 자동 설치됨 ( :TSInstall yaml json bash dockerfile hcl ) 업데이트 방법 nvim 내에서: - :Lazy sync — 플러그인 업데이트 - :Mason — LSP 서버 관리 - :TSUpdate — treesitter 파서 업데이트 Neovim 자체 업데이트: curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.appimage chmod +x nvim-linux-x86_64.appimage sudo mv nvim-linux-x86_64.appimage /usr/local/bin/nvim"
    },
    {
      "title": "Neovim + LazyVim 사용 가이드",
      "desc": "날짜: 2026-02-21 대상: DevOps/SRE 워크플로우 (YAML, Bash, Dockerfile, Terraform, JSON)",
      "url": "/public/devtools/nvim__guide-lazyvim-usage.html",
      "domain": "devtools",
      "text": "Neovim + LazyVim 사용 가이드 날짜 : 2026-02-21 대상 : DevOps/SRE 워크플로우 (YAML, Bash, Dockerfile, Terraform, JSON) 1. 기본 개념 LazyVim은 Neovim 위에 올라가는 배포판이다. Leader key는 <Space> 이며, 거의 모든 기능이 <Space> + 후속 키로 시작한다. 모르겠으면 <Space> 만 누르면 된다 — which-key가 가능한 키 목록을 팝업으로 보여준다. Vim 모드 요약 모드 진입 용도 Normal Esc 탐색, 명령 실행 (기본 상태) Insert i , a , o 텍스트 입력 Visual v , V , Ctrl-v 텍스트 선택 Command : Ex 명령 실행 2. 파일 열기/닫기 nvim # LazyVim 대시보드로 시작 nvim file.yaml # 특정 파일 열기 nvim dir/ # 디렉토리 열기 (neo-tree) nvim 안에서 키 동작 <Space>ff 파일 이름으로 검색 (picker) <Space>fr 최근 열었던 파일 <Space>fg 파일 내용 검색 (live grep, ripgrep 사용) <Space>fb 열린 버퍼 목록 <Space>e 파일 탐색기 토글 (neo-tree 사이드바) <Space>E 파일 탐색기 (루트 디렉토리) 버퍼 (탭) 관리 키 동작 <S-h> (Shift+h) 이전 버퍼 <S-l> (Shift+l) 다음 버퍼 <Space>bd 현재 버퍼 닫기 <Space>bo 다른 버퍼 전부 닫기 <Space>bb 버퍼 전환 (picker) 저장/종료 키 동작 :w 저장 :q 닫기 :wq 또는 ZZ 저장 후 닫기 :qa 전체 종료 <Space>qq 전체 종료 (LazyVim) 3. 편집 — 자주 쓰는 조작 이동 키 동작 h/j/k/l 좌/하/상/우 (화살표 대신) w / b 단어 앞으로/뒤로 0 / $ 줄 처음/끝 gg / G 파일 처음/끝 { / } 빈 줄 단위로 이동 (문단) Ctrl-d / Ctrl-u 반 페이지 아래/위 :<숫자> 특정 줄로 이동 (예: :42 ) 편집 키 동작 i 커서 앞에 입력 시작 a 커서 뒤에 입력 시작 o / O 아래/위에 새 줄 만들고 입력 dd 줄 삭제 (잘라내기) yy 줄 복사 p / P 아래/위에 붙여넣기 u 되돌리기 (undo) Ctrl-r 다시 하기 (redo) ciw 단어 하나 교체 (change inner word) ci\" 따옴표 안의 내용 교체 di{ 중괄호 안의 내용 삭제 . 마지막 동작 반복 선택 (Visual 모드) 키 동작 v 문자 단위 선택 시작 V 줄 단위 선택 시작 Ctrl-v 블록(직사각형) 선택 선택 후 d 삭제 선택 후 y 복사 선택 후 > / < 들여쓰기/내어쓰기 4. 검색과 바꾸기 파일 내 검색 키 동작 /패턴 아래로 검색 ?패턴 위로 검색 n / N 다음/이전 결과 * 커서 위 단어 검색 <Space>ur 검색 하이라이트 끄기 파일 내 바꾸기 :%s/old/new/g \" 파일 전체에서 old → new :%s/old/new/gc \" 하나씩 확인하며 바꾸기 :10,20s/old/new/g \" 10~20줄에서만 바꾸기 프로젝트 전체 검색/바꾸기 키 동작 <Space>fg 프로젝트 전체 내용 검색 (live grep) <Space>sw 커서 위 단어로 프로젝트 검색 <Space>sr 검색 & 바꾸기 (grug-far) 5. Flash — 화면 내 빠른 점프 LazyVim에는 flash.nvim이 포함되어 있다. 화면에 보이는 아무 위치로 2~3키로 점프. 키 동작 s + 문자 2개 해당 위치로 점프 S treesitter 기반 선택 (코드 블록 단위) 예시: sya → 화면에서 \"ya\"로 시작하는 위치에 라벨이 표시되고, 라벨 키를 누르면 점프. 6. 윈도우 분할 키 동작 <Space>- 수평 분할 <Space>\\| 수직 분할 <Ctrl-h/j/k/l> 분할 창 사이 이동 <Space>wd 현재 창 닫기 <Space>wm 현재 창 최대화 토글 7. LSP — 코드 지능 첫 실행 시 Mason이 자동으로 LSP 서버를 설치한다. :Mason 으로 상태 확인 가능. 코드 탐색 키 동작 K 커서 위 심볼의 문서/설명 (hover) gd 정의로 이동 (go to definition) gr 참조 목록 (references) gI 구현으로 이동 (implementation) 진단 (에러/경고) 키 동작 ]d / [d 다음/이전 진단으로 이동 <Space>cd 줄 진단 상세 보기 <Space>xx Trouble 진단 패널 토글 코드 액션 키 동작 <Space>ca 코드 액션 (자동 수정 제안) <Space>cr 이름 바꾸기 (rename) <Space>cf 포맷팅 8. Git 연동 gitsigns.nvim이 변경 사항을 줄 옆에 표시한다. 키 동작 ]h / [h 다음/이전 변경 hunk로 이동 <Space>ghs hunk 스테이지 (git add 부분) <Space>ghr hunk 되돌리기 <Space>ghp hunk 미리보기 (diff) <Space>gb git blame (줄별 커밋 정보) LazyGit 연동 ( <Space>gg )은 lazygit이 설치되어 있어야 동작한다: sudo apt install lazygit # 또는 go install 9. 플러그인 관리 키/명령 동작 <Space>l Lazy 플러그인 매니저 열기 :Lazy sync 플러그인 업데이트 :Lazy health 플러그인 상태 점검 :Mason LSP/포매터/린터 서버 관리 :TSUpdate treesitter 파서 업데이트 :checkhealth 전체 환경 건강 진단 10. DevOps 실전 워크플로우 YAML 편집 (Kubernetes manifest, docker-compose 등) nvim deployment.yaml — yamlls가 자동으로 스키마 감지 자동완성: Insert 모드에서 타이핑하면 blink.cmp가 제안 표시 - Tab / Shift-Tab — 제안 선택 - Enter — 확정 K — 현재 필드 설명 (hover) 에러 발생 시 줄 왼쪽에 빨간 표시 → <Space>cd 로"
    },
    {
      "title": "tmux 설치 및 설정 구성",
      "desc": "sudo apt install -y tmux tmux 3.4-1ubuntu0.1 설치됨.",
      "url": "/public/devtools/tmux__guide-setup-and-plugins.html",
      "domain": "devtools",
      "text": "tmux 설치 및 설정 구성 작업일 : 2026-02-21 환경 : Ubuntu 24.04.4 LTS / tmux 3.4 설정 파일 : ~/.tmux.conf 플러그인 경로 : ~/.tmux/plugins/ 1. 설치 sudo apt install -y tmux tmux 3.4-1ubuntu0.1 설치됨. 2. 설정 파일 (~/.tmux.conf) 2.1 Prefix 키 Ctrl-a (primary)와 Ctrl-b (secondary) 둘 다 사용 가능 하도록 구성. set -g prefix C-a set -g prefix2 C-b bind C-a send-prefix bind C-b send-prefix -2 2.2 기본 동작 설정 값 설명 mouse on pane 클릭, 리사이즈, 스크롤 history-limit 50000 스크롤백 버퍼 (기본 2000) base-index 1 window 번호 1부터 시작 pane-base-index 1 pane 번호 1부터 시작 escape-time 0 ESC 지연 제거 (vim 필수) renumber-windows on window 닫으면 번호 재정렬 default-terminal tmux-256color 256색 + true color 지원 2.3 키바인딩 pane 분할 (tmux 기본값 유지) 키 동작 prefix + % 수직 분할 prefix + \" 수평 분할 pane 이동 (vim 스타일 추가) 키 동작 prefix + h 왼쪽 pane prefix + j 아래 pane prefix + k 위 pane prefix + l 오른쪽 pane pane 리사이즈 키 동작 prefix + H 왼쪽으로 5칸 prefix + J 아래로 5칸 prefix + K 위로 5칸 prefix + L 오른쪽으로 5칸 -r 플래그로 반복 입력 가능 (prefix 한 번 누르고 H/J/K/L 연타). 기타 키 동작 prefix + r 설정 파일 리로드 복사 모드 (vi 키바인딩) 키 동작 prefix + [ 복사 모드 진입 v 선택 시작 y 선택 복사 후 복사 모드 종료 2.4 상태바 One Dark 테마 기반 색상 구성: - 좌측: 세션명 (초록) - 우측: 호스트명 (파랑) + 시각 (노랑) - 현재 window: 파란 배경 볼드 3. 플러그인 3.1 TPM (Tmux Plugin Manager) git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm .tmux.conf 맨 마지막에 run '~/.tmux/plugins/tpm/tpm' 필수. TPM 조작 키 (tmux 세션 안에서) 키 동작 prefix + I 새 플러그인 설치 (.tmux.conf에 추가 후) prefix + U 설치된 플러그인 업데이트 prefix + Alt + u 목록에서 제거된 플러그인 삭제 플러그인 추가 방법 .tmux.conf 에 set -g @plugin 'tmux-plugins/플러그인명' 추가 tmux 세션에서 prefix + I 입력 설치 완료 메시지 확인 3.2 tmux-resurrect — 세션 저장/복구 리부트나 tmux 서버 종료 후에도 세션 레이아웃을 복원할 수 있다. 저장 대상 : window 배치, pane 레이아웃, 각 pane의 작업 디렉토리, 실행 중인 프로그램. 키 동작 prefix + Ctrl-s 현재 상태 저장 prefix + Ctrl-r 마지막 저장 상태 복구 저장 파일 위치: ~/.tmux/resurrect/ 복구 절차 (리부트 후): 1. tmux 실행 (빈 세션 열림) 2. prefix + Ctrl-r 입력 3. 이전 pane 레이아웃 + 디렉토리 복원됨 3.3 tmux-continuum — 자동 저장 resurrect를 15분 간격으로 자동 실행한다. 수동 저장을 잊어도 최근 상태가 보존됨. set -g @continuum-save-interval '15' 별도 키 조작 없음. tmux 세션이 살아있는 동안 백그라운드로 동작. 4. 설정 파일 전문 # ============================================ # Prefix: Ctrl-a (primary) + Ctrl-b (secondary) # ============================================ set -g prefix C-a set -g prefix2 C-b bind C-a send-prefix bind C-b send-prefix -2 # ============================================ # 기본 동작 # ============================================ set -g mouse on set -g history-limit 50000 set -g base-index 1 setw -g pane-base-index 1 set -sg escape-time 0 set -g renumber-windows on set -g default-terminal \"tmux-256color\" set -ga terminal-overrides \",xterm-256color:Tc\" # ============================================ # 키바인딩 # ============================================ # pane 이동 (vim 스타일) bind h select-pane -L bind j select-pane -D bind k select-pane -U bind l select-pane -R # pane 리사이즈 bind -r H resize-pane -L 5 bind -r J resize-pane -D 5 bind -r K resize-pane -U 5 bind -r L resize-pane -R 5 # 설정 리로드 bind r source-file ~/.tmux.conf \\; display \"Config reloaded!\" # 복사모드 vi 키바인딩 setw -g mode-keys vi bind -T copy-mode-vi v send -X begin-selection"
    },
    {
      "title": "k8s-etcd-lab",
      "desc": "KVM 기반 3-노드 Kubernetes 클러스터를 구성하고 etcd 장애 시나리오를 실험하는 로컬 랩 환경입니다.",
      "url": "/public/etcd/etcd__MOC-lab-overview.html",
      "domain": "etcd",
      "text": "k8s-etcd-lab KVM 기반 3-노드 Kubernetes 클러스터를 구성하고 etcd 장애 시나리오를 실험하는 로컬 랩 환경입니다. 목적 kubespray로 HA 컨트롤 플레인(3-node etcd 클러스터) 구성 실습 etcd 단일 노드 장애 / 다수 노드 장애 / 스냅샷 백업·복구 실험 실제 프로덕션 장애 패턴을 안전한 VM 환경에서 재현 환경 스펙 항목 값 Host OS Ubuntu 24.04.4 LTS Hypervisor KVM + QEMU 8.2.2 + Libvirt 10.0.0 VM OS Ubuntu 22.04 LTS (cloud image) VM 수 3개 (전부 control plane + etcd) 네트워크 203.0.113.0/24 (k8s-lab 전용 NAT 네트워크) Kubernetes kubespray (최신 release 브랜치) CNI Calico VM 구성 호스트명 IP vCPU RAM Disk 역할 k8s-ctrl1 203.0.113.11 4 8GB 40GB control-plane + etcd k8s-ctrl2 203.0.113.12 4 8GB 40GB control-plane + etcd k8s-ctrl3 203.0.113.13 4 8GB 40GB control-plane + etcd Host 리소스: Ryzen 9 5900X (24t) / 40GB RAM → VM 3대에 12vCPU + 24GB 할당, Host 여유 충분 디렉토리 구조 k8s-etcd-lab/ ├── README.md ├── docs/ │ ├── 01-prerequisites.md # 사전 준비 및 패키지 설치 │ ├── 02-vm-setup.md # VM 생성 가이드 │ ├── 03-kubespray-deploy.md # kubespray 설치·배포 가이드 │ ├── 04-etcd-experiments.md # etcd 장애 실험 시나리오 │ └── 05-recovery.md # 복구 절차 ├── network/ │ └── k8s-lab-network.xml # libvirt 전용 네트워크 정의 ├── cloud-init/ │ ├── user-data # VM 초기화 설정 (SSH, 패키지 등) │ └── meta-data # VM 메타데이터 (hostname, instance-id) ├── scripts/ │ ├── lib/common.sh # 공통 함수 │ ├── 01-setup-network.sh # libvirt 네트워크 생성 │ ├── 02-create-vms.sh # VM 3대 생성 │ ├── 03-setup-kubespray.sh # kubespray clone + 인벤토리 구성 │ ├── 04-run-kubespray.sh # 클러스터 배포 실행 │ └── 99-cleanup.sh # 전체 환경 정리(삭제) ├── kubespray/ │ └── inventory/ │ └── k8s-etcd-lab/ │ ├── hosts.yaml │ └── group_vars/ │ ├── all/all.yml │ └── k8s_cluster/ │ ├── k8s-cluster.yml │ └── addons.yml └── experiments/ ├── lib/etcd-helpers.sh # etcd 공통 헬퍼 함수 ├── 01-single-node-failure.sh # 시나리오1: 단일 노드 장애 ├── 02-majority-failure.sh # 시나리오2: 과반수 장애 ├── 03-snapshot-backup.sh # 시나리오3: 스냅샷 백업 └── 04-restore-from-snapshot.sh # 시나리오4: 스냅샷 복구 빠른 시작 순서 # 1. 사전 준비 확인 cat docs/01-prerequisites.md # 2. 전용 네트워크 생성 bash scripts/01-setup-network.sh # 3. VM 3대 생성 bash scripts/02-create-vms.sh # 4. kubespray 준비 bash scripts/03-setup-kubespray.sh # 5. 클러스터 배포 (약 20~30분) bash scripts/04-run-kubespray.sh # 6. 실험 진행 bash experiments/01-single-node-failure.sh bash experiments/03-snapshot-backup.sh bash experiments/04-restore-from-snapshot.sh # 7. 전체 정리 bash scripts/99-cleanup.sh 참고 자료 kubespray GitHub kubespray 공식 문서 etcd 운영 가이드 etcd 재해 복구"
    },
    {
      "title": "03. kubespray 배포 가이드",
      "desc": "kubespray는 Ansible 기반의 프로덕션급 Kubernetes 클러스터 배포 도구입니다.",
      "url": "/public/etcd/etcd__guide-kubespray-deploy.html",
      "domain": "etcd",
      "text": "03. kubespray 배포 가이드 kubespray 개요 kubespray는 Ansible 기반의 프로덕션급 Kubernetes 클러스터 배포 도구입니다. kubespray (Ansible Playbook) └── cluster.yml ├── 부트스트랩 (python, pip 설치) ├── etcd 클러스터 구성 (3-node) ├── kube-apiserver, kube-scheduler, kube-controller-manager ├── kubelet, kube-proxy └── CNI (Calico) kubespray Clone 및 환경 구성 cd /home/jinsoo/Documents/k8s-etcd-lab # kubespray clone git clone https://github.com/kubernetes-sigs/kubespray.git /tmp/kubespray-src # 인벤토리 복사 cp -rfp /tmp/kubespray-src/inventory/sample kubespray/inventory/k8s-etcd-lab # Python 가상환경 활성화 후 의존성 설치 source .venv/bin/activate pip install -r /tmp/kubespray-src/requirements.txt # ansible 연결 테스트 ansible all -i kubespray/inventory/k8s-etcd-lab/hosts.yaml \\ -m ping \\ --private-key ~/.ssh/k8s-lab \\ -u ubuntu 인벤토리 구조 kubespray/inventory/k8s-etcd-lab/hosts.yaml 파일을 사용합니다. # 3개 노드 모두 control-plane + etcd + worker 역할 all: hosts: k8s-ctrl1: ansible_host: 203.0.113.11 k8s-ctrl2: ansible_host: 203.0.113.12 k8s-ctrl3: ansible_host: 203.0.113.13 children: kube_control_plane: hosts: [k8s-ctrl1, k8s-ctrl2, k8s-ctrl3] etcd: hosts: [k8s-ctrl1, k8s-ctrl2, k8s-ctrl3] kube_node: hosts: [k8s-ctrl1, k8s-ctrl2, k8s-ctrl3] 주요 설정값 (group_vars) k8s-cluster.yml 핵심 설정 항목 값 이유 kube_version v1.31.x 최신 stable container_manager containerd 표준 network_plugin calico 안정적, eBPF 지원 kube_proxy_mode iptables 기본값, 실험 용이 etcd_deployment_type host VM 자체에 etcd 바이너리 직접 설치 (컨테이너 아님 → 직접 조작 용이) etcd_deployment_type: host 설정 이유: etcd를 컨테이너로 올리면 실험 시 etcdctl 접근이 번거롭습니다. host 바이너리로 설치하면 systemctl stop etcd 등으로 직접 제어 가능합니다. 클러스터 배포 실행 cd /tmp/kubespray-src source /home/jinsoo/Documents/k8s-etcd-lab/.venv/bin/activate # 전체 클러스터 배포 (약 20~30분 소요) ansible-playbook \\ -i /home/jinsoo/Documents/k8s-etcd-lab/kubespray/inventory/k8s-etcd-lab/hosts.yaml \\ --private-key ~/.ssh/k8s-lab \\ -u ubuntu \\ -b \\ cluster.yml # 특정 태그만 실행 (재배포 시 빠름) ansible-playbook ... --tags \"etcd\" ansible-playbook ... --tags \"master\" 배포 후 검증 # kubeconfig 가져오기 ssh -i ~/.ssh/k8s-lab ubuntu@203.0.113.11 \\ \"sudo cat /etc/kubernetes/admin.conf\" > ~/.kube/k8s-etcd-lab.conf export KUBECONFIG=~/.kube/k8s-etcd-lab.conf # 노드 상태 확인 kubectl get nodes -o wide # 예상 출력 # NAME STATUS ROLES AGE VERSION # k8s-ctrl1 Ready control-plane,master 5m v1.31.x # k8s-ctrl2 Ready control-plane,master 5m v1.31.x # k8s-ctrl3 Ready control-plane,master 5m v1.31.x # etcd 클러스터 상태 확인 ssh -i ~/.ssh/k8s-lab ubuntu@203.0.113.11 \\ \"sudo ETCDCTL_API=3 etcdctl \\ --endpoints=https://127.0.0.1:2379 \\ --cacert=/etc/ssl/etcd/ssl/ca.pem \\ --cert=/etc/ssl/etcd/ssl/member-k8s-ctrl1.pem \\ --key=/etc/ssl/etcd/ssl/member-k8s-ctrl1-key.pem \\ member list -w table\" # 예상 출력 # +------------------+---------+-----------+----------------------------+----------------------------+------------+ # | ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER | # +------------------+---------+-----------+----------------------------+----------------------------+------------+ # | xxxxxxxxxxxxxxx1 | started | k8s-ctrl1 | https://203.0.113.11:23"
    },
    {
      "title": "01. 사전 준비",
      "desc": "",
      "url": "/public/etcd/etcd__guide-prerequisites.html",
      "domain": "etcd",
      "text": "01. 사전 준비 호스트 패키지 확인 및 설치 # 필수 패키지 확인 dpkg -l | grep -E \"virt-install|qemu-kvm|libvirt-daemon|genisoimage|cloud-image-utils|sshpass\" # 설치 (없는 것만) sudo apt-get install -y \\ qemu-kvm \\ libvirt-daemon-system \\ libvirt-clients \\ virt-install \\ genisoimage \\ cloud-image-utils \\ sshpass \\ python3-pip \\ python3-venv # libvirt 그룹 확인 (현재 사용자가 포함되어 있어야 함) groups $USER | grep libvirt # 없으면: sudo usermod -aG libvirt $USER && newgrp libvirt SSH 키 준비 # 랩 전용 SSH 키 생성 (기존 키와 분리 권장) ssh-keygen -t ed25519 -f ~/.ssh/k8s-lab -N \"\" -C \"k8s-lab\" # 생성 확인 ls -la ~/.ssh/k8s-lab* # ~/.ssh/k8s-lab ← private key # ~/.ssh/k8s-lab.pub ← public key (cloud-init에 삽입됨) Ubuntu 22.04 Cloud Image 다운로드 # 저장 위치: OS SSD에 여유 충분 (367GB available) CLOUD_IMAGE_DIR=\"/var/lib/libvirt/images\" IMAGE_NAME=\"ubuntu-22.04-server-cloudimg-amd64.img\" IMAGE_URL=\"https://cloud-images.ubuntu.com/jammy/current/${IMAGE_NAME}\" sudo wget -O \"${CLOUD_IMAGE_DIR}/${IMAGE_NAME}\" \"${IMAGE_URL}\" # 무결성 확인 sudo qemu-img info \"${CLOUD_IMAGE_DIR}/${IMAGE_NAME}\" Python 가상환경 + kubespray 의존성 준비 cd /home/jinsoo/Documents/k8s-etcd-lab # kubespray clone (03-setup-kubespray.sh에서 자동 처리) git clone https://github.com/kubernetes-sigs/kubespray.git /tmp/kubespray-src # Python venv 생성 python3 -m venv .venv source .venv/bin/activate # kubespray 의존성 설치 pip install -r /tmp/kubespray-src/requirements.txt # ansible 버전 확인 ansible --version 리소스 요건 확인 항목 필요 현재 호스트 vCPU 12개 (4×3) 24스레드 ✅ RAM 24GB (8×3) 40GB ✅ 디스크 120GB (40×3) SSD 367GB 여유 ✅ 네트워크 NAT 브릿지 1개 libvirt 사용 ✅ 사전 확인 체크리스트 # KVM 사용 가능 여부 kvm-ok || sudo apt-get install cpu-checker # libvirtd 실행 상태 sudo systemctl status libvirtd # 기존 네트워크 확인 (libvirt default network) virsh net-list --all # 기존 VM 목록 확인 virsh list --all"
    },
    {
      "title": "02. VM 구성 가이드",
      "desc": "Host (203.0.113.2) └── libvirt NAT 네트워크: k8s-lab (203.0.113.0/24) ├── k8s-ctrl1 203.0.113.11 (control-plane + etcd) ├── k8s-ctrl2 203.0.113.12 (control-plane +…",
      "url": "/public/etcd/etcd__guide-vm-setup.html",
      "domain": "etcd",
      "text": "02. VM 구성 가이드 아키텍처 개요 Host (203.0.113.2) └── libvirt NAT 네트워크: k8s-lab (203.0.113.0/24) ├── k8s-ctrl1 203.0.113.11 (control-plane + etcd) ├── k8s-ctrl2 203.0.113.12 (control-plane + etcd) └── k8s-ctrl3 203.0.113.13 (control-plane + etcd) Host → VM: SSH, kubectl 접근 가능 (NAT 아웃바운드) VM ↔ VM: 동일 브릿지 내 직접 통신 외부 → VM: Host에서 포트 포워딩 또는 virsh console 사용 전용 libvirt 네트워크 생성 network/k8s-lab-network.xml 정의를 사용합니다. # 네트워크 정의 등록 virsh net-define network/k8s-lab-network.xml # 네트워크 시작 및 자동시작 설정 virsh net-start k8s-lab virsh net-autostart k8s-lab # 확인 virsh net-list --all virsh net-dumpxml k8s-lab Cloud Image 기반 VM 생성 방식 Base Image를 공유하고 각 VM마다 overlay qcow2를 생성하는 방식을 사용합니다. (디스크 공간 절약 + 빠른 VM 생성) ubuntu-22.04-server-cloudimg-amd64.img ← Base (읽기 전용) │ ├── k8s-ctrl1.qcow2 ← overlay (쓰기) ├── k8s-ctrl2.qcow2 ← overlay (쓰기) └── k8s-ctrl3.qcow2 ← overlay (쓰기) 1) Overlay 디스크 생성 (VM당) BASE=\"/var/lib/libvirt/images/ubuntu-22.04-server-cloudimg-amd64.img\" IMGDIR=\"/var/lib/libvirt/images\" for i in 1 2 3; do sudo qemu-img create \\ -f qcow2 \\ -F qcow2 \\ -b \"${BASE}\" \\ \"${IMGDIR}/k8s-ctrl${i}.qcow2\" \\ 40G done 2) Cloud-init ISO 생성 (VM당) 각 VM의 hostname이 다르므로 meta-data만 다르게 생성합니다. for i in 1 2 3; do # meta-data: hostname 지정 cat > /tmp/meta-data-ctrl${i} <<EOF instance-id: k8s-ctrl${i} local-hostname: k8s-ctrl${i} EOF # cloud-init ISO 생성 genisoimage \\ -output \"${IMGDIR}/k8s-ctrl${i}-cloud-init.iso\" \\ -volid cidata \\ -joliet \\ -rock \\ cloud-init/user-data \\ /tmp/meta-data-ctrl${i} done 3) VM 생성 (virt-install) for i in 1 2 3; do virt-install \\ --name \"k8s-ctrl${i}\" \\ --memory 8192 \\ --vcpus 4 \\ --disk path=\"/var/lib/libvirt/images/k8s-ctrl${i}.qcow2\",format=qcow2,bus=virtio \\ --disk path=\"/var/lib/libvirt/images/k8s-ctrl${i}-cloud-init.iso\",device=cdrom \\ --os-variant ubuntu22.04 \\ --network network=k8s-lab,mac=\"52:54:00:00:01:0${i}\",model=virtio \\ --graphics none \\ --console pty,target_type=serial \\ --import \\ --noautoconsole done VM 부팅 확인 # VM 상태 확인 virsh list --all # VM 콘솔 접근 (cloud-init 완료 대기) virsh console k8s-ctrl1 # 빠져나오기: Ctrl + ] # cloud-init 완료 확인 ssh -i ~/.ssh/k8s-lab ubuntu@203.0.113.11 \"cloud-init status\" # 결과: status: done 확인 SSH 접근 확인 # /etc/hosts 또는 ~/.ssh/config 등록 (편의용) cat >> ~/.ssh/config <<'EOF' Host k8s-ctrl1 HostName 203.0.113.11 User ubuntu IdentityFile ~/.ssh/k8s-lab StrictHostKeyChecking no Host k8s-ctrl2 HostName 203.0.113.12 User ubuntu IdentityFile ~/.ssh/k8s-lab StrictHostKeyChecking no Host k8s-ctrl3 HostName 203.0.113.13 User ubuntu IdentityFile ~/.ssh/k8s-lab StrictHostKeyChecking no EOF # 접속 테스트 ssh k8s-ctrl1 hostname ssh k8s-ctrl2 hostname ssh k8s-ctrl3 hostname VM 관리 명령어 참고 # VM 시작/정지 virsh start k8s-ctrl1 virsh shutdown k8s-ctrl1 virsh destroy k8s-ctrl1 # 강제 종료 (전원 OFF) # VM 삭제 (디스크 포함) virsh undefine k8s-ctrl1 --remove-all-storage # VM 스냅샷 (실험 전 상태 보존용) virsh snapshot-create-as k8s-ctrl1 \"before-experiment\" \"실험 전 스냅샷\" virsh snapshot-list k8s-ctrl1 virsh snapshot-revert k8s-ctrl1 \"before-experiment\""
    },
    {
      "title": "05. etcd 복구 절차",
      "desc": "etcd 데이터가 살아있고 단순히 프로세스가 죽은 경우입니다.",
      "url": "/public/etcd/etcd__runbook-recovery.html",
      "domain": "etcd",
      "text": "05. etcd 복구 절차 복구 시나리오 분류 상황 방법 단일 노드 장애 (데이터 intact) etcd 재시작으로 자동 동기화 단일 노드 장애 (데이터 손상) 멤버 제거 → 재추가 과반수 장애 (데이터 intact) 모든 노드 재시작 전체 장애 / 데이터 손상 스냅샷으로 복구 복구 1: 단일 노드 자동 재동기화 etcd 데이터가 살아있고 단순히 프로세스가 죽은 경우입니다. # ctrl3 재시작 ssh k8s-ctrl3 \"sudo systemctl start etcd\" # Raft 로그 동기화 확인 (수 초 ~ 수십 초) ssh k8s-ctrl3 \"sudo journalctl -u etcd -f | grep -E 'synced|leader'\" # 멤버 상태 확인 ssh k8s-ctrl1 \"sudo ETCDCTL_API=3 etcdctl \\ --endpoints=https://203.0.113.11:2379 \\ --cacert=/etc/ssl/etcd/ssl/ca.pem \\ --cert=/etc/ssl/etcd/ssl/member-k8s-ctrl1.pem \\ --key=/etc/ssl/etcd/ssl/member-k8s-ctrl1-key.pem \\ endpoint status -w table\" 복구 2: 스냅샷 백업 → 복구 스냅샷 생성 (정상 상태에서) ssh k8s-ctrl1 \"sudo ETCDCTL_API=3 etcdctl \\ --endpoints=https://127.0.0.1:2379 \\ --cacert=/etc/ssl/etcd/ssl/ca.pem \\ --cert=/etc/ssl/etcd/ssl/member-k8s-ctrl1.pem \\ --key=/etc/ssl/etcd/ssl/member-k8s-ctrl1-key.pem \\ snapshot save /tmp/etcd-snapshot-$(date +%Y%m%d-%H%M%S).db\" # 스냅샷 검증 ssh k8s-ctrl1 \"sudo ETCDCTL_API=3 etcdctl snapshot status /tmp/etcd-snapshot-*.db -w table\" 전체 클러스터 스냅샷 복구 절차 이 절차는 etcd 데이터가 완전히 유실된 극단적 상황에서 사용합니다. 모든 노드에서 동시에 진행해야 합니다. Step 1: 모든 etcd + kube-apiserver 정지 for node in k8s-ctrl1 k8s-ctrl2 k8s-ctrl3; do ssh $node \"sudo systemctl stop etcd kube-apiserver\" done Step 2: 기존 etcd 데이터 백업 (혹시 모를 상황 대비) for node in k8s-ctrl1 k8s-ctrl2 k8s-ctrl3; do ssh $node \"sudo mv /var/lib/etcd /var/lib/etcd.bak.$(date +%s)\" done Step 3: 각 노드에 스냅샷 복사 후 복구 SNAPSHOT=\"/tmp/etcd-snapshot-20240101-120000.db\" # ctrl1 복구 ssh k8s-ctrl1 \"sudo ETCDCTL_API=3 etcdctl snapshot restore ${SNAPSHOT} \\ --name=k8s-ctrl1 \\ --initial-cluster=k8s-ctrl1=https://203.0.113.11:2380,k8s-ctrl2=https://203.0.113.12:2380,k8s-ctrl3=https://203.0.113.13:2380 \\ --initial-advertise-peer-urls=https://203.0.113.11:2380 \\ --data-dir=/var/lib/etcd\" # ctrl2 복구 ssh k8s-ctrl2 \"sudo ETCDCTL_API=3 etcdctl snapshot restore ${SNAPSHOT} \\ --name=k8s-ctrl2 \\ --initial-cluster=k8s-ctrl1=https://203.0.113.11:2380,k8s-ctrl2=https://203.0.113.12:2380,k8s-ctrl3=https://203.0.113.13:2380 \\ --initial-advertise-peer-urls=https://203.0.113.12:2380 \\ --data-dir=/var/lib/etcd\" # ctrl3 복구 ssh k8s-ctrl3 \"sudo ETCDCTL_API=3 etcdctl snapshot restore ${SNAPSHOT} \\ --name=k8s-ctrl3 \\ --initial-cluster=k8s-ctrl1=https://203.0.113.11:2380,k8s-ctrl2=https://203.0.113.12:2380,k8s-ctrl3=https://203.0.113.13:2380 \\ --initial-advertise-peer-urls=https://203.0.113.13:2380 \\ --data-dir=/var/lib/etcd\" Step 4: etcd 재시작 for node in k8s-ctrl1 k8s-ctrl2 k8s-ctrl3; do ssh $node \"sudo systemctl start etcd\" done # 클러스터 상태 확인 sleep 10 ssh k8s-ctrl1 \"sudo ETCDCTL_API=3 etcdctl \\ --endpoints=https://203.0.113.11:2379,https://203.0.113.12:2379,https://203.0.113.13:2379 \\ --cacert=/etc/ssl/etcd/ssl/ca.pem \\ --cert=/etc/ssl/etcd/ssl/member-k8s-ctrl1.pem \\ --key=/etc/ssl/etcd/ssl/member-k8s-ctrl1-key.pem \\ endpoint health -w table\" Step 5: kube-apiserver 재시작 for node in k8s-ctrl1 k8s-ctrl2 k8s-ctrl3; do ssh $node \"sudo systemctl start kube-apiserver\" done kubectl get nodes 복구 3: vi"
    },
    {
      "title": "04. etcd 장애 실험 시나리오",
      "desc": "3-node etcd 클러스터: - 쿼럼(과반수) = 2 - 1개 장애: 쿼럼 유지 (2/3 살아있음) → 클러스터 정상 운영 - 2개 장애: 쿼럼 붕괴 (1/3만 살아있음) → 클러스터 읽기 전용 / 쓰기 불가 - 3개 장애: 완전 다운 장애 허용 공식: floor((n-1)/2)…",
      "url": "/public/etcd/etcd__src-failure-experiments.html",
      "domain": "etcd",
      "text": "04. etcd 장애 실험 시나리오 etcd Raft 쿼럼 기본 원리 3-node etcd 클러스터: - 쿼럼(과반수) = 2 - 1개 장애: 쿼럼 유지 (2/3 살아있음) → 클러스터 정상 운영 - 2개 장애: 쿼럼 붕괴 (1/3만 살아있음) → 클러스터 읽기 전용 / 쓰기 불가 - 3개 장애: 완전 다운 장애 허용 공식: floor((n-1)/2) 3노드: floor((3-1)/2) = 1개까지 허용 실험 전 준비 export KUBECONFIG=~/.kube/k8s-etcd-lab.conf # 실험 전 etcd 상태 베이스라인 기록 kubectl get nodes kubectl get pods -n kube-system # etcd 헬스체크 (ctrl1에서) ssh k8s-ctrl1 \"sudo ETCDCTL_API=3 etcdctl \\ --endpoints=https://203.0.113.11:2379,https://203.0.113.12:2379,https://203.0.113.13:2379 \\ --cacert=/etc/ssl/etcd/ssl/ca.pem \\ --cert=/etc/ssl/etcd/ssl/member-k8s-ctrl1.pem \\ --key=/etc/ssl/etcd/ssl/member-k8s-ctrl1-key.pem \\ endpoint health -w table\" 시나리오 1: 단일 노드 장애 (쿼럼 유지) 목표 : etcd 노드 1개 정지 후 클러스터 운영 영향 확인 장애 주입 # ctrl3의 etcd 서비스 정지 ssh k8s-ctrl3 \"sudo systemctl stop etcd\" # 또는 virsh로 VM 자체를 강제 종료 (더 극단적) virsh destroy k8s-ctrl3 관찰 포인트 # 1. etcd 클러스터 상태 - ctrl1에서 확인 ssh k8s-ctrl1 \"sudo ETCDCTL_API=3 etcdctl \\ --endpoints=https://203.0.113.11:2379,https://203.0.113.12:2379 \\ --cacert=/etc/ssl/etcd/ssl/ca.pem \\ --cert=/etc/ssl/etcd/ssl/member-k8s-ctrl1.pem \\ --key=/etc/ssl/etcd/ssl/member-k8s-ctrl1-key.pem \\ endpoint health\" # → ctrl3은 unreachable, ctrl1+ctrl2는 healthy # 2. K8s 클러스터 - 여전히 정상 동작해야 함 kubectl get nodes kubectl create deployment test-nginx --image=nginx kubectl get pods # 3. API Server 로그에서 etcd 연결 오류 확인 ssh k8s-ctrl1 \"sudo journalctl -u kube-apiserver -n 50 | grep etcd\" 복구 # etcd 서비스 재시작 ssh k8s-ctrl3 \"sudo systemctl start etcd\" # 또는 VM 재시작 virsh start k8s-ctrl3 # 멤버 재합류 확인 (자동 재합류됨) ssh k8s-ctrl1 \"sudo ETCDCTL_API=3 etcdctl \\ --endpoints=https://203.0.113.11:2379 \\ --cacert=/etc/ssl/etcd/ssl/ca.pem \\ --cert=/etc/ssl/etcd/ssl/member-k8s-ctrl1.pem \\ --key=/etc/ssl/etcd/ssl/member-k8s-ctrl1-key.pem \\ member list\" 시나리오 2: 과반수 장애 (쿼럼 붕괴) 목표 : etcd 노드 2개 정지 후 K8s 클러스터가 어떻게 반응하는지 관찰 주의: 이 상태에서는 kubectl 명령이 타임아웃됩니다. 장애 주입 # ctrl2, ctrl3 동시 정지 virsh destroy k8s-ctrl2 virsh destroy k8s-ctrl3 관찰 포인트 # 1. kubectl 응답 불가 확인 kubectl get nodes --request-timeout=10s # → 타임아웃 또는 etcd connection refused # 2. API Server 에러 로그 ssh k8s-ctrl1 \"sudo journalctl -u kube-apiserver -f\" # → \"etcdserver: request timed out\" # 3. etcd 자체 상태 (ctrl1에서) ssh k8s-ctrl1 \"sudo systemctl status etcd\" # → active이지만 리더 없음, no quorum # 4. etcd 내부 로그로 Raft 메시지 확인 ssh k8s-ctrl1 \"sudo journalctl -u etcd -n 100 | grep -E 'raft|leader|quorum'\" # 5. 기존에 실행 중인 파드는 유지됨 확인 (etcd 없어도 kubelet은 동작) ssh k8s-ctrl1 \"sudo crictl ps\" 복구 virsh start k8s-ctrl2 virsh start k8s-ctrl3 # etcd 자동 Raft 재동기화 후 쿼럼 복구됨 # 쿼럼 복구 확인 ssh k8s-ctrl1 \"sudo ETCDCTL_API=3 etcdctl \\ --endpoints=https://203.0.113.11:2379 \\ --cacert=/etc/ssl/etcd/ssl/ca.pem \\ --cert=/etc/ssl/etcd/ssl/member-k8s-ctrl1.pem \\ --key=/etc/ssl/etcd/ssl/member-k8s-ctrl1-key.pem \\ endpoint health\" kubectl get nodes 시나리오 3: etcd 데이터 디렉토리 손상 목표 : 특정 노드의 etcd 데이터가 손상됐을 때 멤버 제거 후 재추가 # ctrl3의 etcd 정지 후 데이터 삭제 (손상 시뮬레이션) ssh k8s-ctrl3 \"sudo systemctl stop etcd\" ssh k8s-ctrl3 \"sudo rm -rf /var/lib/etcd/member\" # ctrl1에서 ctrl3 멤버"
    },
    {
      "title": "istiod 성능은 변경률·할당 리소스·워크로드 수·설정 크기 네 요인의 곱으로 결정되고, Sidecar 리소스로 설정 범위를 좁히는 것이 가장 효과적인 단일 레버다",
      "desc": "istiod를 \"control plane\"이라는 추상명사가 아니라 설정 컴파일러 + 분배기로 보면, 부하의 출처와 튜닝 레버가 한눈에 선다. 이 문서는 (1) istiod 부하를 결정하는 네 요인이 곱셈으로 결합한다는 멘탈모델, (2) event→debounce→snapshot→pus…",
      "url": "/public/istio/arch__note-control-plane-performance-factors.html",
      "domain": "istio",
      "text": "istio performance control-plane scaling sidecar-scope istiod 성능은 변경률·할당 리소스·워크로드 수·설정 크기 네 요인의 곱으로 결정되고, Sidecar 리소스로 설정 범위를 좁히는 것이 가장 효과적인 단일 레버다 ℹ 이 문서가 다루는 것 istiod를 \"control plane\"이라는 추상명사가 아니라 설정 컴파일러 + 분배기 로 보면, 부하의 출처와 튜닝 레버가 한눈에 선다. 이 문서는 (1) istiod 부하를 결정하는 네 요인이 곱셈으로 결합한다는 멘탈모델, (2) event→debounce→snapshot→push queue→xDS로 이어지는 동기화 파이프라인, (3) 메트릭으로 병목을 incoming(compute) vs outgoing(push)으로 갈라 scale up이냐 scale out이냐 를 결정하는 진단법, (4) 왜 Sidecar 리소스가 네 요인을 동시에 미는 단일 최대 레버인지를 다룬다. 결론: istiod 성능 튜닝은 \"CPU를 더 주자\"가 아니라 \" 각 proxy가 받아야 하는 설정량을 줄이자 \"에서 출발한다. 대상환경: Istio 1.30 / Envoy · 대상독자: mesh 규모가 커지며 istiod CPU·push latency가 의심되기 시작한 SRE · 범위: 부하 모델·진단·근본 레버(운영 detail은 위임) · 선행개념: xDS(LDS/RDS/CDS/EDS/SDS), sidecar proxy. 운영 detail은 → 운영 플레이북 , → Sidecar scope 에 위임하고, 여기서는 성능의 멘탈모델에 집중한다. 01. 배경 — 왜 control plane이 병목이 되는가 작은 mesh에서는 istiod 성능을 신경 쓸 일이 없다. 문제는 mesh가 커질 때 비용이 선형이 아니라 곱셈으로 자란다 는 데서 시작된다. 왜 그런지 보려면 istiod가 실제로 무슨 일을 하는지를 추상명사에서 끌어내려야 한다. istiod는 무한 루프를 돈다. K8s API와 Istio CRD를 watch 한다(Service/EndpointSlice/Pod/Secret + Gateway/VS/DR/SE/Sidecar/PeerAuth/AuthZ), 이 입력을 in-memory model로 reconcile 한 뒤, 각 proxy마다 그 proxy가 봐야 할 범위로 Envoy 설정(LDS/RDS/CDS/EDS/SDS)을 계산 하고, xDS로 모든 연결된 proxy에 push 한다. 즉 istiod는 \"선언적 의도(CRD) → Envoy가 실행하는 구체 설정\"을 끊임없이 컴파일 하고, 그 결과물을 수천 개 proxy에 분배 하는 기계다. 여기서 비용이 두 방향으로 갈린다 — 이 구분이 문서 전체를 관통하는 첫 번째 축이다. incoming 부하 (watch + compute) = K8s/CRD 이벤트를 받아 in-memory model을 갱신하고 각 proxy용 Envoy 설정 snapshot을 계산하는 비용 → CPU·메모리 바운드, 단일 istiod 인스턴스에 집중 outgoing 부하 (push + transport) = 계산된 설정을 모든 연결된 proxy에 xDS로 밀어내는 비용 → 연결 수(proxy count) × 설정 크기에 비례, 인스턴스 수로 분산 가능 곱셈이 어디서 나오는가? mesh가 N개 service, M개 proxy로 자라는데 scope를 안 좁히면 각 proxy가 mesh 전체(N개 service)의 설정을 받는다. 그러면 service 하나가 바뀔 때 M개 proxy 전부에 N 크기의 설정을 다시 밀어야 한다 — 변경 1건의 비용이 M×N으로 부푼다. 이 곱셈 구조가 control plane을 병목으로 만드는 근본 원인이고, 뒤의 모든 레버는 이 곱의 항(項)을 하나씩 깎는 일이다. 02. 핵심 멘탈모델 — 네 요인의 곱 이 문서에서 딱 하나만 머리에 남긴다면 이 식이다. istiod 부하는 네 요인의 곱이고, 곱이기 때문에 가장 큰 항을 깎는 것이 압도적으로 효과적이다. istiod 부하 ≈ (변경률) × (영향받는 proxy 수) × (proxy당 설정 크기) / (할당 자원) rate of change number of workloads configuration size allocated resources Istio 공식 performance 모델의 네 요인을, 각자가 incoming/outgoing 중 어디를 미는지와 함께 본다. 요인 무엇인가 미는 곳 대표 트리거 Rate of change 단위 시간당 config/endpoint 변경 횟수 incoming(compute) + outgoing(push) 잦은 deploy, HPA scale, Pod churn, endpoint flapping Allocated resources istiod에 준 CPU/메모리 incoming 처리량 상한 요청·limit 부족 → push 지연 Number of workloads 연결된 proxy(sidecar+gateway) 수 outgoing(push fan-out) mesh 규모, namespace 수 Configuration size 각 proxy가 받는 Envoy 설정의 크기 outgoing(push 바이트) + Envoy 메모리 scope 미설정 → 모든 서비스가 모든 proxy에 곱셈이라는 사실에서 모든 결론이 따라 나온다. 설정 크기를 절반으로 줄이면 모든 push 의 바이트와 Envoy 메모리가 함께 줄 뿐 아니라, \"해당 proxy scope에 무관한 변경\"은 아예 push 대상에서 빠지므로 변경 빈도의 체감값까지 줄어든다 — 한 항을 깎았더니 두 항이 줄어든다. 그래서 §06의 Sidecar 리소스(설정 크기·범위 축소)가 단일 최대 레버가 된다. 반대로 할당 자원만 키우는 것은 분모만 키워 \"같은 곱을 더 빨리 처리\"할 뿐, 곱 자체는 그대로다. ✓ number of workloads vs configuration size — 헷갈리기 쉬운 두 축 둘은 다른 축이다. workload 수는 \"몇 개의 proxy에 보내는가\"(fan-out)이고, config size는"
    },
    {
      "title": "control plane과 data plane의 설치·수명주기를 분리하면 istiod 업그레이드가 data plane에 투명해진다",
      "desc": "\"Istio를 업그레이드한다\"가 왜 \"mesh 전체를 한 번에 흔드는 일\"이 아니라 \"proxy를 하나씩 새 control plane으로 옮기는 일\"이 될 수 있는지를 다룬다. 결론부터: istiod(control plane)와 Envoy(data plane)는 xDS라는 느슨한 gR…",
      "url": "/public/istio/arch__note-install-cp-dp-decoupling.html",
      "domain": "istio",
      "text": "istio install upgrade helm revision 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를 옆에 띄우는 게 가능하다. 그림 1. control/data plane 분리. istiod는 데이터 경로 밖이라 rev별로 공존(1-27 + 1-30 canary)하고, 각 proxy는 자기 rev의 istiod에서만 xDS를 받는다 — rev가 달라도 sidecar A↔B는 pod-to-pod로 그대로 통신한다. 이 그림이 문서의 결론이다. 같은 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에 붙일지 하나씩 바꾸는 일 이다. ★ 한 문장 설치"
    },
    {
      "title": "Phantom workload는 컨트롤 플레인의 endpoint 전파 지연으로 stale 스냅샷을 믿는 데이터 플레인이 이미 사라진 워크로드로 트래픽을 보내는 현상이다",
      "desc": "Phantom workload는 버그가 아니라 메시 아키텍처의 본질적 트레이드오프 — 데이터 플레인 설정의 최종 일관성(eventual consistency) — 가 시간 축에 투영된 현상이다. Pod가 사라진 시점과 그 사실이 모든 Envoy의 EDS 스냅샷에 반영되는 시점 사이에는…",
      "url": "/public/istio/arch__note-phantom-workloads.html",
      "domain": "istio",
      "text": "istio phantom-workloads eds eventual-consistency Phantom workload는 컨트롤 플레인의 endpoint 전파 지연으로 stale 스냅샷을 믿는 데이터 플레인이 이미 사라진 워크로드로 트래픽을 보내는 현상이다 NOTE Phantom workload는 버그가 아니라 메시 아키텍처의 본질적 트레이드오프 — 데이터 플레인 설정의 최종 일관성(eventual consistency) — 가 시간 축에 투영된 현상이다. Pod가 사라진 시점과 그 사실이 모든 Envoy의 EDS 스냅샷에 반영되는 시점 사이에는 항상 전파 지연(propagation lag) 윈도가 존재하고, 그 윈도 안에서 Envoy는 죽은 IP를 healthy로 믿고 트래픽을 보낸다. 이 note는 왜 phantom이 구조적으로 불가피한가 라는 멘탈모델에 집중한다. 단계별 발생 표·완화 YAML·운영 detail은 Phantom workloads 처리 에 위임한다. 대상독자 : Istio 메시에서 \"배포할 때마다 잠깐 503이 뜨는데 app 로그는 깨끗하다\"를 디버깅하는 SRE. 선행개념 : Envoy cluster/EDS( direction|port|subset|fqdn ), istiod xDS push, K8s EndpointSlice. 범위 : 발생 원리·식별·완화 방향 (구체 YAML은 src에 위임). 1. 배경: 라우팅 결정은 누가, 어떤 데이터로 내리나 phantom을 이해하려면 먼저 \"메시에서 트래픽의 목적지 IP를 누가 결정하는가\"를 정확히 짚어야 한다. 단순 Kubernetes에서는 Service ClusterIP로 보내면 kube-proxy/iptables가 그 순간의 살아있는 endpoint로 DNAT한다 — 결정이 데이터 경로 위에서 실시간 으로 일어난다. Istio는 이 모델을 버린다. sidecar Envoy가 트래픽을 가로채(15001 outbound) 자기가 들고 있는 설정만 보고 직접 목적지 IP를 고른다. kube-proxy는 경로에서 빠진다. 그 \"설정\"의 핵심이 Envoy cluster 와 그에 매달린 endpoint 목록 이다. cluster는 outbound|8080||catalog.istioinaction.svc.cluster.local 같은 direction|port|subset|fqdn 이름으로 식별되고, 실제 목적지 IP들은 EDS(Endpoint Discovery Service) 가 푸시하는 ClusterLoadAssignment (CLA)에 담긴다. 즉 Envoy의 라우팅 입력은 두 갈래다. +-------------------+ +---------------------------------+ | CDS: cluster 정의 | ---> | \"outbound|8080||catalog...svc\" | | (어떤 그룹이 있나) | +---------------------------------+ +-------------------+ | v EDS +---------------------------------+ | CLA endpoints (이 그룹의 IP들) | | 10.1.0.7:8080 HEALTHY | | 10.1.0.8:8080 HEALTHY <-- 이게| +---------------------------------+ stale일 수 있다 여기서 결정적 사실: 이 endpoint 목록은 K8s가 아니라 istiod 가 비동기로 푸시한 스냅샷 이다. K8s EndpointSlice가 ground truth이고, Envoy의 CLA는 그 truth를 istiod 가 watch→가공→push한 복제본 이다. truth와 복제본이 따로 존재하고 push가 비동기라는 이 한 가지 사실에서 phantom이 전부 따라 나온다. 그래서 다음 절의 멘탈모델이 성립한다. 2. 핵심 멘탈모델: phantom = \"eventual consistency\"의 시간 단면 한 문장 앵커 : \"실제 endpoint 상태\"와 \"Envoy가 믿는 endpoint 상태\"는 분산 시스템의 두 복제본 이며, 둘은 eventually consistent하다 — 언젠가는 같아지지만 지금 이 순간 같다는 보장은 없다. phantom은 그 둘이 어긋난 짧은 구간을 트래픽이 통과할 때 발생한다. 라우팅 결정의 주체가 컨트롤 플레인이 아니라 각 Envoy가 로컬에 들고 있는 CLA 스냅샷 이라는 점이 핵심이다. istiod 는 그 스냅샷을 xDS로 비동기 푸시 할 뿐이고, Envoy는 자기가 마지막으로 받은 스냅샷만 신뢰한다. Pod가 죽어도 그 IP가 CLA에서 빠지는 push가 아직 안 왔으면, Envoy 입장에서 그 endpoint는 여전히 HEALTHY다. 죽은 줄 모르고 보낸다 — 이게 phantom이다. 그림 1. K8s는 Pod-X 제거를 반영했으나 xDS push 지연으로 Envoy의 EDS는 여전히 HEALTHY(stale) → 죽은 IP로 트래픽 → 503/504. Pod-X: REMOVED (좌)와 Pod-X: HEALTHY (우)가 공존하는 동안, 그 사이를 지나는 요청이 phantom 트래픽이다. lag이 0이면 phantom도 0이지만, 비동기 push 구조상 lag을 0으로 만들 수는 없다. 그래서 phantom은 고칠 버그가 아니라 윈도를 좁히고 흡수해야 할 구조적 현상이다 — 이게 4절 완화 전략 전체의 전제가 된다. lag을 만드는 4단계와 윈도를 넓히는 조건 phantom 윈도 = \"K8s가 endpoint를 떼어낸 t0\"부터 \"마지막 Envoy가 새 EDS를 ACK한 t1\"까지. 그 사이엔 네 개의 직렬 지연이 누적된다(직렬이라 합산된다는 게 중요). 그림 2. phantom 윈도 = debounce + push queue + xDS push + 반영의 합. 이 구간 동안 Envoy는 죽은 endpoint를 살아있다고 본다. 지연 단계 무엇이 늦추나 윈도가 커지는 조건 ① debounce istiod 가 이벤트를 배칭( PILOT_DEBOUNCE_AFTER 100ms / PILOT_DEBOUNCE_MAX 10s) 대량 변경"
    },
    {
      "title": "클러스터 콜드부팅 복구 — \"no route to host\" 전체 노드가 항상 cloud-init 장애는 아니다",
      "desc": "control-plane /etc/hosts 장애 런북과 증상이 닮았다 — kubectl이 전 노드에 no route to host. 하지만 원인은 완전히 다르다: 그 문서는 게스트 OS 안의 이름해석이 죽은 것이고, 이번은 노드를 담은 KVM VM 자체가 꺼져 있던 것이다. 이 문서…",
      "url": "/public/istio/arch__runbook-cold-boot-recovery.html",
      "domain": "istio",
      "text": "kubernetes kvm libvirt control-plane runbook 클러스터 콜드부팅 복구 — \"no route to host\" 전체 노드가 항상 cloud-init 장애는 아니다 NOTE control-plane /etc/hosts 장애 런북 과 증상이 닮았다 — kubectl 이 전 노드에 no route to host . 하지만 원인은 완전히 다르다: 그 문서는 게스트 OS 안의 이름해석 이 죽은 것이고, 이번은 노드를 담은 KVM VM 자체가 꺼져 있던 것이다. 이 문서는 두 장애를 30초 안에 가르는 감별법과, \"ping이 안 되면 죽은 것\"이라는 흔한 오해가 이 환경에서는 아예 거짓인 이유 (ICMP 필터링)를 다룬다. 결론 한 문장: 계층을 먼저 갈라라 — 전원(KVM) → 게스트 네트워크(ARP) → 이름해석(lease). ping은 이 감별에서 쓸모가 없다. Date: 2026-07-01 호스트: homelab (kubespray bare-metal 3노드, 전부 로컬 KVM VM 위에서 구동) 도메인: Kubernetes / KVM/libvirt 상태: ✅ 복구 완료 — 3노드 Ready, lease 실시간 갱신, 전 네임스페이스 pod 정상, 기존 cloud-init 영구수리 유지 대상독자: 홈랩처럼 \"k8s 노드 = 로컬 VM\"인 환경에서, 재부팅/방치 후 클러스터가 안 잡힐 때 원인을 계층별로 빠르게 좁히려는 사람 선행개념: KVM/libvirt VM 상태( virsh ), ARP(L2)와 ICMP(L3)의 차이, leader election lease 1. 배경 — 이 클러스터의 노드는 \"컴퓨터\"가 아니라 \"이 서버 위의 VM\"이다 이 홈랩 kubespray 클러스터의 3노드(control-plane 1 + worker 2)와 apiserver 앞단 로드밸런서는 전부 이 워크스테이션 한 대 위에 떠 있는 KVM VM 이다 — 별도의 물리 머신이 아니다. 즉 \"클러스터가 안 잡힌다\"는 증상 앞에는 물리 전원 문제가 있을 수 없는 대신, 더 흔한 실패 모드가 하나 추가 된다: VM 자체가 shut off 상태인 것. 이건 리눅스 게스트 안에서 무슨 일이 있었는지와 무관하게, 호스트의 libvirt 레벨에서 결정되는 훨씬 앞단의 계층 이다. 이 관점이 중요한 이유는, 예전에 이 클러스터에서 겪은 control-plane 장애 와 겉보기 증상이 완전히 같기 때문 이다( kubectl 이 전 노드에 접속 실패). 하지만 그 문서의 원인(cloud-init이 게스트 안의 /etc/hosts 를 지움)과 이번 원인(VM이 꺼짐)은 서로 다른 계층 에 있다. 증상만 보고 지난번과 같은 수리( /etc/hosts 편집)를 시도했다면 시간을 낭비했을 것이다 — 계층을 먼저 가르는 게 이 문서의 핵심이다. 2. 핵심 — 계층을 가르는 감별법 (메커니즘) 멘탈모델 앵커 : \"no route to host\"가 VIP 하나만 이면 이름해석/라우팅 문제(게스트 OS 계층)를 의심하고, 클러스터의 모든 IP(VIP+전 노드) 가 동시에 no-route면 그보다 훨씬 앞단 — VM 전원(libvirt 계층) 을 먼저 의심한다. 그리고 이 환경에서는 ping(ICMP)이 거짓 정보를 준다 — 이 노드들은 ICMP을 필터링하므로, VM이 멀쩡히 켜져 있어도 ping은 실패한다. 판정은 반드시 서비스 포트(TCP)로 한다. 2.1 두 장애를 가르는 표 이번(2026-07-01, 콜드부팅) 이전( 2026-06-01 outage ) 증상 .211 (VIP)~ .214 (전 노드) 전부 no-route 노드는 응답, VIP 이름 해석만 실패 실패 계층 libvirt(VM 전원) 게스트 OS( /etc/hosts ) VM 자체 상태 shut off running (게스트 안에서 문제) 복구 virsh start /etc/hosts 편집 + cloud-init 템플릿 2.2 ping이 이 환경에서 쓸모없는 이유 VM을 켠 직후 5분 가까이 4개 IP 모두 ping에 무응답이라 \"부팅이 안 되나\" 의심했지만, 실제로는 ARP는 이미 정상 응답 중 이었다( ip neigh get 으로 MAC 주소가 정확히 잡힘 = L2/게스트 네트워크 스택은 살아있다는 뜻). 즉 ping(ICMP, L3)만 막혀 있었을 뿐 실제 서비스 포트(TCP, apiserver :6443 /sshd :22 )는 열려 있었다. ping 무응답을 \"노드가 죽었다\"로 오독하면 이미 복구된 클러스터를 붙잡고 계속 헤매게 된다 — 이게 이 문서에서 가장 시간을 아껴주는 교훈이다. 판정 순서: virsh list --all -> VM 자체가 shut off 인가? (가장 앞단) | running이면 v ip neigh get <ip> -> ARP 응답(MAC 잡힘) = 게스트 네트워크 스택 살아있음 (ping 말고 이걸로 판정) | v TCP :6443 / :22 -> 서비스 포트 자체 확인 (/dev/tcp 또는 timeout+bash) | v kubectl / lease -> control-plane 실제 활성 여부 (renewTime ≈ now) 3. 증상과 추적 경로 3.1 1차 증상 $ kubectl --context homelab get nodes Unable to connect to the server: dial tcp <VIP>:6443: connect: no route to host VIP뿐 아니라 노드 3개 전부 ( ping -c1 -W2 <ip> ) no-route. 이전 장애 의 \"이름만 실패, 노드는 살아있음\" 패턴과 다르다는 첫 단서. 3.2 계층을 앞으로 — libvirt 확인 $ virsh list --all Id Name State ------------------------------ - k8s-master1 shut off - k8s-worker1 shut off - k8s-worker2 shut off - lb-haproxy shut off 4개 VM(3노드 + 로드밸런서) 전부 꺼져 있었다 . 이게 진짜 원인 — 게스트 OS 안을 들여다볼 필요조차 없다. 3.3 기동 후에도 5"
    },
    {
      "title": "클러스터 control-plane 장애 — lb-apiserver.kubernetes.local 이름해석 소실",
      "desc": "Istio 재설치 중 istiod가 0/1로 안 뜨는 원인을 추적하다, 그게 Istio 문제가 아니라 kubespray control-plane 전체가 ~9일째 마비였음을 밝혀낸 장애 분석·수리 런북. 단일 실패점은 /etc/hosts의 단 한 줄, 범인은 cloud-init, 그리고…",
      "url": "/public/istio/arch__runbook-controlplane-outage.html",
      "domain": "istio",
      "text": "kubernetes control-plane kubespray runbook 클러스터 control-plane 장애 — lb-apiserver.kubernetes.local 이름해석 소실 NOTE Istio 재설치 중 istiod가 0/1 로 안 뜨는 원인을 추적하다, 그게 Istio 문제가 아니라 kubespray control-plane 전체가 ~9일째 마비 였음을 밝혀낸 장애 분석·수리 런북. 단일 실패점은 /etc/hosts 의 단 한 줄, 범인은 cloud-init, 그리고 가장 위험한 함정은 \"노드는 Ready인데 control loop는 죽어있는\" 착시 다. Helm 재설치 런북 의 하드 선행 작업 — 이 수리 없이는 데이터플레인이 뜨지 않는다. Date: 2026-06-01 호스트: homelab (kubespray bare-metal, k8s v1.30.6, 3노드) 도메인: Kubernetes (control-plane / kubespray) 상태: ✅ 수리 완료 (2026-06-01 02:16Z) — 3노드 Ready, lease 실시간 갱신, 전 pod 정상 대상독자: kubespray/kubeadm bare-metal을 운영하며 \"control plane이 살아있는지\"를 lease 수준에서 판정하려는 DevOps/SRE. 선행개념: static pod, leader election lease, kubelet authorization(Webhook), /etc/hosts 이름해석. 1. 배경 — 왜 control plane은 이름 한 줄에 목숨이 걸려 있나 Kubernetes의 모든 control loop는 결국 하나의 진실원천(apiserver)에 쓰고 읽으면서 돈다. controller-manager가 Deployment를 보고 ReplicaSet을 만들고, scheduler가 Pod에 노드를 배정하고, kubelet이 노드 상태(lease)를 갱신하는 — 이 모든 reconcile은 \"apiserver에 HTTP 연결이 된다\"를 전제로 한다. 연결이 끊기면 loop는 에러도 거의 안 내고 조용히 멈춘다 . retry를 영원히 돌 뿐이다. 여기서 핵심은 컴포넌트가 apiserver를 IP가 아니라 이름으로 찾는다 는 점이다. HA control plane은 apiserver가 여러 대(또는 VIP 뒤)에 있을 수 있으므로, kubespray는 모든 kubeconfig( controller-manager.conf / scheduler.conf / kubelet.conf )의 server를 고정 IP가 아닌 server: https://lb-apiserver.kubernetes.local:6443 로 적어 둔다. 그래야 apiserver 한 대가 죽거나 VIP가 옮겨가도 kubeconfig를 안 고친다. 대신 그 이름을 어디서 어떻게 푸느냐 가 클러스터마다 갈린다. kubespray에는 두 가지 LB 토폴로지가 있고, 이 차이를 모르면 장애 때 잘못된 응급조치를 한다. LB 모드 lb-apiserver.kubernetes.local 이 가리키는 곳 누가 forward? localhost-LB 127.0.0.1:6443 worker엔 nginx-proxy static pod가 control-plane로 forward, master엔 로컬 apiserver가 직접 listen 외부 VIP 203.0.113.211:6443 (실제 LB/VIP IP) 외부 LB(또는 keepalived VIP)가 직접 받음, worker에 nginx-proxy 없음 두 모드 모두 이름해석은 /etc/hosts 단 한 줄 에 의존한다(CoreDNS가 아니다 — control plane 부팅 자체가 CoreDNS보다 먼저 와야 하니 OS 수준에서 풀어야 한다). 그래서 그 한 줄이 SPOF다. 이 클러스터는 외부 VIP 모드 다(§3에서 증명). 따라서 풀어야 할 이름은 203.0.113.211 lb-apiserver.kubernetes.local 이지 127.0.0.1 이 아니다. 이 구분이 이 문서 전체를 끌고 간다. 2. 핵심 — 한 줄이 사라지면 무슨 일이 벌어지나 (메커니즘) 멘탈모델 앵커 : control-plane의 모든 reconcile은 컴포넌트가 apiserver를 lb-apiserver.kubernetes.local 로 찾는 데서 시작하고, 이 클러스터에서 그 이름은 /etc/hosts 의 203.0.113.211 lb-apiserver.kubernetes.local 단 한 줄 이 해석한다. 그 줄이 사라지면(cloud-init이 /etc/hosts 를 덮어씀) controller-manager·scheduler·kubelet이 일제히 apiserver에 닿지 못해 모든 control loop가 조용히 멈춘다 . 그런데 기존 Pod와 노드 Ready 표시는 그대로 남아 멀쩡해 보인다 — 이 \"살아있는 듯한 정지\" 착시가 진단을 가장 어렵게 만든다. 2.1 왜 cloud-init이 그 줄을 지우나 cloud-init의 manage_etc_hosts: True 는 부팅마다 /etc/cloud/templates/hosts.debian.tmpl 템플릿으로 /etc/hosts 를 통째로 재생성 한다. kubespray가 박아둔 lb-apiserver.kubernetes.local 항목은 그 템플릿에 없으므로 재생성될 때마다 날아간다. 평소엔 재부팅이 드물어 잠복하다가, 한 번 재부팅(여기선 2026-05-24)이 일어나는 순간 클러스터 전체가 무너진다. bare-metal kubespray의 알려진 함정 이다. 2.2 왜 정지가 \"조용한가\" — 두 겹의 착시 control loop가 멈췄는데도 클러스터가 멀쩡해 보이는 이유는 두 개의 독립된 캐싱 때문이다. 그림 1. \"멀쩡해 보이는\" 장애의 인과. leader lease 동결로 모든 control loop가 멈춰도 기존 Pod와 노드는 kubelet/캐싱 덕에 그대로라 healthy처럼 보인다 — 진짜 증상은 새 워크로드가 전혀 안 뜨는 것(istiod 0/1, status {}). 기존 Pod가 살아있는 이유"
    },
    {
      "title": "Istio 1.30.0 — 기존 설치 teardown + Helm 클린 재설치",
      "desc": "홈랩 클러스터의 깨진 Istio 1.30.0 설치를 전부 제거하고 순수 Helm chart(base → istiod → ingress/egress)로 클린 재설치한 런북. 흐름 0단계(설치·구조)의 실제 실행 기록 — 본 아카이브 전체가 이 설치 위에서 동작한다. 하나의 그림: Ist…",
      "url": "/public/istio/arch__runbook-helm-reinstall.html",
      "domain": "istio",
      "text": "istio install helm runbook Istio 1.30.0 — 기존 설치 teardown + Helm 클린 재설치 NOTE 홈랩 클러스터의 깨진 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 장애 수리 런북 (그 수리 없이는 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)은 손이 안 닿아 수동 삭제가 필요하다. 이 한 문장에서 이후 모든 절차가 따라 나온다. 그림으로 고정하자. 그림 1. ownership 경계. Helm은 차트로 apply한 base→istiod→gateway 체인만 소유한다. CRD(keep-policy)와 istiod가 런타임에 만든 configmap은 소유 밖이라 uninstall이 못 건드려 수동 삭제가 필요하다 — install은 아래→위, teardown은 참조 역순(위→아래). 왜 teardown은 역순인가 (참조 순서). base를 먼저 지우면 istiod가 참조하던 CRD가 사라진다. 그러면 istiod release를 정리할 때 finalizer/cleanup 로직이 참조 대상을 못 찾아 순서가 꼬일 수 있다. DB에서 외래키 걸린 행을 부모부터 지우면 깨지는 것과 같은 원리 — 그래서 자식(istiod)부터, 부모(base)는 나중에. (비유의 한계: Istio엔 진짜 FK 제약이 강제돼 있진 않다. \"참조가 끊겨도 대개 견디지만 cleanup 경로에서 비결정적으로 꼬일 수 있다\"가 정확한 표현.) 왜 helm uninstall 한 방으로 안 끝나는가 (own"
    },
    {
      "title": "Istio 대규모 운영 플레이북 — multi-cluster·config scope·upgrade·LLMOps (출처: ChatGPT \"Istio 운영 노하우\" 대화 + Istio 1.30 공식 문서)",
      "desc": "한 그림으로: Istio 운영은 \"Istio를 Envoy 설정 컴파일러(K8s 상태 + CRD → istiod → xDS → 모든 proxy)로 보고, 모든 장애를 YAML → istiod → xDS → Envoy config → response flag로 내려가며 추적하는 것\"이다.…",
      "url": "/public/istio/arch__src-operations-playbook.html",
      "domain": "istio",
      "text": "istio operations multicluster config-scope upgrade llmops gitops Istio 대규모 운영 플레이북 — multi-cluster·config scope·upgrade·LLMOps (출처: ChatGPT \"Istio 운영 노하우\" 대화 + Istio 1.30 공식 문서) ℹ 이 문서가 다루는 것 한 그림으로: Istio 운영은 \"Istio를 Envoy 설정 컴파일러 (K8s 상태 + CRD → istiod → xDS → 모든 proxy)로 보고, 모든 장애를 YAML → istiod → xDS → Envoy config → response flag 로 내려가며 추적하는 것\"이다. 기능을 켜는 단계가 아니라 운영하는 단계에서는 성패가 config 전파량(scope)·upgrade blast radius·trust boundary·retry/timeout 정책 에서 갈린다. 이 문서는 그 한 멘탈모델을 세운 뒤(§01~§02), 모든 장애를 같은 방향으로 추적하는 도구(§03)와, 규모가 커지면 그 도구만으로는 안 되는 4개 운영 주제(§04 multi-cluster·§05 scope·§06 upgrade·§07 GitOps·§08 LLMOps)를 같은 멘탈모델 위에서 펼치고, 마지막에 학습 루트(§09)로 닫는다. 이 문서는 운영자 레벨의 \"큰 그림\"과 production 실무에 집중한다. iptables/포트, response flag별 디버깅, cluster naming 같은 깊은 detail은 형제 문서(→ Sidecar 트래픽 캡처 , → Cluster 해부 , → AuthorizationPolicy 멘탈모델 )에 있으므로 여기서는 요약·연결만 한다. 01. 배경 — 왜 \"서비스 메시\"라는 단어를 버려야 운영이 보이는가 운영자가 가장 먼저 풀어야 할 인식 문제가 하나 있다. \"서비스 메시\"라는 단어는 기능 목록 (\"mTLS·라우팅·관측성을 자동으로 준다\")으로 Istio를 설명한다. 이 시점에 머물면 장애가 났을 때 어디를 봐야 하는지 알 수 없다. 기능 목록에는 \"어디서 무엇이 잘못될 수 있는가\"가 없기 때문이다. 운영에 쓸 수 있는 모델은 단어를 바꾸는 데서 시작한다. ★ 한 문장 멘탈모델 (이 문서 전체의 anchor) Istio는 Kubernetes 상태와 Istio CRD를 Envoy 설정으로 컴파일해서 모든 Pod/Gateway proxy에 배포하는 제어 시스템 이다. 운영이란 이 컴파일러의 입력(YAML)·중간산물(xDS)·출력(Envoy config)·실행결과(response flag)를 한 방향으로 따라 내려가며 보는 일이다. 이 한 문장이 anchor인 이유는, 이 문장에서 운영의 모든 구조가 따라 나오기 때문이다. 컴파일러로 보면 자연히 4개 질문이 생긴다 — ① 무엇이 입력인가(K8s + CRD), ② 누가 컴파일하나(istiod), ③ 무엇으로 배포되나(xDS), ④ 실제로 도착·실행됐는가(Envoy config·ACK·response flag). 장애 추적(§03), 전파량 관리(§05), 업그레이드(§06), GitOps 검증(§07)은 전부 이 네 질문 중 어디를 손대느냐의 문제일 뿐이다. sidecar mode에서 data plane은 각 Pod 옆의 Envoy proxy가, control plane은 istiod가 담당한다. istiod는 ① service discovery, ② configuration, ③ certificate management를 맡고, 고수준 routing rule을 Envoy 전용 설정으로 변환해 sidecar에 전파한다. 즉 컴파일러 본체가 istiod, 컴파일 결과를 실행하는 런타임이 Envoy다. 그림 1. 컴파일러 모델. istiod가 K8s 객체와 Istio CRD를 watch/reconcile해 Envoy 전용 설정으로 컴파일하고 xDS로 push — 컴파일러 본체는 istiod, 런타임은 Envoy. 컴파일러의 출력은 Envoy가 받는 5종 설정이다. 이 5종이 곧 \"장애가 날 수 있는 5개 층\"이므로 의미를 질문 형태로 박아두면 추적이 쉬워진다. Listener : 어디서 받을 것인가 Route : 어떤 HTTP/gRPC 요청을 어디로 보낼 것인가 Cluster : upstream service/subset/policy는 무엇인가 Endpoint : 실제 Pod IP/port는 무엇인가 Secret : mTLS cert/key/trust bundle은 무엇인가 ✓ 핵심: 피상적 이해 vs 내부 이해 피상 : \"VirtualService가 라우팅한다\"에서 멈춤. 내부 : VirtualService의 host/path/match가 Envoy RDS route 로 바뀌고, route의 destination은 outbound|PORT|SUBSET|FQDN 형태의 cluster 를 참조하며, DestinationRule의 subset label과 trafficPolicy가 CDS/EDS/LB/outlier detection 에 영향을 준다. 피상에 머물면 \"VS를 고쳤는데 왜 안 바뀌지?\"에서 막힌다. 내부 모델이 있으면 \"VS는 route로 컴파일된다 → 그럼 그 pod의 route를 보면 된다\"로 바로 손이 간다. cluster naming( outbound|9080|v1|reviews... )과 subset의 정확한 의미는 → Cluster 해부 참조. 02. 핵심 메커니즘 — 요청 하나가 지나가는 경로 = 컴파일된 설정이 실행되는 경로 §01의 anchor를 \"정지한 그림\"이라면, 이번 절은 그 그림이 요청 하나가 흐르는 동안 어떻게 실행되는가 다. 운영자가 외워야 할 단 하나의 경로이고, §03 이후의 모든 디버깅이 이 경로 위의 한 지점을 찍는 일이다. sidecar mode 기준으로 요청은 app container에서 출발해 iptables/CNI redirection으로 Envoy에 빼앗기고, outbound listener에서 protocol·route·cluster·endpoint·mTLS origination을 거쳐 network를 건너"
    },
    {
      "title": "Istio Phantom Workloads 처리 방법",
      "desc": "Phantom workload는 이미 사라진 Pod의 endpoint IP가 Envoy의 EDS-CLA에 stale로 남아 트래픽이 죽은 IP로 흘러 발생하는 유령 라우팅이다. 머릿속에 담을 단 하나의 그림: \"이벤트 발생 → 새 EDS 도달\" 사이의 시간축 윈도(phantom win…",
      "url": "/public/istio/arch__src-phantom-workloads.html",
      "domain": "istio",
      "text": "istio phantom-workloads eds Istio Phantom Workloads 처리 방법 NOTE Phantom workload는 이미 사라진 Pod의 endpoint IP가 Envoy의 EDS-CLA에 stale로 남아 트래픽이 죽은 IP로 흘러 발생하는 유령 라우팅이다. 머릿속에 담을 단 하나의 그림: \"이벤트 발생 → 새 EDS 도달\" 사이의 시간축 윈도 (phantom window). 이 윈도가 열려 있는 동안 들어온 요청이 죽은 IP로 간다. 근본 원인은 컨트롤→데이터 플레인 전파의 최종 일관성(propagation lag) 이라 윈도를 0으로 만들 수 없고, 대응은 같은 윈도를 두 각도에서 친다 — (A) 윈도 폭 줄이기 (전파 가속, 마지막 수단)와 (B) 윈도 안 요청 흡수 (retry + outlier detection + graceful drain, 실무 표준). 1. 배경: 왜 데이터 플레인은 진실을 모르는가 서비스 메시에서 트래픽을 어디로 보낼지 결정하는 주체는 데이터 플레인(Envoy 사이드카) 이다. 그런데 Envoy는 K8s API를 직접 보지 않는다. 라우팅 결정은 전적으로 자기 안에 들고 있는 로컬 설정 스냅샷 에 의존한다. 이 스냅샷, 특히 \"이 서비스의 살아있는 endpoint IP 목록\"(EDS-CLA, Cluster Load Assignment)은 컨트롤 플레인 istiod 가 xDS의 EDS(Endpoint Discovery Service) 채널로 push해 줘야 갱신된다. 여기서 메시 아키텍처의 본질적 전제가 드러난다 — 데이터 플레인 설정은 eventually consistent(최종 일관성) 다. \"진실\"(실제 Pod 상태)은 K8s API에 있고, Envoy가 들고 있는 것은 그 진실의 지연된 복제본 이다. 그래서 실제 endpoint 상태가 바뀐 순간과 Envoy가 그 변화를 인지하는 순간 사이에는 언제나 전파 지연(propagation lag)이 존재한다. 이건 튜닝 부족이 아니라, 중앙 컨트롤 플레인이 수천 개 프록시에 push로 동기화하는 구조라면 피할 수 없는 트레이드오프다. Phantom workload는 바로 이 지연이 빚는 결함이다 — 실제로는 이미 사라졌거나 unhealthy인데, Envoy의 endpoint 목록에는 아직 살아있는 것으로 남아 트래픽이 계속 가는 가짜 endpoint. 죽은 IP를 가리키는 유령이라 \"phantom\"이다. 이 문서는 그 유령이 왜 생기는지(메커니즘), 어떻게 식별하는지(response flag), 그리고 어디서 잡는지(완화 A/B)를 시간축 하나로 꿰어 본다. 2. 메커니즘: phantom window라는 단 하나의 그림 앵커: phantom의 모든 것은 하나의 시간축 윈도 로 환원된다. \"Pod 종료 이벤트 발생\" 시점에 윈도가 열리고 , \"새 EDS-CLA가 Envoy에 도달\" 시점에 닫힌다 . 이 윈도가 열려 있는 동안 호출자 사이드카가 EDS를 참조하면 죽은 IP를 healthy로 믿고 트래픽을 보낸다. 따라서 모든 완화는 두 동작 중 하나다 — 윈도를 빨리 닫거나(A), 윈도가 열린 동안 들어온 요청을 흡수하거나(B). 이 한 줄을 잡으면 아래 표·다이어그램·YAML이 전부 그 자리에 들어맞는다. 윈도가 열리고 닫히는 과정을 단계로 풀면: 단계 발생 과정 결과 1. 워크로드 상태 변화 Pod가 종료/크래시 → kubelet이 Endpoints/EndpointSlice에서 제거, K8s API 이벤트 발생 컨트롤 플레인이 이벤트 수신 ( 윈도 열림 ) 2. 구성 전파 지연 istiod 의 debounce(이벤트 배칭) + push 큐 대기 + xDS push 시간 동안 새 EDS가 아직 Envoy에 도달하지 않음 데이터 플레인이 stale 상태 유지 ( 윈도 지속 ) 3. 유령(Phantom) 라우팅 Envoy는 이미 사라진 endpoint IP를 여전히 healthy로 믿고 트래픽 전송 connect 실패( UF ), 503/504, 타임아웃 윈도 폭은 2단계 가 결정한다 — debounce + push queue + xDS push 의 합이다. 그래서 윈도가 넓어지는 상황은 곧 이 세 항이 커지는 상황이다: 대량 업데이트(롤링 배포, 노드 드레인)로 이벤트가 폭주하거나, istiod 가 CPU saturation으로 push를 못 따라가거나, 네트워크 파티션으로 xDS 채널이 막힐 때. ℹ 근본 원인은 \"컨트롤 플레인 전파 지연\"이며, 이것은 버그가 아니라 메시 아키텍처의 본질적 트레이드오프다. 그래서 \"phantom을 없앤다\"는 목표는 성립하지 않는다 — 윈도를 좁히고 그 안의 요청을 흡수해 **영향을 0에 수렴**시키는 것이 현실적 목표다. 그림 1. Pod 종료 후 EDS 제거가 Envoy에 닿기 전 윈도에서 죽은 IP로 요청 실패. 흡수(B: retry/outlier)와 단축(A: debounce)이 두 처방. 2.1 식별: 어떤 response flag가 phantom인가 윈도가 열린 줄을 어떻게 밖에서 아는가? phantom은 access log의 response flag 로 자기 정체를 드러낸다. 워크로드가 사라져 IP가 죽은 경우 Envoy는 TCP connect 자체에 실패하므로, 1차 신호는 UF (UpstreamConnectionFailure) — 503과 함께 UF 가 찍히는 것이 phantom의 가장 특징적 패턴이다. flag 의미 phantom과의 관계 UF UpstreamConnectionFailure — 죽은 IP로 connect 실패 phantom 1순위 신호 (stale IP가 아직 pool에 살아있음) UH No healthy upstream — pool에 healthy endpoint 0 phantom과 반대 상황 : stale이 제거된 직후 pool이 비었거나, outlier가 과도 eject한 경우 UC Upstream connection termination — 연결 수립 후 종료 app crash로 처리 도중 끊김 UT Upstream request timeout — 응답 지연 연결은 됐으나 응답 없음 이"
    },
    {
      "title": "MOC — Istio Graceful Termination",
      "desc": "이 MOC는 homelab Istio graceful-termination 실험의 허브이자, 그 실험이 \"왜 이렇게 설계됐는가\"를 한 번에 세워주는 멘탈 모델 게이트웨이다. 핵심 결론 한 줄: 서비스 팀은 LB를 직접 명령할 권한이 없으므로, \"끊김 없는 Pod 종료\"는 health…",
      "url": "/public/istio/gt__MOC-graceful-termination.html",
      "domain": "istio",
      "text": "istio graceful-termination production-prep MOC — Istio Graceful Termination NOTE 이 MOC는 homelab Istio graceful-termination 실험 의 허브이자, 그 실험이 \"왜 이렇게 설계됐는가\"를 한 번에 세워주는 멘탈 모델 게이트웨이 다. 핵심 결론 한 줄: 서비스 팀은 LB를 직접 명령할 권한이 없으므로, \"끊김 없는 Pod 종료\"는 health 신호(200↔503)의 타이밍으로 LB의 backend 제거 시점을 간접 제어하는 문제 로 환원된다. 사내 LB(Citrix downStateFlush 류)가 backend DOWN 마킹 시 in-flight RST하는 환경에서도, hc 사이드카 + Envoy graceful drain + active-request 폴링 패턴으로 long/streaming 요청 끊김을 막을 수 있음을 입증했다. 아래는 그 메커니즘을 먼저 세우고, 산출물(W1~W6 + 코드 워크스루)로 드릴다운하는 항법도다. ℹ FSM 명명 매핑 (시리즈 정본 — 여기를 기준으로) READY/DRAINING/DRAINED_WAIT_LB/TERMINATING/FAILED → OPEN/DRAINING/CLOSING/CLOSED/FAULT (게이트 비유). 옛 artifacts( tests/artifacts/2026*/ )는 옛 명칭으로 보존됨. POST /reopen : DRAINING/CLOSING → OPEN abort. CLOSED는 unrecoverable(409). CLOSING→OPEN 응답엔 Warning: 199 ... 헤더(LB rise 2 = +4s 재투입). 개별 src 노트는 이 callout을 중복 서술하지 말고 본 MOC를 cross-ref할 것. 코드 원본 위치: ~/labs/istio/ . 결과 기록: ~/labs/istio/{PLAN.md, EXECUTION-LOG.md} . 0. 배경 — 왜 이 문제가 어려운가 K8s Pod 종료는 본질적으로 여러 비동기 신호의 경쟁(race) 이다. kubelet이 SIGTERM을 보내는 순간, LB가 backend를 pool에서 빼는 순간, Envoy가 listener를 drain하는 순간 — 이 셋은 서로 독립적으로 진행되며 누구도 다른 쪽을 기다려주지 않는다. 종료되는 Pod이 들고 있던 long/streaming 요청은, 이 신호들의 순서가 어긋나는 순간 RST(강제 연결 절단) 로 죽는다. 여기서 핵심 제약은 권한과 판단 근거 두 가지다. 권한 : 서비스 팀은 보통 사내 LB(HAProxy/Citrix NetScaler)에 \"이 backend를 빼라\"는 명령을 직접 내릴 권한이 없다. LB 운영은 인프라 팀 소관이다. 판단 근거 : LB는 오로지 health check 응답(200 vs 503) 하나만으로 backend의 UP/DOWN을 판단한다. 즉 서비스 팀이 LB에 줄 수 있는 입력은 health 신호 하나뿐이다. 이 두 제약을 합치면 문제의 형태가 바뀐다. \"끊김 없는 종료\"는 LB를 직접 제어하는 문제가 아니라, 우리가 쥔 유일한 레버(health 응답)의 타이밍으로 LB가 backend를 빼는 시점을 간접 제어하는 문제 다. 이 한 줄이 시리즈 전체(W1~W6)의 출발점이다. 더 나쁜 점은, 사내 LB( downStateFlush ENABLED / on-marked-down shutdown-sessions )가 backend를 DOWN 마킹하는 순간 그 backend가 들고 있던 in-flight 세션을 즉시 flush(RST) 한다는 것 — graceful close가 아니라 즉시 절단이다. 그래서 \"DOWN 마킹 시점\"이 곧 \"끊김 시점\"이고, 우리의 과제는 그 시점을 모든 요청이 끝난 뒤로 미루는 것이 된다. 선행 개념 : K8s Pod lifecycle(preStop/SIGTERM/terminationGracePeriodSeconds), Service externalTrafficPolicy: Local , Envoy admin API, HAProxy health check( inter / rise / fall ). 모르면 W1 Big Picture §1·§2가 무대를 깔아준다. 1. 핵심 메커니즘 — 멘탈 모델 ANCHOR 머릿속에 담을 단 한 장면부터. event 5(LB DOWN @ ~4s)는 두 모드에서 똑같이 일어난다. 모든 차이는 그 전에 event 1(health 200→503 flip)이 언제 발생하느냐 하나다. 이 한 줄에서 시리즈의 모든 디테일이 파생된다. health 503 flip을 즉시 하면(current) LB는 아직 active 요청을 들고 있는 backend를 DOWN 마킹하고 → in-flight RST. flip을 active=0 뒤로 미루면 (improved) LB가 DOWN 마킹할 때는 이미 끊을 게 없어 → RST가 무력화된다. \"503 flip을 active=0 뒤로 미룬다\" — 이게 전부다. 이 anchor가 동작하는 구조 세 부품이 health 레버를 함께 쥔다. 부품 그게 답하는 질문 정본 노트 포트 분리 (data 30080 / health 30180) 어떻게 \"요청은 계속 받으면서 health만 503\"을 만드나? W1 Big Picture §2 hc FSM 5-state (OPEN/DRAINING/CLOSING/CLOSED/FAULT) health 레버를 누가, 어떤 규칙으로 쥐나? W2 hc FSM §2~3 drain.sh active 폴링 flip 시점을 어떻게 active=0까지 미루나? W2 hc FSM §4~5 포트 분리 가 출발점이다. HAProxy는 traffic 포워딩( worker:30080 )과 health 체크( check port 30180 → hc:18180)를 분리된 포트로 한다. 두 포트가 독립이기 때문에 hc는 traffic은 200으로 계속 받으면서 health만 503으로 떨어뜨려 LB에게만 \"나를 빼라\"고 신호할 수 있다. hc FSM 이 그 레버를 규칙으로 묶는다. 핵심은 DRAINING 상태에서도 /he"
    },
    {
      "title": "Apps Walkthrough — backend + hc + graceful-drain.sh",
      "desc": "graceful termination은 \"Pod를 끈다\"가 단일 행위가 아니라, 서로 분리된 4개 신호(LB health, K8s readiness, Envoy listener, in-flight 요청)를 정해진 순서로 하나씩 끄는 것이라는 사실을 코드로 증명하는 문서다. 홈랩 실험의…",
      "url": "/public/istio/gt__src-apps-walkthrough.html",
      "domain": "istio",
      "text": "istio graceful-termination go golang drain homelab Apps Walkthrough — backend + hc + graceful-drain.sh NOTE graceful termination은 \"Pod를 끈다\"가 단일 행위가 아니라 , 서로 분리된 4개 신호(LB health, K8s readiness, Envoy listener, in-flight 요청)를 정해진 순서로 하나씩 끄는 것 이라는 사실을 코드로 증명하는 문서다. 홈랩 실험의 두 Go 애플리케이션( apps/backend , apps/hc )과 preStop 훅( apps/hc/graceful-drain.sh )을 라인 수준까지 추적 한다. 결론부터: backend는 끌 대상 (in-flight 요청)을 만들고, hc FSM은 어떤 신호를 언제 끌지 를 5-state로 인코딩하며, drain.sh는 그 순서 를 best-effort로 오케스트레이션한다. 세 파일은 같은 한 문제의 세 측면이다. FSM/health 응답표·active=0 전략의 개요 정본 은 W2 — HC FSM , Envoy drain 메커니즘 상세는 Envoy drain listeners 에 둔다. 본 문서는 라인별 코드 추적 에 집중한다. 명명 매핑(2026-04-26) : READY/DRAINING/DRAINED_WAIT_LB/TERMINATING/FAILED → OPEN/DRAINING/CLOSING/CLOSED/FAULT(게이트 비유). 옛 artifacts( tests/artifacts/2026*/ )는 옛 명칭으로 보존. 신규 : POST /reopen — DRAINING/CLOSING → OPEN abort. CLOSED는 unrecoverable(409). CLOSING→OPEN 응답엔 Warning: 199 ... 헤더(LB rise 2 = +4s 재투입). 1. 배경 — 왜 \"Pod 종료\"가 코드 세 개를 부르는가 문제의 출발점 : kubelet이 Pod를 죽일 때 일어나는 일들은 서로 비동기다 . SIGTERM 전달, Service endpoint 제거, 외부 LB(HAProxy)의 health-check 재판정 — 이 셋은 각자의 시계로 돈다. endpoint가 제거되기 전에 컨테이너가 죽으면, 그 사이 도착한 요청은 죽은 Pod로 라우팅돼 끊긴다(connection reset). 이게 graceful termination이 풀려는 단 하나의 문제다. 순진한 해법(\"preStop에서 sleep 15 후 종료\")이 부족한 이유: 고정 sleep은 실제 in-flight 요청이 끝났는지를 모른다 . 60초짜리 /sleep 요청 한 개가 살아 있어도 15초 후 그냥 죽는다. 반대로 트래픽이 없어도 15초를 낭비한다. 필요한 건 시간이 아니라 신호 — \"지금 실제로 처리 중인 요청이 0인가?\"를 관측하고, 그 후에야 LB·endpoint를 끊는 것이다. 여기서 4개 신호가 등장한다. 각각이 서로 다른 관측자 에게 \"이 Pod 쓰지 마\"를 알리는 채널이다: 신호 누가 보나(관측자) 끄면 무슨 일 HAProxy health ( /health_check.html ) 외부 L4/L7 LB LB pool에서 backend 제외(신규 유입 차단) K8s readiness ( /health ) kube-proxy / Service Service endpoint 제거(클러스터 내부 유입 차단) Envoy listener ( drain_listeners ) 사이드카 데이터플레인 신규 conn에 Connection: close 권유 in-flight 요청 backend 프로세스 자신 (끄는 게 아니라 소진 대상) 선행 개념 : HTTP keep-alive와 connection draining, Go http.Server.Shutdown 시맨틱, Envoy admin API( :15000 ), K8s probe(readiness vs liveness)의 분리. 모르면 W2 — HC FSM 을 먼저 본다. 대상 독자 : 이 세 신호를 순서대로 끄는 코드가 실제로 어떤 줄에서 그 일을 하는지 확인하려는 SRE/DevOps. 2. 아키텍처 — 분리된 신호 = 분리된 코드 멘탈모델 앵커 (이 한 그림만 머리에 남겨라) : 세 파일은 graceful termination이라는 한 동사를 명사 세 개 로 쪼갠 것이다 — 대상(backend) · 상태(hc FSM) · 순서(drain.sh) . drain.sh가 \"지금 readiness를 꺼라\"라고 명령하면, hc FSM이 그 명령을 상태 전이 로 받아 health endpoint 응답을 바꾸고, backend는 그 와중에도 in-flight 요청을 끝까지 보호한다. 즉 drain.sh = 지휘자, hc = 신호 패널, backend = 보호 대상. 그림 1. 세 박스의 협업. drain.sh가 순서를 만들고(orchestration), hc FSM이 같은 상태에서 health/readiness 신호를 서로 다른 타이밍에 끄며(상태), backend의 long-req가 srv.Shutdown으로 보호받는다(대상) — 이 시차가 drain window를 만든다. 핵심은 비대칭 이다: 같은 FSM 상태라도 endpoint마다 응답이 다르다. /health_check.html 은 DRAINING까지 200, /health 는 CLOSING까지 200. 하나의 상태가 여러 신호를 서로 다른 타이밍에 끄도록 매핑돼 있고, 이 시차가 곧 \"신규 유입은 막되 in-flight는 살아 있는\" drain window 를 만든다(§2의 hc 핸들러표가 정본). 아래 세 절은 이 그림의 세 박스를 각각 라인 단위로 연다. 3. apps/backend/main.go — 끌 대상(in-flight)을 만드는 코드 backend의 역할은 앵커의 대상 박스 다. /sleep · /stream 은 의도적으로 오래 살아있는 in-flight 요청을 생성해, drain 순서가 틀리면 끊겨버릴 트래픽을 재현한다. 따라서 이 파일에서 봐야 할 핵심은 두 가지다 — (a) 요청이 어떻게 오래 살아남는가"
    },
    {
      "title": "Envoy Graceful Drain",
      "desc": "Pod 종료 시 502를 없애는 Envoy 데이터플레인 진입점은 admin API POST /drain_listeners?graceful&skip_exit 하나다. 핵심 오해를 먼저 깬다: 이 API는 in-flight 요청을 \"보호\"하지 않는다. 새 연결 유입만 discourage해…",
      "url": "/public/istio/gt__src-envoy-drain-listeners.html",
      "domain": "istio",
      "text": "istio envoy graceful-termination drain-listeners Envoy Graceful Drain NOTE Pod 종료 시 502를 없애는 Envoy 데이터플레인 진입점은 admin API POST /drain_listeners?graceful&skip_exit 하나다. 핵심 오해를 먼저 깬다: 이 API는 in-flight 요청을 \"보호\"하지 않는다. 새 연결 유입만 discourage해서 downstream_rq_active 곡선을 단조감소시키는 유입 차단기 일 뿐이다. 실제 in-flight 보호는 상위 hc FSM이 health 신호를 200으로 유지해 LB가 backend를 빼지 않게 막는 데서 온다. 이 문서는 그 차단기 자체 — graceful 모드의 신규 연결 신호 방식(H1 Connection: close / H2 GOAWAY), skip_exit , 완료 판정이 왜 cx 가 아니라 rq 인지 — 에 집중한다. 대상환경 Istio 1.30 / Envoy sidecar · 대상독자 graceful termination을 메커니즘 수준에서 이해하려는 DevOps/SRE · 범위 Envoy admin drain_listeners API 한 계층 · 선행개념 sidecar(15000 admin), HTTP keepalive, HTTP/2 GOAWAY · 상위 계층 drain.sh 오케스트레이션 · hc FSM 1. 배경: \"누가 in-flight를 지키는가\"라는 질문 Pod가 죽을 때 가장 단순한 종료는 listener를 즉시 close하는 것이다. 그러면 그 순간 처리 중이던 요청이 TCP RST로 끊겨 클라이언트가 502를 본다. graceful termination의 목표는 \" 신규 연결은 받지 않으면서 진행 중인 요청은 끝까지 처리 \"하는 것이고, 이를 달성하려면 두 개의 독립된 일을 해야 한다. (a) 새 요청이 더 안 들어오게 막는다 — 그래야 처리 중 요청 수가 시간이 지나며 줄어든다. (b) 줄어드는 동안 기존 요청이 안 끊기게 지킨다 — 그래야 마지막 한 건까지 완주한다. 여기서 핵심 함정: drain_listeners?graceful 은 (a)만 한다. Envoy listener에 \"이제 신규는 discourage하라\"고 지시할 뿐이다. (b)는 Envoy admin API의 일이 아니라 상위 hc FSM 의 일이다 — drain.sh가 /health_check.html 을 200으로 유지해서 외부 LB가 backend를 endpoint pool에서 빼지 않게 붙잡아둔다. LB가 backend를 UP으로 보는 한 기존 연결은 살아 있고, 그래서 in-flight가 보호된다. 이 분리를 못 잡으면 \" drain_listeners 를 부르면 알아서 무중단이 된다\"고 오해하게 된다. 실제로는 (a) 없이 (b)만 하면 폴링이 영원히 안 끝나고, (b) 없이 (a)만 하면 LB가 backend를 빼버려 in-flight가 RST된다. 둘 다 있어야 한다. 이 문서는 (a)를 책임지는 한 계층을 정확히 본다. 2. 멘탈모델 앵커 + admin API 머릿속 그림 하나: drain_listeners?graceful 은 \"입구 수도꼭지를 잠그는\" 동작이다. 물(in-flight rq )이 다 빠질 때까지 기다리는 건 폴링하는 쪽의 몫이고, 빠지는 동안 통(LB endpoint)을 안 비우는 건 hc FSM의 몫이다. 그래서 완료 판정도 통에 든 물의 양 = request( rq )로 하지, 수도관이 연결돼 있는지(connection cx )로 하지 않는다. API는 Envoy admin endpoint다. Istio 기본 바인딩은 127.0.0.1:15000 으로 Pod 내 sidecar에서만 접근 가능하다(localhost-only — 외부 노출 금지가 보안 모범 사례). # graceful drain 시작 → 응답 본문은 \"OK\" curl -X POST \"http://127.0.0.1:15000/drain_listeners?graceful&skip_exit\" # OK # active request 계측 → gauge 라인 형태로 출력 curl -s \"http://127.0.0.1:15000/stats?filter=downstream_rq_active|upstream_rq_active\" # http.inbound_0.0.0.0_8080.downstream_rq_active: 1 # cluster.outbound|80||backend.svc.cluster.local.upstream_rq_active: 0 호출 한 번이 곧 \"유입 차단\" 신호고, 그 뒤로는 위 stats gauge가 0이 되는지를 호출자가 폴링한다. API 자체는 \"기다려준다\"가 없다 — fire-and-forget으로 listener 상태만 바꾼다. 3. graceful 모드: 신규 연결을 \"어떻게\" 막는가 graceful 파라미터 없이 호출하면 listener를 즉시 close 한다(= 위험한 그 동작, 502 유발). graceful 을 붙이면 drain period 동안 새 연결을 discourage 하는 형태로 바뀐다. 여기서 비자명한 디테일은 \"discourage\"가 프로토콜마다 다른 신호로 구현된다는 점이다 — TCP 레벨에서 강제로 끊는 게 아니라, 애플리케이션 프로토콜 신호로 클라이언트가 스스로 새 연결로 옮기게 유도 하기 때문에 진행 중 요청이 안 깨진다. 프로토콜 신규 유입 discourage 신호 메커니즘 HTTP/1.1 응답에 Connection: close 헤더 keepalive 재사용을 막아 다음 요청은 새 연결로 가게 함 HTTP/2 (gRPC 포함) GOAWAY 프레임 Connection 헤더가 없는 프로토콜이라, GOAWAY로 graceful close를 신호 → 클라이언트가 새 stream을 다른 connection으로 기존 connection : 계속 처리 — 신호는 \"다음부터 새 데로 가라\"이지 \"지금 걸 끊어라\"가 아니다. 그래서 in-flight rq 가 보호된다. drain period 종료 후 : listener를 실제"
    },
    {
      "title": "HAProxy Walkthrough — L7 offload + on-marked-down",
      "desc": "홈랩 graceful termination 실험의 haproxy-current.cfg(143줄)를 읽는다. 단, 목적은 cfg 줄 해설이 아니라 한 문장의 멘탈모델을 세우는 것: 실험의 전부는 한 backend(443→IGW)에서 벌어지는 \"누가 먼저 끊느냐\"의 경합이다 — pod의…",
      "url": "/public/istio/gt__src-haproxy-walkthrough.html",
      "domain": "istio",
      "text": "istio graceful-termination haproxy tls-offload on-marked-down homelab HAProxy Walkthrough — L7 offload + on-marked-down NOTE 홈랩 graceful termination 실험의 haproxy-current.cfg (143줄)를 읽는다. 단, 목적은 cfg 줄 해설이 아니라 한 문장의 멘탈모델을 세우는 것 : 실험의 전부는 한 backend(443→IGW)에서 벌어지는 \"누가 먼저 끊느냐\"의 경합 이다 — pod의 preStop이 Envoy listener를 먼저 drain하느냐, LB의 on-marked-down shutdown-sessions 가 먼저 RST를 쏘느냐. 이 경합을 가능하게 만드는 단 하나의 트릭이 data 포트( 30080 )와 health 포트( check port 30180 )의 분리 다. 결론 셋: ① HAProxy cfg는 current/improved가 동일하고 개선 변수는 IGW manifest의 preStop 스크립트에 격리돼 있다, ② retries 3 이 5xx를 흡수해 disruption을 감추므로 5xx + connection_err 로 측정해야 한다, ③ 나머지 네 포트(80/6443/8443/9000)는 이 실험과 무관한 배경 소품이다. 대상환경 Istio 1.30 + HAProxy(systemd, homelab 203.0.113.211 ) · 대상독자 graceful-termination 실험을 재현·해석하려는 SRE · 범위 443→IGW backend의 drain 경합 메커니즘 (나머지 포트는 맥락용 요약) · 선행개념 HC FSM , NodePort, TLS offload vs passthrough. 명명 매핑(2026-04-26) : hc FSM 상태 READY/DRAINING/DRAINED_WAIT_LB/TERMINATING/FAILED → OPEN/DRAINING/CLOSING/CLOSED/FAULT(게이트 비유). 본 문서에서 'CLOSING'은 신 명칭 = 구 DRAINED_WAIT_LB(LB에만 503 신호, Envoy는 살아 in-flight 처리). FSM 전체는 HC FSM walkthrough 참조. ⚠ 정본 cfg 파일 부재 — 이 문서가 유일한 기록 본 문서가 해석하는 haproxy-current.cfg (143줄)와 haproxy-improved.cfg 는 레포 스냅샷 어디에도 실재하지 않는다(전체 디스크 검색으로 확인). 아래 블록별 인용은 정본의 부분 발췌 이며, 그 발췌가 현존하는 유일한 기록이다. 따라서 인라인 fragment를 손으로 이어붙여도 완전한 143줄을 보장할 수 없고, 11번의 scp haproxy/haproxy-current.cfg ... 배포 명령은 그 파일이 다시 확보돼야 재현 가능하다. 1. 배경 — 왜 LB cfg가 graceful termination 실험의 무대인가 graceful termination의 본질 질문은 \"pod가 죽을 때 진행 중(in-flight) 요청을 어떻게 안 끊는가\"다. 이건 pod 혼자 답할 수 없다. pod가 SIGTERM을 받아 Envoy listener를 닫기 시작해도, 그 앞단 LB가 여전히 새 요청을 그 pod로 보내거나, 또는 너무 일찍 기존 연결을 RST로 끊으면 순단이 난다. 즉 graceful termination은 pod와 LB 사이의 타이밍 협상 이고, LB의 health check·세션 관리 설정이 그 협상의 절반을 쥔다. 그래서 이 실험에서 HAProxy cfg를 읽는다. 핵심 등장인물은 셋이다. IGW(Istio ingress gateway) pod — SIGTERM을 받으면 preStop 스크립트가 돌고, 그 안의 hc(health-controller) FSM이 상태를 옮긴다. 죽는 쪽. hc FSM — pod의 health check 응답을 의도적으로 조작하는 컨트롤러. CLOSING (구 DRAINED_WAIT_LB) 상태가 되면 \"data는 살리되 health probe에만 503을 응답\"한다. 즉 LB에게만 거짓말로 '나 곧 죽어'라고 신호 를 보낸다. HAProxy — 그 신호를 health check로 읽고 backend에서 pod를 빼는 쪽. in-flight를 살릴지 끊을지를 on-marked-down 이 결정한다. 이 셋의 상호작용을 이해하려면 HAProxy가 무엇을 보고 무엇을 하는지를 알아야 하고, 그게 이 문서다. 단 HAProxy는 다섯 포트를 운용하는데, 실험과 직접 관련된 건 443 backend 하나뿐 이다(나머지는 §6 배경 요약). 그러니 다섯 포트 표를 먼저 훑어 \"어느 게 무대고 어느 게 소품인지\" 가른 뒤, 무대(443→IGW)로 직행한다. 포트 mode TLS 목적 실험 관련 80 http 없음 HTTPS로 301 redirect 소품 443 http offload (terminate) L7 헤더 주입 + IGW plaintext backend 무대 6443 tcp passthrough kube-apiserver (client-cert 유지) 소품 8443 tcp passthrough Istio gRPC / mTLS (end-to-end TLS) 소품 9000 http 없음 stats UI 관측 다섯 포트의 데이터 경로 — TLS가 443에서만 끊기고(평문으로 backend 진입) 6443/8443은 byte stream 그대로 통과한다. 443만 backend health check 대상이고, 거기만 on-marked-down 이 붙는다. 그림 1. health 포트 503 → HAProxy가 inter 2s×fall 2 ≈ 4s 후 DOWN 마킹 → on-marked-down shutdown-sessions로 active TCP RST(log D). detection window 동안 in-flight는 data 포트로 정상 처리. 2. 핵심 메커니즘 — 두 포트 분리가 만드는 drain window 2.1 멘탈모델 anchor 하나만 기억하라 : check port 30180"
    },
    {
      "title": "Manifests Walkthrough — IGW 커스텀 Deployment + Service + Gateway/VS",
      "desc": "홈랩 graceful termination 실험의 manifests/ 7개 파일을 \"왜 이 값인가\"까지 해부한 정본 워크스루다. 하나의 그림으로 잡을 것: 이 매니페스트의 모든 숫자와 모든 path·selector·externalTrafficPolicy는 단 두 축에서 파생된다 — (…",
      "url": "/public/istio/gt__src-manifests-walkthrough.html",
      "domain": "istio",
      "text": "istio graceful-termination k8s ingressgateway manifests homelab Manifests Walkthrough — IGW 커스텀 Deployment + Service + Gateway/VS NOTE 홈랩 graceful termination 실험의 manifests/ 7개 파일을 \"왜 이 값인가\"까지 해부한 정본 워크스루다. 하나의 그림으로 잡을 것 : 이 매니페스트의 모든 숫자와 모든 path·selector· externalTrafficPolicy 는 단 두 축 에서 파생된다 — (A) 타이밍 불변식 : 가장 긴 in-flight 요청( /sleep?seconds=300 )이 어느 종료 단계에서도 먼저 끊기지 않도록 모든 데드라인을 정렬한다, (B) 순서 제어 : 트래픽을 LB 먼저·endpoint 나중 순서로 빼낸다. 5가지 파일 차이·NodePort 라우팅·Gateway selector 필연성은 전부 이 두 축의 따름정리다. 멘탈모델은 IGW 커스텀 deployment , 종료 FSM은 HC FSM 을 참고. 명명 매핑(2026-04-26) : READY/.../FAILED → OPEN/DRAINING/CLOSING/CLOSED/FAULT. 신규 POST /reopen (DRAINING/CLOSING→OPEN abort, CLOSED는 409 unrecoverable). ⚠ 관련 파일 · 참조 이 문서가 해부하는 정본 매니페스트( manifests/00-namespace.yaml … 31-virtualservice.yaml 7개)는 현재 레포 스냅샷 어디에도 실파일로 존재하지 않는다. 아래 본문의 인라인 발췌가 유일한 기록이며, 완전한 파일이 아니라 fragment 인용임에 유의(§7의 kubectl apply -f manifests/*.yaml ·§3 diff는 원본 파일 복원 전까지 그대로 재현 불가). 1. 배경 — 왜 IGW를 \"수동으로\" 만드는가 Istio를 쓰면 보통 IngressGateway는 IstioOperator 나 Helm이 자동 생성해준다. 그런데 이 실험은 graceful termination 을 다룬다 — pod이 죽을 때 in-flight 요청(최대 5분짜리 /sleep?seconds=300 )을 한 건도 끊지 않고 흘려보내는 것이 목표다. 자동 생성된 IGW는 종료 시퀀스가 블랙박스라 손댈 수 없다. 그래서 IGW를 평범한 Deployment로 직접 작성 해 envoy 컨테이너 옆에 health-check( hc ) 사이드카를 붙이고, preStop hook과 probe path를 우리 손으로 제어한다. 이 구조를 이해하려면 종료 시점에 누가 누구에게 트래픽을 보내는지 부터 그려야 한다. 그림 1. 트래픽/헬스 두 경로. HAProxy가 TLS를 벗겨 traffic은 30080(Envoy), health는 30180(hc)으로 분기한다. hc의 probe 응답(200/503)이 HAProxy의 라우팅을 좌우하므로 hc가 사실상 Envoy 트래픽을 controls — 이것이 종료 순서 제어의 토대다. 선행 개념 : HAProxy가 앞단에서 TLS를 벗긴 뒤 NodePort 30080(traffic)·30180(health)으로 plaintext를 넘긴다. HAProxy는 30180의 httpchk 응답이 503이 되면 backend를 DOWN으로 마킹해 새 트래픽 차단. 즉 hc 컨테이너의 probe 응답이 HAProxy의 트래픽 라우팅을 좌우 한다 — 이것이 \"순서 제어\"가 가능한 이유다. 종료 FSM(OPEN→DRAINING→CLOSING→CLOSED)의 상태 전이가 각 probe path의 200/503을 결정한다. 2. 핵심 아키텍처 — 두 축이 모든 숫자를 낳는다 축 A — 타이밍 불변식: \"최장 요청이 먼저 끊기면 안 된다\" 종료 경로에는 데드라인이 4개 있다. 어느 하나라도 최장 요청(300s)보다 짧으면 그 지점에서 요청이 잘린다. 그래서 전부 300s를 기준선으로 정렬한다. /sleep?seconds=300 ← 최장 in-flight 요청 (기준선 300s) | +-- backend tGPS = 305s (300 + 여유 5s; kubelet SIGKILL 유예) +-- VS timeout = 305s (backend tGPS와 동일; Envoy가 먼저 504 내면 안 됨) +-- IGW Envoy drain = 150s (terminationDrainDuration) +-- IGW tGPS = 210s (max(drain 150, preStop ~30) + 여유 60) 여기서 가장 비직관적인 부분이 IGW의 tGPS=210s 산정 이다. tGPS는 직렬 합이 아니다. tGPS는 SIGTERM부터 SIGKILL까지의 단일 데드라인이고, 그 구간 안에서 두 컨테이너가 병렬로 종료한다: istio-proxy(Envoy) : SIGTERM 즉시 terminationDrainDuration: 150s 동안 active connection drain → 최대 150s. hc 컨테이너 : SIGTERM 즉시 preStop 실행 → current는 drain→close-lb→sleep 30 이라 ~30s. 두 컨테이너가 동시에 SIGTERM을 받으므로 임계 경로는 직렬 합(150+30)이 아니라 max(150, 30) = 150s 다. tGPS(210) = max(Envoy drain 150s, preStop ~30s) + 여유 60s = 150 + 60 핵심 불변식은 tGPS > terminationDrainDuration (210 > 150). 깨지면 Envoy가 drain을 끝내기 전에 kubelet이 SIGKILL을 보내 in-flight가 잘린다. (improved 모드의 preStop은 DRAIN_TIMEOUT(120)+LB_BUFFER(10) =최대 130s지만 이 역시 drain 150s와 병렬이라 임계 경로는 여전히 150s — 두 모드 모두 210s 안에 든다.) 축 B — 순서 제어: \"LB 먼저, endpoint 나중\" 요청을 안 끊으려면 트래픽을 빼는 순서 가 결정적"
    },
    {
      "title": "Quickstart — 5분 안에 실험 다시 돌리기",
      "desc": "홈랩 Istio graceful-termination 실험을 다시 펼쳐 빠르게 재현하기 위한 명령 시퀀스 + 검증 모음이다. 핵심 결론 한 줄: 모든 재현은 모드를 토글하고 → 종료 이벤트를 만들고 → 그 순간의 in-flight 요청 결과를 측정하는 동일한 3-스텝 루프이며, cur…",
      "url": "/public/istio/gt__src-quickstart.html",
      "domain": "istio",
      "text": "istio graceful-termination homelab haproxy runbook Quickstart — 5분 안에 실험 다시 돌리기 NOTE 홈랩 Istio graceful-termination 실험을 다시 펼쳐 빠르게 재현 하기 위한 명령 시퀀스 + 검증 모음이다. 핵심 결론 한 줄: 모든 재현은 모드를 토글하고 → 종료 이벤트를 만들고 → 그 순간의 in-flight 요청 결과를 측정 하는 동일한 3-스텝 루프 이며, current↔improved의 expect 라인 차이가 곧 가설의 증거다. 메커니즘 설명은 big picture 에, 시나리오 정본 정의(S2 포함)는 test scenarios 에 있다. 대상환경: homelab k8s v1.30.6 (master1/worker1/worker2) + .211 노드 HAProxy + Istio IGW. 대상독자: 이 실험을 이미 한 번 돌려봤고 다시 펼치려는 사람. 범위: 재현 명령·검증·함정만 — 왜 이런 결과가 나오는지의 메커니즘은 링크된 문서로 분리. 선행개념: Envoy drain, k8s preStop/grace-period, HAProxy backend health. 0. 왜 이 실험이 존재하나 — 풀어야 할 문제 쿠버네티스에서 pod가 사라지는 건 일상이다(rollout, scale-down, eviction). 문제는 pod가 죽는 그 짧은 창(window) 동안 이미 그 pod로 들어와 처리 중이던 요청(in-flight request)에 무슨 일이 일어나는가 이다. 순진하게 종료하면 진행 중 요청이 RST(TCP reset)나 stream CANCEL로 끊겨 클라이언트는 5xx·exit≠0을 본다. 이게 \"ungraceful termination\"이고, 트래픽이 많은 프로덕션에서는 배포할 때마다 소수의 요청이 조용히 깨지는 형태로 새어 나온다. graceful termination의 처방은 단순하다 — 죽기 전에 \"나 이제 안 받아요\"를 먼저 알리고(drain), 받은 요청은 끝까지 처리할 시간을 확보한 뒤(지연 종료) 진짜로 죽는다. 이 실험은 그 처방이 실제로 효과가 있는지를 두 모드로 대조 측정한다: 모드 동작 in-flight 요청 운명 current (broken) abrupt shutdown RST/CANCEL로 끊김 → 5xx·exit≠0 improved (graceful) drain + 지연 종료 끝까지 완주 → 200·exit=0 이 문서는 그 대조를 \"어떤 명령을, 어떤 순서로, 무엇을 기대하며\" 돌리는지로 압축한 것이다. 즉 이 문서는 결론을 만드는 절차서 이고, 왜 그 결론이 나오는가 는 big picture · test scenarios 에 있다. 1. 머릿속 한 장 — 모든 재현을 지배하는 3-스텝 루프 ANCHOR: 이 실험에 시나리오가 셋이지만 골격은 하나다 — 토글 → 종료 이벤트 → 측정. 시나리오들은 이 루프의 변수 만 바꾼다: ① 종료 이벤트의 종류(단일 pod kill vs rollout), ② 요청의 형태(단발 long-request vs 지속 트래픽 vs streaming). 그래서 한 시나리오를 이해하면 나머지는 델타만 보면 된다. 그림 1. 측정 루프. 요청을 먼저 in-flight로 띄운 뒤(2a) pod를 죽여(2b) \"종료 순간에 걸쳐 있던 요청\"의 운명을 측정한다(3). current와 improved의 expect 라인이 달라야 처방이 작동한다는 증거가 된다 — 순서가 바뀌면 측정 대상이 사라진다. 왜 이 구조여야 하나 — 측정하려는 건 \"종료 순간에 걸쳐 있던 요청\"의 운명 이다. 그래서 요청을 먼저 띄워 in-flight 상태로 만든 뒤(2a), 그 다음에 pod를 죽이고(2b), 죽는 도중에 그 요청이 어떻게 끝나는지를 본다(3). 순서가 바뀌면(예: 죽이고 나서 요청) 측정 대상이 사라진다. 그리고 current와 improved의 expect 라인이 다르게 나와야 처방이 작동한다는 증거가 된다 — 같으면 실험이 망가진 것이다. 변수 격리: replicas가 측정의 신뢰성을 좌우한다 루프에 숨은 전제가 하나 있다 — 내가 죽이는 그 pod로 트래픽이 실제로 가야 측정이 성립한다. HAProxy가 balance roundrobin 이라, replicas=2면 curl이 살아있는 다른 worker pod로 돌아가 정상 응답을 받아버린다(개입한 변수가 사라짐 → 가설 검증 불가, §5 Q1). 그래서: S1/S4 (replicas=1): 트래픽이 죽일 pod 하나로 강제 → 종료 영향을 정면으로 측정. S3 (replicas=2): 의도적 — rollout disruption(여러 pod가 순차 교체되는 동안의 연결 안정성)을 측정하려면 복수 pod가 필요. 2. 메커니즘 — 모드 전환은 왜 \"3개가 함께\" 움직이나 전환의 한 줄 모델: 한 모드 = {IGW manifest, HAProxy cfg, mode 라벨} 세 가지가 정합된 상태 이고, 토글은 이 셋을 동시에 갈아끼우는 일이다. 하나라도 빠지면 상태가 어긋나 결과가 오염된다. 그림 1. operator 3갈래 작업: IGW manifest apply + node .211 haproxy reload + 구 pod 삭제 → 모두 rollout OK로 수렴. 대상 current improved 왜 바꿔야 하나 IGW manifest manifests/20-igw-current.yaml manifests/21-igw-improved.yaml pod의 preStop/drain/grace 동작 자체를 정의 — 처방의 본체 HAProxy cfg (.211) haproxy/haproxy-current.cfg haproxy/haproxy-improved.cfg L7 앞단의 health-check·연결 처리. pod 동작과 정합돼야 종료가 깔끔 옛 pod mode!=current 강제 삭제 mode!=improved 강제 삭제 새 manifest를 apply해도 옛 mode pod이 deadlock으로 안 죽으면(§4) 옛 동작이 잔존 왜 옛 pod을 손으로 죽여야 하나 — 가장 비자명한 부분"
    },
    {
      "title": "Istio Graceful Termination 사내 도입 런북 (홈랩 실험 → 사내 적용 가이드)",
      "desc": "홈랩 graceful-termination 실험 결과를 프로덕션 IGW 환경에 이식하는 6단계 런북이다. 머릿속에 담을 한 장면: LB는 backend의 살아있음 여부를 오직 health check 응답으로만 판정한다 — 그래서 LB를 직접 명령할 권한이 없어도, hc 사이드카가 pr…",
      "url": "/public/istio/gt__src-runbook.html",
      "domain": "istio",
      "text": "istio graceful-termination runbook envoy haproxy observability Istio Graceful Termination 사내 도입 런북 (홈랩 실험 → 사내 적용 가이드) NOTE 홈랩 graceful-termination 실험 결과를 프로덕션 IGW 환경에 이식하는 6단계 런북이다. 머릿속에 담을 한 장면 : LB는 backend의 살아있음 여부를 오직 health check 응답으로만 판정한다 — 그래서 LB를 직접 명령할 권한이 없어도, hc 사이드카가 preStop 동안 /health_check.html 의 200/503 타이밍을 단계적으로 바꾸면 LB의 \"이 backend를 DOWN 마킹할지\"를 간접 조종할 수 있다. 이 한 줄이 6단계 전부를 푼다. 본 문서는 각 단계가 무엇을 검증·제어하는지 + 어디서 깨지는지 에 집중하고, FSM 상세는 HC FSM 정본 , grace period 산정은 프로덕션 적용 을 참조한다. 대상환경 : Istio 1.30 IGW(istio-ingressgateway) + 외부 L4/L7 LB(Citrix NetScaler 또는 HAProxy). 대상독자 : rolling update 중 5xx/connection drop을 0으로 만들려는 SRE. 선행개념 : hc 사이드카 FSM(OPEN/DRAINING/CLOSING/CLOSED/FAULT), Envoy drain. FSM 상태명은 게이트 비유를 사용한다. 1. 왜 이 런북이 존재하나 — 문제와 전제 평범한 rolling update에서 IGW pod 하나가 종료될 때 무슨 일이 벌어지는지 보자. kubelet이 SIGTERM을 보내고, 거의 동시에 Service의 endpoint에서 빠지지만, 외부 LB는 그 사실을 즉시 모른다 . LB는 자기 health check 주기( inter × fall )가 돌아 backend를 DOWN으로 마킹할 때까지 계속 신규 요청을 그 pod로 보낸다. 더 나쁜 건, LB가 DOWN을 마킹하는 바로 그 순간 downStateFlush ENABLED (HAProxy로 치면 on-marked-down shutdown-sessions ) 옵션이 켜져 있으면, 아직 처리 중이던 in-flight 요청까지 즉시 RST로 끊어버린다 . 클라이언트 입장에서는 502/connection reset이다. 여기서 근본 제약이 하나 더 있다: 서비스 팀에는 LB에 \"이 backend를 빼라\"고 명령할 API 권한이 없다. LB는 인프라 팀 소유다. 그러면 어떻게 무중단 종료를 만드나? 핵심 관찰은 이것이다 — LB가 backend의 생사를 판정하는 유일한 입력은 health check 응답 코드다. 즉 우리가 health 응답을 쥐고 있으면, LB의 backend pool membership을 간접적으로 제어할 수 있다. 이 레버를 쥐는 주체가 IGW pod 안의 hc 사이드카 이고, preStop 동안 응답을 어떤 순서로 바꾸느냐가 graceful termination의 전부다. 이 런북은 그 레버를 사내 LB(Citrix)·사내 Helm chart·사내 Prometheus에 안전하게 배선하는 절차다. 원본은 행동·검증·위험신호 체크리스트 중심이므로, 여기서는 각 단계가 어떤 메커니즘을 검증·제어하는지 와 LB 동작 모델(HAProxy ↔ Citrix 대응)을 보충한다. 2. 핵심 메커니즘 — health 응답 순서가 곧 LB 제어 레버 앵커: \"먼저 다 흘려보내고, 그 다음에 503\" 런북에서 알아야 할 인과는 단 하나다 — LB의 backend 판정은 health check 응답에만 의존 한다. 그래서 hc 사이드카는 preStop 동안 /health_check.html 응답을 단계적으로 바꿔 LB를 간접 제어한다. 정정된 시퀀스의 핵심은 직관과 정반대다: \"503을 먼저 띄우지 말고, in-flight를 다 흘려보낸 뒤(active=0)에야 503을 띄운다.\" 왜? 503을 먼저 띄우면 downStateFlush ENABLED LB가 backend를 DOWN 마킹하면서 in-flight를 즉시 RST 하기 때문이다. 순서를 뒤집는 순간 graceful이 깨진다. 그래서 graceful의 진짜 안전 구간은 health 200을 유지 하는 DRAINING이고, 503 flip은 보호할 게 없어진 뒤에만 일어난다. 3단계 응답표 — 각 단계가 답하는 질문 단계 /health_check.html hc가 하는 일 LB 거동 이 단계가 보장하는 것 DRAINING 200 유지 downstream_rq_active + upstream_rq_active == 0 을 폴링 UP 유지 — 기존 요청 정상 종료 in-flight 보호 (RST 없음) CLOSING active=0 확인 후 503 flip ( /close-lb ) LB가 DOWN 마킹하도록 신호 inter × fall (예: 2s×2=4s) 만에 DOWN → 신규 차단 신규 유입 차단 CLOSED /health (K8s readiness) 503 LB_BUFFER 대기 후 readiness까지 내림 → pod 종료 per-node 판정도 DOWN endpoint 완전 제거 후 SIGTERM 읽는 법: 각 행은 \"이 단계가 어떤 위험을 막는가\"로 보면 된다. DRAINING은 RST 를, CLOSING은 신규 요청 유입 을, CLOSED는 조기 종료 를 막는다. 세 위험이 서로 다른 시점에 발생하므로 단계가 셋으로 나뉜다. 즉 graceful의 심장은 DRAINING(health 200 유지) 이며, 이 200 유지가 본 문서 핵심 결론(\"health 200 유지로 in-flight 보호\")의 실제 메커니즘이다. OPEN(정상 200)과 FAULT(drain timeout 초과 등 비정상)를 포함한 상태별 health 응답표·전이 제약, 그리고 POST /reopen abort 경로와 Warning: 199 헤더(CLOSING→OPEN 재투입) 메커니즘은 HC FSM 정본 이 정본이다. 그림 1. 운영 런북 관점의 종료 흐름: DRAINING(LB UP 유지·acti"
    },
    {
      "title": "Istio Graceful Termination 테스트 하니스 코드 워크스루 (출처: 홈랩 실험 코드 정리)",
      "desc": "홈랩 Istio graceful-termination 실험의 tests/ 디렉터리(5개 실행 스크립트 + 공통 라이브러리)를 코드 레벨로 해부한다. 이 하니스가 측정하려는 단 하나의 명제 — \"in-flight 요청 drain이 LB의 backend DOWN 마킹보다 먼저 끝나는가\"…",
      "url": "/public/istio/gt__src-tests-walkthrough.html",
      "domain": "istio",
      "text": "istio graceful-termination envoy haproxy test-harness k8s Istio Graceful Termination 테스트 하니스 코드 워크스루 (출처: 홈랩 실험 코드 정리) NOTE 홈랩 Istio graceful-termination 실험의 tests/ 디렉터리(5개 실행 스크립트 + 공통 라이브러리)를 코드 레벨 로 해부한다. 이 하니스가 측정하려는 단 하나의 명제 — \"in-flight 요청 drain이 LB의 backend DOWN 마킹보다 먼저 끝나는가\" — 를 축으로, 각 스크립트가 \"왜 그렇게 짰는가\"(측정 격리, exit code 분리, bash 관용구의 macOS 3.x 호환)를 메커니즘 수준에서 설명하고 Envoy/HAProxy 동작과 연결한다. 결론: S1(current)은 502/≈8.25s로 끊김을 입증하고 S2(improved)는 drain 덕에 200/60s로 살아남으며, 5종 스크립트가 6개 이벤트 소스를 timeline.jsonl 로 통합해 그 차이를 시간축에서 증명한다. ℹ 정본 역할 분리 이 문서는 코드/bash 관용구 정본 (스크립트가 어떻게 동작하는가). 시나리오 변수격리 매트릭스·artifacts 해석·재현 회상 Q&A는 W5 테스트 시나리오 설계 가 정본이다. 아래 replicas=1·05 통합 표는 \"코드 구현 관점\"만 남기고 설계 철학은 W5로 위임한다. ⚠ 정본 소스 파일 부재 이 문서가 해부하는 tests/{lib/common.sh, 01-baseline-long-request.sh, 02-improved-long-request.sh, 03-continuous-traffic.sh, 04-rst-capture.sh, 05-collect-timestamps.sh} 는 현재 레포 스냅샷 어디에도 실파일로 존재하지 않는다(전체 디스크 검색으로 확인). 아래 인라인 발췌(cleanup trap, Envoy poller, pull_pcap 등)는 부분 fragment이자 유일하게 남은 기록 이며, 완전한 스크립트 전문은 아니다. 1. 배경: 이 하니스는 무엇을 증명하려고 존재하나 graceful termination은 \"pod이 죽을 때 이미 받아 처리 중이던(in-flight) 요청을 끝까지 살려보내는가\"의 문제다. K8s가 pod을 지우면 두 시계가 동시에 돈다 — 하나는 앱이 in-flight 요청을 처리·배수(drain)하는 시계 , 다른 하나는 상위 로드밸런서(여기선 HAProxy)가 그 backend를 죽었다고 판정하고 세션을 끊는 시계 다. 이 둘의 순서 가 모든 것을 가른다. drain이 먼저 끝나면 LB가 끊어도 잃을 요청이 없다(안전). LB가 먼저 끊으면 아직 처리 중인 요청 위로 TCP RST가 날아가 502가 된다(끊김). 말로는 자명하지만, 실제 분산 시스템에서 이 순서를 증명 하기는 어렵다. 두 시계는 서로 다른 호스트(앱 pod vs lb-haproxy 노드)에서 돌고, 각자 다른 로그·메트릭·패킷에 흔적을 남긴다. \"끊겼다/안 끊겼다\"는 curl 한 줄로 보이지만 왜 그 순서였는지는 6군데 증거를 하나의 시간축에 겹쳐야만 읽힌다. 이 tests/ 하니스 전체가 바로 그 작업 — 흩어진 증거를 한 시간축으로 모아 순서의 부호를 읽는 측정 장치다. 선행 개념은 셋이다: (1) K8s pod termination 라이프사이클(preStop hook → SIGTERM → terminationGracePeriodSeconds 후 SIGKILL), (2) HAProxy active health check가 backend를 DOWN으로 마킹하는 detection window, (3) Envoy sidecar가 in-flight 요청 수를 노출하는 admin /stats . 이 셋이 위 \"두 시계\"의 구체적 구현체다. 대상독자는 이 실험을 재현·디버그하려는 SRE이며, 범위는 스크립트 코드의 동작 원리 다(시나리오 설계 철학은 W5). 2. 핵심 mental model: 하나의 판정선, 두 시계, 6개 프로브 머릿속에 담을 한 장면 : 이 하니스 전체는 결국 단 하나의 판정선을 측정하기 위한 장치다 — active=0 (in-flight drain 완료)이 HAProxy backend DOWN 보다 먼저 오느냐 나중에 오느냐. 먼저면 drain 성공(S2), 나중이면 끊김(S1). 5개 스크립트는 이 두 이벤트의 순서·부호를 시간축에 찍기 위한 6개 측정 프로브이고, 05 가 그것을 하나의 timeline으로 합쳐 부호를 읽어낸다. 이 한 문장에서 나머지 모든 설계가 따라 나온다. \"판정선이 단 하나\"이므로 측정은 그 선 양쪽 이벤트의 타임스탬프 만 정확하면 된다 — 그래서 모든 프로브가 UTC ISO-8601로 시각을 찍고(사전순=시간순), 로그 줄 순서가 OS 스케줄링에 흔들려도 05 가 타임스탬프 기준으로 다시 정렬한다. \"두 시계의 순서\"가 본질이므로 다른 pod의 트래픽이 섞이면 인과가 깨진다 — 그래서 replicas=1 격리가 전제가 된다. \"in-flight 카운트\"가 판정선의 한쪽 축이므로 그 수를 노출하는 Envoy admin /stats (15000)가 핵심 프로브가 된다. measurement target = sign of one interval ------------------------------------------------------------ drain start | clock A: |====== in-flight draining ======> active=0 clock B: |=== HAProxy detection window ===> backend DOWN | S2 (safe): active=0 THEN backend DOWN (positive gap) S1 (broken): backend DOWN while active>0 (RST over live rq) 판정선의 양쪽을 찍는 데 6개 이벤트 소스 가 동원되고, 그 소스를 만들어내는 게 5개 스크립트다. 의존 관계는: common.sh (공통 헬퍼·artifacts 골격) → 01 / 02"
    },
    {
      "title": "W1. Big Picture — 트래픽 경로 + 6 events + 4 시나리오",
      "desc": "graceful-termination 학습 시리즈(W1~W6)의 첫 hub 노트. 무중단 Pod 종료를 한 문장으로 환원한다: 우리는 LB를 직접 제어할 수 없고 오직 health 신호(200/503) 하나만 LB에 줄 수 있으므로, \"끊김 없는 종료\"는 6개 timestamp를 옳은…",
      "url": "/public/istio/gt__src-w1-big-picture.html",
      "domain": "istio",
      "text": "istio graceful-termination envoy haproxy k8s homelab W1. Big Picture — 트래픽 경로 + 6 events + 4 시나리오 NOTE graceful-termination 학습 시리즈(W1~W6)의 첫 hub 노트. 무중단 Pod 종료 를 한 문장으로 환원한다: 우리는 LB를 직접 제어할 수 없고 오직 health 신호(200/503) 하나 만 LB에 줄 수 있으므로, \"끊김 없는 종료\"는 6개 timestamp를 옳은 순서로 정렬해 LB가 backend를 늦게 빼게 만드는 타이밍 문제 다. 이 문서는 그 무대(트래픽 5-hop 경로), 그 변수(종료 시 6개 시점), 그 측정(4 시나리오)을 세운다. 시리즈 위치 : 대응 코드 워크스루는 코드 워크스루(quickstart) . 다음 단계는 W2 hc FSM , 전체 인덱스는 graceful-termination MOC . FSM 명명 : 2026-04-26 이후 OPEN/DRAINING/CLOSING/CLOSED/FAULT (게이트 비유). 옛 명칭( READY/DRAINING/DRAINED_WAIT_LB/TERMINATING/FAILED )은 옛 artifacts에만 남아 있음. 신규 endpoint : POST /reopen — DRAINING/CLOSING → OPEN abort. CLOSED는 unrecoverable(409). CLOSING→OPEN 응답엔 Warning: 199 ... 헤더. 이때 LB는 inter 2s × rise 2 = +4s 후 backend를 다시 UP 마킹(재투입). reopen 시나리오·전이 상세는 W2 hc FSM §전이/reopen. 1. 배경 지식 — 왜 Pod 종료가 어려운가 kubectl delete pod 한 줄이면 끝날 것 같지만, 그 순간 in-flight 요청이 끊긴다. 원인을 이해하려면 K8s Pod 종료가 여러 독립 주체의 비동기 신호 경쟁(async signal race) 이라는 사실에서 출발해야 한다. Pod가 사라질 때 최소 네 주체가 서로의 진행을 모른 채 동시에 움직인다. kubelet — 종료를 결정하고 preStop hook을 돌린 뒤 SIGTERM을 보낸다. K8s control plane — readiness가 깨지면 EndpointSlice에서 이 Pod를 비동기로 뺀다. Envoy(istio-proxy) — SIGTERM을 받으면 listener를 drain하기 시작한다. 외부 LB(HAProxy/Citrix) — health check 응답만 보고 backend를 UP/DOWN으로 판정한다. 여기서 두 가지 제약 이 모든 설계를 지배한다. 이 둘이 W1~W6 전체의 출발점이다. 권한 제약 — 서비스 팀은 보통 LB에 backend down 같은 명령을 내릴 권한이 없다. LB를 직접 조작할 수 없다. 판단 근거 제약 — LB는 backend의 내부 상태를 모른다. 오직 health check 응답 200 vs 503 하나로만 UP/DOWN을 판단한다. 두 제약을 합치면 결론이 강제된다: 우리가 LB에 줄 수 있는 입력은 health 신호 한 채널뿐이다. 따라서 \"in-flight를 안 끊는다\"는 목표는 LB를 멈추는 문제가 아니라, health 신호의 타이밍으로 LB가 backend를 빼는 순간을 미루는 문제로 바뀐다. LB가 backend를 빼는 순간(= on-marked-down shutdown-sessions 로 기존 connection을 RST하는 순간)이 in-flight 요청이 다 끝난 뒤 에 오도록 신호를 늦추면 무중단이 된다. 선행 개념 : K8s preStop hook / SIGTERM / readiness probe / EndpointSlice, Envoy listener drain, HAProxy backend health check( inter / rise / fall , on-marked-down ). 각 부품의 디테일은 후속 W2~W6에 위임하고, W1은 이들이 어떤 순서로 맞물려야 하는가 만 본다. 대상 환경 : Istio 1.30 IGW + 외부 HAProxy(L7 TLS offload) + bare-metal worker NodePort. 대상 독자 : 무중단 배포/종료를 직접 튜닝하는 DevOps/SRE. 범위 : W1은 문제 정의와 측정 골격까지; 코드·FSM·운영 산출식은 cross-ref. 2. 멘탈모델 + 트래픽 경로 — 무대 세우기 ANCHOR (머리에 하나만 담을 그림): LB는 눈먼 backend pool 관리자다. 그가 보는 신호는 health 200/503 하나, 그가 할 수 있는 행동은 \"backend를 빼고 그 connection을 끊는다\" 하나. 그러므로 graceful termination = \"backend의 in-flight가 0이 될 때까지 health=200을 거짓말처럼 유지하다가, 0이 된 그때 비로소 503으로 떨어뜨려 LB가 뒤늦게 빼게 만드는\" 타이밍 조율. 나머지 모든 디테일은 이 한 그림에서 따라 나온다. 이 조율이 어디서 일어나는지 보려면 트래픽이 지나는 hop을 알아야 한다. health 신호를 읽는 주체 (HAProxy)와 drain 대상 (IGW Envoy)이 경로상 어디에 앉아 어떤 포트로 분리돼 있는지가 §3 events의 무대다. Mac client (curl) | HTTPS, --resolve example.local:443:203.0.113.211 v [ lb-haproxy 203.0.113.211 ] | HAProxy bind *:443 ssl alpn h2,http/1.1 | L7 TLS offload, X-Forwarded-Proto/Host/For 주입 | backend istio-http-backend balance roundrobin | option httpchk GET /health_check.html | check port 30180 inter 2s rise 2 fall 2 | on-marked-down shutdown-sessions v [ worker NodePort 30080 (tra"
    },
    {
      "title": "W2. Backend + hc + drain.sh — Go 코드 메커니즘 (출처: 홈랩 graceful termination 학습 시리즈 W2)",
      "desc": "머릿속에 담을 한 장: health endpoint의 응답 코드는 LB의 backend pool membership을 제어하는 원격 스위치다. hc 사이드카는 5-state FSM(OPEN/DRAINING/CLOSING/CLOSED/FAULT)으로 이 스위치를 쥐고, drain.sh는…",
      "url": "/public/istio/gt__src-w2-hc-fsm.html",
      "domain": "istio",
      "text": "istio graceful-termination go golang drain fsm W2. Backend + hc + drain.sh — Go 코드 메커니즘 (출처: 홈랩 graceful termination 학습 시리즈 W2) NOTE 머릿속에 담을 한 장 : health endpoint의 응답 코드는 LB의 backend pool membership을 제어하는 원격 스위치 다. hc 사이드카는 5-state FSM (OPEN/DRAINING/CLOSING/CLOSED/FAULT)으로 이 스위치를 쥐고, drain.sh는 \"active=0 확인 전까지 /health_check.html 200 유지\"로 스위치를 끄는 시점 을 모든 요청 완료 이후로 밀어내 HAProxy가 in-flight 연결을 끊지 않게 한다. 본 문서는 이 동작을 멘탈 모델/왜 층에서 다루며, 라인별 정본 추적은 코드 워크스루(apps-walkthrough) 에 위임한다. 시리즈 위치 : W2. W1 big picture 의 6 events·4 시나리오를 코드 수준으로 추적. 대응 라인별 코드 워크스루는 apps-walkthrough . 대상독자 : graceful termination을 LB-level까지 추적하려는 SRE. 선행개념 : HAProxy health check( fall / rise / shutdown-sessions ), Envoy drain_listeners , Go interface embedding. 1. 배경 — 왜 health endpoint를 코드로 쥐어야 하나 문제는 한 문장이다: 바닐라 K8s에서는 LB가 backend를 pool에서 빼는 타이밍 을 직접 제어할 수 없다. Pod이 종료될 때 endpoint 제거와 새 트래픽 차단은 비동기로 일어나고, 그 사이에 LB가 in-flight 연결을 끊으면 클라이언트는 RST(5xx)를 본다. graceful termination의 본질은 \"endpoint를 빼는 순간\"을 \"마지막 요청이 끝난 순간\" 뒤로 미루는 것이다. 이 타이밍을 손에 넣는 도구가 health endpoint 다. HAProxy는 traffic을 보낼지 말지를 오직 health check 응답 코드로 판단한다 — 200이면 UP(트래픽 받음), 503이면 fall N 회 연속 후 DOWN. 그래서 health 응답 코드는 \"이 backend로 트래픽을 보내라/말라\"를 외부에서 켜고 끄는 원격 스위치 다. 종료 시퀀스가 이 스위치를 언제 503으로 내리느냐가 전부를 결정한다. 여기서 결정적 전제: health 포트와 traffic 포트가 분리되어 있어야 스위치를 독립적으로 쥘 수 있다. HAProxy traffic | | health 30080 | | 30180 v v +------------------------+ | IGW Pod | | backend :8080 (traffic, 항상 200) | hc :18180 (health, FSM이 코드 결정) +------------------------+ traffic(30080→IGW→backend:8080)과 health(30180→hc:18180)가 다른 포트이기 때문에, hc는 요청을 계속 받으면서도(traffic 정상) health만 503으로 떨어뜨려 \"나를 빼라\"고 신호할 수 있다. 이 분리가 없으면 health를 내리는 순간 traffic도 같이 죽어 graceful이 불가능하다. 핵심 인과 사슬은 한 줄로 압축된다: health 503 → HAProxy가 fall 2 (~4s) 후 DOWN 마킹 → on-marked-down shutdown-sessions 로 in-flight RST. drain.sh의 전 전략은 이 사슬의 마지막 RST가 터지는 시점 을 active=0(모든 요청 완료) 이후로 미루는 것이다. §2~§4가 그 시점을 만들어내는 상태 기계·응답 표·시퀀스이고, §5가 \"왜 그게 핵심인가\"의 결론이다. W1의 전체 경로( Mac → HAProxy → worker NodePort 30080 → IGW Pod → backend Service )에서 hc 는 IGW Pod 안의 사이드카 컨테이너로 포트 18180 청취, backend 는 ClusterIP 8080으로 도달되는 별도 Deployment Pod이다. 2. 핵심 아키텍처 — 스위치를 쥔 5-state FSM 앵커 : hc는 단 하나의 state 변수와 그 변수를 바꾸는 단 하나의 함수 advance(expectedFrom, to, reason) 로 이루어진 기계다. 외부에서 들어오는 모든 제어( POST /drain , /close-lb , /close , /reopen , /fault )는 이 함수를 통해서만 상태를 옮기고, 그 상태가 health 응답 코드를 결정한다. 즉 \"지금 어떤 코드를 돌려줄까\"는 분기문이 아니라 현재 state 하나 가 답한다. 그림 1. hc FSM: OPEN→DRAINING(/drain)→CLOSING(/close-lb, active=0)→CLOSED(/close). /reopen으로 abort 복귀, 어느 상태든 /fault로 강제 FAULT. 왜 FSM인가 — 불변식을 한 곳에 가둔다. 각 전이는 advance(expectedFrom, to, reason) 로 실행되고 expectedFrom != current 면 HTTP 409 Conflict를 던진다. 이 한 줄이 illegal transition을 코드 레벨에서 봉쇄한다. 가장 중요한 결과가 OPEN → CLOSING 직접 점프 불가 — drain.sh가 반드시 [1] /drain → [4] /close-lb 의 2-step을 밟아야 하는 이유다. 상태 불변식을 호출부(여러 핸들러)에 흩지 않고 전이 함수 한 곳에서 강제하므로, 새 핸들러를 추가해도 불법 전이가 새지 않는다. sync.RWMutex 로 보호해 동시 요청에서도 state가 찢어지지 않는다. 왜 reopen이 필요한가 — drain은 취소 가능해야 한다. 운영 중 preStop이 발동했는데 종료를 철회해야 할 때가 있다. POST /reopen 은 advance(e"
    },
    {
      "title": "W3. IGW 커스텀 Deployment 설계 — K8s 매니페스트 구조",
      "desc": "홈랩 graceful termination 시리즈 W3. IGW를 Helm 표준 대신 커스텀 Deployment로 구성해 hc 사이드카·volume·probe·preStop·Service NodePort 설계를 manifest 레벨에서 직접 제어한다. 결론(붙잡을 한 그림): 이 Po…",
      "url": "/public/istio/gt__src-w3-igw-deployment.html",
      "domain": "istio",
      "text": "istio graceful-termination k8s ingressgateway anti-affinity sds W3. IGW 커스텀 Deployment 설계 — K8s 매니페스트 구조 NOTE 홈랩 graceful termination 시리즈 W3. IGW를 Helm 표준 대신 커스텀 Deployment 로 구성해 hc 사이드카·volume·probe·preStop·Service NodePort 설계를 manifest 레벨에서 직접 제어한다. 결론(붙잡을 한 그림) : 이 Pod는 두 개의 독립된 종료 평면(plane)에 신호를 나눠 보내는 health surface 다. hc 컨테이너가 LB(HAProxy)에는 /health_check.html 로, K8s kubelet에는 /health · /live probe로 따로 종료를 알리고, istio-proxy는 그동안 in-flight를 drain한다. 이 평면 분리 하나가 manifest 전체(포트·probe·preStop·affinity)를 관통하고, current vs improved 의 in-flight 보존 여부도 이 분리를 얼마나 정교하게 다루느냐의 5가지 차이로 갈린다. 1. 배경: 왜 IGW 종료가 어려운가 — 두 평면 문제 IngressGateway(IGW)는 클러스터의 북쪽 가장자리 다. 외부 LB(HAProxy)가 그 앞에 서고, 안쪽에서는 K8s가 endpoint를 관리한다. IGW pod 하나를 내릴 때(롤링 업데이트·노드 drain) \"무중단\"이려면 두 주체가 서로 다른 시점에 이 pod를 빼야 한다. LB 평면(HAProxy) : 이 backend로 새 요청을 보내도 되는지 를 health check 폴링으로 판단. 빼야 하는 순간 = \"더 받지 마\". 가장 먼저 일어나야 한다. K8s 평면(kubelet/Service) : readiness probe로 Service endpoint 포함 여부를 결정. 너무 일찍 빠지면 LB가 아직 보내는 요청이 갈 곳을 잃는다. 이 둘이 같은 신호를 공유하면 종료가 깨진다. LB가 backend를 빼기도 전에 K8s가 endpoint를 먼저 제거하면, LB는 살아있다고 믿고 보낸 요청이 빈 endpoint로 떨어진다(in-flight 유실). 반대로 LB가 늦게 빼면 drain 중인 Envoy로 새 요청이 계속 들어온다. 무중단의 본질은 \"LB가 빼는 시점\"과 \"K8s가 빼는 시점\"을 의도적으로 어긋나게(stagger) 만드는 것 이고, 그러려면 두 평면에 물리적으로 다른 신호 경로 를 줘야 한다. 표준 Helm ingressgateway 로는 이 신호 경로를 실험적으로 빠르게 바꾸기 어렵다. probe path·preStop·env 조합을 실험마다 갈아끼우려면 values.gateways.istio-ingressgateway.podAnnotations 나 additionalContainers overlay 계층을 거쳐야 하는데, 이 간접 계층이 학습을 흐린다. 커스텀 Deployment는 모든 설계 결정을 YAML 한 파일에 노출 해 실험 의도가 코드에 그대로 드러나고, current↔improved 변경점 5개가 diff로 즉시 보인다. ℹ 사내(Helm) 적용은 별도 프로덕션처럼 Istio가 Helm으로 운영 중이면 표준 IGW Deployment에 hc를 더하는 현실적 방법은 values.gateways.istio-ingressgateway.additionalContainers[0] 에 hc 스펙을, extraVolumes / extraVolumeMounts 로 shared socket 마운트를 추가하는 것이다. upgrade 시 additionalContainers 가 의도치 않게 초기화될 수 있으므로 values를 GitOps로 관리하고 upgrade 후 pod 재기동을 확인 해야 한다. 상세 사내 적용 절차는 W6. 프로덕션 적용 참조. 2. 아키텍처: 평면 분리가 manifest를 관통하는 방식 멘탈모델 앵커 : hc 컨테이너의 세 endpoint 는 곧 세 소비자 이고, 소비자가 다르다는 것이 \"두 평면을 분리한다\"의 구현이다. 한 컨테이너가 같은 상태(FSM)를 세 갈래로 응답하되, 누가 폴링하느냐에 따라 다른 종료 시점을 트리거 한다. endpoint (hc, :18180) 소비자 (평면) 답하는 질문 종료 시 역할 /health_check.html HAProxy (LB 평면) \"이 backend로 새 요청 보내도 돼?\" 가장 먼저 503 → LB가 backend 제거 /health K8s kubelet (readiness) \"Service endpoint에 넣어도 돼?\" 나중에 실패 → endpoint 제거 /live K8s kubelet (liveness) \"이 컨테이너 재시작해야 돼?\" 종료 중엔 건드리지 않음 이 표가 핵심이다. LB 평면과 K8s 평면이 같은 hc 컨테이너의 다른 path를 폴링 하므로, FSM이 /health_check.html 만 먼저 503으로 떨구고 /health 는 잠시 더 200으로 유지하면, LB가 먼저 빠지고 K8s endpoint는 나중에 빠지는 시점 어긋남 이 자연스럽게 만들어진다. istio-proxy는 이 신호들과 무관하게 자기 listener를 drain한다 — Envoy의 drain은 hc FSM이 아니라 Envoy 자신의 종료 타이머가 몰고 간다. FSM( OPEN/DRAINING/CLOSING/CLOSED/FAULT ·신규 POST /reopen ) 상세는 W2. hc FSM 참조. 그림 1. IGW Pod = istio-proxy(Envoy router) + hc 사이드카. HAProxy는 /health_check.html을 poll, kubelet은 /health·/live를 검사. hc가 두 검사 경로를 분리 제어. istio-proxy 쪽 포트는 평면이 아니라 Envoy 자체 표면 임에 유의: :8080 은 실제 트래픽, :15021 은 status(Envoy의 readiness /healthz/ready ), :15090 은 prometheus. runAsUser 1337"
    },
    {
      "title": "W5. 테스트 시나리오 4종 설계 의도 + artifacts 해석",
      "desc": "graceful termination을 \"증명\"하려면 먼저 disruption을 측정 가능한 양으로 만드는 실험 설계가 필요하다. 이 문서는 그 설계를 다룬다 — 4개 시나리오(S1~S4)가 각각 어떤 단일 변수를 격리하고, 실행 후 artifacts(curl/pcap/timeline…",
      "url": "/public/istio/gt__src-w5-test-scenarios.html",
      "domain": "istio",
      "text": "istio graceful-termination testing envoy haproxy http2 W5. 테스트 시나리오 4종 설계 의도 + artifacts 해석 NOTE graceful termination을 \"증명\"하려면 먼저 disruption을 측정 가능한 양으로 만드는 실험 설계 가 필요하다. 이 문서는 그 설계를 다룬다 — 4개 시나리오(S1~S4)가 각각 어떤 단일 변수를 격리하고, 실행 후 artifacts(curl/pcap/timeline)를 어떻게 읽는지. 결론: 측정값을 믿으려면 ① 라우팅 고정(replicas=1) 과 ② 프로토콜별 RST 가면(HTTP/2 exit 92 vs HTTP/1.1 exit 18/502)을 모두 집계 , 이 두 통제가 선행돼야 한다. 대상 : graceful termination 시리즈 W5. 선행 : W1 big-picture(전체 체인), W2 hc FSM. 각도 : 스크립트의 왜 (설계 의도) + artifacts 읽는 법 . 대응 코드 워크스루 : W5 tests walkthrough (스크립트 01~05 라인별 해설). FSM 명명 : OPEN/DRAINING/CLOSING/CLOSED/FAULT , 신규 POST /reopen . 1. 배경 — 왜 \"그냥 테스트\"가 아니라 \"실험 설계\"인가 graceful termination이 동작하는지 보려면 단순히 \"요청 던지고 끊기나 본다\"로는 부족하다. 끊김이 보이면 그게 graceful 메커니즘의 실패 인지, 아니면 측정 환경이 만든 artifact 인지 구분할 수 없기 때문이다. 두 가지가 실제 거동을 가린다. 라우팅이 분산되면 한 pod을 죽여도 traffic이 다른 pod으로 새서 \"끊김 0\"이 나온다 — 이건 graceful이 잘된 게 아니라 그 pod을 한 번도 안 때린 것 이다. 하나의 disruption이 프로토콜에 따라 다른 얼굴 을 한다. 같은 TCP RST가 HTTP/1.1에선 502로, HTTP/2에선 connection-level exit으로 나타난다. 5xx만 세면 후자를 놓쳐 \"끊김 없음\"으로 오판한다. 따라서 이 시리즈의 핵심 질문은 \"drain이 되나?\"가 아니라 \"내가 보는 숫자가 진짜 거동인가, 측정 artifact인가?\" 다. 4개 시나리오는 그 신뢰성을 확보하려고 각자 변수 하나만 흔들고 나머지를 고정한다. (전체 종료 체인 — preStop hc → HAProxy DOWN → Envoy drain → grace period — 의 메커니즘은 W1 big-picture , W2 hc FSM 참조. 본 문서는 그 위에서 어떻게 측정하나 를 다룬다.) 2. 멘탈모델 앵커 — \"단 하나의 disruption을 측정 가능하게 만든다\" 머릿속에 둘 하나의 그림: 모든 시나리오는 disruption 하나를 격리·관측 가능하게 만드는 실험이다. 이 앵커에서 두 축이 따라 나온다. 측정 가능한 disruption ▲ ┌───────────────┴───────────────┐ 축1: 라우팅 고정 축2: 집계 완전성 (replicas=1) (5xx + conn err) \"그 pod에 귀속\" \"RST 가면 모두 카운트\" │ │ 분산되면 traffic이 같은 RST가 HTTP/2는 샌다 -> 끊김 0 오판 exit92, HTTP/1.1은 502 축1 (라우팅 고정) : replicas=1이어야 disruption을 특정 pod의 종료 에 귀속시킬 수 있다. multi-replica + roundrobin이면 죽인 pod 대신 살아있는 pod이 응답해 \"끊김 없음\"이 측정 artifact로 찍힌다. 축2 (집계 완전성) : 단일 TCP RST가 프로토콜·시나리오에 따라 5xx에 잡히기도(S1) 빠지기도(S4) 한다. 그래서 disruption 집계는 5xx와 connection error 양쪽을 모두 세야 한다. 이 두 축을 통제하지 못하면 실험은 진짜 거동이 아니라 측정 artifact를 본다. §3~§4가 각 축을 메커니즘 수준으로 푼다. 2.1 변수 격리 매트릭스 각 시나리오는 관심 변수 하나만 바꾸고 나머지는 상수 고정하거나 의도적으로 배제한다. # 이름 잡는 변수 잡지 못하는 변수 S1 01-baseline-long-request.sh 단일 in-flight HTTP 요청 RST 시점 (current vs improved) 다중 동시 conn 거동, streaming chunk 손실 S2 02-improved-long-request.sh improved drain.sh active 폴링 FSM 전이 타이밍 LB 동작 (improved에서 backend는 60s 내내 UP) S3 03-continuous-traffic.sh continuous load + rollout 시 disruption rate (5xx + conn err) 단일 long-request, streaming chunk 손실 S4 04-rst-capture.sh streaming chunk 손실 시점 + TCP RST 패킷 가시화 non-streaming (S1과 상호 보완) 설계 원칙 (왜 S1/S2를 쌍으로) : S1·S2는 동일한 /sleep?seconds=60 을 보내되 S1은 expected_failure=true (baseline: 끊김 확인), S2는 expected_failure=false (validation: 안 끊김 확인). 입력을 똑같이 두고 current vs improved drain.sh 만 바꾸므로, 결과의 차이가 곧 개선의 효과다 — 변수 하나만 다르게 만드는 격리 설계의 교과서적 적용이다. 3. 축1 메커니즘 — replicas=1이 왜 전제인가 S1·S2·S4가 replicas=1을 강제하는 이유는 HAProxy balance roundrobin 의 traffic isolation 효과 때문이다. replica가 2개면 죽인 pod의 끊김이 살아있는 pod의 정상 응답에 묻힌다. replicas=2 함정 (S1 1·2차 시도): curl -> HAProxy(roundrobin) -> worker1(pod-A) [삭제 대상] -> worker2"
    },
    {
      "title": "W6. 프로덕션 적용 가이드 — 실험 결론 → 온프렘 매핑",
      "desc": "머릿속 한 장: 서비스 팀은 LB를 직접 명령할 권한이 없지만 health 응답(200↔503)은 제어할 수 있다 — hc 사이드카 + graceful-drain.sh가 Envoy의 rq_active==0이 될 때까지 health 200을 붙잡아 LB의 backend 제거 시점을 간접…",
      "url": "/public/istio/gt__src-w6-production-apply.html",
      "domain": "istio",
      "text": "istio graceful-termination citrix-netscaler production-rollout observability terminationgraceperiod W6. 프로덕션 적용 가이드 — 실험 결론 → 온프렘 매핑 NOTE 머릿속 한 장 : 서비스 팀은 LB를 직접 명령할 권한이 없지만 health 응답(200↔503)은 제어할 수 있다 — hc 사이드카 + graceful-drain.sh 가 Envoy의 rq_active==0 이 될 때까지 health 200을 붙잡아 LB의 backend 제거 시점을 간접 제어 하는 것이 이 시리즈 전체의 골격이다. W6(종합편)은 graceful termination 시리즈 W1~W5의 실험 결론을 사내 온프렘(Citrix NetScaler LB + 워커 containerd)으로 매핑하고, 적용을 막는 운영 디테일을 정면 돌파한다. 결론: 사내 LB가 Citrix downStateFlush ENABLED 면 홈랩 HAProxy 결론이 그대로 직접 적용 되고, 남는 유일한 미지수는 사내 long request p99 분포 다. 적용 절차·체크리스트 정본은 graceful termination runbook , 6 events 정의 정본은 W1 big picture . 시리즈 위치 : W6(종합). W1~W5 실험 결론을 사내 온프렘 환경으로 매핑하고 미적용 운영 디테일을 정면 돌파. FSM 명명 : OPEN/DRAINING/CLOSING/CLOSED/FAULT . 신규 POST /reopen . 1. 배경 — 왜 이 문제가 존재하는가 풀어야 할 문제 한 줄 : pod 하나가 종료될 때(배포·스케일다운·노드 드레인) 이미 들어와 처리 중인 요청을 끊지 않으면서 그 pod를 트래픽 경로에서 빼는 것. 이게 왜 어려운가. pod 종료는 두 주체가 동시에 비동기로 움직인다. K8s 쪽 : kubelet이 SIGTERM 을 보내고 terminationGracePeriodSeconds 후 SIGKILL . 동시에 EndpointSlice에서 pod를 빼 service 경로를 끊는다. 외부 LB 쪽 : LB는 K8s를 모른다. 자기 health check(주기적 probe)가 실패해야 비로소 backend를 pool에서 뺀다. 두 타임라인이 어긋나면 in-flight 요청이 끊긴다. 가장 흔한 사고는 LB가 backend를 빼는 순간 아직 처리 중인 요청이 남아 있는 것 — LB가 downStateFlush / shutdown-sessions 로 기존 세션까지 RST하면 그 요청들이 502/connection error로 죽는다. 여기서 핵심 제약이 등장한다. 서비스 팀은 LB(Citrix/HAProxy)를 직접 명령할 권한이 없다. \"지금 이 backend 빼\"라고 LB에 명령할 API가 없다. 가진 건 단 하나 — LB가 주기적으로 찌르는 health endpoint의 응답을 200으로 줄지 503으로 줄지 결정할 권한 이다. 이 좁은 제어 표면만으로 LB의 backend 제거 타이밍을 원하는 순간으로 미루는 것이 이 시리즈 전체가 푸는 문제다. 선행 개념 3개 (없으면 아래가 안 풀린다): - 6 events : pod 종료 시 일어나야 하는 6개 사건(drain start, active=0, health fail, readiness fail, LB down, RST). 정의 정본은 W1 . 이들의 순서 가 무결성을 결정한다. - Envoy drain : Envoy admin :15000/drain_listeners?graceful&skip_exit 로 새 conn은 거부하되 기존 in-flight는 유지하는 상태. - rq_active : Envoy /stats 의 downstream_rq_active + upstream_rq_active . \"지금 이 pod를 통과 중인 요청 수\". 이게 0이면 끊을 게 없다는 뜻. 대상 환경 : 프로덕션 온프렘 (Citrix NetScaler LB, 워커 containerd, Helm 배포 IGW). | 대상 독자 : 홈랩 결론을 프로덕션에 옮기려는 SRE. | 범위 : 매핑·적용 결정·미적용 디테일. 실험 자체는 W1~W5. | 선행 : 위 3개 + HAProxy on-marked-down . 2. 입증된 메커니즘 — 간접 제어가 왜 통하는가 (W1~W5 종합) 이 문서의 앵커 한 문장 : LB는 health 응답(200 vs 503) 하나로만 backend pool membership을 정한다. 그러므로 \"active=0이 될 때까지 health 200을 붙잡는다\"는 단 한 줄의 정책 이 LB의 backend 제거를 in-flight 종료 후로 미룬다. 이 한 줄에서 모든 게 따라 나온다. 메커니즘을 인과로 펼치면: drain.sh가 Envoy drain을 먼저 켠다 ( drain_listeners?graceful&skip_exit ). 이 순간부터 새 TCP conn은 거부 되지만 기존 in-flight는 그대로 산다. 즉 active 수는 단조 감소만 한다(새 유입 차단). drain.sh가 rq_active 를 폴링 한다. 새 유입이 없으니 active는 0으로 수렴한다. 0을 볼 때까지 /health_check.html 은 200 유지 — LB는 아무 일 없다고 본다. active==0을 확인한 뒤에야 health를 503으로 flip한다. 이제 LB가 backend를 DOWN 마킹하고 downStateFlush 로 세션을 flush해도 끊을 in-flight가 0이다 . RST는 발사되지만 대상이 없다. 핵심 통찰은 순서를 뒤집는 것 이다. 순진한 종료는 \"health 죽이고 → LB가 뺀다 → 그제야 in-flight 정리\"라 RST가 살아있는 요청을 친다. improved는 \"in-flight 먼저 비우고(active=0) → 그다음 health 죽인다\"로 순서를 반대로 깔아, LB가 backend를 빼는 시점엔 이미 끊을 게 없게 만든다. 왜 DRAIN_TIMEOUT 은 무결성이 아닌가 : active=0 폴링이 무결성을 보장 한다(0을 봐야 health를 죽"
    },
    {
      "title": "Istio Egress Gateway 도입 가이드 — Passthrough vs mTLS Passthrough",
      "desc": "대상: 플랫폼/인프라/보안 담당자 환경: on-prem Kubernetes, Istio 1.30.0 (sidecar mode), PCI-DSS 준수 IDC 결정 요약: mTLS Passthrough(ISTIO_MUTUAL) 패턴 채택. proxy 도입에 따른 TCP 운영 이슈는 방식과…",
      "url": "/public/istio/gw__guide-egress-adoption-passthrough-vs-mtls.html",
      "domain": "istio",
      "text": "istio egress mtls passthrough adoption decision tcp operations Istio Egress Gateway 도입 가이드 — Passthrough vs mTLS Passthrough 대상: 플랫폼/인프라/보안 담당자 환경: on-prem Kubernetes, Istio 1.30.0 (sidecar mode), PCI-DSS 준수 IDC 결정 요약: mTLS Passthrough(ISTIO_MUTUAL) 패턴 채택. proxy 도입에 따른 TCP 운영 이슈는 방식과 무관한 공통 비용이며 별도 완화 설정으로 대응함. 1. 배경 및 목표 모든 대외(외부 기관) 통신을 통제된 단일 경로(egress gateway)로 수렴 필요. 보안 요건: ① 외부행 경로 강제, ② 워크로드 단위 최소권한(어떤 앱이 어떤 목적지로 나가는지 차등 통제), ③ 주체 식별 가능한 감사 추적, ④ mesh 외부 클라이언트의 gateway 사용 차단. 제약: 앱은 HTTPS를 직접 발신(종단간 TLS 유지 필요). 앱 코드/프로토콜 변경 불가. [app + sidecar] ---> [egress gateway] ---> IDC FW ---> 대외기관 API (HTTPS 발신) (유일한 출구) (gw 대역만 허용) 2. 두 방안 비교 두 방식 모두 앱↔외부 서버의 종단간 TLS는 유지 됨(gateway는 inner TLS를 복호화하지 않음). 차이는 sidecar→gateway 구간을 mesh mTLS로 한 겹 더 감싸는지 여부. A. TLS Passthrough app --[원본 TLS]--> sidecar --[원본 TLS 그대로]--> gw --[원본 TLS]--> 외부 gw는 SNI만 보고 통과 (신원 정보 없음) B. mTLS Passthrough (ISTIO_MUTUAL) app --[원본 TLS]--> sidecar --[mesh mTLS[원본 TLS]]--> gw --[원본 TLS]--> 외부 gw가 outer만 종단 -> SPIFFE 신원 추출 항목 A. Passthrough B. mTLS Passthrough gateway에서 호출 주체 식별 불가 (pod IP뿐, IP는 휘발성) 가능 (SPIFFE: ns/sa 단위) AuthorizationPolicy 표현력 목적지(SNI)·포트만 주체(principal) × 목적지 워크로드별 차등 통제 불가 — gateway 도달 가능한 모든 pod가 allowlist 합집합 사용 가능 — \"app-a→A기관만\" 식 최소권한 mesh 외부 클라이언트 차단 불가 (gateway Svc는 클러스터 전역 도달 가능) TLS handshake 단계에서 거부 (mesh CA 인증서 요구) 감사 로그 src pod IP SPIFFE ID ( DOWNSTREAM_PEER_URI_SAN ) 사고 시 즉시 회수 목적지 단위 전체 차단만 해당 SA 정책 1건 삭제 설정 복잡성 낮음 (DR 불필요, VS 양쪽 tls 라우트) 높음 (Gateway hosts ↔ DR sni ↔ VS 3자 정합) 디버깅 와이어에 원본 TLS 한 겹 mesh 구간만 TLS 2겹 (gw→외부는 단일 TLS로 동일) 연결 수립 레이턴시 기준 신규 연결당 outer 핸드셰이크 1회 추가 CPU 기준 sidecar(암호화)·gw(복호화) 증가 학습 곡선 낮음 라우트 타입 규칙, 인증서 체계 등 학습 필요 3. 결정: mTLS Passthrough 채택 근거 Passthrough의 결손은 구조적이며 운영 중 메울 수 없음. 검문소(gateway)는 만들어지지만 \"누가\"를 판정할 재료가 없음. 한 목적지를 뚫는 순간 전사 모든 워크로드(비-mesh pod 포함)에 그 경로가 개방됨 — 최소권한 요건 미충족. 감사 시점에 pod IP → 워크로드 역추적은 IP 휘발성 때문에 신뢰 불가 — 주체 식별 요건 미충족. 추후 신원이 필요해지면 결국 ISTIO_MUTUAL 전환이 유일한 경로 → 지금의 구축 비용을 그 시점에 지불하게 됨. mTLS의 비용은 인지하고 감수함. 비용: 신규 연결 수립 레이턴시 증가(핸드셰이크 1회분), 암호화 CPU 부하, 초기 설정 복잡성과 학습 곡선, mesh 구간 TLS-in-TLS 디버깅. 감수 근거: 이 비용은 설계 시점에 1회 지불되고 템플릿에 동결 됨(목적지 추가 = 표준 1벌 복사). 반면 passthrough의 결손은 런타임에 상존하며 사고·심사 시점에 청구됨. 가시성과 트래픽 통제가 비용을 상회한다고 판단. 디버깅 부담의 실제 범위: 장애 다발 구간인 gw→외부는 단일 TLS로 passthrough와 동일. 이중 TLS는 mesh 내부 hop에 한정되며, 해당 구간 1차 진단 도구는 tcpdump가 아닌 access log / istioctl로 표준화(§7). 참고: 두 모드는 같은 gateway Deployment에서 포트 단위로 공존 가능(예: 8443=ISTIO_MUTUAL, 9443=PASSTHROUGH). 필요 시 목적지별 점진 전환 경로 존재. 4. 아키텍처 ns: app-namespace ns: istio-egress +------------------+ +-----------------------------------+ | app + sidecar | outer mTLS | egressgateway Deployment (3+) | | (sa: app-a) ----+--------------->| :8443 ISTIO_MUTUAL | +------------------+ Svc:443 | 1) tls_inspector: outer SNI로 | -> pod:8443 | filter chain 선택 | | 2) outer 종단 -> principal 추출 | | 3) AuthzPolicy(principal x sni) | | 4) tcp_proxy -> 외부:443 | +-----------------+-----------------+ | (inner TLS만, 단일 겹) v 전용 노드풀 -> IDC FW -> 대외기관 핵심 동작 규칙 (트러블슈팅의 기준) 규칙 내용 라우트 타입 = 종단 여부 미종단 hop(s"
    },
    {
      "title": "Egress 4-CRD 직관 — \"한 번의 curl = 두 hop\", 4개를 어떤 순서로 어떻게 채우나",
      "desc": "egress gateway 설정이 \"Gateway·VirtualService·ServiceEntry·DestinationRule을 왜 4개나, 어느 필드를 어디에\"로 막히는 이유는 멘탈모델 없이 필드부터 보기 때문이다. 이 문서는 거꾸로 간다 — 먼저 \"한 번의 외부 호출이 두 hop…",
      "url": "/public/istio/gw__guide-egress-crd-mental-model.html",
      "domain": "istio",
      "text": "istio egress gateway virtualservice serviceentry destinationrule mental-model how-to Egress 4-CRD 직관 — \"한 번의 curl = 두 hop\", 4개를 어떤 순서로 어떻게 채우나 NOTE egress gateway 설정이 \"Gateway·VirtualService·ServiceEntry·DestinationRule을 왜 4개나, 어느 필드를 어디에\"로 막히는 이유는 멘탈모델 없이 필드부터 보기 때문 이다. 이 문서는 거꾸로 간다 — 먼저 \"한 번의 외부 호출이 두 hop으로 쪼개지고, 4개 CRD는 각자 다른 hop의 다른 질문에 답하는 부품\"이라는 그림을 세운 뒤, 실제로 두 패턴(passthrough / outer-mTLS)을 어떤 순서로 어떻게 만들었는지 를 그때의 YAML과 함께 따라간다. 결론: 4개는 따로 노는 게 아니라 두 hop을 굴리기 위한 질문 4개 이고, 패턴 전환은 \"3개 델타\"일 뿐이다. 필드 단위 레퍼런스(=사전)는 Egress Gateway 정본 · HTTPS over mTLS 해부 , 검증 랩은 이중 gateway 가이드 . 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico), Istio 1.30.0 , Helm gateway chart 대상 독자: egress gateway config를 직접 만들어야 하는데 4개 CRD 관계가 안 잡히는 사람 선행 개념: sidecar 트래픽 캡처(15001 outbound), Envoy listener/cluster, mTLS·SNI 기초 다루는 것: ① 왜 egress·왜 4개 ② 멘탈모델·부품표 ③ 구성 순서대로 실제 YAML과 이유 ④ 필드 정렬 지도 ⑤ 단 하나의 비대칭(tls vs tcp) 1. 배경 — 왜 egress gateway이고, 왜 하필 CRD가 4개인가 mesh 안의 pod가 curl https://example.org 를 하면, sidecar가 outbound 트래픽(15001)을 가로챈다. 여기서 두 가지를 \"통제하고 싶다\"는 욕구가 egress gateway의 존재 이유다: 관측·정책의 단일 통로. 외부로 나가는 트래픽을 pod마다 제각각 내보내면 \"누가 어디로 나갔나\"를 한 곳에서 볼 수 없다. egress gateway는 모든 외부 호출을 한 노드(pod)로 모으는 깔때기 다 — 거기서 egress IP 고정, 로그·메트릭 집중, 정책 한 점 적용이 가능해진다. 신원 있는 외부 호출. \"어느 app이 이 외부 host로 나갔는가\"를 mesh mTLS(SPIFFE)로 증명하려면, sidecar→gateway 구간을 mTLS로 감싸고 gateway에서 그 신원을 검증해야 한다. 이게 outer-mTLS 패턴이다. 문제는, 이 \"통로\"를 만들려면 단 한 번의 외부 호출이 두 개의 구간으로 쪼개진다 는 점이다. app은 한 번 curl했지만 물리적으로는 app→gateway , gateway→외부 두 개의 hop이 생긴다. 두 hop을 각각 \"어디로 보낼지·어떻게 말 걸지·어느 문으로 받을지·이 host가 존재하긴 하는지\"로 설정해야 하니, 답해야 할 질문이 자연히 여러 개가 된다. Istio는 이 질문들을 관심사별로 다른 CRD에 분산 시켰다 — 그래서 4개다. 4개라서 어려운 게 아니라, 두 hop × 관심사 분리 의 결과가 4개일 뿐이다. 이 문서는 그 4개를 \"질문 4개\"로 되돌려 직관을 세운다. 같은 4-CRD 골격은 ingress가 아니라 외부로 나가는 egress 시나리오 전용 멘탈모델이다. 더 넓은 운영 맥락은 egress operations , 라우팅 스코핑은 VS 스코핑 . 2. 모든 걸 푸는 한 문장 (멘탈모델 anchor) curl https://example.org 한 번은 Istio 안에서 \"두 개의 hop\"으로 쪼개진다. CRD 4개는 각자 다른 hop의 다른 질문에 답하는 부품일 뿐이다. 이 한 문장만 머리에 박으면 나머지는 다 따라온다. 물리적으로 일어나는 일은 이게 전부다: 그림 1. 두 hop 멘탈모델. 트래픽은 app → egress gateway → 외부로 두 번 hop한다. VS leg-1·DR는 hop1(app이 나가는 길)을, Gateway·VS leg-2는 hop2(gateway가 받아 내보내는 길)를 설정한다 — ServiceEntry는 \"그 외부 호스트가 존재한다\"는 전제일 뿐 어느 hop에도 묶이지 않는다. ASCII로 같은 그림 — \"두 hop\"의 골격만 먼저: app ───hop1───> egress gateway pod ───hop2───> example.org (sidecar가 (listener가 (외부 서버) 나가는 길) 받는 자리) 나머지 모든 설정은 \"이 두 hop을 어떻게 동작시킬까\"를 채우는 것이다. 리소스가 4개인 이유는 두 hop을 굴리려면 답해야 할 질문이 4개 이기 때문이다 — §1에서 본 \"관심사 분리\"가 여기서 4개의 부품으로 떨어진다. 3. 부품표 — 각 CRD = 하나의 질문에 대한 답 답해야 할 질문 답 = CRD 한 줄 직관 \"이 외부 host가 존재하긴 하나? 메시가 알아도 되나?\" ServiceEntry 메시의 주소록에 등록 . 없으면 unknown(REGISTRY_ONLY면 차단). \"트래픽이 뜨면 어디로?(hop1) / gateway 도착 뒤 어디로?(hop2)\" VirtualService 라우팅 규칙 . 4개 중 유일하게 두 hop에 다 걸친다 → route 블록 2개. \"sidecar가 gateway에게 어떻게 말 거나? 평문? mTLS로 감싸서?\" DestinationRule hop1 목적지(gateway)에게 말 거는 방식 . \"mTLS로 감싸라\"를 여기서 지정. \"gateway는 어느 문(listener) 을 열어 hop1을 받나? cert를 요구하나?\" Gateway egress pod에 포트를 열고 보안 모드 설정(ISTIO_MUTUAL = cert 요구·검증). 이 표가 핵심이다. \"필드 4묶음\"이 아니라 \"질문 4개\" 로 보면, 작성은 질"
    },
    {
      "title": "GSLB 뒤 DNS resolution 재현 랩 — \"세션이 끊길 수도 있다\"를 눈으로 보기",
      "desc": "ServiceEntry resolution 정본은 DNS(STRICT_DNS)와 DNS_ROUND_ROBIN(LOGICAL_DNS)의 차이를 이론으로 정리했다. 이 문서는 그 이론을 자기완결형 랩으로 라이브 재현한다 — 공유 인프라(egress gateway·cluster CoreDN…",
      "url": "/public/istio/gw__guide-egress-dns-gslb-repro-lab.html",
      "domain": "istio",
      "text": "istio egress serviceentry dns gslb resolution envoy mental-model GSLB 뒤 DNS resolution 재현 랩 — \"세션이 끊길 수도 있다\"를 눈으로 보기 NOTE ServiceEntry resolution 정본 은 DNS (STRICT_DNS)와 DNS_ROUND_ROBIN (LOGICAL_DNS)의 차이를 이론으로 정리했다. 이 문서는 그 이론을 자기완결형 랩으로 라이브 재현 한다 — 공유 인프라(egress gateway·cluster CoreDNS)를 한 줄도 안 건드리고, 사설 GSLB 시뮬레이터 하나로 \"IP가 매번 바뀌는 도메인\"을 통제해 ① 기존 세션이 끊기는 순간 ② 죽은 IP로 트래픽이 새는 순간 ③ LOGICAL_DNS가 그 대가로 stale IP를 뒤늦게 붙잡고 있는 순간을 각각 만든다. 결론 한 문장: resolution 필드는 \"GSLB의 변덕을 Envoy가 펼쳐서 매번 반영하느냐(STRICT), 접어서 기존 연결은 안 건드리느냐(LOGICAL)\"의 선택이고, 그 선택이 곧 세션 생존 여부다. 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico), Istio 1.30.0 대상 독자: STRICT_DNS/LOGICAL_DNS 차이를 이론으로는 아는데, \"진짜 세션이 끊기는 장면\"을 직접 보고 싶은 사람 선행 개념: ServiceEntry resolution 정본 (STRICT_DNS/LOGICAL_DNS, DNS refresh ≠ health check), TLS origination vs passthrough, outlier detection 다루는 것: ① 왜 자체 GSLB 시뮬레이터인가 ② 멘탈모델·부품표 ③ 실제 구성(전체 YAML + 이유) ④ 3가지 재현 모드 ⑤ 함정 1. 배경 — 이론은 있는데 왜 \"재현\"이 따로 필요한가 기존 runbook은 resolution 필드가 Envoy cluster type을 가른다는 것, DNS refresh가 liveness가 아니라는 것, GSLB 뒤엔 LOGICAL_DNS가 낫다는 것까지 전부 정리했다. 그런데 이 문서들은 전부 정적 조사 다 — \"endpoint가 몇 개 잡히는가\"는 dig+proxy-config로 스냅샷을 찍으면 끝나지만, \"GSLB가 IP를 바꾸는 그 순간 이미 맺혀 있던 연결이 끊기는가/유지되는가\"는 시간축이 있는 이벤트 라 스냅샷으로는 못 본다. 이걸 보려면: GSLB를 내가 통제 해야 한다 — 실제 GSLB는 언제 IP를 바꿀지 내가 못 정한다. 롱커넥션이 있어야 한다 — 매 요청 새 커넥션이면 애초에 \"끊길 기존 세션\"이 없어 STRICT든 LOGICAL이든 차이가 안 보인다. 공유 인프라를 안 건드려야 한다 — egress gateway나 cluster CoreDNS를 실험용으로 고치면 다른 시나리오 (10/20/30-*)에 영향이 번진다. 이 세 요구를 동시에 만족하려고 scenarios/50-dns-resolution/ 에 사설 GSLB 시뮬레이터 + keepalive 부하생성기 로 된 독립 ns( dns-lab )를 만들었다. 아래는 그 구성을 어떻게, 왜 그렇게 만들었는지다. 2. 멘탈모델 한 문장 GSLB는 \"같은 도메인, 매번 다른 IP\"를 주는 존재일 뿐이다. resolution 필드는 그 변화를 Envoy가 \"펼쳐서 매번 반영\"(STRICT_DNS)할지 \"접어서 기존 연결은 안 건드리고 신규만 반영\"(LOGICAL_DNS)할지를 정하고, 그 선택이 곧 \"GSLB가 IP를 바꾸는 순간 내 세션이 죽느냐 사느냐\"다. resolution: DNS -> Envoy STRICT_DNS host set = A record 전체. DNS refresh 시 사라진 IP의 host를 cluster에서 제거 -> 그 host로 물려있던 커넥션을 정리(drain) => 기존 세션 끊김 [Mode 1] -> refresh 전 \"죽었지만 아직 목록에 있는\" IP를 LB가 고르면 connect 실패 => 유실 [Mode 2] resolution: DNS_ROUND_ROBIN -> Envoy LOGICAL_DNS host = 논리적 endpoint 1개. 새 커넥션만 최신 DNS로 연결 기존 커넥션은 \"절대 건드리지 않음\" => 기존 세션 유지 [Mode 1 대조] 대가: 물고 있는 IP가 나중에 죽어도 Envoy는 DNS로는 못 알아챔 [Mode 3] 세 대괄호 표시(Mode 1/2/3)가 이 랩이 실제로 만드는 세 장면이다. 이 한 문장 + 세 장면만 머리에 있으면 나머지 구성은 전부 \"이 장면을 어떻게 세팅하나\"의 디테일이다. 3. 부품표 — 각 조각 = 그게 없으면 안 되는 이유 답해야 할 질문 부품 왜 이게 필요한가 \"GSLB를 어떻게 내 손으로 흉내내나?\" lab-dns (CoreDNS + writer 사이드카) gslb.lab.internal 1개 이름을 authoritative(ttl 5s)로 응답. writer 가 A record를 실시간 재작성 = \"GSLB가 IP를 바꾸는 순간\"을 내가 만든 버튼 \"그 도메인을 누가 질의하나?\" client dnsConfig sidecar Envoy(c-ares)가 cluster DNS 대신 lab-dns를 보게 강제 — egress gateway·cluster CoreDNS 무변경으로 GSLB를 격리 \"펼치나 접나?\" ServiceEntry.resolution DNS =STRICT_DNS(펼침) / DNS_ROUND_ROBIN =LOGICAL_DNS(접음) — 이 랩이 대조하는 유일한 독립변수 \"drain이 눈에 보이는 층(L7)에서 일어나게 하려면?\" VS(80→443) + DR(TLS origination) client가 평문 HTTP로 부르고 sidecar가 TLS로 승격해야 Envoy가 upstream을 HTTP conn pool(L7)로 관리 → host 제거 시 GOAWAY/close가 관측 가능 해짐. https 직접 호출이면 SNI passthrough(L4)라 drain이 안 보임 \"죽은 IP를 어"
    },
    {
      "title": "이중 egress gateway — passthrough와 mTLS를 별도 ns·별도 pod로 나란히",
      "desc": "단일 egress gateway를 멀티-gateway 토폴로지로 일반화하면서, 두 egress 패턴(① TLS PASSTHROUGH, ② outer 메시 mTLS + inner 앱 TLS)을 서로 다른 namespace의 서로 다른 gateway pod로 동시에 띄워 직접 비교한 ho…",
      "url": "/public/istio/gw__guide-egress-dual-gateway.html",
      "domain": "istio",
      "text": "istio egress gateway multi-gateway tls-passthrough istio-mutual namespace-isolation 이중 egress gateway — passthrough와 mTLS를 별도 ns·별도 pod로 나란히 NOTE 단일 egress gateway를 멀티-gateway 토폴로지로 일반화 하면서, 두 egress 패턴(① TLS PASSTHROUGH, ② outer 메시 mTLS + inner 앱 TLS)을 서로 다른 namespace의 서로 다른 gateway pod 로 동시에 띄워 직접 비교한 homelab 검증 랩. 결론: 진짜 격리는 물리 분리(pod/ns/label)와 논리 스코핑( exportTo / sourceLabels )을 함께 갖춰야 성립 한다 — 둘 중 하나라도 빠지면 \"분리된 것처럼 보이는\" 상태에 그친다. 본 문서는 그 둘을 어떻게 맞물리는지에 집중한다(패턴 자체의 구조·운영은 기존 문서로 링크). 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico, 3노드), Istio 1.30.0 (Helm gateway chart) 대상 독자: 단일 egress gateway는 띄워봤고, 이제 패턴/tier별로 gateway를 쪼개려는 SRE/DevOps 범위: \"왜 쪼개나\" → 격리의 두 축(물리·논리) → 경로별 Istio 객체 → 트래픽·Envoy 검증 → 실측 함정 2건 선행 개념: egress route 스코핑 (멀티-gateway의 전제) · egress CRD 멘탈모델 1. 배경 — 왜 egress gateway를 둘로 쪼개나 단일 egress gateway면 메시의 모든 외부 호출이 한 pod를 통과한다. 처음엔 단순해서 좋지만, 운영이 커지면 그 한 pod가 여러 책임을 한 몸에 떠안는 구조가 문제가 된다: 장애 격리 부재 — 한 tier의 graceful drain·재시작·crashloop이 모든 외부 트래픽을 흔든다. SNAT 포트 고갈 공유 — 한 워크로드가 source port를 다 써버리면 무관한 다른 트래픽도 외부 연결을 못 연다. 정책 충돌 — passthrough(SNI만 보고 통과)와 mTLS 종단(client cert 검증)은 listener의 TLS 동작이 정반대 다. 한 pod에 얹으면 포트를 갈라야 하고, 한쪽 정책 변경이 다른 쪽 listener를 건드릴 위험이 상존한다. 그래서 패턴/tier별로 gateway pod를 분리한다. 그런데 \"Deployment를 둘로 나눴다\"만으로 격리가 끝나지 않는다는 게 이 문서의 핵심 교훈이다. Istio에서 한 메시 안의 리소스는 기본적으로 전역 가시성 을 갖기 때문에, 물리적으로 떨어진 두 pod라도 잘못된 전역 리소스 하나가 양쪽 config를 동시에 망가뜨릴 수 있다(함정 ②). 진짜 격리에는 두 번째 축 이 필요하다. 선행 개념인 egress route 스코핑 을 먼저 잡아두면 이 문서의 exportTo / client-VS·gateway-VS 분리가 자연스럽게 읽힌다. 멀티-gateway는 그 스코핑 규칙을 gateway 개수만큼 곱한 것에 가깝다. 2. 멘탈모델 — 격리는 두 축의 곱(物理 × 論理) 머릿속에 담을 한 장면: 두 gateway는 pod·ns·label로 물리적으로 분리돼 있지만 여전히 하나의 메시 다. 그래서 격리는 다음 두 축이 둘 다 채워질 때만 성립한다 — 하나라도 비면 \"분리된 척\"에 그친다. isolation = PHYSICAL × LOGICAL (분리) (가시성·적용대상) PHYSICAL axis LOGICAL axis +--------------------+ +---------------------------+ | label (selector) | | exportTo (누가 이 리소스를 | | Gateway가 자기 | | 볼 수 있나) | | pod만 잡음 | | sourceLabels (어떤 client에 | | ns (소유권 경계) | | 라우트가 붙나) | | pod (장애·SNAT 경계)| +---------------------------+ +--------------------+ | | v v \"딴 pod의 변경이 \"전역 리소스 하나가 내 listener를 안 건드림\" 모든 gateway를 못 얼리게\" PHYSICAL — 각 ns의 Gateway 리소스가 자기 label만 selector로 잡으므로 한쪽 변경이 다른 pod로 새지 않는다. ns는 소유권/RBAC 경계, pod는 장애·SNAT 경계. PILOT_SCOPE_GATEWAY_TO_NAMESPACE=true 환경에서도 Gateway 리소스와 workload가 같은 ns라 안전하다. LOGICAL — exportTo: [\".\"] 로 SE/VS의 가시성을 자기 ns에 닫아, 전역 push에 끼어 남의 config를 얼리지 않게 한다 (함정 ②). sourceLabels 는 \"어떤 client가 어느 gateway로 가나\"를 명시한다(현재 랩은 host로 분기, §다음 작업 참고). 이 한 장면에서 나머지가 따라 나온다. 아래 토폴로지가 그 물리 축을 그림으로 보인 것이다 — 같은 client( sleep )가 두 외부 host를 호출하지만, host에 따라 다른 ns의 다른 gateway pod 로 갈린다. 그림 1. host별로 갈리는 dual egress — example.org는 egress-pt가 SNI만 보고 복호화 없이 PASSTHROUGH, www.wikipedia.org는 egress-mtls가 OUTER mesh mTLS를 종단·검증하고 INNER 앱 TLS만 tcp_proxy로 흘려보낸다. 왼쪽 경로는 gateway가 복호화하지 않는 passthrough, 오른쪽은 gateway가 outer mesh mTLS를 종단 하고 inner 앱 TLS만 흘려보내는 패턴이다. inner 앱 TLS는 두 경로 모두 client↔external 간 end-to-end로 유지 된다 — passthrough는 애초에 건드리지 않고, mTLS 경로는 outer mesh 레이어"
    },
    {
      "title": "Istio 1.30 Egress Gateway — 외부 HTTPS 통신 설치·구성·테스트 가이드",
      "desc": "homelab(kubespray bare-metal, k8s v1.30.6, CNI Calico, Istio 1.30.0)에서 egress gateway를 Helm으로 구성하고, 앱이 직접 https://를 호출하는 TLS Passthrough(SNI 라우팅) 시나리오를 끝까지 구성·…",
      "url": "/public/istio/gw__guide-egress-gateway-https.html",
      "domain": "istio",
      "text": "istio egress gateway tls-passthrough sni Istio 1.30 Egress Gateway — 외부 HTTPS 통신 설치·구성·테스트 가이드 NOTE homelab(kubespray bare-metal, k8s v1.30.6, CNI Calico , Istio 1.30.0 )에서 egress gateway를 Helm으로 구성하고, 앱이 직접 https:// 를 호출하는 TLS Passthrough(SNI 라우팅) 시나리오를 끝까지 구성·검증한다. 머릿속에 담을 한 장의 그림: 메시의 모든 외부 송신을 egress gateway라는 단일 choke point로 모으되, TLS는 끝까지 암호화된 채로 두고 gateway는 SNI만 보고 라우팅한다(2-홉: mesh→gateway, gateway→external). 핵심 결론: egress의 \"완료\"는 200이 아니라 트래픽이 egress gateway를 실제로 경유했음을 증명 하는 것이며, 호출 결과 / proxy-config / access log 세 가지를 교차 확인한다. 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico, 3노드), Istio 1.30.0 (Helm chart) 범위: egress gateway Helm 설치/구성 → 외부 HTTPS 테스트 앱 구성 → 필요한 Istio 객체 → 테스트·검증 절차 난이도 전제: Istio sidecar/Gateway/VirtualService 기본 개념을 알고 있음. egress 특유의 동작에 초점. 0. 배경 지식 — 왜 egress gateway를 거치게 만드는가 기본 상태의 메시는 외부 송신을 막지 않는다 . sidecar의 outboundTrafficPolicy 가 ALLOW_ANY 이면 각 워크로드의 Envoy가 모르는 목적지를 PassthroughCluster 로 그냥 흘려보낸다 — pod마다 인터넷으로 나가는 구멍이 하나씩 생기는 셈이다. 이게 운영에서 곤란한 이유는 세 가지다: 방화벽 화이트리스트가 불가능 — 송신 출발 IP가 노드 수만큼, pod 수만큼 흩어진다. \"이 IP에서만 나간다\"를 외부 방화벽에 적을 수가 없다. 송신 감사가 불가능 — 누가 어디로 나갔는지 한 곳에 로그가 없다. egress 정책 강제 지점이 없다 — \"결제 워크로드만 PG사로 나갈 수 있다\" 같은 규칙을 걸 단일 지점이 없다. egress gateway는 이 흩어진 송신을 단일 지점(choke point) 으로 모으는 전용 Envoy다. 메시의 외부 송신이 모두 이 pod 한 종류를 통과하면 — 출발 IP가 하나로 고정(방화벽 화이트리스트 가능), 송신 로그가 한 곳에 모이고 (감사 가능), 정책을 이 한 지점에 걸 수 있다(강제 지점 확보). 이 문서는 그 choke point를 homelab에 실제로 세우고, 앱이 https:// 를 직접 호출하는 가장 흔한 케이스를 통과시킨 뒤, 트래픽이 정말 그 지점을 경유했는지 까지 증명한다. 선행 개념 한 줄씩 (모르면 먼저 채울 것): 개념 이 문서에서 왜 필요한가 sidecar 트래픽 캡처( :15001 outbound) 앱이 보낸 패킷을 Envoy가 가로채야 egress로 우회시킬 수 있다 outboundTrafficPolicy (ALLOW_ANY / REGISTRY_ONLY) 통제를 \"강제\"로 바꾸는 스위치 — §7.4의 차단 검증 핵심 TLS handshake의 SNI passthrough에서 gateway가 라우팅에 쓸 수 있는 유일한 평문 키 Gateway / VirtualService / DestinationRule / ServiceEntry 2-홉을 잇는 4객체 (§5에서 관계, §6에서 YAML) 왜/모드결정/2-leg 라우팅의 개념 정본 은 egress gateway 개념 정본 §01·§02·§04에 있다. 본 가이드는 그 개념을 homelab에서 실제로 구성·검증 하는 절차에 집중하므로 이론은 정본에 위임하고 여기서는 \"왜 이 객체가 이 모양인지\"만 짚는다. 1. 핵심 아키텍처 — 한 장의 그림과 그로부터 따라오는 모든 것 머릿속 앵커 한 문장 : choke point로 모으되 TLS는 절대 풀지 않는다 . 이 한 가지 제약이 이후 모든 설계를 결정한다. 그림 1. 2-홉 egress 경로 — sidecar가 :15001에서 outbound를 가로채지만 SNI만 읽고 payload는 암호화 유지. hop1(mesh→egress)·hop2(egress→external) 모두 PASSTHROUGH라 ciphertext가 외부 호스트까지 그대로 전달된다. 그림이 말하는 핵심은 2-홉 이다. 외부 호출이 sidecar에서 외부로 직접 가지 않고, 일부러 한 번 더 꺾여 egress gateway를 경유한다(hop1: mesh→gateway, hop2: gateway→external). 이 \"일부러 꺾기\"가 choke point를 만든다. 그리고 양쪽 홉 모두 암호문이 그대로 유지된다(end-to-end TLS) — gateway는 봉투를 뜯지 않는다. 여기서 핵심 긴장이 나온다. choke point로 모으면 보통은 \"거기서 트래픽을 들여다보겠다\"가 따라오는데, 앱이 이미 https:// 로 종단간 암호화 를 걸어 보냈으므로 gateway가 봉투를 뜯으면 그 암호화가 깨진다. 그래서 봉투를 안 뜯는다 — 이게 TLS Passthrough 다. 봉투를 안 뜯으니 gateway가 라우팅에 쓸 수 있는 정보는 평문 HTTP 헤더/경로가 아니라, TLS handshake 때 평문으로 노출되는 목적지 호스트명 — SNI 하나뿐이다. 이 SNI 제약이 §6의 모든 객체 모양을 한 줄로 설명한다. 왜 이 모양인가 를 미리 깔아두면 §6 YAML이 전부 \"당연\"해진다: 설계 선택 SNI 제약에서 따라오는 이유 ServiceEntry protocol: TLS (HTTP 아님) Envoy가 평문 헤더를 못 보니 L7 HTTP 로 등록할 수 없다 → L4 TLS Gateway server tls.mode: PASSTHROUGH 봉투를 뜯지 않고 그대"
    },
    {
      "title": "Egress TCP 병목 재현 랩 — 한계를 줄여서 부딪힌다",
      "desc": "TCP 병목 정본의 한계 수치(Envoy 1024, 포트 28k, conntrack 26만)는 그대로 재현하려면 연결 수만 개가 필요하다. 이 랩의 기법은 반대다 — 한계를 5~20으로 줄여서, 같은 메커니즘을 연결 수십 개로 관찰한다. 재현 4종 각각이 운영에서 만날 실패 시그니처(…",
      "url": "/public/istio/gw__guide-egress-tcp-failure-reproduction.html",
      "domain": "istio",
      "text": "istio egress tcp reproduction lab connection-pool port-exhaustion conntrack Egress TCP 병목 재현 랩 — 한계를 줄여서 부딪힌다 NOTE TCP 병목 정본 의 한계 수치(Envoy 1024, 포트 28k, conntrack 26만)는 그대로 재현하려면 연결 수만 개가 필요하다. 이 랩의 기법은 반대다 — 한계를 5~20으로 줄여서, 같은 메커니즘을 연결 수십 개로 관찰한다. 재현 4종 각각이 운영에서 만날 실패 시그니처( UO / UF / 무응답 / 정시 절단) 하나씩을 직접 떠올리고, 그 시그니처 구분이 곧 reset 분기 런북이 된다. 대상 환경 : Istio 1.30.0, Egress 신원 기반 통제 구성 의 테스트 클러스터(ns istio-egress 의 egressgateway + ns egress-test 의 netshoot-a)가 떠 있는 상태. 대상 독자 : 도입 보고서의 병목 표를 \"봤다\"에서 \"겪었다\"로 바꾸고 싶은 사람. 범위 : 병목 1·2·3·4 재현 + 복구. 완화 운영값 자체는 정본 §06 으로 위임. 0. 원칙 — 왜 줄여서 부딪히나 28,000개 연결을 만들어 한계에 도달하는 게 아니라, 한계를 5~20으로 줄여 같은 메커니즘을 소규모로 관찰한다. [운영 한계] [랩 재현] maxConnections: 1024(기본) maxConnections: 5 ephemeral ports: 28,232 ip_local_port_range: 20개 nf_conntrack_max: ~260k nf_conntrack_max: 200 idle timeout(FW): 30~60min idleTimeout: 30s | | +--- 같은 메커니즘, 같은 시그니처 ---+ 공유 테스트 클러스터에서 안전하고, 외부 sandbox 엔드포인트에 가는 부하도 연결 수십 개 수준이라 무해하다. (그래도 대상 LB에 이상탐지가 있다면 사전 공유 권장.) 비유 하나: 둑의 높이를 1m로 낮추고 물 한 양동이로 범람을 관찰하는 것. 한계: 비율이 다른 현상은 못 본다 — 예컨대 연결 수만 개 규모에서만 나타나는 Envoy 메모리 압박·FD 고갈은 이 기법으로 재현되지 않는다. 1. 관찰 도구 준비 # gw pod에 디버그 컨테이너 부착 (ss, conntrack 등 사용) GW=$(kubectl get pod -n istio-egress -l istio=egressgateway -o jsonpath='{.items[0].metadata.name}') kubectl debug -n istio-egress $GW -it --image=registry.example.com/netshoot:latest \\ --target=istio-proxy -- bash # 이 셸에서: ss -tan state time-wait | wc -l 등 실행 # Envoy 통계 조회 (별도 터미널, 반복 사용할 함수) stats() { kubectl exec -n istio-egress deploy/egressgateway -c istio-proxy -- \\ pilot-agent request GET stats | grep \"api-a\" | grep -E \"$1\" } # 부하 발생용 alias A=\"kubectl exec -n egress-test deploy/netshoot-a -c netshoot --\" kubectl debug --target=istio-proxy 로 붙이는 이유: ephemeral 컨테이너가 istio-proxy와 같은 프로세스/네트워크 네임스페이스를 공유 해야 그 pod의 소켓( ss )이 보인다. 2. 재현 1 — Envoy cluster 연결 상한 (운영 기본 1024 → 5) 부딪히는 순서 1번, 가장 먼저 맞을 함정. 상한을 5로 줄이고 10개 연결을 시도한다. # dr-api-a-tiny.yaml — 외부 호스트용 DR. gw의 upstream cluster에 적용됨 apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: api-a-external namespace: istio-egress spec: host: api-a.example.com trafficPolicy: connectionPool: tcp: maxConnections: 5 # 재현용. 미설정 시 기본 1024 kubectl apply -f dr-api-a-tiny.yaml # 동시 연결 10개 보유 (sleep이 stdin을 잡아 5분간 연결 유지) $A bash -c 'for i in $(seq 1 10); do (sleep 300 | openssl s_client -connect api-a.example.com:443 \\ -servername api-a.example.com -quiet > /tmp/c$i.log 2>&1) & done; sleep 15; grep -l \"BEGIN CERT\" /tmp/c*.log | wc -l' 관찰 명령 기대 성공 연결 수 위 마지막 출력 5 (6번째부터 handshake 실패) 거부 카운터 stats upstream_cx_overflow 5 이상 증가 활성 연결 stats upstream_cx_active 5에서 고정 access log gw 로그의 response flag UO (Upstream Overflow) 핵심 체감 : 클라이언트 증상이 AuthorizationPolicy 거부와 똑같은 \"TLS 실패/reset\"이다. flag( UO vs rbac 로그)로만 구분 가능 — 런북에 들어갈 내용이 바로 이것. 복구: kubectl delete dr api-a-external -n istio-egress (운영에서는 정본 §06-1 의 운영값으로 재생성). 3. 재현 2 — Ephemeral 포트 고갈 + TIME_WAIT (28,232개 → 20개) ip_local_port_range 는 K8s safe sysctl 이라 pod 단위로 바로 설정 가능하다(kubelet allowlist 불필요). # values-egre"
    },
    {
      "title": "Istio 회로 차단은 connection pool 한도로 부하를 빠르게 잘라내고 outlier detection으로 unhealthy 인스턴스를 격리해 연쇄 장애를 끊는다",
      "desc": "Istio의 \"circuit breaking\"은 단일 기능이 아니라 두 개의 독립된 방어선이다 — ① connectionPool 한도(동시성·pending·retry 상한 = Envoy circuit_breakers)로 클라이언트가 upstream을 과부하시키지 못하게 빠르게 실패시키…",
      "url": "/public/istio/gw__note-circuit-breaking-mechanisms.html",
      "domain": "istio",
      "text": "istio circuit-breaking connection-pool outlier-detection resilience Istio 회로 차단은 connection pool 한도로 부하를 빠르게 잘라내고 outlier detection으로 unhealthy 인스턴스를 격리해 연쇄 장애를 끊는다 ℹ 이 문서가 다루는 것 Istio의 \"circuit breaking\"은 단일 기능이 아니라 두 개의 독립된 방어선 이다 — ① connectionPool 한도(동시성·pending·retry 상한 = Envoy circuit_breakers )로 클라이언트가 upstream을 과부하시키지 못하게 빠르게 실패 시키고, ② outlierDetection 으로 에러를 뱉는 endpoint를 LB pool에서 일시 격리 한다. 둘 다 CircuitBreaker CRD가 아니라 DestinationRule trafficPolicy 에 정의되고 istiod가 Envoy cluster로 컴파일한다. 이 note는 왜 이 메커니즘이 존재하고 어떻게 동작하는가 에 집중하고, 필드별 컴파일 매핑·전체 YAML 레퍼런스는 Envoy Cluster 해부 로 위임한다. 대상환경 : Istio 1.30 / Envoy sidecar · 대상독자 : cascading failure를 끊고 싶은 DevOps/SRE · 범위 : 멘탈모델·메커니즘·관측 · 선행개념 : DestinationRule, Envoy cluster, response flag 01. 배경 — 왜 회로 차단이 필요한가 (cascading failure의 메커니즘) 회로 차단을 이해하려면 먼저 무엇으로부터 보호하는가 를 알아야 한다. 답은 연쇄 장애(cascading failure) 다. 분산 시스템에서 한 서비스의 느려짐이 전체로 번지는 가장 흔한 경로가 이것이다. 메커니즘은 이렇다. upstream B가 느려지면(GC pause, DB 락, GPU 큐 적체 등), B를 호출하는 A의 요청들이 응답을 기다리며 A 안에 쌓인다 . 요청 하나당 thread·socket·메모리를 점유한 채 blocking되므로, B가 느린 시간 동안 A의 자원이 고갈된다. 이제 A 자신이 느려지고, A를 호출하는 C에서 같은 일이 반복된다. 느림이 호출 체인을 거슬러 올라가며 증폭 되어, 결국 B 하나의 문제가 mesh 전체를 마비시킨다. B 느려짐 → A의 요청이 응답 못 받고 쌓임 (thread/conn/mem 점유) → A 자원 고갈 → A도 느려짐 → C의 요청이 A를 기다리며 쌓임 → ... 체인 전체로 전파 (cascading failure) 핵심 통찰은 \"느린 실패가 빠른 실패보다 나쁘다\" 는 것이다. B가 즉시 거절(fast fail)했다면 A는 자원을 즉시 회수하고 fallback·degraded mode로 전환할 수 있다. 하지만 무한정 기다리면(slow fail) 자원이 묶인 채 장애가 번진다. 회로 차단의 본질은 느린 실패를 빠른 실패로 바꿔 장애 전파의 사슬을 끊는 것 이다. 여기에 두 번째 문제가 겹친다. upstream이 여러 host(Pod)로 이뤄질 때, 그중 일부만 죽는 경우다. LB는 죽은 host로도 계속 트래픽을 보내고, 그 요청들은 매번 실패한다 — 정상 host가 멀쩡한데도 서비스 에러율이 올라간다. 죽은 host를 자동으로 솎아내는 장치가 따로 필요하다. Istio는 이 두 문제를 두 개의 다른 메커니즘 으로 푼다. 사람들이 \"circuit breaker 켜자\"고 뭉뚱그리는 그 한 단어 뒤에는, 성격이 전혀 다른 두 방어선이 있다. 02. 핵심 아키텍처 — 방향이 다른 두 방어선 (멘탈모델 anchor) ★ 한 문장 멘탈모델 머릿속에 잡을 그림은 방향의 차이 다. connection pool은 내가 upstream을 얼마나 밀어붙이는가 (나가는 트래픽의 양 을 자름 → fail fast), outlier detection은 upstream의 어느 host가 나쁜가 (받는 쪽 host 를 솎음 → passive health check). 전자는 traffic shaping, 후자는 런타임 host 판별이다. 이 둘을 한 덩어리로 묶으면 \"한도를 걸었는데 왜 endpoint가 안 빠지지?\" 같은 혼선이 생긴다. 이 anchor 하나에서 나머지가 다 따라 나온다. 01절의 두 문제 — 과부하로 인한 자원 고갈 과 일부 host의 죽음 — 가 정확히 이 두 메커니즘에 대응한다. 그림 1. DestinationRule.trafficPolicy의 connectionPool/outlierDetection이 Envoy cluster의 circuit_breakers.thresholds와 outlier_detection으로 컴파일. 한도 초과는 503 UO 즉시 실패. 여기서 먼저 잡아야 할 구조적 사실: CircuitBreaker 라는 CRD는 없다. 두 메커니즘 모두 DestinationRule의 trafficPolicy 안에 필드로 들어가고, istiod가 그것을 xDS로 각 sidecar의 Envoy cluster 설정으로 컴파일 한다. connectionPool 은 cluster의 circuit_breakers.thresholds 로, outlierDetection 은 cluster의 outlier_detection 으로 간다. 즉 \"회로 차단을 켠다\"는 건 목적지(host)를 가리키는 DestinationRule을 쓰는 일 이지 별도 리소스를 만드는 일이 아니다. 두 메커니즘은 같은 cluster 안에 공존하지만 독립적으로 동작 한다. connectionPool 한도에 걸려도 outlier detection 판정과 무관하고, 그 반대도 마찬가지다. 보통 둘을 같은 DestinationRule에 같이 둔다 — 하나는 양을, 하나는 host를 막으므로 상호 보완적이다. 축 connection pool ( connectionPool ) outlier detection ( outlierDetection ) 답하는 질문 \"이 upstream으로 동시에 얼마나 보낼까?\" \"이 upstream의 어느 host 가 나쁜가?\" 보호 대상 u"
    },
    {
      "title": "east-west gateway는 목적지 클러스터를 SNI에 인코딩해, mTLS를 복호화하지 않고 암호화된 채로 원격 워크로드까지 프록시한다",
      "desc": "east-west gateway는 mTLS를 풀지 않고 봉투 겉면(ClientHello의 SNI)에 적힌 목적지만 읽어 다음 hop으로 넘기는 L4 SNI 라우터다. 멀티클러스터(network 분리) 메시에서 한 클러스터의 sidecar가 다른 클러스터의 워크로드를 부를 때, side…",
      "url": "/public/istio/gw__note-eastwest-gateway-sni.html",
      "domain": "istio",
      "text": "istio multicluster eastwest-gateway sni mtls east-west gateway는 목적지 클러스터를 SNI에 인코딩해, mTLS를 복호화하지 않고 암호화된 채로 원격 워크로드까지 프록시한다 NOTE east-west gateway는 mTLS를 풀지 않고 봉투 겉면(ClientHello의 SNI)에 적힌 목적지만 읽어 다음 hop으로 넘기는 L4 SNI 라우터 다. 멀티클러스터(network 분리) 메시에서 한 클러스터의 sidecar가 다른 클러스터의 워크로드를 부를 때, sidecar는 목적지 식별자를 SNI 필드에 인코딩 해 보내고, 게이트웨이는 그 SNI만 읽어( AUTO_PASSTHROUGH ) 암호 바이트를 그대로 흘린다. 종단을 안 하므로 워크로드↔워크로드 mTLS가 관통해 보존된다. 이 문서는 그 메커니즘과 왜 를 다룬다(운영 매니페스트는 위임). 대상환경 : Istio 1.30, multi-network 멀티클러스터 · 대상독자 : 멀티클러스터 mTLS 신원 보존이 왜·어떻게 동작하는지 알고 싶은 SRE · 선행개념 : SPIFFE/mTLS 신원 , Cluster 해부 1. 배경: 단일 클러스터 mTLS가 멀티클러스터에서 깨지는 두 지점 east-west gateway가 왜 존재하는지는, 그것이 없을 때 무엇이 깨지는지에서 나온다. 먼저 정상 상태부터. 단일 클러스터 에서는 sidecar가 목적지 pod IP를 직접 알고, Envoy cluster가 그 endpoint로 mTLS를 맺는다. 워크로드 A의 SPIFFE 신원이 워크로드 B에 그대로 제시되고, B의 AuthorizationPolicy는 \"A가 부른 게 맞다\"를 그 신원으로 판정한다( SPIFFE/mTLS 신원 ). 이 그림에서 sidecar↔sidecar는 1-hop이고, 중간에 끼어드는 자가 없다. 멀티클러스터 — 특히 network가 분리 된 경우(클러스터 간 pod CIDR이 서로 라우팅 불가) — 에서는 이 그림이 두 군데서 깨진다. 연결성 : cluster1의 sidecar가 cluster2의 pod IP로 직접 TCP를 못 연다. 두 network는 L3에서 서로의 pod CIDR을 모른다. 신원 보존 : 누군가 두 클러스터 사이에서 TLS를 종단(복호화)하면, 그 지점에서 peer 신원이 그 중간자의 신원으로 바뀐다. B가 보는 건 더 이상 A가 아니다. end-to-end SPIFFE가 끊긴다. 순진한 해법(클러스터 경계에 보통 게이트웨이를 세워 TLS 종단 후 재암호화)은 연결성은 풀지만 신원을 죽인다. 신원을 살리려면 중간 게이트웨이가 절대 TLS를 풀지 않아야 한다. 그런데 풀지 않으면 게이트웨이는 안을 못 보는데, 어떻게 \"어디로 보낼지\"를 정하나? 이 긴장이 east-west gateway 설계 전체를 결정한다. 2. 핵심 아키텍처: 봉투 겉면(SNI)에 목적지를 적어 라우터에게 읽힌다 머릿속에 담을 한 장면(anchor) : 게이트웨이가 편지 안 을 못 열게 하려면, 받는 사람을 봉투 겉면 에 적으면 된다. TLS에서 그 겉면이 평문으로 노출되는 유일한 칸이 ClientHello의 SNI 다. 그래서 sidecar는 목적지 식별자를 SNI에 인코딩해 보내고, 게이트웨이는 그 한 줄만 읽어 라우팅한다 — 복호화 0. 이 한 문장에서 나머지 모든 디테일이 따라 나온다. 메커니즘을 세 부품으로 분해하면. 부품 그게 답하는 질문 어떻게 SNI 인코딩 (sidecar 쪽) \"게이트웨이가 안을 못 보는데 목적지를 어떻게 전달하나?\" 목적지를 outbound_.PORT_.SUBSET_.FQDN 로 SNI에 적음 AUTO_PASSTHROUGH (게이트웨이 listener) \"게이트웨이가 어떻게 복호화 없이 SNI만 읽나?\" tls_inspector 로 ClientHello의 SNI만 추출, TLS는 안 풂 sni-dnat (게이트웨이 라우팅) \"추출한 SNI를 어디로 보내나?\" SNI를 동일 이름 내부 cluster로 역매핑해 tcp_proxy 전체 흐름을 한 장에 담으면. 그림 1. cluster1 workload A → cluster2 east-west GW(AUTO_PASSTHROUGH). GW는 SNI(outbound_.port._.B.ns.svc)만 읽어 같은 암호 바이트를 B로 전달. 경로는 양방향 대칭. NOTE A의 sidecar는 cluster2의 east-west GW(EW2)로 곧장 mTLS를 맺는다 — 자기 클러스터 EW1을 거치지 않는다. EW1은 반대 방향 (cluster2 → cluster1의 워크로드) 트래픽의 진입점이다. 게이트웨이는 network마다 하나 있는 양방향 관문 이고, 트래픽은 항상 목적지 network의 게이트웨이로 들어간다. 2.1 부품 ①: SNI 인코딩 — sidecar가 목적지를 봉투에 적는다 평범한 Envoy cluster의 transport socket은 TLS를 맺을 때 SNI를 목적지 hostname(또는 비움)으로 채운다 — \"어떤 server 인증서를 줄까\"용이다. east-west 경로용 cluster는 이 SNI를 목적지 식별자 문자열 로 덮어쓴다. istiod가 멀티클러스터 sidecar에 내려주는 cluster의 모양은 이렇다. # istioctl proxy-config cluster deploy/workload-a --fqdn B.ns.svc.cluster.local -o json 의 발췌 { \"name\": \"outbound|8080||B.ns.svc.cluster.local\", \"transportSocket\": { \"name\": \"envoy.transport_sockets.tls\", \"typedConfig\": { \"sni\": \"outbound_.8080_._.B.ns.svc.cluster.local\" # ← 목적지를 SNI에 인코딩 } }, \"loadAssignment\": { \"endpoints\": [ { \"lbEndpoints\": [ { \"endpoint\": { \"address\": { \"socketAddress\": { \"address\": \"<EW2 gateway IP>\", \"p"
    },
    {
      "title": "Egress 신원, 이중 TLS 없이 — passthrough + Calico가 HTTPS over mTLS를 대체하는 근거",
      "desc": "\"egress에서 호출 워크로드 신원을 통제하려면 HTTPS over mTLS(이중 TLS)가 필요하다\"는 통념에 대한 반론. 멘탈모델 한 줄: 강제(enforcement)는 어차피 chokepoint(egress gateway)에서 일어난다 — 패턴 선택은 \"그 앞에서 호출자를 어떻…",
      "url": "/public/istio/gw__note-egress-identity-without-mtls.html",
      "domain": "istio",
      "text": "istio egress mtls passthrough calico networkpolicy decision spiffe Egress 신원, 이중 TLS 없이 — passthrough + Calico가 HTTPS over mTLS를 대체하는 근거 NOTE \"egress에서 호출 워크로드 신원을 통제하려면 HTTPS over mTLS(이중 TLS)가 필요하다\"는 통념에 대한 반론. 멘탈모델 한 줄: 강제(enforcement)는 어차피 chokepoint(egress gateway)에서 일어난다 — 패턴 선택은 \"그 앞에서 호출자를 어떻게 식별 하느냐\"의 문제일 뿐이다. 식별을 메시 mTLS handshake로 할지, CNI(Calico)가 커널에서 이미 하는 pod-selector 로 할지. 단일 클러스터·노드 신뢰·Calico 단독관리 환경에선 후자가 동일 결과를 더 싸게 낸다. 이중 TLS의 추가 비용(handshake 2회·포트분리 함정·L7 사각)은 그 위협모델에서 보안 이득이 0에 가깝다. 대상 구조의 상세는 HTTPS over mTLS 구조 정본 참조 — 본 문서는 그 반대 선택의 근거 다. 맥락: 사내 egress 도입에서 \"HTTPS over mTLS냐 passthrough냐\" 결정 토론용 근거 문서. 환경 전제: 단일 클러스터, CNI Calico(플랫폼팀 단독관리) , 노드 신뢰 위협모델, 외부 파트너 감사는 source IP로 충분. 대상 독자: egress 패턴을 고르는 DevOps/SRE. 선행 개념: egress gateway가 chokepoint라는 것, mTLS=SPIFFE cert, NetworkPolicy의 selector. 1. 배경 — 애초에 \"egress 신원\"이 왜 문제인가 메시 내부 트래픽은 mTLS로 자동 식별된다. 문제는 밖으로 나가는 트래픽 이다. payment pod가 partner-bank.example.com:443 을 부를 때, 클러스터 운영자는 두 가지를 보장하고 싶다. 누가 나가는지 통제 — payment 만 partner-bank로 나가고 나머지는 차단. 누가 나갔는지 귀속 — 외부 파트너 감사·내부 로그에서 \"이 호출은 payments tier가 한 것\"을 증빙. 그런데 외부 목적지는 메시 밖이라 응답 측이 SPIFFE를 검증해 주지 않는다. 식별은 전적으로 나가는 경계(egress gateway)에서, 우리 손으로 해야 한다. 그래서 \"egress 신원\"은 본질적으로 경계 통과자를 어떻게 식별하느냐 의 문제로 환원된다. 표준 처방은 HTTPS over mTLS(이중 TLS) 다: 앱→egress gateway 구간에 메시 mTLS를 한 겹 더 씌워, gateway가 outer handshake의 client cert로 호출자 SPIFFE ID를 확정하고 AuthorizationPolicy 를 건다. 본 문서는 그게 정말 필요한 비용인가 를 따진다. 결론부터: 위 환경 전제에선 CNI가 이미 공짜로 하는 식별로 동일 결과를 낸다. 여기서 먼저 통념의 정체를 정확히 분해해야 토론에서 못 진다. 이중 TLS가 passthrough 대비 추가로 사는 것 은 딱 하나 다. egress gateway가 outer mTLS handshake에서 받은 client cert로 호출 워크로드의 SPIFFE ID ( cluster.local/ns/payments/sa/payment )를 암호학적으로 확정 하고, 그걸 키로 AuthorizationPolicy 를 건다. 즉 사는 물건은 \"egress chokepoint를 통과하는 호출자를 SA 단위로 식별·인가\" 다. 그 이상도 이하도 아니다(L7은 여전히 못 봄 — outer/inner TLS 모두 불투명). 2. 핵심 — 식별과 강제를 분리하면 대안이 보인다 멘탈모델 anchor: egress 보안 = 식별(누구인가) + 강제(못 나가게 막기) , 이 둘은 별개 축이다. 강제는 어느 패턴이든 똑같이 chokepoint(egress gateway + 그 앞의 NetworkPolicy)에서 일어난다. 그러니 패턴 비교는 오직 식별 한 축 으로 좁혀진다 — chokepoint 앞에서 호출자를 무엇으로 식별하느냐. 그림 1. 강제는 chokepoint에서 일어난다 — 패턴 선택은 그 앞에서 호출자를 어떻게 식별하느냐의 문제일 뿐. 왜 이 분리가 결정적인가. 식별 방법 A(mTLS handshake)는 이중 TLS·L7 사각·포트 머지 충돌·핸드셰이크 2회라는 비용을 동반한다. 식별 방법 B(Calico pod-selector)는 CNI가 패킷 라우팅을 위해 어차피 유지하는 endpoint(IP↔pod 매핑) 를 재사용하므로 추가 비용이 0이다. 두 방법이 식별 이라는 같은 일을 하면서 가격표만 다르다면, 강제가 공유되는 한 더 싼 쪽이 합리적이다. 이 anchor에서 \"더 싸게 근사\"가 막연한 말이 아니라 구체적 대안 3개 로 떨어진다. 식별 메커니즘의 핵심 — pod-selector는 \"비-핸드셰이크 신원\"이다 방법 B가 단순 ACL이 아니라 신원 메커니즘 이라는 게 논거의 심장이다. app == 'payment' 는 pod가 헤더로 주장 하는 값이 아니라 Calico가 endpoint(IP↔pod 매핑)에 묶어 커널(iptables/eBPF)에서 강제 하는 라벨이다. 옆 pod가 payment 를 사칭하려면 그 pod의 네트워크 네임스페이스 자체를 위조 해야 하고, 그건 노드 컴프로마이즈 수준이다. → 즉 \"pod-selector NetworkPolicy는 그 자체로 비-핸드셰이크 신원 메커니즘\" 이다. mTLS가 cert로 하는 일(검증된 호출자 식별)을 Calico는 커널 endpoint로 한다 — handshake 비용 0. 이것이 §3·§4 두 논증(중복 구매·노드 신뢰)의 토대다. 3. 근사 3종 — 구체 시나리오 + 실제 YAML 시나리오 고정: \" payment 워크로드만 partner-bank.example.com:443 으로 나갈 수 있다. 나머지는 차단.\" (전형적 사내 egress 요구) 세 근사는 §2의 식별 축을 config 분배 / 커널 강제 / IP 귀속"
    },
    {
      "title": "Egress route 스코핑 — metadata.namespace는 적용 범위가 아니다",
      "desc": "Istio traffic 리소스의 metadata.namespace는 \"어디에 저장했는가\"(소유 경계)이지 \"어느 proxy에 적용되는가\"(적용 범위)가 아니다. 적용 범위는 gateways · exportTo · sourceNamespace/sourceLabels · Sidecar.…",
      "url": "/public/istio/gw__note-egress-vs-scoping.html",
      "domain": "istio",
      "text": "istio egress virtualservice exportTo sourceNamespace sidecar scoping xds Egress route 스코핑 — metadata.namespace 는 적용 범위가 아니다 NOTE Istio traffic 리소스의 metadata.namespace 는 \" 어디에 저장했는가 \"(소유 경계)이지 \" 어느 proxy에 적용되는가 \"(적용 범위)가 아니다. 적용 범위는 gateways · exportTo · sourceNamespace/sourceLabels · Sidecar.egress.hosts 네 레버의 직교 조합 으로 따로 결정된다. 이 분리를 모르면 egress 설정이 다른 namespace sidecar로 새거나(전역 누수), gateway가 필요한 route를 못 본다. egress gateway를 여러 개 (passthrough용 / mTLS용) 두는 순간 스코핑은 선택 이 아니라 전제 가 된다. 대상독자: 멀티-gateway egress를 구성하며 \"어느 client가 어느 gateway를 타는가\"를 안전하게 강제하려는 SRE. 선행개념: VirtualService/ServiceEntry/Sidecar CRD, xDS push 모델. 환경: homelab (k8s v1.30.6, Istio 1.30.0 , CNI Calico). 실측 사례는 §4. 구성 랩 연결: egress gateway HTTPS 가이드 · egress CRD 멘탈모델 . 1. 배경 — 왜 \"namespace에 담았으니 거기만 적용\"이 아닌가 처음 Istio를 쓰면 VirtualService 를 payments namespace에 만들면 그 route가 payments pod에만 적용된다고 가정하기 쉽다. Kubernetes의 다른 리소스( ConfigMap , Secret 등)는 namespace가 곧 작용 경계이기 때문이다. Istio traffic 리소스는 그렇지 않다. 이유는 Istio의 config 전파 모델에 있다. istiod는 mesh 안 모든 proxy(sidecar + gateway)를 향해 config를 push하는 단일 컨트롤 플레인이다. VirtualService 하나가 어느 namespace에 저장됐든, istiod는 그것을 mesh 전역 후보 config 로 보고, \"이 proxy가 이 route를 받아야 하나?\"를 별도의 규칙으로 계산한다. 즉 저장 위치( metadata.namespace )와 적용 대상(어느 proxy)은 설계상 분리 돼 있고, 그 둘을 잇는 게 아래 네 레버다. 이 분리가 존재하는 이유는 실용적이다 — 한 팀이 자기 namespace에 리소스를 두면서도(소유권), 그 route를 mesh 전체 또는 특정 gateway에 걸 수 있어야(공유) 하기 때문이다. 분리를 모르고 namespace만 믿으면 두 방향으로 사고가 난다: route가 의도보다 넓게 새거나(전역 누수 → §4의 NACK 전파), 의도한 gateway가 route를 못 보거나 (필요한 leg 누락). 2. 멘탈모델 — 한 리소스를 보는 네 개의 직교 축 핵심 그림 하나만 머리에 넣으면 된다. 한 traffic 리소스는 서로 다른 질문에 답하는 네 개의 독립 필드를 가진다. 어느 하나도 다른 것을 함의하지 않는다(직교). metadata.namespace = 어디에 저장했나 (소유/관리 경계) ← 적용 범위 아님 spec.gateways = 어느 proxy 종류에 붙나 (mesh=sidecars / <gw>=gateway) spec.exportTo = 어느 namespace에 보이나 (가시성) tls.match.sourceX = 그 중 어느 workload에 (applicability selector, 런타임 match 아님) 그림 1. 한 VirtualService, 네 직교 축. 같은 리소스의 서로 다른 필드가 \"어디 저장 / 어느 proxy / 어느 ns에 보임 / 어느 workload\"라는 독립된 네 질문에 각각 답한다 — 저장 위치(namespace)는 적용 범위와 무관하다는 게 핵심 함정. 각 축이 답하는 질문과 함정을 부품표로: 레버 답하는 질문 기본값 함정 spec.gateways 어느 proxy 종류? mesh (모든 sidecar) 생략 = 전 sidecar에 적용. namespace로 안 좁혀짐 exportTo 어느 namespace에 보이나? * (전체) 누락 = 전역 가시성 → 누수의 근원 sourceNamespace / sourceLabels 그 중 어느 workload? (전체) 런타임 packet match가 아니라 applicability filter Sidecar.egress.hosts sidecar가 import할 config 범위 (전체) 방화벽 아님 — config scoping일 뿐 mesh 는 VirtualService.spec.gateways 의 reserved word 로 \"메시 안 모든 sidecar\"를 뜻한다. gateways 를 생략하면 기본값이 mesh 다. 그래서 payments namespace의 VS가 기본적으로 전 sidecar에 적용될 수 있는 것이다. 좁히는 네 레버를 메커니즘으로: exportTo: [\".\"] — 리소스를 선언된 namespace 안에서만 보이게 한다. 기본값은 전체 export( * ). ServiceEntry·VirtualService·DestinationRule 모두 지원. 소유권 분리와 전역 누수 차단에 가장 직접적인 레버. sourceNamespace / sourceLabels ( tls.match ) — 어떤 workload에 이 route를 적용할지 거르는 selector. 패킷이 들어왔을 때 매칭하는 게 아니라, istiod가 \"이 proxy에 이 route를 줄까\"를 정할 때 쓰는 applicability filter다. Sidecar.egress.hosts — 해당 sidecar가 import할 config 범위를 줄인다(성능 + governance). 단 이는 방화벽이 아니라 config scoping 이다. scope 밖으로 보"
    },
    {
      "title": "Sidecar 리소스는 '좁은 범위가 넓은 범위를 통째로 이기는' override 계층으로, 사이드카에 푸시할 설정을 좁혀 성능과 egress 거버넌스를 동시에 얻는다",
      "desc": "Istio의 기본 가정은 \"메시 안 모든 워크로드가 다른 모든 워크로드에 접근 가능\"이다. 그래서 istiod는 각 Envoy에 메시 전체 설정을 푸시하고, 규모가 커지면 이것이 메모리·xDS push·istiod CPU를 동시에 압박한다. Sidecar 리소스는 두 개의 서로 다른…",
      "url": "/public/istio/gw__note-sidecar-scope.html",
      "domain": "istio",
      "text": "istio sidecar egress performance Sidecar 리소스는 '좁은 범위가 넓은 범위를 통째로 이기는' override 계층으로, 사이드카에 푸시할 설정을 좁혀 성능과 egress 거버넌스를 동시에 얻는다 NOTE Istio의 기본 가정은 \"메시 안 모든 워크로드가 다른 모든 워크로드에 접근 가능\"이다. 그래서 istiod는 각 Envoy에 메시 전체 설정 을 푸시하고, 규모가 커지면 이것이 메모리·xDS push·istiod CPU를 동시에 압박한다. Sidecar 리소스는 두 개의 서로 다른 레버 로 이를 푼다 — egress.hosts (이 워크로드가 알아야 할 설정의 범위 를 축소 → 성능)와 outboundTrafficPolicy (레지스트리 밖 트래픽의 차단 정책 → 거버넌스). 이 문서가 세우려는 단 하나의 멘탈모델: 세 scope(Mesh / Namespace / Workload)는 merge가 아니라 가장 좁은 하나가 통째로 이기는 override 의미론 이다. 운영 detail·YAML 전문은 Sidecar scope 운영 가이드 참조. 1. 배경 — Istio의 \"모두가 모두를 안다\"는 기본 가정과 그 비용 Istio를 쓰는 순간 service registry(Service / ServiceEntry로 채워지는 메시의 주소록)에 등록된 모든 서비스가 그 워크로드의 잠재적 upstream으로 취급된다. 이건 편의성의 산물이다 — 개발자가 아무 서비스나 이름으로 부르면 곧장 라우팅되니까. 대가는 데이터 평면 쪽 Envoy 하나하나가 메시 전체의 cluster/listener/route/endpoint를 통째로 들고 있어야 한다는 것이다. 규모가 작을 땐 안 보이지만, 워크로드가 N개로 늘면 사이드카 하나가 보유할 설정은 대략 O(N)으로 커지고, push 비용은 변경 빈도와 곱해져 더 빠르게 나빠진다. 구체적으로 세 곳을 동시에 누른다. 메모리 : 사이드카 하나가 수 MB 설정 보유 (실측 사례: 2MB → Sidecar 적용 후 644KB). xDS push 폭증 : registry의 어느 서비스가 바뀌어도, 그걸 호출하지도 않는 워크로드까지 새 설정을 받는다. istiod CPU : push 대상 × 설정 크기가 그대로 부하. 여기서 던질 질문은 \"왜 Envoy는 자기가 부르지도 않는 서비스의 설정까지 받아야 하나?\"이다. 받을 이유가 없다 — istiod가 그 워크로드의 실제 의존 대상을 알 길이 없어서 보수적으로 전부 보낼 뿐 이다. Sidecar 리소스는 바로 그 정보를 운영자가 명시적으로 주입하는 통로다. \"이 워크로드가 실제로 호출하는 대상 만 알면 된다\"고 선언해 위 곱셈을 끊는다. 이는 control-plane 성능 튜닝의 1순위 권고이며, 동일 맥락의 다른 요인(스코핑 외의 push 빈도·proxy 수 등)은 control plane 성능 요인 에서 다룬다. 그림 1. Sidecar 리소스가 없으면 istiod가 모든 Envoy에 전체 메시 설정을 push. egress.hosts로 스코프를 좁히면 각 워크로드는 자기 의존 대상만 받음 → 설정 크기·push 부하 급감. 전제 개념 정리: registry (메시가 아는 목적지 목록), xDS (istiod가 Envoy에 설정을 밀어 넣는 푸시 프로토콜 — CDS/LDS/RDS/EDS), PassthroughCluster / BlackHoleCluster (registry에 매칭 안 되는 outbound를 각각 \"통과\" / \"차단\"으로 처리하는 두 합성 cluster). 이 세 가지가 아래 메커니즘의 부품이다. 2. 핵심 멘탈모델 — Sidecar는 'override 계층'이고, 그 위에 두 개의 독립 레버가 얹힌다 머릿속에 그릴 단 하나의 그림은 이것이다. Sidecar는 \"이 Pod가 받을 설정\"을 정하는 override 계층이다. 어느 Pod에든 적용되는 Sidecar는 항상 정확히 하나뿐이며 — 가장 좁은 범위가 이긴다 — 그 하나가 egress.hosts (무엇을 알게 할지=범위)와 outboundTrafficPolicy (모르는 곳을 어떻게 처리할지=차단)라는 서로 독립된 두 손잡이를 함께 든다. 이 한 문장에서 나머지가 전부 따라 나온다. 좁은 범위가 넓은 범위에 \"합쳐지는\" 게 아니라 통째로 갈아치운다 는 것, 그리고 범위(성능)와 차단(거버넌스)이 같은 리소스 안의 다른 손잡이 라는 것 — 이 둘이 거의 모든 오해의 진원지다. 2-1. 두 개의 레버: egress.hosts vs outboundTrafficPolicy 가장 흔한 오해는 \"egress.hosts에 안 적으면 차단된다\"는 것이다. 아니다. 둘은 독립된 레버 이고, 답하는 질문 자체가 다르다. 레버 답하는 질문 빼면 일어나는 일 egress.hosts \"이 사이드카가 무엇을 알게 할까?\" (푸시할 cluster/listener의 범위) 범위 축소 안 됨 = 메시 전체 설정 유지(성능 이득 없음). 차단과는 무관 outboundTrafficPolicy.mode \"registry에 없는 호스트로 가는 트래픽을 어떻게 처리할까?\" 기본값 ALLOW_ANY → 미등록 외부 호출이 PassthroughCluster로 그냥 통과 왜 굳이 둘을 쪼갰나? \"범위를 줄이는 일\"과 \"모르는 목적지를 막는 일\"은 본질적으로 다른 결정이기 때문이다. 설정을 가볍게 하고 싶다고 해서 반드시 외부를 차단하고 싶은 건 아니다(그 반대도 마찬가지). 그래서 egress.hosts 만 좁히고 outboundTrafficPolicy 를 생략하면, 설정은 가벼워져도 \"기본 deny\" 거버넌스는 생기지 않는다 . zero-trust egress(\"등록 안 된 외부는 막는다\")를 원하면 반드시 mode: REGISTRY_ONLY 를 함께 둬야 한다. 미등록 목적지의 처리는 결국 어느 합성 cluster로 보내느냐로 갈린다. REGISTRY_ONLY : registry(Service / ServiceEntry)에 없는 목적지는 BlackHoleCluster 로 보내 차단 → 호출 측은 보통 502 . ALLOW_ANY (기본):"
    },
    {
      "title": "Runbook — ServiceEntry resolution: DNS 동작과 진단 (STRICT_DNS)",
      "desc": "외부 도메인(httpbin.org)을 ServiceEntry(resolution: DNS)로 등록하면 istiod가 이를 Envoy의 STRICT_DNS cluster로 변환한다. 이 문서는 그 변환·DNS 갱신 메커니즘, \"죽은 IP\" 처리, ambient DNS 질의 경로를 홈랩…",
      "url": "/public/istio/gw__report-2026-06-07_dns-resolution.html",
      "domain": "istio",
      "text": "istio egress dns serviceentry envoy Runbook — ServiceEntry resolution: DNS 동작과 진단 (STRICT_DNS) NOTE 외부 도메인( httpbin.org )을 ServiceEntry(resolution: DNS) 로 등록하면 istiod가 이를 Envoy의 STRICT_DNS cluster 로 변환한다. 이 문서는 그 변환·DNS 갱신 메커니즘, \"죽은 IP\" 처리, ambient DNS 질의 경로를 홈랩 실측·재현 명령과 함께 한 줄의 멘탈모델로 꿴다. 결론 한 문장: DNS refresh는 health check가 아니다 — DNS는 \"목록\"만 줄 뿐 IP의 생사를 모르므로, 죽은 IP 회피는 outlier detection으로 명시 해야 한다. 환경: cluster homelab (k8s v1.30.6, istiod 1.30.0 / istioctl 1.27.0 client·skew) / egress gateway 경유 / 관련: ingress·egress 통합 리포트 1. 배경 — 왜 mesh가 외부 도메인의 IP를 직접 들어야 하나 mesh 안 트래픽은 전부 Envoy가 가로채 라우팅한다(sidecar/gateway). 그런데 Envoy는 \"IP:port의 묶음\"인 cluster 단위로만 upstream을 안다 — Envoy의 세계에는 호스트네임이 없고 endpoint 집합만 있다. mesh 내부 서비스는 istiod가 k8s Endpoints / EndpointSlice 를 watch해서 cluster를 자동으로 채워준다. 하지만 httpbin.org 같은 mesh 밖 외부 도메인 은 k8s에 Endpoints가 없다. istiod가 그 IP를 알 길이 없다. ServiceEntry 는 바로 이 공백을 메우는 CRD다 — \"이 호스트는 mesh의 일부로 취급하라, IP는 이렇게 알아내라\"를 선언한다. 그 \"IP를 알아내는 방식\"이 spec.resolution 필드이고, 이 한 필드가 Envoy cluster의 종류를 결정 한다. 즉 외부 도메인 한 줄을 등록할 때 진짜로 고르는 것은 \"Envoy가 IP를 들고 LB하는 전략\"이다. 여기서 자연스럽게 따라오는 질문 4개가 이 문서의 뼈대다 — 이 순서대로 읽으면 길을 잃지 않는다: resolution -> cluster type -> 갱신 주기 -> 죽은 IP -> 회피 구성 (어떻게 (STRICT_DNS (DNS TTL을 (DNS는 (outlier IP를 안다) / LOGICAL) 따라 재질의) 생사 모름) detection+retry) 선행 개념: Envoy cluster = endpoint(IP:port) 집합 + LB 정책 / cluster 이름 규칙 direction|port|subset|fqdn (예: outbound|443||httpbin.org ) / egress gateway = mesh 밖으로 나가는 트래픽을 한 노드로 모으는 전용 Envoy. cluster 구조 자체는 Cluster 해부 참조. 2. 머릿속에 둘 그림 — DNS는 \"목록\", 생사는 별도 책임 이 문서 전체를 꿰는 앵커 한 문장 : resolution 이 Envoy cluster type을 정하고, cluster type이 \"IP를 어떻게 들고 LB할지\"를 정한다. 그러나 어느 type이든 DNS는 IP \"목록\"만 줄 뿐 그 IP가 살아있는지는 모른다 — liveness는 outlier detection의 별도 책임이다. 이 한 문장에서 나머지가 전부 파생된다. resolution을 STRICT_DNS로 두면 A record의 모든 IP를 펼쳐 Envoy가 직접 LB하고(§3), 갱신 주기는 DNS TTL을 따른다(§4). 하지만 TTL 만료 전에 IP가 죽으면 Envoy는 그 IP를 여전히 HEALTHY로 들고 있어 연결이 실패한다(§5) — DNS refresh는 단지 \"목록 다시 받기\"이지 \"이 IP 살아있냐\"를 묻는 게 아니기 때문 이다. 그래서 죽은 IP 회피는 outlier detection + retry로 명시 해야 하고(§7), GSLB 뒤라면 LB 권한을 DNS에 위임하는 LOGICAL_DNS가 더 맞는다(§6). 마지막으로 \"그 DNS 질의를 실제로 누가 쏘는가\"가 진단의 출발점이다 — egress 경유 시 그 주체는 egress gateway의 Envoy(c-ares)다(§8). 왜 이렇게 책임을 쪼갰는가? DNS와 health는 원래 다른 시스템 이기 때문이다. DNS 서버는 \"이 이름에 어떤 IP들이 매핑되는가\"의 권위자일 뿐, 그 IP 뒤 프로세스가 지금 요청을 받을 수 있는지는 모른다(특히 같은 A record 안에서 일부만 죽은 경우). Envoy는 이 둘을 의도적으로 분리한다 — DNS는 endpoint 발견(discovery) , outlier detection은 endpoint 축출(ejection) . 이 분리를 모르면 \"DNS만 믿으면 알아서 죽은 IP 빠지겠지\"라는 가장 흔한 오해에 빠진다. 3. 변환 구조 — resolution 이 cluster type을 결정한다 ServiceEntry.spec.resolution 필드가 Envoy cluster type을 결정한다. 이 매핑을 먼저 확립해야 이후 갱신 주기(§4)·죽은 IP(§5)를 STRICT/LOGICAL 대비로 읽을 수 있다. resolution Envoy type endpoint 적재 적합 대상 DNS STRICT_DNS A record 모든 IP 를 펼침 개별 IP를 Envoy가 직접 LB하고 싶을 때 DNS_ROUND_ROBIN LOGICAL_DNS 첫 IP 1개 만, 연결 재사용 CDN/GSLB/대형 LB 뒤 \"논리적 단일 endpoint\" 차이의 본질은 \"endpoint를 펼치느냐(STRICT) vs 한 점으로 접느냐(LOGICAL)\" 한 곳이다 — 나머지(갱신·LB·연결 재사용)는 전부 이 한 델타에서 따라온다. resolution: DNS (STRICT_DNS) 변환 결과: ServiceEntry(resolution: DNS, hosts: httpbin.or"
    },
    {
      "title": "Test Report — Ingress / Egress Gateway 동작 검증",
      "desc": "홈랩 클러스터에서 Ingress·Egress gateway 동작을 실측 검증한 리포트. 하나의 그림: gateway는 메시 경계에 선 전용 Envoy proxy다. ingress는 자기가 TLS를 끝내므로(termination) L7 전체가 보여 host/path로 분기하고, egre…",
      "url": "/public/istio/gw__report-2026-06-07_ingress-egress.html",
      "domain": "istio",
      "text": "istio ingress egress gateway report Test Report — Ingress / Egress Gateway 동작 검증 NOTE 홈랩 클러스터에서 Ingress·Egress gateway 동작을 실측 검증한 리포트. 하나의 그림: gateway는 메시 경계에 선 전용 Envoy proxy다. ingress는 자기가 TLS를 끝내므로(termination) L7 전체가 보여 host/path로 분기 하고, egress는 나가는 HTTPS를 복호화하지 않고 SNI만 보고 L4로 한 chokepoint를 거치게 강제 한다 — 이 L7 vs L4 비대칭이 두 gateway 검증법까지 갈라놓는다. 결론 — Ingress : host/path 라우팅 + TLS termination PASS. Egress : HTTPS SNI PASSTHROUGH로 외부 호출이 egress gateway를 강제 경유함을 access log로 입증(200). REGISTRY_ONLY 미등록 차단은 메시 전역 영향 탓에 의도적 보류. Date: 2026-06-07 Cluster: homelab (kubespray bare-metal, k8s v1.30.6, Calico) Istio: 1.30.0 (istiod + ingress/egress gateway, Helm 설치) Scenario: 10-ingress, 20-egress NS: mesh-test (istio-injection=enabled) 0. 배경 — 왜 gateway가 따로 있고, 왜 둘의 검증법이 다른가 sidecar가 이미 모든 pod에 붙어 mTLS·라우팅·관측을 한다면, 경계에 또 하나의 Envoy(gateway)를 세우는 이유는 무엇인가. 답은 경계 트래픽은 sidecar가 다루기 곤란한 특성 을 갖기 때문이다. Ingress 쪽 문제: 외부에서 오는 클라이언트는 mesh identity(SPIFFE cert)가 없다. 그냥 공개 HTTPS로 들어온다. 누군가는 그 외부 TLS를 종료(termination) 하고, 복호화한 Host/path를 보고 어느 내부 서비스로 보낼지 정해야 한다. 이걸 모든 pod에 흩뿌릴 수 없으니 단일 진입점 이 필요하다. Egress 쪽 문제: 기본값( outboundTrafficPolicy: ALLOW_ANY )에선 각 sidecar가 외부로 제멋대로 나간다. 그러면 egress IP가 노드마다 흩어지고, 방화벽 화이트리스트가 복잡해지고, 외부 호출 로그가 흩어진다. 그래서 나가는 트래픽을 한 pod(chokepoint)로 모아 단일 통제·관측 지점을 만든다. 그래서 ingress와 egress는 같은 \"경계 gateway\"지만 푸는 문제가 정반대 다. ingress는 외부 TLS를 끝내고 안을 들여다봐야 하고(그래야 path로 분기), egress는 나가는 TLS를 그대로 둔 채 어디로 가는지만 알면 된다. 이 한 줄의 차이가 이 리포트의 핵심 — ingress = L7 termination, egress = L4 SNI passthrough — 으로 굳는다. 그림 1. ingress/egress 비대칭. ingress GW는 TLS를 종료해 L7(Host/path)로 라우팅하므로 status code로 검증한다. egress GW는 payload를 복호화하지 않고 SNI만 보고 L4 passthrough하므로 status를 못 보고 access log로 \"경유했는가\"를 검증한다. 이 비대칭의 직접적 귀결: 검증법이 다르다. ingress는 \"복호화가 됐나 + path가 맞게 갈렸나\"를 status code(200/418/404)로 본다. egress는 payload가 암호화돼 gateway가 status를 볼 수 없으니, \"200이 떴나\"가 아니라 \"정말 그 pod를 경유 했나\" 를 access log로 본다. 이 차이를 모르면 egress의 200 을 \"gateway가 반환한 200\"으로 오독한다(→ §2, What you might be missing). 선행 개념: Envoy listener/route/cluster, cluster 이름 규칙 direction|port|subset|fqdn , SNI(TLS handshake에 평문 노출되는 목적지 호스트명), ServiceEntry/Gateway/VirtualService/DestinationRule 4종 CRD. 깊은 레퍼런스는 Egress Gateway 정본 , Cluster 해부 . 1. 사전 확인 — 출발선이 깨끗한가 검증 전에 메시 자체가 정상이어야 결과를 믿을 수 있다. 메시 상태 정상: istiod/ingress/egress 모두 1/1 Running , proxy-status 2 proxies SYNCED. ⚠️ 초기 \"비정상\" 의심은 istioctl 1.27 클라이언트로 1.30 컨트롤플레인을 조회 한 버전 불일치 착시였음. 실제 이상 없음 — 진단 도구의 버전부터 의심하라 는 교훈. sample app( httpbin , sleep ) READY 2/2 → sidecar 주입 정상. baseline: 내부(sleep→httpbin) 200, 외부(httpbin.org) 200 (기본 outboundTrafficPolicy: ALLOW_ANY — 이 시점엔 egress gateway를 안 거치고도 외부가 뚫린다는 뜻. §2의 \"경유 강제\"는 이 baseline 위에 길을 새로 까는 작업이다). 2. Ingress Gateway — L7 termination 검증 메커니즘: ingress gateway는 외부에서 받은 HTTPS를 자기가 termination (복호화)한다. 일단 복호화하면 L7 전체(Host 헤더·path·method)가 보이므로, 그 정보로 내부 서비스에 분기할 수 있다. egress가 SNI만 보는 L4 라우팅인 것과 정확히 대비되는 지점이다 — 자기가 TLS를 끝내므로 L7 전체가 보인다. 그래서 검증 포인트는 둘로 갈린다: (1) TLS termination이 동작하는가, (2) 복호화된 L7로 host/path 분기가 올바른가(매칭 안 되면 404). 적용 manifest"
    },
    {
      "title": "Test Report — Egress Gateway \"HTTPS over mTLS\" (ISTIO_MUTUAL)",
      "desc": "egress gateway에서 sidecar↔gw 구간만 Istio mTLS(ISTIO_MUTUAL)로 감싸 게이트웨이가 호출자의 SPIFFE 신원을 검증하면서, 앱이 보낸 HTTPS는 외부까지 end-to-end로 보존되는 \"이중 TLS\" 패턴을 홈랩에서 실측 검증한 리포트. 결론:…",
      "url": "/public/istio/gw__report-2026-06-08_egress-mtls.html",
      "domain": "istio",
      "text": "istio egress mtls istio-mutual spiffe gateway report Test Report — Egress Gateway \"HTTPS over mTLS\" (ISTIO_MUTUAL) NOTE egress gateway에서 sidecar↔gw 구간만 Istio mTLS( ISTIO_MUTUAL )로 감싸 게이트웨이가 호출자의 SPIFFE 신원을 검증 하면서, 앱이 보낸 HTTPS는 외부까지 end-to-end로 보존 되는 \"이중 TLS\" 패턴을 홈랩에서 실측 검증한 리포트. 결론: 동작하지만(200), 처음 manifest 그대로는 깨졌고 그 실패 두 개가 이 패턴의 핵심 원리를 그대로 드러낸다 — 종단하면 SNI가 소비된다 는 한 문장에서 모든 설정 결정과 두 함정이 따라 나온다. Date: 2026-06-08 · Cluster: homelab (kubespray bare-metal, k8s v1.30.6, Calico) · Istio: 1.30.0 (istiod + egress gateway, Helm) Scenario: scenarios/20-egress/ — *-cnn-mtls 세트 · NS: mesh-test (istio-injection=enabled) / 외부: edition.cnn.com 대상독자: egress 패턴은 한 번 봤지만 ISTIO_MUTUAL과 PASSTHROUGH가 왜 다른 라우트 타입을 강요하는지 궁금한 SRE · 선행: egress gateway 개념, mesh mTLS/SPIFFE, TLS SNI 0. 배경 지식 — PASSTHROUGH는 \"누가 나가는지\"를 모른다 egress gateway의 기본형은 HTTPS PASSTHROUGH 다(이 아카이브의 control: httpbin.org). 게이트웨이가 TLS를 풀지 않고 단일 TLS 레이어의 SNI만 읽어 외부로 포워딩한다. 구현은 단순하고 앱 TLS가 end-to-end로 보존되는 장점이 있지만, 보안상 빈 칸이 하나 있다: 메시 내부 leg(sidecar→gw)가 평문 TCP (그 위에 앱 TLS만 얹힘)다. 게이트웨이 입장에서 들어온 바이트에는 호출 워크로드의 신원 이 없다. 즉 \"누가 외부로 나가는가\"를 암호학적으로 식별하지 못한다. egress를 보안 경계로 쓰려면 이게 치명적이다 — 신원 없이는 그 위에 인가(authorization)도 못 건다. 이 빈 칸을 메우는 변형이 \"HTTPS over mTLS\"다. in-mesh leg를 메시 mTLS( ISTIO_MUTUAL )로 묶어 게이트웨이가 SPIFFE 신원을 검증하게 하되, 앱은 평문 HTTP가 아니라 HTTPS를 그대로 보낸다. 결과는 두 개의 TLS 레이어가 중첩된 구조 — 바깥은 mesh mTLS(신원), 안쪽은 앱 HTTPS(기밀성). 전제 개념 셋만 잡고 가면 된다. 개념 한 줄 정의 이 리포트에서의 역할 TLS termination vs passthrough 게이트웨이가 TLS를 푸는가 (terminate) 그냥 흘리는가 (passthrough) 둘의 차이가 라우트 타입( tcp vs tls )을 강제 — 함정 2의 뿌리 SNI TLS handshake가 평문으로 싣는 목적지 호스트명 passthrough면 라우팅 키, terminate면 풀리는 순간 소비 되어 다음 leg에선 못 씀 SPIFFE / mesh mTLS sidecar가 제시하는 워크로드 신원 인증서( spiffe://…/sa/… ) egress-gw가 누가 호출했는지 검증하는 근거 → mTLS/SPIFFE 신원 비교 기준선: Ingress/Egress 검증 리포트 (PASSTHROUGH control) · 운영 주의점 Egress 운영 가이드 · 필드 매뉴얼 src-egress-gateway 1. 핵심 아키텍처 — \"종단하면 SNI가 소비된다\" 머릿속에 둘 그림 하나: egress-gw에서 바깥 TLS는 종단되고 안쪽 TLS는 통과한다 . 바깥(mesh mTLS)을 푸는 순간 그 handshake가 싣고 온 SNI는 그 자리에서 소비 된다. 그래서 그 뒤 leg-2는 더 이상 SNI로 라우팅할 수 없고, 불투명한 바이트( tcp proxy) 로만 흘려야 한다. 이 한 문장이 아래 모든 설정과 두 함정의 원천이다. 그림 1. 이중 TLS — 바깥 메시 mTLS는 egress-gw에서 종단(신원 검증)되지만, 안쪽 앱 TLS는 app부터 cnn까지 end-to-end로 보존된다. 두 레이어를 각각 보면: outer (sidecar↔egress) = Istio mTLS. sidecar가 SPIFFE client cert를 제시하고, egress-gw가 그걸 강제 (requireClientCertificate)하며 mesh CA로 검증한다. 여기서 누가 나가는지가 결정된다. inner (app↔cnn) = 앱 HTTPS. egress-gw는 바깥 mesh TLS만 풀고 안쪽 앱 TLS는 복호화하지 않은 채 tcp_proxy로 cnn까지 전달. 그래서 게이트웨이는 끝까지 평문 payload를 못 본다. 왜 PASSTHROUGH와 ISTIO_MUTUAL이 라우트 타입을 가르나 본질은 \"종단(terminate) 여부\" 한 축이다. 이게 SNI의 운명을 정하고, SNI의 운명이 leg-2의 라우트 타입을 정한다. outer TLS를 푸는가 leg-1에서 SNI는 leg-2 라우팅 키 leg-2 라우트 타입 PASSTHROUGH 안 푼다 (그냥 흘림) 끝까지 살아 있음 SNI( sniHosts ) tls ISTIO_MUTUAL 푼다 (mesh mTLS 종단·SPIFFE 검증) 푸는 순간 소비됨 더 못 씀 → 포트 매칭 tcp (tcp_proxy) PASSTHROUGH는 게이트웨이가 SNI를 끝까지 들고 갈 수 있으니 leg-2도 tls /sniHosts로 받는다. ISTIO_MUTUAL은 종단하는 순간 SNI가 사라지므로, 종단된 listener에는 SNI 기반 network filter가 하나도 안 생긴다. Envoy listener는 filter chain이 0개면 통째로 omit된다(함정 2). 그래서 leg-2는 SNI를 포기하고"
    },
    {
      "title": "Egress Gateway 필드 매뉴얼 — HTTPS passthrough 중심 (출처: LLM 답변 + Istio 1.30 공식 문서)",
      "desc": "머릿속에 둘 단 하나의 그림(ANCHOR): egress gateway는 외부 트래픽을 유도하는 라우팅 수렴점일 뿐 강제 장치가 아니며, 외부 구간의 TLS를 누가 종단하느냐가 게이트웨이의 가시성·앱 변경·암호화 범위를 한꺼번에 결정한다. 앱이 끝까지 TLS를 쥐면(https) 게이트…",
      "url": "/public/istio/gw__src-egress-gateway.html",
      "domain": "istio",
      "text": "istio egress passthrough mtls tls-origination service-mesh Egress Gateway 필드 매뉴얼 — HTTPS passthrough 중심 (출처: LLM 답변 + Istio 1.30 공식 문서) ℹ 이 문서가 다루는 것 머릿속에 둘 단 하나의 그림(ANCHOR): egress gateway는 외부 트래픽을 유도하는 라우팅 수렴점 일 뿐 강제 장치가 아니며, 외부 구간의 TLS를 누가 종단하느냐 가 게이트웨이의 가시성·앱 변경·암호화 범위를 한꺼번에 결정한다. 앱이 끝까지 TLS를 쥐면( https ) 게이트웨이는 SNI만 보는 passthrough (주류), 앱이 평문을 보내면( http ) 게이트웨이가 TLS를 대신 맺는 origination (특수) — 이 한 갈래에서 나머지(가시성·앱 변경·암호화·인증서 위치)가 전부 따라 나온다. - Sidecar 모드 (Ambient 아님) · CRD는 Istio 1.30 / networking.istio.io/v1 기준 (공식 문서 검증, 출처는 문서 끝) - 주류 경로 = 앱이 https:// 로 직접 호출 → 게이트웨이 PASSTHROUGH (섹션 04, 가장 비중 큼) - 특수 경로 = 앱을 http:// 로 바꿔 게이트웨이가 TLS/mTLS를 대신 맺는 origination (섹션 06, 필요할 때만) 대상환경: Istio 1.30 sidecar mesh · on-prem/홈랩(Calico CNI 전제) · 대상독자: egress 경로를 설계·운영하는 DevOps/SRE · 범위: 설계·TLS 모델 정본(운영 deep-dive는 egress 운영 정본 에 위임) · 선행개념: 아래 박스. ℹ 선행 개념 — 이 7개만 손에 쥐면 본문이 풀린다 본문은 이 단어들 위에서 돈다. 모르는 것만 집어 읽고 넘어가라. - TLS / 종단(termination) : 주고받는 내용을 암호화하는 규약( https 의 's'). 암호를 푸는 지점 이 종단인데, 한 연결에서 종단은 단 한 번뿐 이다 — 이 제약이 passthrough vs origination을 가르는 물리 법칙이다. - SNI : TLS를 열 때 평문으로 같이 실리는 목적지 도메인(예: api.partner.example.com). 암호를 못 풀어도 \"어느 호스트로 가는 연결인지\"는 보인다 → passthrough 라우팅의 유일한 단서. - passthrough vs origination : 전자는 암호를 안 풀고 SNI만 보고 통과, 후자는 게이트웨이/사이드카가 외부행 TLS를 직접 새로 맺음 . - L4 / L7 : L4 = 연결(TCP) 수준(\"어디로 얼마나\"), L7 = 요청(HTTP) 수준(\"어떤 요청이 200이었나\"). 게이트웨이가 L7을 보려면 암호를 풀어야 한다. - 사이드카 vs egress gateway : 사이드카 = 앱마다 붙는 Envoy(모든 트래픽 통과). egress gateway = 외부행만 모으는 전용 Envoy. - 메시 자동 암호화 (mesh mTLS, ISTIO_MUTUAL) : 클러스터 안 사이드카끼리를 Istio가 자동 양방향 암호화하는 것. 외부 파트너와의 mTLS와는 다른 구간, 다른 인증서 다(혼동이 모든 오해의 출발점). - 설정 리소스 4종 : ServiceEntry (어떤 외부를 허용·등록) · Gateway (게이트웨이가 받는 포트) · VirtualService (어디로 보낼지 규칙) · DestinationRule (보낸 뒤 TLS·묶음 정책). 01. 배경 — 왜 egress gateway가 존재하나 (해결하는 문제) 사이드카를 주입하면 기본값( outboundTrafficPolicy: ALLOW_ANY )에서 각 Pod는 자기 사이드카를 거쳐 외부로 직접 나간다 — 출구가 노드 수만큼 흩어지고, 누가 어디로 나갔는지 한 곳에 안 남으며, 파트너에게 등록할 출구 IP도 고정되지 않는다. egress gateway가 푸는 문제는 이 흩어진 외부행을 하나의 제어점(choke point)으로 수렴 시키는 것이고, 거기서 네 가지가 파생된다. 동기 무엇을 얻는가 노드 고정 (가장 흔한 목표) 외부 NAT·방화벽 통로를 특정 노드에만 부여. 앱 노드는 외부 라우트가 없고 egress 노드만 인터넷에 도달. 출구 IP를 고정해 파트너 IP allowlist에 등록 가능 관측·감사 외부 접근이 단일 지점을 통과 → 누가·어디로·얼마나 나갔는지 한 곳에서 수집. PCI-DSS 감사 추적에 직접 연결 인증서 집중 파트너가 client 인증서(mTLS)를 요구할 때, 모든 앱이 각자 들고 있는 대신 게이트웨이 한 곳에 모음. 단, 이건 origination(섹션 06)에서만 공격면 축소 침해된 Pod의 데이터 유출 경로를 게이트웨이로 한정. 단, 강제 계층이 따로 받쳐줄 때만 — 섹션 02 대부분의 도입 목적은 첫 줄 — \"외부행을 특정 노드로만\" — 이다. 그리고 그 경우 앱은 보통 이미 https:// 로 외부를 부르고 있어서, 앱을 건드리지 않고 게이트웨이만 얹는 passthrough 가 자연스러운 선택이 된다. ℹ What you might be missing egress gateway는 ingress gateway의 거울상이 아니다. Ingress는 외부 트래픽이 물리적으로 그 LoadBalancer IP를 거칠 수밖에 없어 자연히 강제된다. Egress엔 그런 물리적 강제가 없다 — Pod는 여전히 자기 사이드카에서 임의 목적지로 나갈 수 있다. 공식 문서도 명시한다: \"egress Gateway를 정의하는 것 자체는 그 게이트웨이가 도는 노드에 어떤 특별한 취급도 부여하지 않는다.\" 이 비대칭성이 egress 운영 난이도의 근원이다. 02. 전제 교정 — 게이트웨이는 강제 장치가 아니다 도입 사고가 가장 많이 나는 지점이라 먼저 못을 박는다. \"트래픽을 게이트웨이로 라우팅\" 과 \"트래픽이 게이트웨이를 거치도록 강제\" 는 완전히 다른 레이어의 일이다. VirtualService로 \"이 호스트는 egress gateway를 거쳐라\"라고 쓰면, 그 규칙을 따르는 사이드카는 게이"
    },
    {
      "title": "Egress 외부 endpoint 프로토콜별 Istio 설정 — HTTP vs HTTPS",
      "desc": "외부로 나가는 트래픽의 endpoint가 평문 HTTP일 때와 HTTPS일 때 ServiceEntry / Gateway / VirtualService / DestinationRule을 어떻게 설정하고, 왜 한쪽 설정을 다른 쪽에 그대로 쓰면 깨지는지를 Envoy 필터 체인 수준에서 정…",
      "url": "/public/istio/gw__src-egress-http-vs-https.html",
      "domain": "istio",
      "text": "istio egress http https tls-origination service-entry mental-model Egress 외부 endpoint 프로토콜별 Istio 설정 — HTTP vs HTTPS NOTE 외부로 나가는 트래픽의 endpoint가 평문 HTTP일 때 와 HTTPS일 때 ServiceEntry / Gateway / VirtualService / DestinationRule을 어떻게 설정하고, 왜 한쪽 설정을 다른 쪽에 그대로 쓰면 깨지는지 를 Envoy 필터 체인 수준에서 정리한다. 결론은 한 문장: 외부 endpoint가 첫 바이트에 실제로 말하는 프로토콜 과 ServiceEntry port protocol 이 일치해야 하고, 평문에 TLS 설정을 쓰면 istiod가 그 listener에 거는 필터 체인 자체가 달라져 connection reset·핸드셰이크 실패로 깨진다. (검증 기준 Istio 1.30 / networking.istio.io/v1 , sidecar 모드) 1. 배경 — 왜 \"외부가 무엇을 받느냐\"가 모든 설정을 가른다 egress 설정을 처음 만지면 \"TLS 옵션 몇 개를 켜고 끄는 문제\"처럼 보인다. 그래서 HTTPS 예제에서 동작하던 매니페스트를 평문 endpoint에 복붙하고, protocol: TLS / sniHosts 만 남겨둔 채 connection reset에 빠진다. 이 문서가 풀려는 혼란이 바로 이것이다. 핵심은 egress에서 두 개의 독립된 사실 이 자꾸 한 단어(\"https\")로 뭉뚱그려진다는 점이다. 외부 endpoint가 받는 것 — 파트너 서버가 80에서 평문을 받나, 443에서 TLS를 받나. 이건 상대가 정한 고정값 이다. 앱이 보내는 것 — 우리 앱 코드가 http:// 를 부르나 https:// 를 부르나. 이건 우리가 바꿀 수 있는 값 이다. 이 둘이 같을 필요는 없다. 앱이 http:// 를 보내도 Istio가 중간에서 외부로 새 TLS를 맺어줄 수 있다(origination). 그래서 \"https로 나가야 하나요?\"라는 질문은 사실 \"외부 endpoint가 TLS를 받아주는가?\" 라는, 우리가 못 바꾸는 쪽 사실로 환원된다. 받아주면 TLS를 쓸 수 있고(권장), 안 받아주면 평문 외엔 선택지가 없다. NOTE 선행 개념: ServiceEntry(외부 서비스를 mesh 레지스트리에 등록), TLS origination(앱은 평문, Istio가 외부로 TLS를 새로 맺음), passthrough(앱의 TLS를 Istio가 안 건드리고 SNI로만 라우팅). 이 셋의 CRD 골격은 Egress gateway 매뉴얼 에 정본이 있고, 여기서는 HTTP vs HTTPS 경계 만 깊게 판다. 네 가지 경로로 좌표를 잡기 \"외부 endpoint가 무엇을 받느냐 × 앱이 무엇을 보내느냐\"의 조합이 실무 패턴 네 가지를 만든다. ①②③은 모두 외부가 HTTPS 라 \"TLS를 누가·어디서 종단하느냐\"의 차이일 뿐이고, ④만 외부가 평문 HTTP 라 TLS가 아예 없다 — 이 문서가 추가로 채우는 칸이 ④다. # 외부 endpoint 앱 호출 패턴 외부 구간 암호화 GW L7 가시성 앱 변경 ① HTTPS https:// passthrough end-to-end 유지 ❌ (SNI/L4만) 없음 ② HTTPS http:// mTLS origination (MUTUAL) GW부터 새 TLS ✅ 전부 필요 ③ HTTPS http:// TLS origination (SIMPLE) GW부터 새 TLS ✅ 전부 필요 ④ HTTP (평문) http:// plain HTTP ❌ 없음(평문 노출) ✅ 전부 없음 ②③이 앱을 http:// 로 바꾼다고 ④와 같아지지 않는다 — ②③의 외부 구간은 여전히 TLS다. 이 착각이 평문 노출 사고의 단골 원인이다(→ §6 What you might be missing). 2. 핵심 — protocol 선언이 Envoy 필터 체인을 고른다 멘탈 모델 앵커 한 문장: ServiceEntry/Gateway 포트의 protocol 값은 옵션이 아니라 스위치 다 — istiod가 그 listener(LDS)에 어떤 필터 체인을 박을지 를 고르고, 필터 체인은 입력 첫 바이트의 형태 를 전제한다. 그래서 선언한 protocol과 실제로 들어오는 첫 바이트가 어긋나면, 옵션이 안 맞는 게 아니라 체인 자체가 못 맞물려 깨진다. CR→xDS 멘탈 모델 의 원리 그대로다: CR은 입력, 진실은 Envoy config. protocol 을 바꾸면 같은 포트라도 전혀 다른 listener가 생성된다. protocol 입력 첫 바이트 핵심 필터 체인 라우팅 키 보이는 메트릭 HTTP GET / HTTP/1.1... (평문) HTTP Connection Manager Host 헤더 (RDS) istio_requests_total (method/path/status) TLS TLS ClientHello tls_inspector → tcp_proxy SNI istio_tcp_* 만 (복호화 안 함) HTTPS TLS ClientHello TLS 종단(복호화) → HTTP Connection Manager 복호 후 Host 헤더 istio_requests_total (주로 origination 외부 leg) 이 표가 왜 호환 불가를 만드는지는 첫 바이트 를 보면 즉시 드러난다. 평문 HTTP가 와이어에 처음 흘리는 것은: GET /v1/foo HTTP/1.1\\r\\nHost: api.partner.example.com\\r\\n... 이건 TLS ClientHello가 아니다. 그런데 이 endpoint를 protocol: TLS 로 등록하면 Envoy는 그 포트에 tls_inspector 를 걸고 첫 바이트에서 SNI를 뽑으려 한다 → GET ... 에는 ClientHello 구조가 없으니 SNI 추출 실패 → 매칭되는 filter chain 없음 → connection reset (또는 SNI 없는 filter chain으로 떨어져 미스매치). protocol: HTTPS 로 쓰면 Envoy가 TLS 핸드셰"
    },
    {
      "title": "Egress \"HTTPS over mTLS\" (ISTIO_MUTUAL) — 구조·CRD·장단점·활용·운영",
      "desc": "머릿속에 넣을 한 장의 그림: 이 패턴은 \"end-to-end 암호화 보존\"과 \"egress에서 호출자 신원 식별\"이라는 서로 독립인 두 축의 교집합 칸이다 — 둘 다 Yes일 때만 정답인 좁은 셀. 구현은 이중 TLS: 앱은 그대로 https://(inner end-to-end TL…",
      "url": "/public/istio/gw__src-egress-https-over-mtls.html",
      "domain": "istio",
      "text": "istio egress mtls istio-mutual spiffe double-tls gateway Egress \"HTTPS over mTLS\" (ISTIO_MUTUAL) — 구조·CRD·장단점·활용·운영 NOTE 머릿속에 넣을 한 장의 그림: 이 패턴은 \" end-to-end 암호화 보존 \"과 \" egress에서 호출자 신원 식별 \"이라는 서로 독립인 두 축의 교집합 칸이다 — 둘 다 Yes일 때만 정답인 좁은 셀. 구현은 이중 TLS : 앱은 그대로 https:// (inner end-to-end TLS)로 호출하고, sidecar↔egress 구간만 Istio 메시 mTLS( ISTIO_MUTUAL )로 한 겹 더 감싸 (outer) gateway가 그 outer를 종단 하며 호출 워크로드의 SPIFFE 신원을 암호학적으로 검증 하되, 안쪽 앱 TLS는 풀지 않고 tcp_proxy 로 외부까지 그대로 흘린다. passthrough(신원 없음)와 TLS origination(앱 평문화)의 사각지대를 메우는 패턴이며, 실측 검증은 Egress mTLS 리포트 , 운영 정본은 Egress 운영 , 개념 정본은 Egress Gateway 정본 , 이 신원 위에 올라가는 통제 (AuthorizationPolicy·테스트 매트릭스)는 Egress 신원 기반 통제 가이드 참조. 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico), Istio 1.30.0 검증 도메인: edition.cnn.com (외부 HTTPS) — 기존 httpbin.org passthrough를 control로 보존하고 직접 비교 전제 지식: sidecar/Gateway/VirtualService/DestinationRule 기본 + egress 2-leg 라우팅( 정본 §04) 1. 배경 — 왜 이 패턴이 존재하나 (passthrough와 origination 사이의 빈칸) egress gateway로 외부 HTTPS를 내보내는 방식은, 단 두 개의 독립된 질문으로 완전히 좌표가 잡힌다. 이 두 질문이 이 문서 전체의 출발점이다. gateway가 앱 payload를 복호화해도 되는가? (= end-to-end 암호화를 포기할 수 있나) egress에서 \"누가 나가는지\"를 식별해야 하는가? (= 호출 워크로드 신원이 필요한가) 표준 egress 패턴 두 개는 이 좌표의 대각선 두 칸 만 채운다. PASSTHROUGH : 가장 단순·흔함. gateway는 SNI만 보고 앱 TLS를 그대로 흘린다 → end-to-end는 지켜지지만, in-mesh leg가 평문 TCP라 호출자가 누군지 암호학적으로 모른다 . 상세 정본 , 가이드 HTTPS passthrough 가이드 . TLS origination : gateway가 외부로 TLS를 시작 하므로 앱은 평문 HTTP를 메시에 보내야 한다 → gateway가 L7(method/path/status)을 보고 정책·감사·중앙 cert 관리가 가능하지만, end-to-end 암호화가 깨진다 (payload가 gateway 메모리에 평문으로 존재). 비교 HTTP vs HTTPS . 여기서 빈 칸 이 하나 남는다: \"payload는 절대 못 보게 하면서(end-to-end) + 그래도 누가 나가는지는 알아야 한다.\" passthrough는 신원이 없어서, origination은 복호화를 해서 둘 다 이 칸을 못 채운다. 그 빈 칸을 메우려고 등장한 게 바로 이 \"HTTPS over mTLS\" 패턴 이다. 이 한 문장이 이 패턴이 존재하는 유일한 이유이고, 이후 모든 설계 결정(이중 TLS, ISTIO_MUTUAL, leg-2가 tcp인 것)은 전부 이 칸을 채우기 위한 귀결이다. end-to-end 암호화 보존? Yes No +----------------+----------------+ egress No | PASSTHROUGH | (의미 없음) | 신원 | 가장 흔함 | | 식별? +----------------+----------------+ Yes | HTTPS over mTLS | TLS origination| | <-- 본 문서 | gw L7+cert중앙 | +----------------+----------------+ 그림 1. egress 패턴 선택 결정 트리 — payload 복호화 허용/egress 신원 필요 두 축으로 세 패턴이 갈린다(본 문서는 HTTPS over mTLS). 세 패턴을 한 표로 정렬하면 \"무엇을 누가 푸느냐\"가 한눈에 보인다. gateway가 앱 TLS를 in-mesh leg(sidecar->gw)가 gateway가 본 패턴 복호화하는가 암호+신원검증인가 L7(method/path/status) --------------- ------------------- -------------------------- --------------------- PASSTHROUGH No (SNI만 봄) No (평문 TCP, 앱 TLS만) No (L4 only) HTTPS over mTLS No (이중 TLS) Yes (메시 mTLS 종단+SPIFFE) No (L4 only) <-- 본 문서 TLS origination Yes (gw가 종단) 선택(메시 mTLS는 별개) Yes (L7 visible) 여기서 이 패턴이 niche인 이유까지 미리 못 박아두자 — 뒤의 모든 설계 함정을 받아들이는 마음가짐이 된다. 본 패턴은 위 좌표의 한 칸일 뿐이고, 그 칸에 드는 시나리오 자체가 드물다. 대부분의 \"단순 외부 HTTPS\"는 passthrough로 충분하고(신원 불필요·cert 관리 없음·4객체 중 최단순), 신원·정책이 필요한 조직은 보통 origination을 택한다(egress gateway를 두는 주된 이유 인 L7 감사/per-URL 정책/중앙 cert를 다 주므로). 본 패턴은 payload는 절대 노출 불가 + 신원은 필요 라는 교집합에서만 최적이고, 결정타로 이 패턴을 써도 L7 egress 정책은 여전히 불가 하다(이중 TLS라 gateway가 L7을 못 봄). 추가"
    },
    {
      "title": "Egress Gateway 운영 가이드 — passthrough + ISTIO_MUTUAL (출처: LLM 답변 + Istio 1.30 공식 문서)",
      "desc": "앱이 HTTPS로 직접 호출(end-to-end TLS 유지) + 게이트웨이는 PASSTHROUGH(복호화 안 함) + proxy→egress 구간은 ISTIO_MUTUAL로 mTLS. 결론부터: 이 게이트웨이가 보는 것은 \"암호화된 장기 TCP 스트림\" 하나뿐이고, 모니터링(L4+S…",
      "url": "/public/istio/gw__src-egress-operations.html",
      "domain": "istio",
      "text": "istio egress passthrough monitoring graceful-shutdown operations Egress Gateway 운영 가이드 — passthrough + ISTIO_MUTUAL (출처: LLM 답변 + Istio 1.30 공식 문서) ℹ 대상 구조 앱이 HTTPS로 직접 호출(end-to-end TLS 유지) + 게이트웨이는 PASSTHROUGH(복호화 안 함) + proxy→egress 구간은 ISTIO_MUTUAL로 mTLS. 결론부터: 이 게이트웨이가 보는 것은 \"암호화된 장기 TCP 스트림\" 하나뿐이고, 모니터링(L4+SNI만)·운영 함정(연결을 소수 노드에 모음)·graceful shutdown(요청이 아니라 연결이 닫히길 기다림)이 전부 이 한 사실에서 연역된다. 이 문서는 그 연역을 따라간다. 대상환경 Istio 1.30 / networking.istio.io/v1 · 대상독자 egress를 운영하는 DevOps/SRE · 범위 관측·운영함정·종료. 구조의 조립 은 구조 정본 에 위임 · 선행개념 egress gateway 기본 구성 00. 배경 — 왜 이 게이트웨이는 운영이 다른가 일반 게이트웨이(ingress, 또는 TLS를 종단하는 egress)는 트래픽을 복호화해서 안을 본다. HTTP method·path·status code가 다 보이니, 모니터링은 istio_requests_total 로 에러율을 보고, 정책은 path 기반 라우팅을 걸고, graceful shutdown은 \"in-flight 요청 이 끝나길\" 기다린다. 이 모든 운영 상식이 L7이 보인다는 전제 위에 서 있다. 그런데 이 구조는 그 전제를 의도적으로 깬다. 앱이 외부와 end-to-end TLS 를 맺고(게이트웨이가 중간에서 복호화하면 그 보장이 깨지므로), 게이트웨이는 PASSTHROUGH — 암호문 봉투를 뜯지 않고 그대로 중계한다. 왜 그렇게 하나: 외부 파트너에 대한 기밀성을 게이트웨이가 신뢰 경계에서 빼앗지 않으면서도, egress를 소수 노드로 강제 경유 시켜 출구 IP를 고정(파트너 방화벽 allowlist)하고 중앙에서 관측·통제하려는 것이다. proxy→egress 안쪽 한 구간만 ISTIO_MUTUAL로 감싸 메시 내부 신원을 입힌다. 이 절충의 대가가 바로 운영의 차이다. 게이트웨이는 자기가 나르는 트래픽의 내용을 원리적으로 알 수 없다 — 복호화를 안 하기로 설계했으니까. 그래서 L7에 기대던 운영 상식이 전부 한 단계 내려가야 한다. 무엇이 어떻게 내려가는지가 이 문서의 전부다. 01. 핵심 — \"암호화된 장기 TCP 스트림\" 하나에서 모든 게 따라 나온다 ℹ 머릿속 앵커 (이거 하나만) 게이트웨이 입장에서 트래픽은 요청이 아니라 연결이다. 외부 TLS를 안 풀기 때문에 L7(HTTP)이 없고, 한 번 맺힌 암호화 TCP 스트림이 오래 살아 있다. 이 그림에서 세 가지 운영 특성이 기계적으로 연역된다 — ①관측은 L4+SNI만 ②연결을 소수 노드에 모으니 연결 단위 자원이 병목 ③종료는 연결이 닫히길 기다림. 세 갈래로 펼쳐 보면: 그림 1. 앵커 — 게이트웨이가 보는 것은 암호화된 장기 TCP 스트림 하나뿐. ①관측 ②운영 함정 ③종료가 전부 여기서 연역된다 세 갈래의 공통 뿌리 가 하나(앵커)라는 게 핵심이다. 아래 섹션은 각 갈래를 \"앵커에서 왜 이렇게 되는가\"로 전개한다. 갈래마다 디테일이 다르지만, 막힐 때마다 앵커로 돌아오면 답이 나온다. 02. 갈래 1 — 모니터링: L7이 없으니 관측이 한 단계 내려간다 앵커에서 곧장 따라 나오는 제약을 한 줄로: 게이트웨이에서는 HTTP 메트릭(요청 수, 상태 코드, 지연)이 나오지 않는다. 외부 TLS를 종단하지 않으니 Envoy가 그 트래픽을 TCP로만 인식해서다. 그래서 게이트웨이 메트릭은 전부 istio_tcp_* 계열이다. \"요청이 없고 연결만 있다\"가 메트릭 이름표에 그대로 박힌다. 게이트웨이에서 나오는 메트릭 (L4) 메트릭 의미 istio_tcp_connections_opened_total 열린 연결 수 — \"어느 외부로 몇 번 연결했나\" istio_tcp_connections_closed_total 닫힌 연결 수 — opened와의 차이로 현재 활성 연결 추정 istio_tcp_sent_bytes_total / istio_tcp_received_bytes_total 송수신 바이트 — 트래픽 양·이상 급증 탐지 비-HTTP egress 트래픽의 표준 관측이 바로 이 TCP 메트릭이다. 게이트웨이 워크로드를 기준으로 집계하면 \"egress 게이트웨이를 통과한 전체 외부 연결\"을 중앙에서 볼 수 있다. # 외부 호스트별 신규 연결률 (source = egress 게이트웨이) sum(rate(istio_tcp_connections_opened_total{ source_workload=\"istio-egressgateway\" }[5m])) by (destination_service_name) # 외부 호스트별 송신 바이트율 sum(rate(istio_tcp_sent_bytes_total{ source_workload=\"istio-egressgateway\" }[5m])) by (destination_service_name) # 현재 활성 연결 추정 (opened - closed 누적) sum(istio_tcp_connections_opened_total{source_workload=\"istio-egressgateway\"}) - sum(istio_tcp_connections_closed_total{source_workload=\"istio-egressgateway\"}) ⚠ opened−closed의 한계 *_opened_total / *_closed_total 은 monotonic counter 라 Pod 재시작·카운터 리셋 시 두 합의 차가 음수나 과대값으로 튄다. 또 두 합을 다른 라벨 집합으로 묶으면 매칭이 어긋난다. 정밀한 활성 연결 은 게이지를 직접 보는 편이 정확하다 — Prometheus(15090)에서는 envoy_cluster_upstream_cx_activ"
    },
    {
      "title": "Egress TCP 병목 정본 — L4 proxy가 연결을 모으면 무엇이 고갈되는가",
      "desc": "egress gateway 도입은 mTLS 여부와 무관하게 L4 proxy 하나를 전사 외부행 경로에 끼워 넣는 일이다. 이 문서는 그때 발생하는 TCP 병목 5종을 — Envoy 연결 상한 → ephemeral port/TIME_WAIT → conntrack → 중간장비 half-o…",
      "url": "/public/istio/gw__src-egress-tcp-bottlenecks.html",
      "domain": "istio",
      "text": "istio egress tcp connection-pool port-exhaustion conntrack keepalive snat Egress TCP 병목 정본 — L4 proxy가 연결을 모으면 무엇이 고갈되는가 NOTE egress gateway 도입은 mTLS 여부와 무관하게 L4 proxy 하나를 전사 외부행 경로에 끼워 넣는 일 이다. 이 문서는 그때 발생하는 TCP 병목 5종을 — Envoy 연결 상한 → ephemeral port/TIME_WAIT → conntrack → 중간장비 half-open → drain — 산술적 한계 수치와 부딪히는 순서 까지 박아서 정리하고, 완화 운영값 전체(YAML), reset 원인 분기 런북, Prometheus 알람까지 한 곳에 둔다. 모니터링 일반론·graceful shutdown 상세는 Egress 운영 정본 에 있고, 이 문서는 그 §03(연결 단위 자원 병목)을 수치·처방 레벨로 전개한 심화다. 대상 환경 : Istio 1.30 sidecar mode, egress gateway = tcp_proxy (PASSTHROUGH 또는 ISTIO_MUTUAL — 어느 쪽이든 동일 적용). 대상 독자 : gateway를 운영하게 될 사람, 도입 전 캐파 산정을 해야 하는 사람. 선행 개념 : Egress 4-CRD 멘탈모델 . 01. 멘탈모델 — 문제의 뿌리는 단 하나 L4 proxy는 암호화된 바이트 파이프다. 앱 연결 1개 = gateway 소켓 2개(1:1, 재사용 불가)이고, gateway 도입은 전사에 분산되어 있던 출발지 IP 다양성을 의도적으로 소수 pod로 붕괴시킨다. 이 한 문장에서 병목 전부가 연역된다. \"1:1 + 집중\"이라는 구조가 연결 단위로 세는 모든 자원(Envoy 풀, ephemeral port, conntrack 엔트리, FW 세션 테이블)을 동시에 압박한다. [before] [after] pod1(IP1) ----\\ pod1 --\\ pod2(IP2) -----> partner-a:443 pod2 ---> gw1(IPa) --> partner-a:443 ... ----/ ... --/ gw2(IPb) --> pod500(IP500) pod500-/ port space: 28k x 500 port space: 28k x 2 포트 고갈은 새로 생기는 문제가 아니다. 원래 전사에 분산돼 있던 자원(src IP 다양성)을 gateway가 보안상 이득(방화벽 등록 IP 축소)과 맞바꿔 포기 하는 구조라서 따라오는 청구서다. 동전의 양면이라는 점이 도입 보고서에 적혀야 할 문장이다. 02. 기본 산술 — 4-tuple에서 한계 수치가 나온다 TCP 연결 하나는 커널 입장에서 (srcIP, srcPort, dstIP, dstPort) 4-tuple로 식별된다. 4개가 모두 같은 연결은 동시에 2개 존재할 수 없다. 1 connection = ( srcIP, srcPort, dstIP, dstPort ) | | | | gw pod 29,xxx partner-a 443 (fixed) (variable) (fixed) (fixed) 외부 API 호출에서 dstIP:443은 고정, srcIP도 그 pod의 IP로 고정 — 변수는 srcPort 하나뿐 이다. 커널이 발신 연결에 자동으로 빌려주는 이 포트(ephemeral port)의 리눅스 기본 범위는 32768–60999 , 약 28,232개 . usable srcPort pool (per srcIP, per destination) |--------------- 28,232 ports ----------------| 32768 60999 한계 ① 동시 연결 : gw pod 1개 → 목적지 1개의 동시 연결 상한 ≈ 28,000 . 설정이 아니라 산술이다. 한계 ② 신규 연결률 : 연결이 닫혀도 포트는 즉시 반환되지 않는다. 먼저 close한 쪽이 그 4-tuple을 TIME_WAIT 상태로 60초 (커널 고정) 보존하고, proxy는 보통 먼저 닫는 쪽이라 TIME_WAIT는 gateway에 쌓인다. t=0s t=5s t=65s connect --- close ---------- port returned |--- TIME_WAIT (60s) ---| port 29314 locked for this dst 포트 하나가 \"사용 시간 + 60초\"를 점유하므로, short-lived 연결이 계속 생기면: 28,232 ports ÷ 60s ≈ 470 new connections/sec (gw pod 1개 -> 목적지 1개) 구체 예: pod 500개가 keep-alive 없이 각자 초당 1회 호출하면 gw에 500 conn/s가 모인다. gw pod 1개면 470/s를 넘어 수십 초 안에 EADDRNOTAVAIL (빌려줄 포트 없음)이 나기 시작한다. 처방의 우선순위가 ① 앱 keep-alive(분자 줄이기) ② replica 증설(분모 늘리기) ③ tcp_tw_reuse (60초 규칙 완화) 순인 이유가 이 분수식이다. 03. 왜 연결을 재사용 못 하나 — L7 vs L4의 본질 차이 \"gateway가 외부행 연결을 모아서 재사용(pooling)하면 되지 않나\"가 자연스러운 반론인데, 여기서 L7/L4 proxy의 근본 차이가 갈린다. [L7 proxy (HTTP)] [L4 proxy (tcp_proxy) = egress gw] client A --\\ client A ===[encrypted bytes]===> upstream A client B ---> [pool: conn x2] --> client B ===[encrypted bytes]===> upstream B client C --/ upstream client C ===[encrypted bytes]===> upstream C (요청 경계를 아니까 섞어 태움) (바이트 파이프. 섞으면 TLS 세션 깨짐) L7 proxy는 요청의 경계를 알기 때문에 여러 클라이언트의 요청을 적은 수의 upstream 연결에 다중화할 수 있다. 그러나 egress gateway가 다루는 것은 앱↔외부 서버가 종단간으로 맺은"
    },
    {
      "title": "Istio Sidecar CRD 적용 범위(scope) 설정 방법",
      "desc": "Sidecar 리소스는 한 워크로드 Envoy가 받는 설정을 좁혀 config를 경량화하고, outboundTrafficPolicy와 결합해 egress 거버넌스를 만든다. 멘탈모델 하나: scope는 Mesh / Namespace / Workload 세 단계지만 어느 Pod에든 적용…",
      "url": "/public/istio/gw__src-sidecar-scope.html",
      "domain": "istio",
      "text": "istio sidecar egress Istio Sidecar CRD 적용 범위(scope) 설정 방법 NOTE Sidecar 리소스는 한 워크로드 Envoy가 받는 설정을 좁혀 config를 경량화 하고, outboundTrafficPolicy 와 결합해 egress 거버넌스 를 만든다. 멘탈모델 하나: scope는 Mesh / Namespace / Workload 세 단계지만 어느 Pod에든 적용되는 Sidecar는 정확히 하나 — 가장 좁은 것이 통째로 이긴다 (merge 아님, override). 이 문서는 그 override 의미론을 세 scope의 완전한 YAML(주석 포함) 로 따라 만들고, REGISTRY_ONLY 차단을 실측으로 굳힌다. 개념 전반은 Sidecar scope 개념 노트 에. 대상독자: 메시 규모가 커져 istiod push 비용이 보이기 시작했거나, egress를 \"기본 deny\"로 잠그려는 SRE. 선행개념: xDS push 모델(CDS/LDS/RDS/EDS), service registry, ServiceEntry. 환경: Istio 1.30, Envoy. cluster 이름 규칙 direction|port|subset|fqdn . 범위: Sidecar의 3 scope YAML 전문 + 판정 규칙 + REGISTRY_ONLY 검증. 차단 메커니즘의 왜 는 개념 노트로 위임. 1. 배경 — Sidecar 리소스가 푸는 문제 Istio의 기본 가정은 \"메시 안 모든 서비스가 다른 모든 서비스에 접근 가능하다\"이다. 편의성의 산물이다 — 개발자가 아무 서비스나 이름으로 부르면 곧장 라우팅된다. 대가는 데이터 평면 쪽이 치른다: istiod 는 그 워크로드가 실제로 무엇을 부르는지 알 길이 없으니, 보수적으로 메시 전체 설정 (모든 cluster/endpoint/listener)을 각 Envoy에 푸시한다. 200개 워크로드 규모만 돼도 사이드카 하나당 설정이 수 MB에 이르고, 이것이 메모리·xDS push· istiod CPU를 동시에 압박한다. 핵심 질문은 \"왜 Envoy는 자기가 부르지도 않는 서비스의 설정까지 받아야 하나?\"이다. 받을 이유가 없다 — istiod가 의존 대상을 모르기 때문일 뿐이다. Sidecar 리소스(CRD)는 운영자가 바로 그 정보를 명시적으로 주입하는 통로이고, 그것으로 두 방향의 문제를 동시에 푼다: 설정 범위 축소(performance) — egress.hosts 로 \"이 워크로드가 실제로 호출하는 대상\"만 선언하면, istiod가 그 사이드카에 보내는 설정 크기가 극적으로 줄고( Istio in Action 11장: 2MB → 644KB), registry 변경이 scope 밖이면 push 자체가 발생하지 않는다. 실측은 §4. 컨트롤 플레인을 좌우하는 다른 요인은 컨트롤 플레인 성능 요인 . 트래픽 거버넌스(security) — outboundTrafficPolicy.mode: REGISTRY_ONLY 와 결합하면 메시 레지스트리에 등록되지 않은 외부 호출을 차단하는 zero-trust egress 기본값이 된다. ℹ Sidecar 리소스는 \"보안 정책\"이자 동시에 \"성능 최적화 도구\"다. `Istio in Action` 11장이 컨트롤 플레인 성능 튜닝의 첫 권고로 \"항상 Sidecar 리소스를 정의하라\"고 강조하는 이유가 이것이다. 2. 핵심 멘탈모델 — '가장 좁은 하나가 통째로 이긴다' 머릿속에 그릴 단 하나의 그림(ANCHOR): Sidecar는 \"이 Pod가 받을 설정\"을 정하는 override 계층이다. 어느 Pod에든 적용되는 Sidecar는 항상 정확히 하나 — 가장 좁은 범위가 이긴다 — 그리고 그 하나가 egress.hosts (무엇을 알게 할지=범위)와 outboundTrafficPolicy (모르는 곳을 어떻게 처리할지=차단)라는 독립된 두 손잡이를 함께 든다. 이 한 문장에서 나머지가 전부 따라 나온다. 좁은 범위가 넓은 범위에 \"합쳐지는\" 게 아니라 통째로 갈아치운다 는 것, 그리고 범위(성능)와 차단(거버넌스)이 같은 리소스 안의 다른 손잡이 라는 것 — 이 둘이 거의 모든 오해의 진원지다. 세 scope와 우선순위: 범위 우선순위 적용 대상 주된 활용 판정 키 Mesh-wide ① 가장 낮음 메시 안 모든 Pod egress 기본 차단, 공통 기본값 강제 rootNamespace + name: default + selector 없음 Namespace-wide ② 해당 NS 모든 Pod 팀별 규칙(내부 호출만 허용) 대상 NS + workloadSelector 없음 Workload-specific ③ 가장 높음 selector 일치 특정 Pod 민감 서비스만 추가 egress 허용 workloadSelector.labels 지정 좁은 범위가 넓은 범위를 덮어쓴다 (Workload > Namespace > Mesh). 직관적으로는 \"더 구체적인 규칙이 일반 규칙 위에 얹힌다(덮어쓴다)\"고 기대하기 쉬운데, 여기선 일반 규칙이 통째로 사라진다 . 한 Pod가 어떤 Sidecar를 적용받는지 결정 흐름: 그림 1. effective Sidecar 결정 흐름 — workloadSelector 일치 시 그 Sidecar만(병합 없음), 없으면 NS default, 그것도 없으면 rootNamespace mesh-wide default, 모두 없으면 full mesh config가 통째로 push되는 무거운 default로 떨어진다(override 의미론). 이 흐름도가 override의 실체다 — 매칭은 위에서 아래로 첫 hit 하나에서 멈춘다 . workload가 match하면 NS/mesh default는 아예 보지 않는다. §3-3의 누락 함정이 여기서 나온다. ℹ \"Mesh-wide는 `istio-system` 네임스페이스에 둔다\"는 규칙의 정확한 근거. mesh-wide로 동작하는 조건은 \" 메시 루트 네임스페이스(기본값 istio-system )에 있고, 이름이 default 이며, workloadSelector 가 없을 것 \"이다. 단순히 istio-system 에 둔"
    },
    {
      "title": "Egress 신원 기반 통제 — mTLS 신원이 공용 gateway에 멀티테넌시를 만든다",
      "desc": "\"egress gateway만 세우면 외부 통신이 통제된다\"는 직관의 빈칸을 메우는 문서. 방화벽·NetworkPolicy는 \"외부로 나가는 유일한 경로가 gateway인가\"(Q1) 에만 답하고, \"그 gateway를 누가, 어느 목적지로 쓸 수 있나\"(Q2) 에는 답하지 못한다. Q…",
      "url": "/public/istio/sec__guide-egress-mtls-identity-control.html",
      "domain": "istio",
      "text": "istio egress mtls spiffe identity authorizationpolicy security multi-tenancy Egress 신원 기반 통제 — mTLS 신원이 공용 gateway에 멀티테넌시를 만든다 NOTE \"egress gateway만 세우면 외부 통신이 통제된다\"는 직관의 빈칸을 메우는 문서. 방화벽·NetworkPolicy는 \"외부로 나가는 유일한 경로가 gateway인가\"(Q1) 에만 답하고, \"그 gateway를 누가, 어느 목적지로 쓸 수 있나\"(Q2) 에는 답하지 못한다. Q2의 판정 재료가 mesh mTLS가 운반하는 SPIFFE 신원 이고, 판정 장치가 gateway 위의 AuthorizationPolicy(principal × SNI) 다. 이 문서는 ① 그 논리를 통제 체인(경로 강제 → 검문소 → 신원 판정)으로 세우고 ② SA 2개가 서로 다른 목적지만 허용받는 테스트 클러스터 전체 구성 (YAML 주석 포함)과 검증·함정까지 따라간다. 이중 TLS 구조 자체의 해부는 HTTPS over mTLS 정본 , 4-CRD 직관은 Egress 4-CRD 멘탈모델 이 정본 — 본 문서는 그 구조 위에 올라가는 \"통제\" 를 다룬다. 대상 환경: Istio 1.30.0 , sidecar mesh, Helm gateway chart (테스트 클러스터) 대상 독자: egress gateway를 보안 요건(최소권한·감사 추적) 충족 수단으로 도입하려는 DevOps/SRE 범위: 신원 기반 통제의 논리 → 테스트 클러스터 구성 → 검증 매트릭스 → 설계 결정 노트(분리 전략·TCP·wildcard) 선행 개념: egress 2-leg 라우팅( 4-CRD 멘탈모델 ), SPIFFE 신원( 정본 ), AuthorizationPolicy 평가( 멘탈모델 ) 1. 멘탈모델 한 문장 — 통제는 체인이고, 신원은 그 마지막 고리다 강제(enforcement)는 경로가 하고(NetworkPolicy·방화벽), 판정은 검문소(gateway)가 하며, 판정에 \"누가\"를 공급하는 유일한 장치가 sidecar↔gateway 구간의 mesh mTLS(SPIFFE 인증서)다. NetworkPolicy / IDC firewall : \"egress gw 경유 외 외부행 차단\" (경로 강제, L3/L4) | v egress gateway : 유일한 검문소가 됨 | v 검문소에서 \"누가 -> 어디로\" 판정 필요 <- 여기서 신원(SPIFFE)이 등장 이 체인에서 어느 고리 하나만 빠져도 통제가 성립하지 않는다. 경로 강제 없이 gateway만 → 침해된 pod가 sidecar를 우회(iptables 조작, UID 1337)해 직접 나간다. Istio 레이어는 우회를 못 막는다. 검문소까지 만들고 신원 없이 → gateway가 보는 건 source pod IP(휘발)와 SNI(클라이언트 제공 값)뿐. gateway에 도달 가능한 모든 pod가 gateway에 설정된 모든 경로를 쓸 수 있다. PG사 경로를 하나 뚫는 순간 전사 모든 워크로드가 PG사로 나갈 수 있는 상태가 된다. 신원 기반 authz는 결국 \"전용 gateway N개를 정책 N줄로 치환\" 하는 장치다. 물리 분리 없이 공용 gateway 위에 논리적 멀티테넌시를 만드는 것 — 이게 이 패턴의 본질이다. 2. 왜 신원인가 — 흔한 두 반론을 메커니즘으로 해소 반론 1: \"ServiceEntry로 특정 namespace에서 외부 주소를 못 보게 막으면 되지 않나\" ServiceEntry(+exportTo)·REGISTRY_ONLY는 차단 장치가 아니라 설정 배포 범위 제어다. \"team-a namespace에는 이 외부 호스트 설정을 안 내려준다\"일 뿐이고, 집행 주체가 클라이언트 자신의 sidecar 라는 게 구조적 약점이다. sidecar는 pod와 같은 network namespace에 있어서, root 권한을 가진 침해 pod는 sidecar를 건너뛰고 직접 나갈 수 있다. Istio 공식 Security Best Practices 가 REGISTRY_ONLY를 보안 경계가 아닌 best-effort 로 간주하라고 명시하는 이유다. 그래서 집행은 공격자가 통제할 수 없는 지점 (CNI NetworkPolicy, IDC 방화벽, gateway)으로 옮겨야 한다. 반론 2: \"어차피 egress 노드가 아니면 방화벽에서 막히는데, 그걸로 충분하지 않나\" 절반은 맞다. 방화벽과 신원은 다른 질문에 답하는 장치 라서 분리해야 한다. Q1. 외부로 나가는 유일한 경로가 gateway인가? -> 방화벽이 답함 (O) Q2. gateway를 \"누가\" \"어느 목적지로\" 쓸 수 있나? -> 방화벽은 못 답함 방화벽이 보는 건 gateway 노드 CIDR → 대외기관 IP 뿐이다. 모든 워크로드가 같은 SNAT IP로 나가니 app-a와 app-b를 구분할 수 없고, gateway Service는 ClusterIP라 클러스터 내 모든 pod가 도달 가능하다. 보안 요건이 \"모든 외부 통신은 통제된 경로 + 전사 공통 allowlist\"까지라면 passthrough + 방화벽으로 충분 하다. 요건에 워크로드 단위 최소권한(least privilege)과 주체 식별 가능한 감사 추적 이 포함될 때 비로소 신원이 필요해진다 — 그 판정을 할 수 있는 유일한 지점이 gateway이고, 판정 재료가 신원이기 때문이다. ✓ A/B 선택의 실제 분기점 \"passthrough(A)냐 이중 TLS(B)냐\"는 기술 선호가 아니라 사내 보안 심사 요건 문서에 \"워크로드별 차등 통제·주체 식별\"이 명시되어 있는지 로 판가름한다. 없으면 A로 시작하고, 요건이 생기면 같은 토폴로지에서 Gateway tls.mode + DR trafficPolicy + AuthorizationPolicy만 추가 하는 증분 적용이 가능하다( 델타 3곳 ). 같은 환경에서 신원을 CNI pod-selector로 대신 식별하는 반대 선택의 근거는 이중 TLS 없는 egress 신원 . 신원이 있을 때만 가능해지는 통제 제어 내용 신원 없이 가능? 워크로드별 목적"
    },
    {
      "title": "Istio는 SPIFFE 표준으로 워크로드 신원을 X.509 SVID의 SAN에 박아 mTLS 인증의 토대로 삼는다",
      "desc": "Istio의 workload identity는 IP·hostname·header가 아니라 SPIFFE ID(spiffe://<trust-domain>/ns/<ns>/sa/<sa>)다. 이 ID는 X.509 인증서(SVID)의 SAN(URI type) 에 박혀 발급되고, mTLS han…",
      "url": "/public/istio/sec__note-mtls-spiffe-identity.html",
      "domain": "istio",
      "text": "istio mtls spiffe identity sds Istio는 SPIFFE 표준으로 워크로드 신원을 X.509 SVID의 SAN에 박아 mTLS 인증의 토대로 삼는다 ℹ 이 문서가 다루는 것 Istio의 workload identity는 IP·hostname·header가 아니라 SPIFFE ID ( spiffe://<trust-domain>/ns/<ns>/sa/<sa> )다. 이 ID는 X.509 인증서(SVID)의 SAN(URI type) 에 박혀 발급되고, mTLS handshake에서 양측이 서로의 SVID를 trust bundle(CA root)로 검증한다. 검증된 SPIFFE ID가 곧 source.principal 로 이어져 인가(authz)의 입력이 된다. 즉 mTLS는 \"암호화\"가 아니라 검증 가능한 신원의 운반 수단 이며, 그 신원이 없으면 principal 기반 정책 자체가 성립하지 않는다. 인증서는 istio-agent가 SDS 로 메모리상에서 발급·로테이션하므로 디스크에 평문 key가 떨어지지 않는다. 이 note는 SPIFFE/SVID/SDS의 개념·멘탈모델 을 정본으로 다룬다. 이 신원이 어떻게 정책 입력으로 평가되는가 (authz 평가 순서, mTLS 유무별 가능 조건, egress passthrough)는 AuthorizationPolicy 멘탈모델 을, 세 리소스(PeerAuthentication·RequestAuthentication·AuthorizationPolicy)의 역할 분담은 보안 리소스 trio 를 본다. 01. 배경 — 왜 신원이 먼저인가 (IP/header/JWT를 못 믿는 이유) 분산 mesh에서 서버가 \"이 요청을 보낸 client가 누구인가\"를 판단할 근거는 빈약하다. 가진 후보를 하나씩 떨어뜨려 보면 왜 mTLS만 남는지 보인다. IP? NAT·proxy·Pod 재생성으로 휘발. spoof 가능. L3 신원은 약함. header? app 레벨에서 위조 가능. 누구나 X-User를 채울 수 있음. JWT? end-user(사람) 신원엔 적합하나 service-to-service 신원과는 별개 축. mTLS? sidecar가 workload 신원으로 발급받은 cert를 handshake에서 상호 검증. 서비스 간(service-to-service) 인증에는 암호학적으로 위조 불가능한 신원이 필요하다. mTLS의 X.509 cert는 CA 서명으로 위조를 막고, 그 cert 안에 신원 문자열을 박아 운반한다. 이 신원 문자열의 표준 포맷이 SPIFFE ID 다. 이 배경에서 이 문서 전체를 관통하는 멘탈모델 한 줄 이 나온다. 신원 발급 → mTLS로 검증 → 검증된 신원으로 인가 — 이 단방향 파이프라인이 척추다. 인가(authz)는 신원이 확립된 다음에야 의미를 갖는다. principal 기반 정책은 그 앞 단계인 mTLS가 신원을 운반해 줘야만 입력값이 생긴다. 그림 1. 신원의 일생. agent가 SA token으로 CSR을 보내 CA에게 SVID를 받고(발급), mTLS handshake에서 peer가 그 SVID를 제시하면 trust bundle로 검증해 SPIFFE ID를 추출하며(검증), 그 ID에서 scheme을 떼낸 source.principal로 RBAC이 ALLOW/DENY를 판단한다(인가). 이 파이프라인을 따라가며 채워 넣는다 — 신원이 무엇이고 (§02 포맷, §03 SVID/SAN), 애초에 그 cert가 어떻게 손에 들어오며 (§04 SDS 발급·로테이션), 어떻게 검증되고 (§05 trust bundle, §06 secure naming), 이 모든 게 일반 TLS와 왜 다른가 (§07). 02. SPIFFE ID 포맷과 Kubernetes 매핑 SPIFFE(Secure Production Identity Framework For Everyone)는 워크로드 신원의 이름 규칙 을 정한 표준이다. Istio가 발급하는 SPIFFE ID는 URI다. spiffe://<trust-domain>/ns/<namespace>/sa/<service-account> 예: spiffe://cluster.local/ns/default/sa/productpage spiffe://cluster.local/ns/payments/sa/payment-api spiffe://cluster.local/ns/inference/sa/model-gateway 구성요소 출처 의미 trust-domain mesh 설정(기본 cluster.local ) 신뢰 경계. 같은 trust-domain끼리 root CA를 공유 ns/<namespace> Pod의 namespace 격리 단위 sa/<service-account> Pod의 ServiceAccount 신원의 실질 주체 핵심은 Istio 신원이 Pod·IP가 아니라 Kubernetes ServiceAccount에 1:1로 묶인다 는 점이다. 같은 SA를 쓰는 Pod가 10개로 스케일아웃해도 신원은 하나다. 따라서 \"신원을 분리하려면 ServiceAccount를 분리한다\"가 운영 제1원칙이 된다 — Deployment마다 전용 SA를 주는 습관이 곧 최소권한 정책의 토대다. ★ authz 표기와의 차이 AuthorizationPolicy의 source.principal 에는 spiffe:// scheme을 떼고 cluster.local/ns/default/sa/productpage 형태로 쓴다. cert SAN에는 spiffe://... 전체가 들어가지만, 정책 비교 시점에는 scheme이 제거된 값이 principal이다(§01 파이프라인의 \"scheme 제거\" 화살표). 이 표기 불일치를 모르면 정책이 조용히 매칭 실패한다. 03. SVID — 신원을 어디에 박는가 (URI SAN) 신원을 운반하는 인증서를 SPIFFE에서는 SVID (SPIFFE Verifiable Identity Document)라 부른다. Istio는 X.509 형태의 SVID를 쓰며, SPIFFE ID는 cert의 Subject Alternative Name 중 URI 타입 에 들어간다(CN이 아"
    },
    {
      "title": "Istio 보안은 PeerAuthentication·RequestAuthentication·AuthorizationPolicy 세 리소스가 \"transport 인증 · end-user 인증 · 인가\"의 역할을 분담한다",
      "desc": "Istio의 워크로드 보안은 한 리소스가 아니라 세 CRD의 분업으로 성립한다. PeerAuthentication은 서비스 간 transport 계층(mTLS)을 강제해 peer identity(SPIFFE)를 확보하고, RequestAuthentication은 요청에 실린 JWT를…",
      "url": "/public/istio/sec__note-security-resource-trio.html",
      "domain": "istio",
      "text": "istio security peerauthentication requestauthentication authorizationpolicy Istio 보안은 PeerAuthentication·RequestAuthentication·AuthorizationPolicy 세 리소스가 \"transport 인증 · end-user 인증 · 인가\"의 역할을 분담한다 ℹ 이 문서가 다루는 것 Istio의 워크로드 보안은 한 리소스가 아니라 세 CRD의 분업 으로 성립한다. PeerAuthentication은 서비스 간 transport 계층(mTLS)을 강제해 peer identity (SPIFFE)를 확보하고, RequestAuthentication은 요청에 실린 JWT를 검증해 end-user identity (claim)를 추출하며, AuthorizationPolicy는 앞 둘이 채워준 attribute를 입력받아 허용/차단 을 결정한다. 이 문서가 박으려는 단 하나의 멘탈모델: 앞의 두 리소스는 신원을 증명·추출 만 하고 자기 자신은 거의 차단하지 않는다 — 실제 게이트는 AuthorizationPolicy 뿐 이다. 운영 사고의 대부분은 이 역할 경계를 혼동(PeerAuthentication STRICT를 인가로 착각, RequestAuthentication만 걸고 AuthorizationPolicy 누락)하는 데서 나온다. 00. 배경 — 왜 인증·인가가 \"세 조각\"으로 쪼개졌나 보안 질문을 한 줄로 줄이면 \" 이 요청을 처리해도 되는가 \"다. 그런데 이 한 줄은 사실 성격이 전혀 다른 세 개의 질문을 포개 놓은 것이다. AuthN (인증) — \"너는 정말 너인가?\" ← 신원을 증명하는 단계 ├─ peer: 이 요청을 *물어다 준 워크로드*가 누구인가? (service-to-service) └─ end-user: 이 요청을 *애초에 일으킨 사람*이 누구인가? (browser/user) AuthZ (인가) — \"그래서 이걸 해도 되는가?\" ← 증명된 신원으로 허용/차단 여기서 두 가지 직교(orthogonal)하는 분리가 일어난다. 이 직교성이 세 리소스로 쪼개진 근본 이유다. AuthN과 AuthZ의 분리. 신원을 증명하는 일 과 그 신원으로 결정을 내리는 일 은 별개다. 증명은 암호학(cert 서명 검증, JWT signature 검증)의 영역이고, 결정은 정책(누가 무엇을 해도 되는가)의 영역이다. 한 리소스에 섞으면 \"인증을 켰으니 막힌 거겠지\"라는 치명적 착각이 생긴다 — 인증은 누구인지 를 알려줄 뿐, 막는 건 인가다. peer와 end-user의 분리. 같은 payment-api 워크로드(peer)가 보낸 connection 하나 안에서도, 그 위에 실린 JWT의 사용자(end-user)는 요청마다 다르다. 서비스 신원은 L4 transport(mTLS cert)에 박히고, 사용자 신원은 L7 헤더( Authorization: Bearer )에 실린다 — 물리적으로 다른 계층에 있으니 다른 메커니즘으로 확보해야 한다. 세 리소스는 정확히 이 세 칸을 하나씩 맡는다. 그래서 셋은 경쟁 하지 않고 조립 된다. 질문 답하는 리소스 어디서(계층) 물어다 준 워크로드가 누구? PeerAuthentication L4 transport (mTLS cert) 일으킨 사용자가 누구? RequestAuthentication L7 (JWT 헤더) 그래서 허용? 차단? AuthorizationPolicy L7/L4 RBAC ℹ 선행 개념 이 문서는 mTLS가 어떻게 SPIFFE identity를 peer cert SAN에 박는지는 SPIFFE workload identity 에, AuthorizationPolicy의 deny-by-default 평가 내부는 AuthorizationPolicy 멘탈모델 에 위임한다. 여기서는 세 리소스가 어떻게 분업·의존하는지 의 큰 그림만 다룬다. 대상환경: Istio 1.30, sidecar 모드. 01. 핵심 아키텍처 — \"채우는 둘 + 결정하는 하나\" 멘탈모델 앵커는 이것 하나다: 세 리소스는 같은 자리(워크로드 inbound Envoy의 filter chain)에 차례로 끼어들지만, 앞 둘은 attribute를 채우기만 하고 마지막 하나만 verdict를 낸다 . 세 리소스가 같은 셋업 메커니즘을 공유한다는 점이 이 구조의 출발이다. 셋 다 security.istio.io/v1 apiVersion이고, istiod가 selector 로 워크로드를 골라 그 Envoy의 inbound listener에 filter를 주입 한다. 다른 건 무엇을 검사하고, 검사 결과로 무엇을 하느냐 뿐이다. 한 요청이 inbound sidecar를 통과할 때 세 filter가 끼어드는 순서: 그림 1. 점선이 핵심. 실선은 통과/거부의 데이터 흐름이지만, 점선은 PeerAuthentication·RequestAuthentication이 RBAC에 공급하는 attribute(source.principal, request.auth.claims)다 — 이 공급이 없으면 RBAC은 IP/port/path 같은 raw 정보밖에 못 본다. 점선이 이 그림의 핵심이다. 실선(왼→오른쪽)은 \"통과/거부\"라는 데이터 흐름이지만, 점선은 \"attribute 공급\"이다. PeerAuthentication과 RequestAuthentication이 자기 차단(평문 거부, JWT 형식 오류 거부)을 하긴 한다. 하지만 그보다 훨씬 중요한 산출물은 AuthorizationPolicy가 정책 조건으로 쓸 attribute를 채워 넣는 것 — source.principal (peer가 누구), request.auth.claims (user의 권한)다. 이 점선이 없으면 마지막 RBAC filter는 IP·port·path 같은 L4/L7 raw 정보밖에 못 본다. 이 한 표가 전체 멘탈모델의 압축이다. 세 가지 함정이 여기서 바로 읽힌다. 리소스 계층 검사 대상 산출(authz 입력) 자체 차단 없을 때 기본값 PeerAuthentication L4 transport (mTLS) client"
    },
    {
      "title": "AuthorizationPolicy 멘탈모델 — inbound 적용·mTLS identity·HTTP vs TCP·egress (출처: ChatGPT \"Istio 운영 노하우\" 대화 + Istio 1.30 공식 문서)",
      "desc": "AuthorizationPolicy를 \"왜 그렇게 평가되는가\"의 멘탈모델로 다룬다. 결론부터: 이 정책은 client의 outbound를 막는 게 아니라, selector가 고른 workload의 inbound listener에 RBAC filter를 심어 \"그 서버로 들어오는 요청\"…",
      "url": "/public/istio/sec__src-authorizationpolicy-mental-model.html",
      "domain": "istio",
      "text": "istio security authorizationpolicy mtls spiffe egress rbac AuthorizationPolicy 멘탈모델 — inbound 적용·mTLS identity·HTTP vs TCP·egress (출처: ChatGPT \"Istio 운영 노하우\" 대화 + Istio 1.30 공식 문서) ℹ 이 문서가 다루는 것 AuthorizationPolicy를 \"왜 그렇게 평가되는가\"의 멘탈모델로 다룬다. 결론부터: 이 정책은 client의 outbound를 막는 게 아니라, selector가 고른 workload의 inbound listener에 RBAC filter를 심어 \"그 서버로 들어오는 요청\"을 검사한다. 검사에 쓸 수 있는 조건은 그 inbound Envoy가 실제로 본 것 — peer cert(mTLS identity)와 복호화된 L7 — 으로만 한정된다. 이 한 장면에서 평가 순서, mTLS 의존성, egress passthrough 한계, TCP DENY 사고가 전부 따라 나온다. 리소스 정의는 mTLS/SPIFFE 신원 · 세 리소스 역할 분담 을 정본으로 참조한다. 대상환경 : Istio 1.30, sidecar mesh, security.istio.io/v1 대상독자 : \"AuthorizationPolicy가 안 먹는다 / TCP가 통째로 끊겼다\"를 겪는 DevOps/SRE 범위 : 평가 멘탈모델 — selector 의미, 평가 순서, mTLS 의존 조건, egress/passthrough, HTTP vs TCP 함정 선행개념 : sidecar inbound/outbound listener, mTLS, SPIFFE identity(SAN), xDS LDS 01. 배경 — authz는 어떤 문제를 푸는가, 왜 mTLS가 전제인가 먼저 \"왜 이 리소스가 존재하나\"부터. mesh 안에서 서비스 A가 서비스 B를 호출할 때, B는 한 가지 질문에 답해야 한다: \"지금 나에게 들어온 이 요청을 처리해도 되나?\" network reachability(누가 나에게 패킷을 보낼 수 있나)는 L3/L4 방화벽의 영역이고, AuthorizationPolicy는 그보다 한 층 위 — application 수준의 인가(authorization) — 를 담당한다. \"POST /admin 은 admin SA만\", \"payments namespace에서 온 GET만\" 같은 규칙이다. 그러면 B는 \"누가 호출했는가\"를 무엇으로 믿을까? 여기서 mTLS가 전제로 들어온다. 평문 네트워크에서 server가 client를 식별할 수단은 전부 약하다. IP? 믿기 어려움. NAT, proxy, pod 재생성, spoofing 문제. Header? app이 위조 가능. JWT? end-user identity에는 좋지만 service-to-service identity와는 별도. mTLS cert? sidecar가 workload identity로 발급받고 handshake에서 검증. 그래서 Istio의 service-to-service 인가는 mTLS로 증명된 workload identity 위에 선다. authz가 보는 \"누가\"의 정본 출처는 peer certificate의 SPIFFE SAN이다. 이 의존 관계 — mTLS가 없으면 principal 조건을 쓸 수 없다 — 가 이 문서 전체의 골격이고, §03·§04에서 메커니즘으로 풀어낸다. SPIFFE 형식·SDS provisioning의 디테일은 mTLS/SPIFFE 신원 이 정본이다. 이 문서는 사용자가 \"AuthorizationPolicy를 잘 모르겠다\"고 한 지점에서 출발한다. 그래서 정의 → 평가 → identity 토대 → 조건 경계 → egress → TCP 함정 순으로, 운영에서 실제로 터지는 사고까지 한 줄로 잇는다. 02. anchor — authz는 \"보호받는 서버\"의 inbound를 본다 가장 많이 헷갈리는 지점부터 못 박는다. 머리에 하나만 담는다면 이것이다. ★ 한 문장 멘탈모델 AuthorizationPolicy는 selector가 고른 workload/gateway의 inbound listener에 RBAC filter를 심어 , \"그 서버로 들어오는(inbound) 요청\"을 허용/차단한다. 정책의 target은 보호받는 서버 쪽 이지, client의 outbound를 막는 게 아니다. 평가가 일어나는 지점을 그림으로 박으면 나머지가 따라온다 — 검사는 server측 sidecar의 inbound 에서 일어난다. 그림 1. AuthorizationPolicy는 server 쪽 inbound sidecar에 부착되어 거기서 authz를 평가. 즉 정책의 평가 지점은 항상 수신측 사이드카. 예시 정책으로 이 anchor를 확인한다. apiVersion: security.istio.io/v1 kind: AuthorizationPolicy metadata: name: allow-productpage namespace: default spec: selector: matchLabels: app: reviews # 이 정책의 보호 대상 = reviews action: ALLOW rules: - from: - source: principals: - cluster.local/ns/default/sa/productpage to: - operation: methods: [\"GET\"] paths: [\"/reviews/*\"] 이 정책의 의미는 \" reviews workload로 들어오는 요청 중 source principal이 productpage SA이고 GET /reviews/* 인 것만 허용\"이다. productpage의 outbound를 막는 정책이 아님 . reviews의 inbound를 보호하는 정책 임 . 이 한 줄을 잡으면 \"왜 내 정책이 안 먹지?\"의 절반이 해결된다. 대부분 정책을 잘못된 쪽(client) workload에 selector로 붙였기 때문 이다. selector가 client를 가리키면 그 client의 inbound 만 제어할 뿐 outbound는 손대지 못한다"
    },
    {
      "title": "데이터 플레인은 eventually consistent하게 동기화되고, proxy-status의 SYNCED/NOT SENT/STALE가 각 xDS 타입별 컨트롤 플레인 sync 상태를 드러낸다",
      "desc": "Istio 데이터 플레인(Envoy proxy 무리)은 컨트롤 플레인(istiod)과 강한 일관성이 아니라 최종 일관성(eventual consistency) 으로 맞춰진다. 그래서 \"설정을 apply했다\"와 \"그 설정이 모든 proxy에 반영됐다\"는 별개의 사건이고, 둘 사이엔 항상…",
      "url": "/public/istio/xds__note-data-plane-sync-state.html",
      "domain": "istio",
      "text": "istio xds proxy-status eventual-consistency diagnosis 데이터 플레인은 eventually consistent하게 동기화되고, proxy-status의 SYNCED/NOT SENT/STALE가 각 xDS 타입별 컨트롤 플레인 sync 상태를 드러낸다 ℹ 이 문서가 다루는 것 Istio 데이터 플레인(Envoy proxy 무리)은 컨트롤 플레인(istiod)과 강한 일관성이 아니라 최종 일관성(eventual consistency) 으로 맞춰진다. 그래서 \"설정을 apply했다\"와 \"그 설정이 모든 proxy에 반영됐다\"는 별개의 사건이고, 둘 사이엔 항상 전파 window가 있다. istioctl proxy-status 는 그 window를 가시화하는 계기판으로, 각 proxy가 CDS/LDS/EDS/RDS(/ECDS) 타입별로 istiod의 마지막 push를 ACK했는지를 SYNCED / NOT SENT / STALE / (누락) 으로 보고한다. 이 멘탈모델을 잡으면 트러블슈팅 1단계가 \"내 의도가 Envoy까지 전달됐는가\"로 고정된다. 운영 진단 명령 전체 흐름은 xDS 5계층과 진단 으로 위임한다. ⚠️ 버전 skew 주의 : 이 환경은 istiod 1.30.0 ↔ 로컬 istioctl 1.27.0이다. proxy-status 의 VERSION 컬럼이나 proxy-config 일부 필드가 어긋나 보일 수 있으나 이는 client 표시 한계이며 메시 동작 이상이 아니다. 정밀 진단은 버전을 맞춘 1.30 istioctl 사용 권장. 01. 배경 — apply는 \"의도 등록\"이지 \"반영 완료\"가 아니다 먼저 풀어야 할 문제부터. Istio mesh는 하나의 istiod가 수백~수천 개 Envoy proxy에 설정을 push하는 fan-out 분산 시스템 이다. 사용자가 kubectl apply -f virtualservice.yaml 을 실행하면 일어나는 일은 이렇다. K8s API server에 객체가 저장됨 istiod의 watch가 변경을 감지하고, 영향받는 proxy 집합에 대해 새 Envoy 설정을 컴파일 istiod가 각 proxy의 ADS gRPC stream으로 update를 push 각 Envoy가 설정을 적용하고 ACK (거부 시 NACK)를 돌려줌 이 4단계는 proxy마다 독립적·비동기적 으로 진행된다. proxy A는 이미 ACK했는데 B는 아직 push 대기 중일 수 있고, 네트워크가 느린 C는 더 늦다. 강한 일관성을 보장하려면 모든 proxy의 ACK를 기다린 뒤에야 apply를 \"완료\"로 인정해야 하는데, 그건 수천 proxy 규모에서 비현실적이고 한 proxy의 장애가 mesh 전체 변경을 막는 단일 실패점 이 된다. 그래서 Istio는 \"각자 받는 대로 수렴하되, 언젠가는 모두 같은 상태에 도달한다\" 는 eventual consistency를 택한다 — 가용성과 확장성을 위해 순간적 불일치를 의도적으로 허용하는 거래다. 이 설계의 직접적 귀결이 운영자가 알아야 할 진실 하나 다: apply가 성공해도 트래픽은 즉시 안 바뀐다. \"VirtualService를 고쳤는데 트래픽이 안 바뀐다\"는 증상은 두 갈래다 — (a) 내 의도(YAML)가 틀렸거나, (b) 의도는 맞는데 아직/영영 Envoy까지 전달이 안 됐거나 . (b)를 먼저 배제하지 않으면 멀쩡한 YAML을 붙들고 헤맨다. 다음 절의 도구가 바로 이 (b)를 1초 만에 판별한다. 02. 핵심 모델 — proxy-status는 \"타입별 수렴 계기판\"이다 한 문장 앵커: proxy-status 의 각 셀은 \"이 proxy가 이 xDS 타입에 대해 istiod의 마지막 push와 합의(ACK)됐는가\"라는 단 하나의 질문에 답한다. 이 그림만 머리에 박으면 모든 상태값·모든 컬럼이 여기서 파생된다. 왜 하나의 SYNCED가 아니라 타입별 컬럼 으로 쪼개져 있을까. xDS 자체가 LDS/RDS/CDS/EDS/SDS의 독립 resource type으로 나뉘고(계층 개념은 xDS API 계층 ), ADS가 이들을 단일 gRPC stream으로 묶더라도 각 타입은 개별적으로 versioned·ACK된다 . Envoy는 \"CDS v17을 ACK\", \"RDS v23을 ACK\"처럼 타입마다 따로 응답한다. proxy-status는 그 per-type 합의 상태를 그대로 한 줄에 펼쳐 보여주는 overview일 뿐이다. 그림 1. sync 상태의 기원. istiod가 compile해 연결된 Envoy에 push하면, ACK가 돌아온 A는 SYNCED, push됐지만 ACK가 없는 B는 STALE, 애초에 연결 안 된 C는 proxy-status 목록에 아예 안 뜬다 — 상태값은 \"push 여부 × ACK 여부\"의 조합일 뿐이다. 상태값 4종 — 앵커에서 그대로 따라 나온다 셀의 값은 \"ACK됐는가\"의 결과일 뿐이다. 그러니 4종은 ACK 여부 + push 여부의 조합으로 읽힌다. 상태 의미 대표 원인 SYNCED Envoy가 istiod의 마지막 push를 ACK 함 (정상, 수렴 완료) — NOT SENT istiod가 그 타입으로 보낼 게 없었음 gateway에 적용할 HTTP route가 없어 RDS가 NOT SENT, extension 없는 proxy의 ECDS NOT SENT — 대개 정상 STALE istiod가 push했지만 Envoy ACK를 못 받음 (수렴 미완·정체) proxy↔istiod 네트워크 문제, istiod 과부하/push 지연, Envoy가 NACK 유발하는 잘못된 설정 (목록에서 proxy 자체 누락) 그 proxy가 현재 istiod에 연결 안 됨 sidecar injection 누락, istio-agent crash, mTLS/네트워크 차단 — 거의 항상 문제 핵심 구분은 NOT SENT vs STALE 이다. NOT SENT 은 \"보낼 게 없음\"(정상일 수 있음) , STALE 은 \"보냈는데 응답이 없음\"(수렴이 막힘) 이다. ingress gateway의 RDS가 NOT SENT 인 건 흔히 그 gateway에 묶인 ro"
    },
    {
      "title": "Envoy Admin API(config_dump/clusters/stats)는 데이터 플레인이 실제로 적용한 설정과 런타임 상태를 보는 1차 진단 도구다",
      "desc": "istioctl·Kiali·access log가 모두 \"추상화된 뷰\"라면, Envoy Admin API는 데이터 플레인의 그라운드 트루스(ground truth) 다. istiod가 \"의도한 설정\"과 Envoy가 \"실제로 적용한 설정\"이 갈라질 때(STALE/NACK), 그 갈라짐은…",
      "url": "/public/istio/xds__note-envoy-admin-api-diagnosis.html",
      "domain": "istio",
      "text": "istio envoy xds admin-api diagnosis Envoy Admin API(config_dump/clusters/stats)는 데이터 플레인이 실제로 적용한 설정과 런타임 상태를 보는 1차 진단 도구다 NOTE istioctl·Kiali·access log가 모두 \"추상화된 뷰\"라면, Envoy Admin API 는 데이터 플레인의 그라운드 트루스(ground truth) 다. istiod가 \"의도한 설정\"과 Envoy가 \"실제로 적용한 설정\"이 갈라질 때(STALE/NACK), 그 갈라짐은 결국 이 Admin API에서만 확정된다. 이 노트는 \"Admin API가 무엇을 보여주고 왜 1차 도구인가\" 의 멘탈모델을 세우고, config_dump · /clusters · /stats 세 면을 \"설정 → 멤버 health → 트래픽 결과\"의 인과로 묶어 읽는 법을 다룬다. ⚠️ 버전 skew 주의 : 이 환경은 istiod 1.30.0 ↔ 로컬 istioctl 1.27.0이다. proxy-status 의 VERSION 컬럼이나 proxy-config 일부 필드가 어긋나 보일 수 있으나 이는 client 표시 한계이며 메시 동작 이상이 아니다. 정밀 진단은 버전을 맞춘 1.30 istioctl 사용 권장. 01. 배경 — \"설정을 줬다\"와 \"설정대로 돈다\"는 다른 사건이다 Istio는 선언형이다. CRD( VirtualService , DestinationRule …)를 apply하면 istiod가 그걸 Envoy 설정으로 번역해 xDS로 각 프록시에 push한다. 그래서 디버깅할 때 우리는 본능적으로 CRD(의도) 를 들여다본다. 그런데 트래픽이 실패할 때 진짜 물어야 할 질문은 다르다 — \"내가 의도한 그 설정이 지금 이 Envoy 안에 실제로 들어가 있고, 그 endpoint가 살아있고, 요청이 어디서 깨지나?\" 이 질문이 중요한 이유는 xDS가 eventually consistent 이기 때문이다. istiod와 Envoy는 비동기 스트림으로 연결돼 있고, push → ACK 사이엔 시간차가 있다. push가 실패하거나(연결 끊김), Envoy가 설정을 거부하거나(NACK), endpoint warming이 안 풀리면 — CRD는 완벽한데 Envoy 안엔 옛 설정이 박혀 있는 상태가 생긴다. CRD만 봐선 이걸 절대 못 잡는다. CRD는 \"내 의도\"일 뿐 \"데이터 플레인의 현실\"이 아니기 때문이다. 그래서 진단엔 세 개의 분리된 검증 축이 필요하다. 검증 축 무엇을 답하나 도구 의도 검증 내 CRD가 문법·정합성에 맞나 istioctl analyze 전달 검증 istiod가 push했고 Envoy가 ACK했나 istioctl proxy-status 적용 검증 그래서 Envoy 안에 지금 뭐가 들어 있나 Envoy Admin API ★ 세 축이 모두 일치할 때만 \"설정대로 동작 중\"이라 단정할 수 있다. 그리고 마지막 적용 검증의 최종심급 이 Admin API다 — 다른 무엇도 Envoy 내부를 직접 보여주지 못한다. 선행 개념 : xDS 5계층(LDS/RDS/CDS/EDS/SDS), cluster 이름 규칙 direction|port|subset|fqdn , sync 상태값(SYNCED/STALE/NOT SENT). 5계층 운영 매핑은 xDS 5계층과 istioctl 진단 , sync 상태값은 데이터 플레인 sync 상태 에 위임한다. 02. 멘탈모델 ANCHOR — Admin API는 추상화 사다리의 맨 밑이다 ★ 한 장의 그림 Istio 진단 도구는 추상화 사다리를 이룬다. 위로 갈수록 사람이 읽기 쉽고, 아래로 갈수록 프록시가 실제로 하는 일에 가깝다. 위의 모든 도구는 결국 맨 밑 Admin API(또는 동일 소스인 xDS) 위에 얹힌 가공물이다. 가공 과정에서 정보가 빠지거나 도구 버전이 어긋날 때, \"진짜 Envoy 안에 뭐가 들어 있나\"를 확정하는 최종심급이 맨 밑 칸 이다. Kiali / Grafana ← 메트릭 집계 뷰 (mesh 전체 그래프) istioctl analyze ← istiod가 \"의도\"를 검증 (CRD 정합성) istioctl proxy-status ← 전달(delivery) 검증 (ACK/NACK 받았나) istioctl proxy-config ← Envoy 설정을 사람이 보기 좋게 가공 Envoy Admin API ← Envoy 안의 raw 설정·런타임 상태 (ground truth) ★ 이 사다리가 진단 전략의 전부다. istioctl proxy-config 는 내부적으로 Envoy의 config_dump 를 호출해 보기 좋게 포맷팅·필터링 한 것이고, proxy-status 는 istiod가 가진 push 기록과 Envoy의 ACK를 대조 한 것이다. 즉 상위 도구는 편의를 위해 raw를 깎아낸다 — 그래서 빠른 답엔 좋지만, 깎인 정보(warming 상태, outlier 통계, filter chain 세부)가 바로 원인일 땐 무력하다. 그때 맨 밑으로 내려간다. 핵심 멘탈모델은 이렇게 닫힌다. proxy-status가 \"보냈는데 ACK 못 받음(STALE)\" 을 알려주면, config_dump가 \"그래서 지금 Envoy 안엔 옛날 설정이 있다\" 를 증명한다. 전자는 istiod의 관점, 후자는 Envoy의 관점 — eventually consistent의 두 끝을 각각 확정하는 것이다. 03. Admin endpoint는 어디에 — localhost:15000과 포트 분리의 설계 Istio istio-proxy(sidecar 및 ingress/egress gateway)에서 Envoy Admin API는 127.0.0.1:15000 에 바인딩된다. 127.0.0.1 (localhost) 바인딩이 설계의 핵심이다 — admin 인터페이스는 인증이 없고 /quitquitquit (Envoy 종료)· /drain_listeners · /logging 같은 상태를 바꾸는 위험 엔드포인트 를 포함하므로, Pod 네트워크 밖으로 절대 새면 안 된다. localhost 바인딩이 1차 방어선이고, 그래서 같은 Pod의 app"
    },
    {
      "title": "Envoy는 listener의 network filter와 HCM의 HTTP filter chain으로 요청을 처리하며, Lua·Wasm·external processing으로 재컴파일 없이 확장된다",
      "desc": "Envoy의 요청 처리는 두 겹의 filter chain이다 — L4(connection) 단의 network filter chain과, 그 안의 HTTP connection manager(HCM)가 여는 L7 HTTP filter chain. 이 한 장의 그림만 잡으면 두 가지가 동…",
      "url": "/public/istio/xds__note-envoy-filter-chain-extension.html",
      "domain": "istio",
      "text": "istio envoy filter-chain hcm wasm lua envoyfilter Envoy는 listener의 network filter와 HCM의 HTTP filter chain으로 요청을 처리하며, Lua·Wasm·external processing으로 재컴파일 없이 확장된다 ℹ 이 문서가 다루는 것 Envoy의 요청 처리는 두 겹의 filter chain 이다 — L4(connection) 단의 network filter chain 과, 그 안의 HTTP connection manager(HCM)가 여는 L7 HTTP filter chain . 이 한 장의 그림만 잡으면 두 가지가 동시에 풀린다: ① \"L7 정책(JWT·CORS·L7 RBAC·path 라우팅)이 왜 안 먹는가\" 같은 진단, ② Lua·Wasm·external processing이라는 세 확장 경로가 사실은 같은 L7 chain에 필터 하나를 더 끼우는 같은 일 이라는 것. 결론부터: 확장은 별개 메커니즘이 아니라 전부 HCM의 L7 chain 조작이고, Istio에서 그 chain을 직접 건드리는 escape hatch가 EnvoyFilter 다. 운영 가드레일·우선순위 사다리는 xDS 5계층과 진단 §07로 위임하고, 이 note는 멘탈모델 에 집중한다. 대상환경 Istio 1.30 / Envoy · 대상독자 L7 정책·확장이 \"조용히 무시되는\" 이유를 메커니즘으로 알고 싶은 DevOps/SRE · 범위 filter chain 구조와 확장 주입 메커니즘(가드레일은 링크) · 선행개념 xDS layer 개관 , listener/route의 LDS/RDS 관계. 01. 배경 — 왜 \"두 겹\"이어야 했나 프록시가 풀어야 하는 근본 긴장은 이거다: 한쪽 끝에는 raw TCP 바이트 (커널이 주는 건 이게 전부다)가 있고, 다른 쪽 끝에는 HTTP 의미 (이 요청의 path는 뭐고 JWT는 유효한가, 이 method를 이 principal이 호출해도 되는가)로만 쓸 수 있는 정책이 있다. 바이트 단에서는 \"GET /admin\"이라는 개념 자체가 없으니 path 기반 RBAC을 적용할 방법이 없다. 그렇다고 모든 커넥션을 HTTP로 파싱하면, 순수 TCP(DB, Redis, mTLS passthrough)까지 깨진다. Envoy의 답은 두 레이어를 중첩 시키는 것이다. 바깥은 바이트를 다루는 L4 network filter chain , 안쪽은 그것을 HTTP 객체로 승격시킨 뒤 도는 L7 HTTP filter chain . 그리고 두 레이어를 잇는 경첩이 단 하나의 컴포넌트 — HCM( http_connection_manager ) 이다. HCM이 chain에 놓이면 그 지점부터 L7 세계가 열리고, 없으면 그 커넥션은 영원히 바이트 단에 머문다. 이 구조를 모르면 Istio에서 가장 흔한 함정에 그대로 빠진다: AuthorizationPolicy를 정확히 썼는데 path match가 무시된다, VirtualService 라우팅이 안 먹는다 — 정책 오타가 아니라 그 포트에 HCM이 안 깔린 것 (L7 chain 자체가 없는 것)이 원인인 경우다. 그래서 이 note는 진단의 토대이기도 하다. 02. 핵심 그림 — L4 chain ⊃ HCM ⊃ L7 chain 머릿속에 박을 단 하나의 그림: HCM은 \"또 하나의 network filter\"다. L4와 L7은 별도 시스템이 아니라, L4 chain의 한 슬롯에 HCM이 들어앉으면 거기서부터 L7 chain이 열리는 중첩 구조다. 나머지는 전부 여기서 따라 나온다. 커넥션이 listener에 도착하면 바이트는 곧장 HTTP가 되지 않고 먼저 L4 network filter chain 을 통과한다. network filter는 raw TCP 스트림을 다루는 필터다 — tls_inspector (ClientHello에서 SNI/ALPN 추출), http_connection_manager (HCM), tcp_proxy (L4 그대로 upstream에 전달) 등. L4 chain의 슬롯에 무엇이 들어가느냐가 그 커넥션의 운명을 가른다. 슬롯에 HCM 이 들어가면 → 바이트가 request line/header/body로 파싱되고, 그 안에서 별도의 L7 HTTP filter chain 이 돈다. path·header·JWT·L7 RBAC이 전부 이때 적용된다. 슬롯에 tcp_proxy 만 있으면 → L7 해석 없이 통과(SNI 기반 passthrough, mTLS, L4 RBAC까지만). 이것이 \"포트가 HTTP로 인식 안 되면 라우팅/정책이 안 먹는\" 증상의 근본 이유다. 그림 1. L4에서 inspector→filter_chain_match로 HCM 또는 tcp_proxy 분기. HTTP면 HCM 안의 L7 체인(cors→jwt_authn→rbac→lua/wasm→router)을 순차 통과. AuthorizationPolicy는 rbac 필터. 이 그림이 xDS와 어떻게 맞물리는지도 한 줄로 잡아두면 진단이 쉬워진다: L4 network filter chain은 LDS 로 내려오는 listener 설정의 일부이고, HCM 안의 route 결정은 RDS 가 채운다. 즉 한 listener = \"L4 chain + (HCM이면) L7 chain + route table\"의 합성이다. 03. HCM이라는 경첩 — 바이트→HTTP 추상화 경계 HCM이 왜 그렇게 중요한지는 \"그 하나가 무엇을 책임지는가\"를 보면 분명해진다. HCM은 Envoy에서 L4와 L7을 가르는 단일 컴포넌트 이고, 그 책임이 곧 L7 세계의 존재 조건이다. 프로토콜 파싱/협상 : HTTP/1.1, HTTP/2, (설정 시) HTTP/3를 인식해 바이트를 request/response 객체로 변환. ALPN· http2_protocol_options 로 결정. HTTP filter chain 실행 : 파싱된 요청을 §04의 L7 필터들에 순서대로 통과시킴. 마지막 router 필터가 route table을 보고 upstream cluster를 선택. route table 연결 : 인라인"
    },
    {
      "title": "Envoy 요청 라우팅은 listener→route→cluster→endpoint 체인을 따르며 istioctl proxy-config로 각 단계를 짚어 오설정을 찾는다",
      "desc": "Envoy가 요청 하나를 처리하는 경로는 항상 Listener → Route → Cluster → Endpoint (+ mTLS면 Secret) 라는 고정된 체인이다. 그래서 장애 진단은 자유로운 추리가 아니라 \"이 고정 체인의 어느 단계에서 끊겼는가\" 하나만 묻는 결정적 절차가 된다…",
      "url": "/public/istio/xds__note-envoy-routing-chain-debugging.html",
      "domain": "istio",
      "text": "istio envoy xds proxy-config troubleshooting Envoy 요청 라우팅은 listener→route→cluster→endpoint 체인을 따르며 istioctl proxy-config로 각 단계를 짚어 오설정을 찾는다 ℹ 이 문서가 다루는 것 Envoy가 요청 하나를 처리하는 경로는 항상 Listener → Route → Cluster → Endpoint (+ mTLS면 Secret) 라는 고정된 체인이다. 그래서 장애 진단은 자유로운 추리가 아니라 \"이 고정 체인의 어느 단계에서 끊겼는가\" 하나만 묻는 결정적 절차 가 된다. 그 답을 단계별로 dump해 주는 도구가 istioctl proxy-config {listener,route,cluster,endpoint,secret} , 시작점을 바로 가리켜 주는 단서가 access log의 response flag 다. 이 note는 그 체인 멘탈모델과 단계별 디버깅 절차 에 집중하고, flag 전체 표·xDS 계층 내부·cluster 필드 매핑 같은 깊은 detail은 각 src 문서로 위임한다. 01. 배경 — 왜 \"체인\"으로 생각해야 하나 Istio 메시에서 \"v2가 안 떠요\", \"503이 나요\" 같은 증상은 원인 후보가 너무 많다. VirtualService 오타일 수도, DestinationRule 누락일 수도, Pod label 불일치일 수도, mTLS mode 충돌일 수도 있다. 증상에서 곧장 원인을 찍으려 하면 추측이 되고, 추측은 틀린 곳을 고치게 만든다. 빠져나갈 길은 Envoy 자체의 구조에 있다. Envoy는 요청을 받으면 매번 똑같은 순서로 \"이 요청을 어떻게 처리할지\"를 resolve한다. 이 순서는 설정마다 달라지지 않는 불변(invariant)이다. 즉 어떤 라우팅 장애든, 이 고정된 결정 파이프라인 위의 정확히 한 지점 에서 답이 안 나와 끊긴 것이다. 그러면 진단은 \"원인이 무엇일까\"라는 열린 질문에서, \"이 파이프라인을 위에서 아래로 내려가며 처음 답이 빈 단계를 찾아라\" 라는 닫힌 절차로 바뀐다. 이것이 체인 멘탈모델의 전부이고, 이 문서의 나머지는 그 체인을 짚는 법이다. 이 멘탈모델을 쓰려면 두 가지 선행 개념이 필요하다. xDS = Envoy의 동적 설정 프로토콜. istiod가 각 Envoy(sidecar/gateway)에게 listener(LDS)·route(RDS)·cluster(CDS)·endpoint(EDS)·secret(SDS)을 push한다. 체인의 각 단계는 그대로 하나의 xDS 종류에 대응한다. xDS 계층 자체의 내부 동작은 → xDS 계층 개념 . cluster ≠ endpoint. Envoy의 cluster는 \"어디로 보낼지\"라는 논리적 목적지 (upstream pool)이고, 그 안에 실제로 누가 있는지 (Pod IP)는 별도 채널(EDS)로 내려온다. 이 분리가 진단의 핵심 함정을 만든다(§02 anchor, §03). 02. 핵심 — 체인의 각 단계가 답하는 질문 Anchor: 머릿속에 그릴 그림은 이 한 줄이다 — 요청은 Listener → Route → Cluster → Endpoint (+Secret) 를 따라 흐르고, 각 단계는 직전 단계의 결정을 입력으로 받아 \"예/아니오\"를 하나씩 답한다. 어느 단계가 처음 \"아니오\"를 내는지가 곧 근본 원인의 위치다. 그림 1. Envoy 라우팅 체인: Listener(수신)→Route(cluster 선택)→Cluster(pool)→Endpoint(Pod IP). Cluster는 mTLS 시 Secret(SDS)을 참조. 503 진단은 이 체인을 따라 내려가며 끊긴 고리를 찾음. 각 단계를 \"필드\"가 아니라 \"그게 답하는 질문\"으로 보면 체인이 직관적으로 잡힌다. 단계 xDS 결정하는 것 한 줄 질문 Istio 리소스(주된 것) Listener LDS 어느 포트/주소에서 받고 어떤 filter chain으로 처리할지 \"트래픽을 받긴 했나\" Gateway , Sidecar , PeerAuthentication , Service port Route RDS 이 요청(host/path/header)을 어느 cluster로 보낼지 \"보낼 곳을 정했나\" VirtualService , Gateway , HTTPRoute Cluster CDS 그 목적지(upstream pool)가 존재하는지 + LB/TLS/CB 정책 \"목적지가 정의됐나\" Service , ServiceEntry , DestinationRule Endpoint EDS 그 cluster 안의 실제 Pod IP 가 healthy하게 있는지 \"보낼 실체가 있나\" EndpointSlice , readiness, selector, WorkloadEntry Secret SDS mTLS handshake에 쓸 cert/key/CA \"신원 증명을 할 수 있나\" istiod CA, PeerAuthentication , DestinationRule TLS 왜 cluster와 endpoint가 분리됐나 — 가장 흔한 함정의 뿌리 cluster(CDS)는 \"reviews v1으로 보내라\"는 논리적 목적지 이고, 그 안에 실제로 누가 있는지(EDS)는 따로 내려온다. 이렇게 쪼갠 이유는 운영상 명확하다 — Pod는 scale·재시작·장애로 IP가 수시로 바뀌지만 \"reviews v1으로 보낸다\"는 라우팅 의도는 그대로다. endpoint만 자주 갱신하고 cluster/route는 안정적으로 두기 위해 두 채널을 분리했다. 그 대가로 \"cluster는 있는데 endpoint가 0개\"라는 상태가 정상적으로 존재 하고, 이것이 진단을 헷갈리게 하는 1순위 함정이다. \"cluster가 보인다 = 트래픽이 간다\"는 거짓이다. cluster 이름 — 진단이 곧 문자열 대조인 이유 cluster 이름은 direction|port|subset|fqdn 규칙을 따른다(예: outbound|9080|v1|reviews.default.svc.cluster.local ). subset이 없으면 가운데가 비어 outbound|9080||reviews... 가 된다. rou"
    },
    {
      "title": "Envoy 정적 설정은 부트스트랩 시점에 고정되고 동적 설정은 컨트롤 플레인이 런타임에 푸시해 재시작 없이 반영한다",
      "desc": "머릿속에 담을 한 장: 정적은 \"xDS를 받는 통로\", 동적은 \"그 통로로 흘러드는 트래픽 설정\"이다. 정적(static)은 프로세스가 부팅하며 읽는 부트스트랩 YAML에 박제돼 재시작 전까지 불변이고, 동적(dynamic)은 컨트롤 플레인이 xDS로 런타임에 밀어넣어 프로세스 재시작…",
      "url": "/public/istio/xds__note-envoy-static-vs-dynamic-config.html",
      "domain": "istio",
      "text": "istio envoy xds Envoy 정적 설정은 부트스트랩 시점에 고정되고 동적 설정은 컨트롤 플레인이 런타임에 푸시해 재시작 없이 반영한다 NOTE 머릿속에 담을 한 장: 정적은 \"xDS를 받는 통로\", 동적은 \"그 통로로 흘러드는 트래픽 설정\"이다. 정적(static)은 프로세스가 부팅하며 읽는 부트스트랩 YAML에 박제돼 재시작 전까지 불변이고, 동적(dynamic)은 컨트롤 플레인이 xDS로 런타임에 밀어넣어 프로세스 재시작 없이 listener/route/cluster/endpoint를 갈아끼운다. Istio에서 사이드카·gateway의 거의 모든 라우팅·보안 설정은 동적이며, 정적은 부트스트랩(노드 ID, xDS 채널, admin, 정적 stats sink)만 담는다. 이 문서는 \"왜 둘로 나뉘고 무엇이 어느 쪽에 가는가\"를 그 메커니즘(warming + atomic swap, ADS 의존 순서)까지 정리한다. 파일 기반 xDS의 운영 함정과 LDS/RDS/CDS/EDS 세부는 각각 다른 문서로 위임한다. 1. 배경 — 왜 설정을 \"정적/동적\" 둘로 나누는가 Envoy는 그 자체로는 정책을 모르는 순수 데이터 평면 프록시 다. 어떤 포트로 받고(listener), 그걸 어디로 보내고(route), 누구에게 보내는지(cluster·endpoint)를 설정으로 주입받아야만 동작한다. 문제는 그 설정이 두 종류의 수명 을 갖는다는 점이다. 어떤 설정은 프로세스의 정체성 이다 — 나는 누구이며(node ID) 어디로 admin/metrics를 노출하는가. 이건 살아있는 동안 바뀔 이유가 없다. 어떤 설정은 트래픽 정책 이다 — canary 가중치 5%→50%, Pod 스케일 2→4, circuit breaker 임계값. 이건 운영 중에 끊임없이 바뀐다. 이 둘을 같은 메커니즘으로 다루면 한쪽이 다른 쪽을 망친다. 정체성을 자주 바꿀 일은 없는데 트래픽 정책 하나 바꾸겠다고 프로세스를 통째로 재시작하면 진행 중 연결이 끊긴다. 반대로 정체성까지 런타임 푸시에 맡기면 \"푸시를 받는 통로 자체\"를 받을 길이 없는 부트스트랩 역설에 빠진다. 그래서 Envoy는 설정을 로딩 시점 으로 가른다. 부팅 때 한 번 읽고 굳히는 정적(static) , 컨트롤 플레인이 런타임에 흘려보내는 동적(dynamic) . 이 문서를 읽기 전 알아둘 선행 개념은 셋뿐이다 — listener(진입점)·route(목적지 규칙)·cluster(백엔드 그룹)라는 데이터 평면의 3요소, 그리고 그것을 외부에서 밀어주는 프로토콜이 xDS 라는 것. 그 위에서 \"무엇이 정적이고 무엇이 동적인가\"가 이 문서의 전부다. 2. 정적 부트스트랩: listener-route-cluster를 한 파일에 박제 먼저 동적이 없는 세계를 보면 동적의 존재 이유가 선명해진다. 가장 단순한 Envoy는 부트스트랩 YAML 하나로 데이터 평면 전체를 정의한다. static_resources 아래에 listener(트래픽 진입점) → route(어디로 보낼지) → cluster(백엔드 그룹)를 모두 적고, 프로세스는 부팅 시 이 파일을 1회 읽어 인메모리 설정으로 굳힌다. 모든 것이 파일에 박혀 있고, 파일은 부팅 후 불변이다. static_resources: listeners: - name: listener_httpbin address: { socket_address: { address: 0.0.0.0, port_value: 15001 } } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: \"@type\": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http route_config: # 라우트가 listener 안에 인라인 name: local_route virtual_hosts: - name: vhost_all domains: [\"*\"] routes: - match: { prefix: \"/\" } route: { cluster: httpbin_svc } http_filters: - { name: envoy.filters.http.router, typed_config: { \"@type\": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router } } clusters: - name: httpbin_svc type: LOGICAL_DNS load_assignment: { ... } # 엔드포인트도 인라인 여기서 핵심은 중첩 구조 다 — route가 listener 안에 인라인으로 박히고( route_config ), endpoint가 cluster 안에 인라인으로 박힌다( load_assignment ). 동적화란 바로 이 인라인을 끊고 \"외부에서 받아오라\"로 바꾸는 일인데(§3에서 다룸), 그 전에 이 인라인 구조의 세 가지 한계 가 곧 동적 설정을 요구하는 동기다. 불변성 : 라우팅 가중치를 5%→50%로 바꾸려면 파일을 고치고 프로세스를 재시작해야 한다. 재시작은 기존 연결을 끊고 트래픽을 일시 단절시킨다(graceful drain 없이는 더 거칠다). 스케일 미반영 : 백엔드 Pod가 2→4로 늘어도 인라인 엔드포인트 목록은 그대로다. 새 IP가 트래픽을 못 받는다. 운영 비대칭 : 메시 안 프록시가 수백 개면 각 프록시의 부트스트랩을 따로 관리할 수 없다. 중앙에서 한 번 계산해 밀어주는 모델이 필요하다. 세 한계는 모두 같은 뿌리를 가진다 — 설정이 데이터 평면 프로세스 안에 갇혀 있고, 그걸 바꾸는 유일한 손잡이가 \"재시작\"뿐 이라는 것. 동적 설정은 이 손잡이를 \"런타임 교체\"로 바꾼다. 책(Istio in Action Ch.3.2)은 Envoy v3 설정 API ( typed_config )만 다룬다. v1/v2는 폐기됐다. 정적 부트스트랩 실습 전문은 정적/"
    },
    {
      "title": "파일 기반 xDS는 DiscoveryResponse 포맷·move 교체만 감지하고 EDS의 \"클러스터당 CLA 1개\" 제약 때문에 디버깅이 까다롭다",
      "desc": "머릿속에 둘 한 장면: 파일 기반 xDS는 gRPC ADS의 구독 계약을 그대로 둔 채 전송(transport)만 \"로컬 파일\"로 바꾼 것이다. 그래서 세 가지 좁은 제약은 따로 생긴 규칙이 아니라 모두 그 계약에서 흘러나온다 — ① 파일은 인라인 조각이 아니라 루트에 resource…",
      "url": "/public/istio/xds__note-file-based-xds-constraints.html",
      "domain": "istio",
      "text": "istio envoy xds 파일 기반 xDS는 DiscoveryResponse 포맷·move 교체만 감지하고 EDS의 \"클러스터당 CLA 1개\" 제약 때문에 디버깅이 까다롭다 NOTE 머릿속에 둘 한 장면: 파일 기반 xDS는 gRPC ADS의 구독 계약을 그대로 둔 채 전송(transport)만 \"로컬 파일\"로 바꾼 것 이다. 그래서 세 가지 좁은 제약은 따로 생긴 규칙이 아니라 모두 그 계약 에서 흘러나온다 — ① 파일은 인라인 조각이 아니라 루트에 resources: 배열을 둔 DiscoveryResponse 응답 이어야 하고, ② 파일 watcher가 신뢰하는 변경 신호는 move(rename) 이벤트 하나뿐이며, ③ EDS만은 \"응답 1개 = 클러스터 1개\"라 클러스터당 ClusterLoadAssignment 1개 만 허용된다. 결론: LDS/RDS 학습용으로는 훌륭하지만 파일 EDS는 함정 이고, xDS를 진짜로 보려면 gRPC ADS(Istio면 istioctl proxy-config )로 우회하는 편이 빠르다. 깊은 실습 절차는 정적/동적 xDS 실습 에 위임하고, 여기서는 왜 그런 제약이 생기고 어떻게 판단할지 의 멘탈모델만 다룬다. 1. 배경 — 왜 파일 기반 xDS를 손으로 만져보는가 xDS는 한 문장으로 \" 설정을 누가, 어떤 전송으로 Envoy에 주입하는가 \"의 문제다. Envoy는 Listener(LDS)·Route(RDS)·Cluster(CDS)·Endpoint(EDS)를 부팅 시 한 번 읽고 끝내는 게 아니라, 런타임에 구독 해서 받아온다. 이 구독을 누가 채워주느냐가 전송(transport)이고, 같은 LDS/RDS/CDS/EDS 리소스라도 전송은 세 가지로 나뉜다: 전송 방식 config_source 푸시 주체 용도 static (없음, 부트스트랩 인라인) 없음(고정) 최소 부팅·테스트 filesystem(파일) path: / path_config_source: 로컬 파일 watcher 단독 실습·엣지 케이스 gRPC ADS api_config_source: {api_type: GRPC} 컨트롤 플레인(istiod) 프로덕션(Istio) 프로덕션에서 Istio는 gRPC ADS 한 채널 로 모든 리소스를 단일 스트림에 실어 보낸다. 그런데 그 ADS 스트림 안을 직접 들여다보긴 어렵다 — istiod가 KRM(VirtualService·DestinationRule 등)을 번역해 푸시하는 결과만 보일 뿐, \"Envoy가 한 개의 RouteConfiguration을 받으면 무슨 일이 벌어지는가\"를 손으로 한 줄씩 바꿔보긴 힘들다. 파일 기반 xDS는 그 ADS 스트림을 로컬 파일로 \"외재화(externalize)\"한 것 이다. 컨트롤 플레인 없이 lds.yaml · rds.yaml 을 직접 쓰고, 파일 한 줄 바꾸면 Envoy가 재시작 없이 반영한다. 그래서 \"동적 라우팅이 실제로 어떻게 갱신되는가\"를 컨트롤 플레인의 추상화를 걷어내고 맨손으로 체득하는 최고의 실습 도구다. 선행 개념: 정적/동적의 경계와 어떤 계층을 동적화할지는 정적 vs 동적 설정 , 계층 분할(LDS→RDS, CDS→EDS의 의존 사슬)의 전체 그림은 xDS API 계층 에 있다. 이 글은 그 위에서 \"파일로 바꾸면 어떤 함정이 새로 생기나\" 만 다룬다. 2. 핵심 멘탈모델 — \"transport만 바꾼 것\"에서 세 제약이 따라 나온다 붙잡을 단 하나의 그림: 파일 기반 xDS는 gRPC ADS와 동일한 구독 계약을 쓰되, 메시지를 운반하는 채널만 gRPC 스트림 → 로컬 파일로 바꾼다. Envoy 입장에선 \"응답(DiscoveryResponse)을 어디서 받느냐\"만 다르고, 받은 응답을 어떻게 검증·적용하느냐 는 똑같다. 이 한 줄을 쥐면 뒤의 세 제약이 전부 연역 된다. 각 제약은 새로 만든 규칙이 아니라, 동일한 구독 계약의 어느 부분이 파일이라는 전송에서 드러나는가의 차이일 뿐이다: 그림 1. 하나의 계약, 세 제약. file-based xDS는 전송만 바꿨을 뿐 gRPC ADS와 같은 구독 계약을 공유한다 — 그래서 응답 포맷(①), 갱신 신호(②), EDS 단위(③)가 모두 그 계약에서 강제되고, ①③은 grpc-mux의 공통 검증 경로를 그대로 탄다. 세 제약을 하나씩 계약에서 끌어내며 보면: 2-1. 제약 ① — 파일은 DiscoveryResponse여야 한다 (포맷) ADS 스트림에서 오는 메시지는 DiscoveryResponse 다. 전송만 바꿨으니 파일의 모양도 정확히 DiscoveryResponse 여야 한다. 즉 루트에 resources: 배열이 있고, 각 원소는 \"@type\" 으로 구체 리소스 타입을 명시한다: # rds.yaml — 루트 resources[] + @type 이 계약 version_info: \"0\" # 선택, 디버깅·관찰용 resources: - \"@type\": type.googleapis.com/envoy.config.route.v3.RouteConfiguration name: local_route virtual_hosts: [ ... ] 여기서 오는 혼동: - 부분 설정 YAML이 아니다. \"Listener 한 조각\"을 그냥 쓰면 안 되고, Listener 를 resources[] 로 감싼 응답 형태여야 한다. static 부트스트랩에서 쓰던 인라인 형식과 구조가 다르다는 점이 첫 번째 혼동 지점이다 — 같은 리소스라도 인라인은 필드 값 , 파일 xDS는 응답 페이로드 라는 위상 차이다. - 경로는 부트스트랩 시점에 이미 존재 해야 한다. watcher는 초기 로드에서 파일을 읽으므로, 없으면 빈 구독 상태로 시작해 이후 생성에 반응이 어긋난다. - @type 이 틀리면 조용히 무시되거나 reject된다. 타입 FQDN( envoy.config.route.v3.RouteConfiguration 등)은 v3 API 기준으로 정확히 적어야 한다. 2-2. 제약 ② — 갱신은 move(rename)로만 안정 감지된다 (이벤트) ADS에선 \"새 응답이 왔다\"가 명시적 메시지다. 파일 전송엔 그런 메시지가 없으니, \"파일이 바뀌었다"
    },
    {
      "title": "xDS는 Envoy 설정을 LDS/RDS/CDS/EDS/SDS 계층으로 나누고 ADS가 이를 단일 스트림으로 묶어 적용 순서를 보장한다",
      "desc": "Envoy의 동적 설정은 단일 덩어리가 아니라 5개 xDS API(LDS/RDS/CDS/EDS/SDS)로 분리되어 \"리스닝 지점 / 라우팅 / upstream pool / 실제 endpoint / TLS secret\"이라는 서로 다른 계층을 각각 담당한다. 이 분리는 부분 업데이트를…",
      "url": "/public/istio/xds__note-xds-api-layers.html",
      "domain": "istio",
      "text": "istio xds envoy ads xDS는 Envoy 설정을 LDS/RDS/CDS/EDS/SDS 계층으로 나누고 ADS가 이를 단일 스트림으로 묶어 적용 순서를 보장한다 ℹ 이 문서가 다루는 것 Envoy의 동적 설정은 단일 덩어리가 아니라 5개 xDS API (LDS/RDS/CDS/EDS/SDS)로 분리되어 \"리스닝 지점 / 라우팅 / upstream pool / 실제 endpoint / TLS secret\"이라는 서로 다른 계층을 각각 담당한다. 이 분리는 부분 업데이트 를 가능하게 하지만 동시에 순서 문제 를 낳는다 — route가 아직 없는 cluster를 가리키면 트래픽이 깨진다. ADS(Aggregated Discovery Service) 가 이 여러 xDS를 하나의 gRPC 스트림으로 묶어 적용 순서를 보장함으로써 그 문제를 푼다. 이 문서는 그 개념·멘탈모델 에 집중하고, istioctl proxy-config 로 각 계층을 진단하는 운영 상세는 xDS 5계층과 istioctl 진단 으로 위임한다. 대상환경: Istio 1.30, sidecar mode, Envoy. · 대상독자: xDS를 \"이름은 들었는데 왜 5개로 쪼개고 ADS가 뭘 해결하는지\" 모르는 DevOps/SRE. · 범위: 계층 분리의 동기 → 5계층 역할 → 순서/일관성 메커니즘 → istiod 전달 경로. · 선행: Envoy listener/route/cluster 기본 개념. 01. 배경 — 왜 설정을 한 덩어리가 아니라 계층으로 쪼개나 ★ 한 문장 멘탈모델 (이 그림 하나만 잡으면 된다) xDS = istiod가 K8s/Istio 상태를 Envoy 설정으로 컴파일해, 변화 빈도가 다른 5개 계층(LDS/RDS/CDS/EDS/SDS)을 각각 독립 갱신하는 채널이고, ADS는 그 5개를 한 스트림에 묶어 \"참조 대상이 먼저, 참조하는 쪽이 나중\"이라는 적용 순서를 강제하는 봉투다. 아래 모든 디테일은 이 한 그림의 세부다. Envoy를 정적 부트스트랩 YAML로만 운영하면 listener/route/cluster/endpoint가 한 파일에 고정된다(정적/동적 경계는 정적 vs 동적 설정 참조). 메시 환경에서 이게 깨지는 이유는 단 하나 — 이 설정의 각 부분이 변화 빈도가 완전히 다르다 . Endpoint : Pod가 뜨고 죽을 때마다 변함 → 초 단위로 자주 Cluster : Service/DestinationRule 바뀔 때 → 가끔 Route : VirtualService 바뀔 때 → 가끔 Listener : Gateway/포트 바뀔 때 → 드물게 Secret : 인증서 rotation 주기마다 → 드물게 설정이 단일 blob이라면 Pod 하나가 죽을 때마다 listener·route·cluster 전체를 다시 컴파일해 내려보내야 한다 — 가장 자주 바뀌는 부분(endpoint)이 가장 안 바뀌는 부분(listener)의 재전송을 끌고 다닌다. xDS는 이를 독립적으로 갱신 가능한 5개 API 로 쪼개서, endpoint만 바뀌면 EDS만 push하고 나머지는 손대지 않는다. 이것이 계층 분리의 1차 동기 — 변화 빈도별 분리(decoupling by churn rate) 다. 분리에는 대가가 따르는데(03절의 순서 문제), 그 대가를 ADS가 갚는 구조다. 02. 5개 계층 각각이 담당하는 질문 요청 하나가 Envoy를 통과하는 흐름을 따라가면 다섯 계층의 역할이 한 줄로 꿰진다. 요청은 항상 Listener → Route → Cluster → Endpoint 순으로 resolve되고, mTLS가 걸리면 그 과정에서 Secret 을 쓴다. 각 계층을 \"필드\"가 아니라 \"그게 답하는 질문\"으로 읽으면 외울 필요가 없다. 그림 1. xDS 5계층: LDS(수신)→RDS(cluster 선택)→CDS(upstream pool+policy)→EDS(Pod IP), CDS가 mTLS 시 SDS(인증서)를 참조. 각 계층이 독립 API라 부분 갱신·진단 가능. API 풀네임 답하는 질문 내려주는 것 Istio 리소스(대표) LDS Listener Discovery Service 어디서 트래픽을 받나 listener + filter chain (15001 out / 15006 in / 포트별) Gateway , Sidecar , PeerAuthentication RDS Route Discovery Service 이 HTTP 요청을 어느 cluster로 route config (virtual host, route entry) VirtualService , Gateway CDS Cluster Discovery Service upstream 목적지 정의 와 정책 cluster (upstream pool) + LB/timeout/outlier Service , ServiceEntry , DestinationRule EDS Endpoint Discovery Service 그 cluster의 실제 Pod IP 들 cluster 안의 endpoint 목록 EndpointSlice , WorkloadEntry SDS Secret Discovery Service mTLS handshake에서 workload identity 증명 TLS cert / private key / root CA istiod CA, PeerAuthentication 가장 자주 혼동되는 두 지점을 메커니즘으로 못 박아 두자. RDS의 D는 Discovery다 — \"Route Direct Service\"가 아니다. xDS 전체가 x + Discovery Service 패턴(L/R/C/E/S + DS)이라는 점을 기억하면 안 헷갈린다. CDS와 EDS는 따로 온다 — cluster( outbound|9080|v1|reviews... )는 CDS로 정의되고, 그 cluster 안에 들어갈 endpoint 목록( 10.244.1.12:9080 ...)은 EDS로 별도 스트림으로 온다. 이게 분리돼 있기 때문에 \"cluster 객체는 존재하는데 그 안의 endpoint가 비어서 503 UH (no healthy upstream)\"라는, 처"
    },
    {
      "title": "Envoy Cluster 해부 — 포트 4-layer, subset, DestinationRule 매핑 (출처: \"Istio 운영 노하우\" 정리 + Istio 1.30 공식 문서)",
      "desc": "Envoy cluster는 \"이 트래픽을 어느 upstream으로, 어떻게 보낼까\"를 답하는 단 하나의 객체다 — discovery·LB·connection pool·circuit breaker·outlier detection·TLS를 전부 품는다. 그리고 사람이 쓰는 Destinat…",
      "url": "/public/istio/xds__src-cluster-anatomy.html",
      "domain": "istio",
      "text": "istio envoy cluster destinationrule subset load-balancing connection-pool Envoy Cluster 해부 — 포트 4-layer, subset, DestinationRule 매핑 (출처: \"Istio 운영 노하우\" 정리 + Istio 1.30 공식 문서) ℹ 이 문서가 다루는 것 Envoy cluster 는 \"이 트래픽을 어느 upstream으로, 어떻게 보낼까\"를 답하는 단 하나의 객체다 — discovery·LB·connection pool·circuit breaker·outlier detection·TLS를 전부 품는다. 그리고 사람이 쓰는 DestinationRule 이 istiod를 거쳐 이 cluster의 칸칸으로 컴파일 된다. 이 문서는 ① cluster가 무엇을 품는가, ② 그 cluster를 따라가면 같은 포트 숫자가 왜 4개의 다른 layer로 갈라지는가, ③ DestinationRule의 각 필드가 cluster의 어디로 떨어지는가, ④ 그것을 istioctl proxy-config 로 추적하는 법을 다룬다. 대상환경 : Istio 1.30, sidecar mode (Ambient는 datapath가 다름). 대상독자 : cluster/endpoint/포트가 헷갈려 디버깅이 막히는 SRE. 선행개념 : Kubernetes Service port / targetPort , sidecar가 트래픽을 가로챈다는 사실. 범위 : outbound cluster 중심. route가 cluster를 참조하는 법·xDS 5계층(LDS/RDS/CDS/EDS/SDS)·response flag 상세는 Envoy 응답 플래그 · xDS 5계층과 진단 에서. 01. 배경 — sidecar가 끼어드는 순간 포트가 4개로 갈라진다 sidecar가 없던 세계는 단순하다. app이 reviews:9080 으로 connect하면 kube-proxy가 Service VIP를 Pod IP로 NAT해서 그대로 보낸다. 포트는 사실상 하나의 의미만 가진다. sidecar mode는 그 사이에 Envoy를 강제로 끼워 넣는다. iptables가 app의 outbound TCP를 통째로 가로채 Envoy로 돌리고, Envoy가 \"원래 어디로 가려던 거였지?\"를 복원해 라우팅·정책·mTLS를 적용한 뒤 진짜 backend로 보낸다. 이 가로채기-복원-재전송 과정에서 하나였던 포트 숫자가 의미가 다른 4개의 layer로 분해된다. Istio 디버깅 초심자가 \"cluster는 80인데 endpoint는 왜 8080이지?\"에서 막히는 이유가 바로 이것이다 — 그들은 4개를 하나로 보고 있다. 그 라우팅의 끝, \"이제 어느 backend로 보낼까\"를 결정하는 객체가 cluster 다. cluster는 Envoy의 보편 추상이다: NGINX의 upstream 블록이 답하는 질문(\"어느 서버 그룹으로\")에, Istio가 운영에서 필요로 하는 모든 질문(어떻게 분산할까, 몇 개까지 동시에 보낼까, 죽은 endpoint를 어떻게 뺄까, TLS는 어떻게 걸까)을 한 객체로 합쳐 놓은 것이다. 그래서 \"포트가 왜 4개냐\"와 \"DestinationRule이 어디로 가냐\"는 결국 같은 질문 — 둘 다 \"cluster라는 객체를 정확히 읽을 줄 아느냐\"로 수렴한다. 이 문서는 그 cluster를 해부한다. 02. 핵심 멘탈모델 — cluster 하나가 모든 정책을 흡수한다 머릿속에 하나만 담으면 이것입니다 : Envoy cluster는 \"upstream backend pool\" 하나에 discovery·LB·pool·CB·outlier·TLS를 다 욱여넣은 객체이고, 사람이 쓰는 DestinationRule이 istiod를 거쳐 그 cluster의 칸칸으로 컴파일 된다 — 그리고 그 cluster를 따라가다 보면 같은 포트 숫자가 4개의 다른 layer(capture 15001 → listener → Service port → endpoint targetPort)로 나타난다. 나머지는 전부 이 그림의 세부다. 개념적으로 cluster는 NGINX upstream 과 출발점이 같다. \"어느 backend 서버들로 보낼 것인가\"라는 질문은 동일하다. # NGINX upstream — backend 목록이 거의 전부 upstream reviews { server 10.0.1.10:9080; server 10.0.2.20:9080; } 차이는 cluster가 그 한 객체에 훨씬 많은 것을 흡수 한다는 점이다. Envoy Cluster proto에는 name , type , eds_cluster_config , connect_timeout , lb_policy , load_assignment , circuit_breakers , HTTP protocol options, DNS 설정, outlier_detection , transport_socket 가 모두 들어간다. (비유의 한계: NGINX도 upstream에 least_conn · keepalive ·health check를 붙일 수 있지만, cluster는 이 모든 운영 축을 xDS로 동적 교체 가능한 한 resource 로 묶었다는 점이 본질적으로 다르다.) NGINX upstream ≈ backend server group Envoy cluster = backend server group + dynamic discovery (EDS/DNS/STATIC) + LB policy + connection pool + circuit breaker + outlier detection + TLS/mTLS transport socket + HTTP/1.1·HTTP/2 protocol option + observability/stat metadata Envoy cluster manager가 이 cluster들을 모두 관리하고, filter stack은 cluster로부터 L3/L4 connection 또는 HTTP connection pool handle을 얻는다. cluster는 static으로 박을 수도, CDS API로"
    },
    {
      "title": "Istio CR 멘탈 모델 — CR은 입력, Envoy config가 진실 (출처: LLM 답변 + Istio 1.30 공식 문서)",
      "desc": "Istio CR을 외우는 가장 빠른 길은 각 CR을 따로 암기하는 게 아니라, \"CR을 만들면 Envoy 설정의 어느 칸이 채워지는가\" 를 하나의 데이터 흐름으로 이해하는 것. 이 문서는 그 한 축으로 모든 CR을 꿰어, CR을 보면 Envoy에 무엇이 박힐지 자연스럽게 떠오르게 만드…",
      "url": "/public/istio/xds__src-cr-xds-model.html",
      "domain": "istio",
      "text": "istio service-mesh envoy xds mental-model Istio CR 멘탈 모델 — CR은 입력, Envoy config가 진실 (출처: LLM 답변 + Istio 1.30 공식 문서) ℹ 이 문서의 목표 Istio CR을 외우는 가장 빠른 길은 각 CR을 따로 암기하는 게 아니라, \"CR을 만들면 Envoy 설정의 어느 칸이 채워지는가\" 를 하나의 데이터 흐름으로 이해하는 것. 이 문서는 그 한 축으로 모든 CR을 꿰어, CR을 보면 Envoy에 무엇이 박힐지 자연스럽게 떠오르게 만드는 게 목표. 대상환경: Istio 1.30 / Envoy / networking.istio.io/v1 · security.istio.io/v1 대상독자: CR은 써봤지만 \"이게 내부에서 뭘 바꾸는지\" 감이 안 잡히는 DevOps/SRE 범위: 트래픽·보안 핵심 6개 CR ↔ 5개 xDS 매핑 + proxy-config 검증 루틴 선행개념: Kubernetes Service/Pod, TLS 핸드셰이크(downstream/upstream), Istio 사이드카가 트래픽을 가로챈다는 사실 01. 배경 — 왜 CR을 외워도 동작이 안 보이나 Istio를 처음 다룰 때의 좌절은 거의 항상 같은 모양이다. VirtualService·DestinationRule·Gateway·PeerAuthentication을 문서대로 썼는데, \"이게 정확히 무슨 일을 일으키는지\" 가 머릿속에 그려지지 않는다. 그래서 안 되면 디버깅할 방향이 없고, CR마다 필드를 따로 외우다 양에 깔린다. 근본 원인은 한 가지다. 데이터 플레인을 실제로 굴리는 건 Istio가 아니라 Envoy인데, Envoy는 Istio CR이라는 개념 자체를 모른다. Envoy 입장에서 VirtualService도 DestinationRule도 세상에 존재하지 않는다. Envoy가 아는 건 오직 자기 네이티브 설정 — listener, route, cluster, endpoint, secret뿐이다. 그 사이를 잇는 게 istiod(컨트롤 플레인) : CR을 watch하다가 Envoy 네이티브 설정(xDS)으로 번역 해서 각 프록시에 gRPC로 push한다. 사람이 쓰는 의도 번역기 Envoy가 실제로 따르는 것 (Kubernetes CR) ---> istiod ---> (xDS native config) VirtualService watch route(RDS) DestinationRule translate cluster(CDS) Gateway push(gRPC) listener(LDS) ... endpoint(EDS), secret(SDS) 이 구조를 모르면 CR과 동작이 두 개의 분리된 세계로 보인다. 이 구조를 알면 둘이 하나의 데이터 흐름으로 합쳐진다 — 그게 이 문서가 세우려는 멘탈 모델이고, 디버깅·활용·학습이 전부 같은 한 질문으로 통일되는 이유다. 02. 핵심 멘탈 모델 — CR은 입력일 뿐, 진실은 Envoy config에 있다 ✓ 한 문장 앵커 (이 그림 하나만 머리에 박아라) Envoy는 Istio CR을 전혀 모른다. istiod가 CR들을 읽어 Envoy 네이티브 설정(xDS)으로 번역 해 각 프록시에 push할 뿐. 그러니 \"CR을 어떻게 쓰지?\"·\"왜 안 되지?\"의 답은 항상 \"이 CR이 Envoy의 어느 xDS 칸을 채우는가?\" 로 환원된다. 이 앵커 하나에서 나머지가 전부 따라 나온다. 디버깅이 통일되는 게 그 첫 결실이다 — 증상이 무엇이든 \"어느 칸\"을 묻는 질문으로 바뀐다. 라우팅이 이상하다 → \"route(RDS)에 뭐가 박혔지?\" 인증서가 안 먹는다 → \"secret(SDS)에 로드됐나?\" 트래픽이 엉뚱한 데로 간다 → \"cluster(CDS)와 endpoint(EDS)가 맞나?\" CR 이름이 아니라 xDS 칸 으로 생각하면, 처음 보는 CR을 만나도 \"얘는 어느 칸을 건드리지?\"만 물으면 된다. 그래서 학습 순서도 거꾸로 간다 — 결과물(xDS)을 먼저 보고, 그다음 그걸 만드는 입력(CR)을 본다. 그림 1. 사용자는 CR(ServiceEntry/Gateway/VS/DR/PeerAuth)을 작성, istiod가 watch·번역해 xDS(LDS/RDS/CDS/EDS/SDS)로 Envoy에 push. CR과 xDS는 다른 추상화 — 진단은 둘 다 봐야 함. 왼쪽(CR)은 사람이 쓰는 의도, 오른쪽(xDS)은 Envoy가 실제로 따르는 설정. istiod가 둘을 잇는 번역기다. ℹ 이 문서의 진행 (멘탈 모델 구축 순서) 먼저 도착지를 본다 — Envoy의 5가지 설정 칸(xDS)이 무엇인지 (섹션 03) 그다음 다리를 놓는다 — 어떤 CR이 어느 칸을 채우는지 매핑 (섹션 04) 칸을 채우는 도구를 하나씩 — CR별 기능·설정·실제 반영 (섹션 05) 흐름으로 통합 — 요청 하나가 5칸을 어떻게 지나는지 (섹션 06) 직접 확인 — istioctl proxy-config 로 진실을 보는 법 (섹션 07) 03. 도착지 먼저 — Envoy의 5가지 설정 (xDS) CR을 이해하려면 \"CR이 만들어 내는 결과물\"부터 알아야 한다. 그 결과물이 xDS다. xDS는 \"x Discovery Service\"의 묶음으로, Envoy가 동적으로 설정을 받아오는 API 종류들이다. Envoy는 이 설정만 보고 동작하지 Istio CR은 모른다. 5칸을 따로 외우려 하면 안 외워진다. 요청 하나가 Envoy를 통과하는 순서대로 보면 자연스럽게 묶인다: 들어와서(LDS) → 어디로 보낼지 정하고(RDS) → 대상 그룹을 고르고(CDS) → 실제 IP를 찾고(EDS) → 필요하면 인증서를 꺼낸다(SDS). 이 순서가 곧 섹션 06의 요청 흐름이자 디버깅 순서가 된다. xDS 칸 이름 무엇을 담나 (요청 처리 순서대로) LDS listener Envoy가 어떤 포트로 받고 , 그 포트에 어떤 필터 체인 (TLS 종단/통과, HTTP 파싱, RBAC 등)을 걸지. \"입구\"를 정의 RDS route HTTP 요청을 어느 cluster로 보낼지 규칙(호스트·경로·헤더 매칭, 가중치, 재시도, 타임아웃). \"길"
    },
    {
      "title": "Envoy Response Flags 운영 레퍼런스 (출처: Istio 1.30 공식 문서 — Envoy access log response flags)",
      "desc": "status code는 \"결과\"만, response flag는 그 결과가 Envoy 처리 파이프라인의 어느 단계에서 났는지를 알려준다. 같은 503도 flag(UF/UH/UO/NR)에 따라 원인 부서가 완전히 다르다 — 그래서 503을 보면 status가 아니라 flag를 본다. 이…",
      "url": "/public/istio/xds__src-envoy-response-flags.html",
      "domain": "istio",
      "text": "istio envoy response-flags access-log telemetry troubleshooting Envoy Response Flags 운영 레퍼런스 (출처: Istio 1.30 공식 문서 — Envoy access log response flags) ℹ 이 문서가 다루는 것 status code는 \"결과\"만, response flag는 그 결과가 Envoy 처리 파이프라인의 어느 단계에서 났는지를 알려준다. 같은 503도 flag(UF/UH/UO/NR)에 따라 원인 부서가 완전히 다르다 — 그래서 503을 보면 status가 아니라 flag를 본다. 이 문서는 ① 왜 status만으로는 부족한지(배경) → ② response flag가 Envoy 파이프라인 단계를 어떻게 1:1로 투영하는지(메커니즘·기억법) → ③ 28개 flag 레퍼런스 표와 503/504 빠른 triage → ④ long flag와 진단 필드를 access log에 노출시키는 설정과 실제 JSON 로그 순으로 전개한다. 01. 배경 — 왜 status code 한 줄로는 원인을 못 찾나 운영에서 5xx를 보면 가장 먼저 떠오르는 질문은 \"어디가 고장났나\"입니다. 그런데 HTTP status code는 그 질문에 답하지 않습니다. 503 하나를 두고 Envoy 입장에서 가능한 시나리오를 펼치면: upstream으로 TCP/TLS connection 자체를 못 맺었다. cluster는 있는데 healthy endpoint가 0이다. circuit breaker(connection pool 한도)가 의도적으로 막았다. 요청에 매칭되는 route가 아예 없다. 심지어 app이 직접 503 body를 만들어 정상 응답으로 돌려줬다 (Envoy는 멀쩡). 이 다섯은 대응 부서가 전부 다릅니다(네트워크/플랫폼/리소스 한도/라우팅 설정/애플리케이션). 그런데 클라이언트가 받는 status code는 모두 똑같이 503 입니다. status code는 \"무엇이 일어났는가(결과)\"만 말하고, \"처리의 어느 지점에서 일어났는가(위치)\"는 말하지 않습니다. 바로 그 \"위치\"를 채워주는 한 토큰이 response flag입니다. 이 문서를 읽기 전에 알아두면 좋은 선행 개념은 Envoy가 요청을 처리하는 큰 골격입니다 — 요청은 listener → filter chain → route → cluster → endpoint(upstream connection) 순으로 흐릅니다. response flag의 핵심은 이 흐름의 각 단계가 실패할 때 서로 다른 flag를 찍는다 는 점이고, 02장이 그 대응을 메커니즘으로 풉니다. 단계별 진단의 정본은 xDS 계층과 진단 을 함께 참조하세요. 대상 독자는 access log로 5xx triage를 해야 하는 SRE/플랫폼 엔지니어이고, 범위는 Istio 1.30(Envoy) 기준 HTTP/TCP response flag 해석과 그 노출 설정입니다. 02. 핵심 — response flag = Envoy 파이프라인 단계의 투영 ★ 한 문장 멘탈모델 (이 그림만 머리에 넣으면 된다) response flag는 요청이 Envoy 처리 파이프라인의 어느 단계에서 죽었는지를 가리키는 좌표 다. flag를 외우는 게 아니라, \"이 요청은 route 단계까지 갔나 / cluster를 찾았나 / endpoint에 연결됐나 / 응답을 받았나\"를 단계 위에 찍는 것이다. 그래서 status가 같아도 flag가 다르면 죽은 위치가 다르다. 요청 하나가 Envoy를 통과하는 동안 여러 관문을 지납니다. 각 관문은 \"통과 / 여기서 실패\"라는 게이트이고, 실패하면 그 관문 고유의 flag를 남기고 응답을 끝냅니다. 이 대응을 그림으로 두면 표 28줄이 전부 derivable해집니다. 그림 1. 파이프라인 단계 = response flag. 요청이 listener→route→cluster→endpoint→pool→connect→response를 통과하며, 각 단계의 실패가 고유 flag로 찍힌다(NR/NC/UH/UO/UF/UT/UR·UC). 끝까지 통과하면 '-', client가 먼저 끊으면 DC — flag는 \"방향(N/U/D) + 사건\"의 조합이라 추론 가능하다. 이 파이프라인을 따라 읽으면 방향 + 사건 이라는 작명 규칙이 왜 이렇게 생겼는지가 보입니다. flag는 통째로 외우는 약어가 아니라, \"누가(방향)\" + \"무엇을(사건)\"의 조합이라 처음 보는 flag도 추론됩니다. 방향 접두사 N = No (없음: NoRoute, NoCluster, NoHealthy) U = Upstream (서버 쪽 / 우리가 보내는 대상) D = Downstream (클라이언트 쪽 / 우리에게 보낸 주체) L = Local (Envoy 자기 자신이 발생시킴) 사건 R = Route / Retry / Reset O = Overflow / Overload T = Timeout F = Failure H = Healthy C = Cluster / Connection 조합하면 의미가 바로 풀립니다 — 그리고 각 조합이 위 파이프라인의 어느 게이트인지도 같이 보입니다. NR = No + Route → route/filter chain 단계에서 매칭 실패 NC = No + Cluster → cluster 조회 단계 실패 UH = (No) + Upstream + Healthy → endpoint 선택 단계, healthy 0 UO = Upstream + Overflow → pool/circuit breaker 게이트에서 차단 UF = Upstream + Failure → connect 단계 실패 UT = Upstream + Timeout → 응답 대기 단계 timeout DC = Downstream + Connection → client가 응답 전에 연결 끊음 LR = Local + Reset → Envoy 자신이 reset(timeout/overload/filter) URX = Upstream + Retry + eXceeded → retry limit 초과 ✓ 핵심 방향 접두사(U/D/L/N)와 사건(F/H/O/"
    },
    {
      "title": "Envoy 정적/동적(xDS) 설정 실습 — Istio in Action Ch.3.2 (출처: 책 + 실습기록 + 공식문서)",
      "desc": "Envoy 설정은 listener → route → cluster → endpoint가 서로를 이름으로 참조하는 한 줄 사슬이다. 정적 모드는 이 사슬을 bootstrap에 통째로 인라인하고, 동적 모드(xDS)는 같은 사슬의 각 노드를 컨트롤 플레인이 런타임에 push로 채운다 —…",
      "url": "/public/istio/xds__src-envoy-static-dynamic-xds-lab.html",
      "domain": "istio",
      "text": "istio envoy xds Envoy 정적/동적(xDS) 설정 실습 — Istio in Action Ch.3.2 (출처: 책 + 실습기록 + 공식문서) NOTE Envoy 설정은 listener → route → cluster → endpoint 가 서로를 이름으로 참조하는 한 줄 사슬이다. 정적 모드는 이 사슬을 bootstrap에 통째로 인라인하고, 동적 모드(xDS)는 같은 사슬의 각 노드 를 컨트롤 플레인이 런타임에 push로 채운다 — 사슬 자체는 동일하고 \"공급 방식\"만 다르다. 이 문서는 그 한 사슬을 정적(bootstrap 인라인) → 파일 기반 동적(LDS/RDS) → 실제 ADS(gRPC) 세 가지 공급 방식으로 직접 돌려, xDS 계층(LDS/RDS/CDS/EDS/SDS+ADS)이 왜 그렇게 설계됐는지를 손으로 체득하는 실습 기록이다. 결론 먼저: 학습이 목적이면 파일 기반 EDS 삽질을 피하고, 파일 동적 LDS/RDS로 라우팅 변화를 눈으로 본 뒤 곧장 Istio on kind의 istioctl proxy-config 로 진짜 ADS를 관찰하는 경로가 가장 효율적이다. 대상환경 : Mac + Docker (Apple Silicon 포함), Envoy v1.35, Istio 1.30. 대상독자 : Envoy/Istio data plane이 \"어떻게 설정을 받는가\"를 메커니즘 수준으로 알고 싶은 DevOps/SRE. 범위 : 한 백엔드 트래픽을 정적·파일동적·ADS로 공급. 선행개념 : HTTP proxy, gRPC, Docker network. 1. 배경 — 왜 Envoy에 \"정적\"과 \"동적\" 두 모드가 있나 Envoy는 reverse proxy다. 트래픽을 받으려면 최소한 네 가지를 알아야 한다: 어느 포트로 받고 (listener), 무슨 규칙으로 분배하고 (route), 어떤 백엔드 그룹으로 보내고 (cluster), 그 그룹의 실제 IP는 무엇인지 (endpoint). 이 네 가지를 한 파일( envoy.yaml )에 다 적어두면 끝 — 이게 정적(static) 설정이다. 단독 proxy를 띄울 땐 완벽하다. 문제는 service mesh다. Istio 사이드카는 mesh 토폴로지가 살아 움직이는 환경에서 동작한다. Pod가 scale-out되면 endpoint가 늘고, VirtualService를 고치면 route가 바뀌고, 새 Service가 생기면 cluster가 추가된다. 이걸 정적 파일로 다루려면 변경마다 사이드카를 재시작 해야 하는데, 그러면 in-flight 연결이 끊기고 mesh 전체가 출렁인다. 재시작 없이, 컨트롤 플레인(istiod)이 런타임에 설정을 push 해 반영하는 메커니즘 — 그게 동적(dynamic) 설정이고, 그 push 프로토콜이 xDS 다. 즉 정적 vs 동적은 \"기능\"의 차이가 아니라 설정을 누가·언제 공급하느냐 의 차이다. 정적은 내가 부팅 시점에 통째로 , 동적은 컨트롤 플레인이 런타임에 조각조각 . 이 한 문장을 잡고 가면 아래 세 실습이 모두 \"같은 사슬, 다른 공급\"으로 보인다. 책은 Envoy v3 설정 API 만 다룬다(v1/v2는 폐기됨). 실습도 v3 typed_config YAML로 진행한다. v3 type URL은 type.googleapis.com/envoy....v3.... 형태로, 각 리소스가 자기 타입을 명시하는 self-describing 구조다(파일/gRPC 어느 경로든 동일). 2. 핵심 아키텍처 — 사슬과 xDS 계층 머릿속 앵커 : Envoy 설정은 listener → route → cluster → endpoint 한 사슬이고, 각 노드는 다음 노드를 이름(string)으로 참조 한다. 정적이든 동적이든 이 사슬의 모양은 같다. xDS의 각 \"x\"는 이 사슬의 한 노드를 런타임에 채워 넣는 API 일 뿐이다. 그림 1. 이름 참조 사슬. listener는 route를 route_config_name으로, route는 cluster를 name으로, cluster는 endpoint를 cluster_name으로 가리킨다. 네 단계(강조)가 모두 동적(xDS) 공급이라, 가리키는 대상이 먼저 도착하지 않으면 참조가 dangling이 되어 순서 제약이 생긴다. 사슬이 \"이름 참조\"라는 점이 모든 설계의 뿌리다. listener는 route를 route_config_name 으로, route는 cluster를 name 으로, cluster는 endpoint를 cluster_name 으로 가리킨다. 가리키는 대상이 아직 없으면 그 참조는 dangling 이다. 그래서 동적 공급에는 순서 제약 이 생긴다: route가 가리키는 cluster가 먼저 도착해야 한다. xDS 계층 — 각 API가 채우는 사슬 노드 각 API는 사슬의 한 노드를 담당한다. \"필드\"가 아니라 \"그게 답하는 질문\"으로 보면 직관적이다: API 답하는 질문 (= 채우는 노드) 비유 LDS (Listener) 어떤 포트로 트래픽을 받을지 현관문 RDS (Route) 받은 요청을 어떤 클러스터로 보낼지(가중치/리트라이/타임아웃) 교통 표지판 CDS (Cluster) 백엔드 서비스 그룹 정의 목적지 건물 목록 EDS (Endpoint) 클러스터에 속한 실제 IP들 건물 안 실제 방 번호 SDS (Secret) TLS 인증서 출입증 ADS (Aggregated) 위 전부를 단일 gRPC 스트림으로 묶어 순서 보장 모든 변경을 한 통로로 왜 ADS인가 — 단일 스트림이 푸는 문제 xDS를 계층별로 따로 스트림으로 받으면(예: LDS는 LDS 스트림, CDS는 CDS 스트림) 계층 간 도착 순서를 보장할 수 없다. route가 먼저 도착하고 그 route가 가리키는 cluster가 아직 안 왔으면, Envoy는 일시적으로 dangling 참조 상태에 빠진다. ADS(Aggregated Discovery Service) 는 모든 계층을 하나의 gRPC 스트림 으로 묶어, 컨트롤 플레인이 \"cluster 먼저, 그 다음 route\" 순서로 보낼 수 있게 한다. Istio는 istiod 가 이 ADS 한 스트림으로 사이드카"
    },
    {
      "title": "Sidecar 트래픽 캡처 — iptables/nftables와 15001·15006",
      "desc": "mesh의 모든 기능(mTLS·route·authz·telemetry)은 \"app의 모든 패킷이 Envoy를 지나간다\"는 단 하나의 전제 위에 서 있다. 그 전제를 app 코드 수정 없이 강제하는 장치가 커널 netfilter(iptables/nftables) redirection이다…",
      "url": "/public/istio/xds__src-sidecar-traffic-capture.html",
      "domain": "istio",
      "text": "istio sidecar iptables nftables traffic-capture envoy dataplane Sidecar 트래픽 캡처 — iptables/nftables와 15001·15006 ℹ 이 문서가 다루는 것 mesh의 모든 기능(mTLS·route·authz·telemetry)은 \"app의 모든 패킷이 Envoy를 지나간다\"는 단 하나의 전제 위에 서 있다. 그 전제를 app 코드 수정 없이 강제하는 장치가 커널 netfilter(iptables/nftables) redirection 이다. 이 문서는 그 redirection을 — 왜 필요한지(배경)부터, 누가 어떻게 rule을 거는지( pilot-agent istio-iptables → IptablesConfigurator.Run → iptables-restore --noflush ), REDIRECT mode의 핵심 체인과 proxy UID/GID 1337 무한 루프 방지 불변식, 실제 *nat 블록· --dry-run ·native nftables 경로·검증 명령까지 — 한 줄기로 따라간다. 결론: app은 평소처럼 service:port 로 connect하지만 모든 트래픽이 app 모르게 투명하게 Envoy를 통과한다. ⚠ 범위 본 문서는 sidecar mode 전용 이다. Ambient mode는 per-pod 15001/15006 모델이 아니라 CNI + ztunnel 기반 redirection을 쓰므로 datapath가 완전히 다르다 — 본 문서 범위 밖. 00. 배경 — 왜 \"투명 캡처\"가 필요한가 mesh가 약속하는 것들 — 두 pod 사이 자동 mTLS, VirtualService route, AuthorizationPolicy 차단, 요청 단위 telemetry — 은 전부 \"트래픽이 어딘가의 proxy를 거쳐야\" 가능하다. proxy가 패킷을 손에 쥐어야 암호화하고, 경로를 바꾸고, 거부하고, 세는 것이 가능하기 때문이다. 문제는 app이 그 proxy의 존재를 몰라야 한다 는 점이다. \"트래픽을 proxy로 보내라\"를 app에게 요구하면(예: 환경변수 HTTP_PROXY , SDK 주입, 호스트네임 변경) mesh는 더 이상 인프라가 아니라 애플리케이션 변경 이 된다. 수백 개 서비스에 코드/설정 변경을 강제할 수 없다. 그래서 Istio가 푸는 진짜 문제는 이것이다. app은 service:9080 으로 평소처럼 connect하는데, 그 패킷이 app도 모르게 같은 pod 안의 Envoy로 빨려 들어가게 만들 수 있는가? 답은 L4 아래, 커널에서 가로채기 다. app의 socket이 보낸 패킷은 커널 network stack을 반드시 통과하고, 커널에는 패킷의 목적지를 바꾸는 hook 지점(netfilter)이 이미 있다. Istio는 그 hook에 rule을 심어, app이 의도한 목적지 대신 로컬 Envoy 포트로 목적지를 바꿔치기(REDIRECT) 한다. app socket 입장에서는 여전히 service:9080 에 연결했다고 믿지만, 실제 패킷은 127.0.0.1:15001 (Envoy)로 들어간다. 이것이 \"투명(transparent)\"의 의미다. 선행 개념 — netfilter REDIRECT 한 가지만 : Linux netfilter의 nat 테이블에는 PREROUTING (들어오는 패킷이 라우팅되기 전), OUTPUT (로컬 프로세스가 내보내는 패킷) 같은 hook이 있고, REDIRECT target은 패킷의 목적지 주소/포트를 로컬 머신의 다른 포트 로 갈아끼운다. iptables는 그 rule을 거는 전통 도구이고, nftables는 후속 세대다. Istio가 하는 일은 결국 이 hook에 \"app 트래픽이면 로컬 Envoy 포트로 REDIRECT\"라는 rule 한 묶음을 pod의 network namespace 안에 박는 것이 전부다. [ 트래픽 캡처가 없으면 ] app socket → 커널 → NIC → 진짜 목적지 (Envoy를 건너뜀 = mesh 무력화) [ 트래픽 캡처가 있으면 ] app socket → 커널 netfilter REDIRECT → Envoy 15001/15006 → ... → 진짜 목적지 └─ app은 이 우회를 전혀 모름 (transparent) ─┘ 이 한 우회가 mTLS/route/authz/telemetry 전부의 전제 다. 캡처가 안 걸리면 Envoy 설정이 아무리 완벽해도 트래픽이 Envoy를 안 지나가니 mesh 기능이 통째로 죽는다. 그래서 \"왜 내 정책이 안 먹지?\"의 1차 용의자는 항상 캡처가 실제로 박혔는지다. 01. 멘탈모델 앵커 — 두 개의 입구, 하나의 불변식 ★ 한 문장 멘탈모델 Istio sidecar 캡처의 본질은 단 세 가지다 — (1) app이 밖으로 나가는 패킷은 커널이 Envoy의 outbound 입구 15001 로 REDIRECT한다, (2) pod로 들어오는 패킷은 Envoy의 inbound 입구 15006 으로 REDIRECT한다, (3) 단 Envoy(UID/GID 1337) 자신이 만든 패킷은 다시 잡지 않는다 (무한 루프 방지). 이 세 줄에서 나머지 모든 체인·포트·예외가 따라 나온다. 이 그림 하나만 머리에 박으면 된다. pod network namespace ┌──────────────────────────────────────────────────────────────┐ │ │ │ app ──connect svc:9080──> [OUTPUT] ─ ISTIO_OUTPUT │ │ │ │ │ uid/gid 1337? ─yes─> RETURN ──────┼─> 진짜 목적지 │ │ no │ (Envoy가 나간 것) │ v │ │ REDIRECT :15001 ─> Envoy out │ │ │ │ remote ──> podIP:9080 ─ [PREROUTING] ─ ISTIO_INBOUND │ │ │ │ │ 15008/15020/15021/15090? ─yes─> RETURN│ │ │ no │ │ v │ │ REDIRECT :15006 ─> Envoy in ─┼─> app :9080 │"
    },
    {
      "title": "xDS 5계층과 istioctl 진단 — LDS/RDS/CDS/EDS/SDS (출처: Istio 1.30 공식 문서 + 홈랩 검증)",
      "desc": "istiod는 컴파일러다. K8s 상태 + Istio CRD를 입력으로 받아 Envoy 설정(Listener/Route/Cluster/Endpoint/Secret)으로 컴파일하고, xDS 채널로 각 프록시에 push한다. 그래서 모든 트래픽 장애는 \"이 5계층 중 어디서 끊겼나\"로 환…",
      "url": "/public/istio/xds__src-xds-layers-and-diagnosis.html",
      "domain": "istio",
      "text": "istio xds envoy istioctl proxy-status envoyfilter diagnosis xDS 5계층과 istioctl 진단 — LDS/RDS/CDS/EDS/SDS (출처: Istio 1.30 공식 문서 + 홈랩 검증) ℹ 이 문서가 다루는 것 istiod는 컴파일러다. K8s 상태 + Istio CRD를 입력으로 받아 Envoy 설정(Listener/Route/Cluster/Endpoint/Secret)으로 컴파일하고, xDS 채널로 각 프록시에 push한다. 그래서 모든 트래픽 장애는 \"이 5계층 중 어디서 끊겼나\"로 환원되고, 진단은 Listener→Route→Cluster→Endpoint(+Secret) 순으로 한 칸씩 내려가며 빈 칸을 찾는 일이 된다. 이 문서는 그 컴파일 산출물 5계층을 운영·진단 관점 (무엇을 내려주나 / 확인 명령 / 영향 주는 리소스 / 고장 증상)으로 정리하고, ADS 적용 순서·장애 분석 순서· proxy-status · x describe · EnvoyFilter ·CRD↔Envoy 번역표·매일 쓰는 명령 세트로 닫는다. 대상환경 Istio 1.30 / Envoy sidecar mode · 대상독자 xDS를 \"YAML이 아니라 Envoy 설정\"으로 사고하려는 DevOps/SRE · 범위 진단·운영(개념 원론은 아래 링크) · 선행개념 Envoy listener/cluster, K8s Service/EndpointSlice 개념 원론은 xDS API 계층 , sync 상태값은 데이터플레인 sync 상태 . 본 문서는 그 둘의 운영·진단 상세판 이다. ⚠️ 버전 skew 주의 : 이 환경은 istiod 1.30.0 ↔ 로컬 istioctl 1.27.0이다. proxy-status 의 VERSION 컬럼이나 proxy-config 일부 필드가 어긋나 보일 수 있으나 이는 client 표시 한계이며 메시 동작 이상이 아니다. 정밀 진단은 버전을 맞춘 1.30 istioctl 사용 권장. 01. 배경 — 왜 xDS인가: Envoy는 \"빈 상자\"이고 istiod가 채운다 Envoy는 그 자체로는 아무 라우팅도 모르는 고성능 L4/L7 프록시 엔진 입니다. \"reviews로 가는 요청은 v1/v2로 나눠라\" 같은 지식이 0이고, 그 지식은 전부 설정(configuration) 으로 외부에서 주입돼야 합니다. 문제는 메시 환경이 정적이지 않다는 데 있습니다 — Pod는 뜨고 죽고, Service endpoint는 분 단위로 바뀌고, VirtualService 한 줄 고치면 수백 개 프록시가 동시에 새 route를 알아야 합니다. 설정 파일을 디스크에 깔고 프로세스를 재시작하는 방식으로는 이걸 못 따라갑니다. 그래서 Envoy는 설정을 런타임에 원격으로 받아오는 메커니즘을 가집니다. 이것이 xDS입니다. xDS = \"무언가 Discovery Service\" 들의 묶음이고, x 자리에 Listener, Route, Cluster, Endpoint, Secret이 들어갑니다. Envoy는 이 동적 설정들을 xDS management server 로부터 streaming gRPC로 받고, Istio에서는 그 management server가 바로 istiod 입니다. 여기서 멘탈모델을 한 단계 끌어올려야 합니다. Istio를 \"서비스 메시\"라는 흐릿한 말로 두지 말고, \"K8s 상태 + Istio CRD를 Envoy 설정으로 컴파일해 모든 프록시에 배포하는 제어 시스템\" 으로 보십시오. istiod = 컴파일러, K8s/CRD = 소스 코드, Envoy 설정 5계층 = 컴파일된 바이너리, xDS = 그 바이너리를 프록시 메모리에 적재하는 로더. 이 관점이 서면 진단이 단순해집니다 — 트래픽이 깨졌다는 건 소스(CRD)가 잘못됐거나, 컴파일이 안 됐거나, 로드가 안 됐거나 셋 중 하나이고, 그걸 가르는 게 아래 도구들입니다. ★ 한 문장 멘탈모델 xDS = istiod가 Kubernetes/Istio 상태를 Envoy 설정(Listener/Route/Cluster/Endpoint/Secret)으로 컴파일해서 각 프록시에 push하는 5개 API. 진단 = 이 5계층을 순서대로 따라가며 빈 칸을 찾는 일. 02. 아키텍처 — 5계층이 \"요청 처리 경로\" 그대로인 이유 핵심 통찰은 이것입니다. 5개 계층은 임의로 나눈 게 아니라, 요청 하나가 Envoy 내부에서 resolve되는 경로 그 자체 입니다. 요청이 들어오면 Envoy는 항상 같은 질문을 순서대로 던지고, 각 질문에 답하는 설정 조각이 곧 xDS 한 계층입니다. LDS \"이 트래픽을 어디서 받나?\" → Listener (소켓 + filter chain) RDS \"그래서 어느 cluster로 보내나?\" → Route (host/path/header → cluster 이름) CDS \"그 cluster는 어떤 pool인가?\" → Cluster (LB/timeout/circuit breaker/TLS) EDS \"그 pool의 실제 Pod IP는?\" → Endpoint (IP:port 목록) SDS \"mTLS 인증서는?\" (가로지름) → Secret (cert/key/CA) 그림 2. 진단 관점의 xDS 계층: Listener(수신)→Route(cluster)→Cluster(pool)→Endpoint(Pod IP), CDS는 mTLS 시 Secret(SDS) 참조. proxy-config의 routes/clusters/endpoints/secret이 각 계층에 대응. 이 그림 한 장이 이 문서의 앵커입니다. 진단도, 적용 순서도, 응답 플래그도 전부 이 사슬에서 파생됩니다. 사슬의 어느 고리가 끊겼는지가 곧 장애의 위치입니다. 02.1 다섯 고리를 하나씩 — 무엇을 내려주고, 어떻게 보고, 왜 깨지나 LDS — Listener Discovery Service. Listener는 \"Envoy가 어디에서 트래픽을 받을 것인가\"입니다. sidecar에는 두 종류가 있습니다 — capture listener와 port별 listener. 0.0.0.0:15001 outbound capture"
    },
    {
      "title": "Istio Egress — 문서 지도",
      "desc": "Istio egress(외부로 나가는 트래픽) 문서를 주제별로 모은 아카이브의 지도(MOC)다. 좌측 카탈로그의 6개 토픽(지도 → 멘탈모델 → 구성·재현 랩 → 신원·mTLS → 필드 정본 → 실측 리포트)이 그대로 읽는 순서이고, 이 페이지는 각 문서에 한두 줄 설명을 붙인 큐레이…",
      "url": "/public/istio-egress/map__MOC-egress-overview.html",
      "domain": "istio-egress",
      "text": "istio egress gateway dns mtls moc Istio Egress — 문서 지도 NOTE Istio egress(외부로 나가는 트래픽) 문서를 주제별로 모은 아카이브의 지도(MOC)다. 좌측 카탈로그의 6개 토픽(지도 → 멘탈모델 → 구성·재현 랩 → 신원·mTLS → 필드 정본 → 실측 리포트)이 그대로 읽는 순서이고, 이 페이지는 각 문서에 한두 줄 설명을 붙인 큐레이션 인덱스다. 전체 istio 카탈로그(비-egress 포함)는 istio 도메인 에 있다. 왜 이 아카이브인가 : istio 도메인이 58편으로 커지면서 arch/gt/gw/sec/xds가 한 카탈로그에 섞여 \"egress만\" 찾기 어려웠다. 그래서 egress 19편을 이 도메인으로 주제별 토픽으로 나눠 복사 하고(원본은 istio에 유지), 비-egress로 나가는 링크는 ../istio/ 로, egress 내부 링크는 로컬로 재작성했다 — egress만 보고 싶을 때 여기 하나만 열면 되게. (원본 문서는 hand-SVG 보존을 위해 렌더된 HTML을 그대로 미러했다.) 1. 🧭 멘탈모델 먼저 egress를 처음 만졌거나, \"CRD가 왜 이렇게 많지\"에서 막힌다면 여기부터. Egress 4-CRD 직관 — 모든 것의 출발점. \"한 번의 curl = 두 hop, CRD 4개는 그 두 hop의 질문 4개\"라는 멘탈모델. 이 지도의 다른 문서 대부분이 이 골격을 전제로 한다. DestinationRule 기초→심화 (신규·라이브 실측) — DR host가 레지스트리와 매칭되어 cluster에 컴파일되는 전 과정, 레벨 3층(top/subset/port)의 \"통째 교체\" 병합 규칙(portLevelSettings 2단 함정 실측 포함), 같은 host 다중 DR의 조용한 병합, 검증 3단계. \"설정했는데 적용 안 됨\" 디버깅의 출발점. Egress route 스코핑 — exportTo 로 SE/VS 가시성을 좁히는 이유. 안 좁히면 mesh-wide 누수·다른 gateway의 CDS NACK으로 번진다. Sidecar scope 개념 노트 · 운영 가이드(전체 YAML) — Sidecar 리소스의 egress.hosts (config 경량화)와 outboundTrafficPolicy (REGISTRY_ONLY 거버넌스). Mesh/Namespace/Workload 세 scope는 merge가 아니라 \"가장 좁은 것이 override\"라는 의미론이 핵심. 2. 🔀 패턴별 구성 & 재현 랩 실제로 무엇을 어떻게 만들었는지 — 전체 YAML과 왜 그렇게 했는지가 담긴 실습형 문서들. Egress 도입 가이드 — passthrough vs mTLS — 4-CRD 멘탈모델을 실전 의사결정 1벌로 압축한, 사내 공유 목적의 표준안. 이중 gateway 검증 랩 — passthrough gateway와 mTLS gateway를 별도 pod로 나란히 띄워 대조. HTTPS passthrough 가이드 — TLS를 종단하지 않고 SNI로만 라우팅하는 가장 기본적인 egress 패턴. TCP 장애 재현 — 한계를 줄여서 관찰 — TCP 병목 정본 의 실측 한계(Envoy 1024, 포트 28k, conntrack 26만)를 그대로 재현하려면 연결 수만 개가 필요한데, 이 랩은 한계를 5~20으로 줄여 같은 메커니즘을 연결 수십 개로 재현한다. 실패 시그니처( UO / UF /무응답/정시절단) 4종이 곧 운영 런북이 된다. DNS/GSLB resolution 재현 랩 (신규·라이브 실측) — ServiceEntry.resolution (DNS vs DNS_ROUND_ROBIN)이 GSLB처럼 매번 다른 IP를 주는 도메인에서 \"기존 세션이 끊기는가/유지되는가\"를 사설 GSLB 시뮬레이터로 라이브 재현. 실측 완료(2026-07-01) — STRICT는 flip 시 upstream_cx_destroy +2 로 기존 연결 drain(트래픽 이전), LOGICAL은 델타 0으로 세션 유지(단 endpoint 덤프는 새 IP를 보여주는 stale-pin 함정). Pod 커널 파라미터 정본 (신규) — TCP 병목 정본 §06-4의 확장. 호스트 sysctl이 pod에 전파되지 않는 이유(netns는 상속이 아니라 커널 기본값으로 초기화), tcp_tw_reuse=1 의 안전 근거(PAWS), unsafe sysctl의 3중 관문(kubelet allowlist → PSA → securityContext)과 관문별 실패 시그니처. Egress TCP 문제별 처방전 (신규) — TCP 병목 정본 이 \"왜\"의 정본이라면 이 문서는 \"그래서 뭘 어디에\"의 실행본. 문제 5종(연결 거부·포트 고갈·silent drop·유휴 절단·배포 절단) 각각에 증상→설정→위치→YAML→검증을 처방하고, 예시 채널(peak 1,500 / 250 conn/s / FW 30분)로 모든 값을 도출. 4레이어 전체 YAML 종합본 포함. 3. 🔐 신원 · mTLS \"gateway만 세우면 통제된다\"는 착각을 깨는 3편 — 신원을 무엇으로 증명하고 어디서 판정하는지. mTLS 없이 신원 통제하기 — \"신원 확인엔 HTTPS over mTLS(이중 TLS)가 필요하다\"는 통념에 대한 반론. 강제는 어차피 chokepoint(gateway)에서 일어나므로, 식별 수단으로 mTLS handshake 대신 Calico pod-selector 를 쓰면 단일 클러스터·노드 신뢰 환경에서 더 싸게 같은 결과를 낸다. mTLS 신원 통제 가이드 — 반대로 mesh mTLS를 쓰기로 했다면: SPIFFE 신원 + gateway의 AuthorizationPolicy(principal × SNI)로 \"누가 어느 목적지를 쓸 수 있나\"(Q2)까지 판정하는 법. HTTPS over mTLS 해부 — 이중 TLS(outer mesh mTLS + inner 앱 TLS)의 필드 단위 정본. 종단 시 SNI가 소비된다는 것이 이 패턴 전체의 핵심 제약. 4. 📖 필드 정본 (사전) 개념을 다시 훑기보다, 특정 필드가 뭘 하는지 바로 찾아볼 때. Egress Gateway"
    },
    {
      "title": "Egress 4-CRD 직관 — \"한 번의 curl = 두 hop\", 4개를 어떤 순서로 어떻게 채우나",
      "desc": "egress gateway 설정이 \"Gateway·VirtualService·ServiceEntry·DestinationRule을 왜 4개나, 어느 필드를 어디에\"로 막히는 이유는 멘탈모델 없이 필드부터 보기 때문이다. 이 문서는 거꾸로 간다 — 먼저 \"한 번의 외부 호출이 두 hop…",
      "url": "/public/istio-egress/mm__guide-crd-mental-model.html",
      "domain": "istio-egress",
      "text": "istio egress gateway virtualservice serviceentry destinationrule mental-model how-to Egress 4-CRD 직관 — \"한 번의 curl = 두 hop\", 4개를 어떤 순서로 어떻게 채우나 NOTE egress gateway 설정이 \"Gateway·VirtualService·ServiceEntry·DestinationRule을 왜 4개나, 어느 필드를 어디에\"로 막히는 이유는 멘탈모델 없이 필드부터 보기 때문 이다. 이 문서는 거꾸로 간다 — 먼저 \"한 번의 외부 호출이 두 hop으로 쪼개지고, 4개 CRD는 각자 다른 hop의 다른 질문에 답하는 부품\"이라는 그림을 세운 뒤, 실제로 두 패턴(passthrough / outer-mTLS)을 어떤 순서로 어떻게 만들었는지 를 그때의 YAML과 함께 따라간다. 결론: 4개는 따로 노는 게 아니라 두 hop을 굴리기 위한 질문 4개 이고, 패턴 전환은 \"3개 델타\"일 뿐이다. 필드 단위 레퍼런스(=사전)는 Egress Gateway 정본 · HTTPS over mTLS 해부 , 검증 랩은 이중 gateway 가이드 . 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico), Istio 1.30.0 , Helm gateway chart 대상 독자: egress gateway config를 직접 만들어야 하는데 4개 CRD 관계가 안 잡히는 사람 선행 개념: sidecar 트래픽 캡처(15001 outbound), Envoy listener/cluster, mTLS·SNI 기초 다루는 것: ① 왜 egress·왜 4개 ② 멘탈모델·부품표 ③ 구성 순서대로 실제 YAML과 이유 ④ 필드 정렬 지도 ⑤ 단 하나의 비대칭(tls vs tcp) 1. 배경 — 왜 egress gateway이고, 왜 하필 CRD가 4개인가 mesh 안의 pod가 curl https://example.org 를 하면, sidecar가 outbound 트래픽(15001)을 가로챈다. 여기서 두 가지를 \"통제하고 싶다\"는 욕구가 egress gateway의 존재 이유다: 관측·정책의 단일 통로. 외부로 나가는 트래픽을 pod마다 제각각 내보내면 \"누가 어디로 나갔나\"를 한 곳에서 볼 수 없다. egress gateway는 모든 외부 호출을 한 노드(pod)로 모으는 깔때기 다 — 거기서 egress IP 고정, 로그·메트릭 집중, 정책 한 점 적용이 가능해진다. 신원 있는 외부 호출. \"어느 app이 이 외부 host로 나갔는가\"를 mesh mTLS(SPIFFE)로 증명하려면, sidecar→gateway 구간을 mTLS로 감싸고 gateway에서 그 신원을 검증해야 한다. 이게 outer-mTLS 패턴이다. 문제는, 이 \"통로\"를 만들려면 단 한 번의 외부 호출이 두 개의 구간으로 쪼개진다 는 점이다. app은 한 번 curl했지만 물리적으로는 app→gateway , gateway→외부 두 개의 hop이 생긴다. 두 hop을 각각 \"어디로 보낼지·어떻게 말 걸지·어느 문으로 받을지·이 host가 존재하긴 하는지\"로 설정해야 하니, 답해야 할 질문이 자연히 여러 개가 된다. Istio는 이 질문들을 관심사별로 다른 CRD에 분산 시켰다 — 그래서 4개다. 4개라서 어려운 게 아니라, 두 hop × 관심사 분리 의 결과가 4개일 뿐이다. 이 문서는 그 4개를 \"질문 4개\"로 되돌려 직관을 세운다. 같은 4-CRD 골격은 ingress가 아니라 외부로 나가는 egress 시나리오 전용 멘탈모델이다. 더 넓은 운영 맥락은 egress operations , 라우팅 스코핑은 VS 스코핑 . 2. 모든 걸 푸는 한 문장 (멘탈모델 anchor) curl https://example.org 한 번은 Istio 안에서 \"두 개의 hop\"으로 쪼개진다. CRD 4개는 각자 다른 hop의 다른 질문에 답하는 부품일 뿐이다. 이 한 문장만 머리에 박으면 나머지는 다 따라온다. 물리적으로 일어나는 일은 이게 전부다: 그림 1. 두 hop 멘탈모델. 트래픽은 app → egress gateway → 외부로 두 번 hop한다. VS leg-1·DR는 hop1(app이 나가는 길)을, Gateway·VS leg-2는 hop2(gateway가 받아 내보내는 길)를 설정한다 — ServiceEntry는 \"그 외부 호스트가 존재한다\"는 전제일 뿐 어느 hop에도 묶이지 않는다. ASCII로 같은 그림 — \"두 hop\"의 골격만 먼저: app ───hop1───> egress gateway pod ───hop2───> example.org (sidecar가 (listener가 (외부 서버) 나가는 길) 받는 자리) 나머지 모든 설정은 \"이 두 hop을 어떻게 동작시킬까\"를 채우는 것이다. 리소스가 4개인 이유는 두 hop을 굴리려면 답해야 할 질문이 4개 이기 때문이다 — §1에서 본 \"관심사 분리\"가 여기서 4개의 부품으로 떨어진다. 3. 부품표 — 각 CRD = 하나의 질문에 대한 답 답해야 할 질문 답 = CRD 한 줄 직관 \"이 외부 host가 존재하긴 하나? 메시가 알아도 되나?\" ServiceEntry 메시의 주소록에 등록 . 없으면 unknown(REGISTRY_ONLY면 차단). \"트래픽이 뜨면 어디로?(hop1) / gateway 도착 뒤 어디로?(hop2)\" VirtualService 라우팅 규칙 . 4개 중 유일하게 두 hop에 다 걸친다 → route 블록 2개. \"sidecar가 gateway에게 어떻게 말 거나? 평문? mTLS로 감싸서?\" DestinationRule hop1 목적지(gateway)에게 말 거는 방식 . \"mTLS로 감싸라\"를 여기서 지정. \"gateway는 어느 문(listener) 을 열어 hop1을 받나? cert를 요구하나?\" Gateway egress pod에 포트를 열고 보안 모드 설정(ISTIO_MUTUAL = cert 요구·검증). 이 표가 핵심이다. \"필드 4묶음\"이 아니라 \"질문 4개\" 로 보면, 작성은 질"
    },
    {
      "title": "DestinationRule 만들기 — 기초부터 심화까지",
      "desc": "DR은 \"어디로 보낼지\"(VS의 일)가 아니라 \"도착이 결정된 목적지와 어떻게 통신할지\"를 정하는 리소스다. host 문자열이 서비스 레지스트리와 매칭되어 Envoy cluster에 컴파일되는 전 과정, trafficPolicy 필드가 cluster의 어느 칸으로 흩어지는지, 레벨 3…",
      "url": "/public/istio-egress/mm__guide-destinationrule-fundamentals.html",
      "domain": "istio-egress",
      "text": "istio destinationrule connection-pool subset tls loadbalancer outlier-detection egress DestinationRule 만들기 — 기초부터 심화까지 NOTE DR은 \"어디로 보낼지\"(VS의 일)가 아니라 \"도착이 결정된 목적지와 어떻게 통신할지\" 를 정하는 리소스다. host 문자열이 서비스 레지스트리와 매칭되어 Envoy cluster에 컴파일되는 전 과정, trafficPolicy 필드가 cluster의 어느 칸으로 흩어지는지, 레벨 3층(top/subset/port)의 병합 규칙(\"통째 교체\"), 같은 host 다중 DR의 조용한 병합, 그리고 만들었으면 반드시 도는 검증 3단계까지. 모든 예시는 홈랩 실측(2026-07-03, mesh-test namespace)이다. 선행 문서 : Egress 4-CRD 직관 — DR이 4-CRD 중 어느 질문을 담당하는지. 01. DR의 자리 — 세 리소스의 분업 host라는 단어가 세 리소스에 다 나오지만 역할이 전부 다르다: Service / ServiceEntry hosts \"이 목적지가 존재한다\" -> istiod가 cluster 생성 DestinationRule host \"그 cluster에 정책 부착\" -> pool/LB/TLS가 cluster에 컴파일 VirtualService hosts+dest \"트래픽을 그 cluster로\" -> 어느 cluster를 탈지 결정 istiod가 만드는 cluster의 이름이 이 구조를 그대로 담는다: outbound | 443 | httpbin | istio-egressgateway.istio-system.svc.cluster.local (방향) (포트) (subset) (레지스트리에 등록된 host — DR.host의 매칭 대상) DR은 VS 없이도 동작한다 — 레지스트리 host와 매칭만 되면 cluster에 정책이 박힌다. 반대로 DR 없이도 트래픽은 흐른다 — 기본 cluster(무제한 풀, keepalive off, plaintext 그대로)로. DR은 \"흐르게 하는\" 리소스가 아니라 \"흐르는 방식을 통제하는\" 리소스다. 02. 기초 — 최소 DR과 host 매칭 규칙 최소 형태 apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: { name: httpbin-ext, namespace: mesh-test } spec: host: httpbin.org # <- 이 문자열이 전부를 결정한다 trafficPolicy: connectionPool: tcp: { maxConnections: 100 } host 매칭 규칙 — \"명확해야 한다\"의 정확한 의미 DR의 host 는 서비스 레지스트리(k8s Service / ServiceEntry)에 등록된 host 문자열과 매칭 된다. VS와 매칭되는 것이 아니다. 대상 레지스트리 등록 형태 DR host에 적을 것 k8s Service <svc>.<ns>.svc.cluster.local (자동) FQDN 전체 권장 ServiceEntry spec.hosts 의 문자열 그대로 그 문자열과 정확 일치 (또는 *.example.com 접두 wildcard) WARN 매칭 실패는 조용하다. DR은 선언적 정책이라 대상 host가 \"나중에 생길 수도\" 있으므로, istiod는 매칭 안 되는 DR을 에러로 취급하지 않는다. 오타 하나로 정책 전체가 소리 없이 무시되고, 증상은 \"설정했는데 적용 안 됨\"으로만 나타난다. 그래서 §06의 검증이 필수다. short-name 확장 함정 : host: istio-egressgateway 처럼 짧게 적으면 DR이 있는 namespace 기준 으로 확장된다. DR이 mesh-test 에 있으면 istio-egressgateway.mesh-test.svc.cluster.local 로 풀리는데, 실제 gateway는 istio-system 에 있으므로 존재하지 않는 host가 되어 조용히 무시된다. 항상 FQDN 전체를 적는 것이 규칙이다. 만들었으면 바로 확인 — 결부(attribution) 검증 istioctl proxy-config cluster deploy/sleep -n mesh-test --fqdn httpbin.org -o json | \\ jq '.[] | {name, dr: .metadata.filterMetadata.istio.config}' # \"dr\": \".../namespaces/mesh-test/destination-rule/httpbin-ext\" <- 결부 성공 # \"dr\": null <- host 매칭 실패 03. trafficPolicy 해부 — 필드 4+1과 컴파일 위치 trafficPolicy의 최상위 필드는 사실상 4+1개이고, 각각 cluster JSON의 다른 칸 으로 컴파일된다. 이 매핑을 모르면 검증에서 \"한 칸만 보고 통과\" 판정을 내리는 사고가 난다. 필드 제어 대상 cluster JSON 위치 connectionPool 연결 풀 한도·타임아웃·keepalive 한도 4종→ circuitBreakers.thresholds , keepalive→ upstreamConnectionOptions , connectTimeout→ connectTimeout , HTTP 공통→ typedExtensionProtocolOptions[...].commonHttpProtocolOptions loadBalancer 엔드포인트 선택 알고리즘 lbPolicy (+ 관련 config) outlierDetection 이상 엔드포인트 자동 격리 outlierDetection tls 이 목적지로 나갈 때의 TLS 모드/SNI transportSocket portLevelSettings 위 4개를 포트 단위로 override 해당 포트 cluster에만 각 필드의 깊은 내용은 전담 문서로: connectionPool 값 도출은 Egress TCP 처방전 , tcpKeepalive 3필드는 keepalive 필드 노트 , 컴파일 위치 전체는 cluster 해부 정본"
    },
    {
      "title": "Sidecar 리소스는 '좁은 범위가 넓은 범위를 통째로 이기는' override 계층으로, 사이드카에 푸시할 설정을 좁혀 성능과 egress 거버넌스를 동시에 얻는다",
      "desc": "Istio의 기본 가정은 \"메시 안 모든 워크로드가 다른 모든 워크로드에 접근 가능\"이다. 그래서 istiod는 각 Envoy에 메시 전체 설정을 푸시하고, 규모가 커지면 이것이 메모리·xDS push·istiod CPU를 동시에 압박한다. Sidecar 리소스는 두 개의 서로 다른…",
      "url": "/public/istio-egress/mm__note-sidecar-scope.html",
      "domain": "istio-egress",
      "text": "istio sidecar egress performance Sidecar 리소스는 '좁은 범위가 넓은 범위를 통째로 이기는' override 계층으로, 사이드카에 푸시할 설정을 좁혀 성능과 egress 거버넌스를 동시에 얻는다 NOTE Istio의 기본 가정은 \"메시 안 모든 워크로드가 다른 모든 워크로드에 접근 가능\"이다. 그래서 istiod는 각 Envoy에 메시 전체 설정 을 푸시하고, 규모가 커지면 이것이 메모리·xDS push·istiod CPU를 동시에 압박한다. Sidecar 리소스는 두 개의 서로 다른 레버 로 이를 푼다 — egress.hosts (이 워크로드가 알아야 할 설정의 범위 를 축소 → 성능)와 outboundTrafficPolicy (레지스트리 밖 트래픽의 차단 정책 → 거버넌스). 이 문서가 세우려는 단 하나의 멘탈모델: 세 scope(Mesh / Namespace / Workload)는 merge가 아니라 가장 좁은 하나가 통째로 이기는 override 의미론 이다. 운영 detail·YAML 전문은 Sidecar scope 운영 가이드 참조. 1. 배경 — Istio의 \"모두가 모두를 안다\"는 기본 가정과 그 비용 Istio를 쓰는 순간 service registry(Service / ServiceEntry로 채워지는 메시의 주소록)에 등록된 모든 서비스가 그 워크로드의 잠재적 upstream으로 취급된다. 이건 편의성의 산물이다 — 개발자가 아무 서비스나 이름으로 부르면 곧장 라우팅되니까. 대가는 데이터 평면 쪽 Envoy 하나하나가 메시 전체의 cluster/listener/route/endpoint를 통째로 들고 있어야 한다는 것이다. 규모가 작을 땐 안 보이지만, 워크로드가 N개로 늘면 사이드카 하나가 보유할 설정은 대략 O(N)으로 커지고, push 비용은 변경 빈도와 곱해져 더 빠르게 나빠진다. 구체적으로 세 곳을 동시에 누른다. 메모리 : 사이드카 하나가 수 MB 설정 보유 (실측 사례: 2MB → Sidecar 적용 후 644KB). xDS push 폭증 : registry의 어느 서비스가 바뀌어도, 그걸 호출하지도 않는 워크로드까지 새 설정을 받는다. istiod CPU : push 대상 × 설정 크기가 그대로 부하. 여기서 던질 질문은 \"왜 Envoy는 자기가 부르지도 않는 서비스의 설정까지 받아야 하나?\"이다. 받을 이유가 없다 — istiod가 그 워크로드의 실제 의존 대상을 알 길이 없어서 보수적으로 전부 보낼 뿐 이다. Sidecar 리소스는 바로 그 정보를 운영자가 명시적으로 주입하는 통로다. \"이 워크로드가 실제로 호출하는 대상 만 알면 된다\"고 선언해 위 곱셈을 끊는다. 이는 control-plane 성능 튜닝의 1순위 권고이며, 동일 맥락의 다른 요인(스코핑 외의 push 빈도·proxy 수 등)은 control plane 성능 요인 에서 다룬다. 그림 1. Sidecar 리소스가 없으면 istiod가 모든 Envoy에 전체 메시 설정을 push. egress.hosts로 스코프를 좁히면 각 워크로드는 자기 의존 대상만 받음 → 설정 크기·push 부하 급감. 전제 개념 정리: registry (메시가 아는 목적지 목록), xDS (istiod가 Envoy에 설정을 밀어 넣는 푸시 프로토콜 — CDS/LDS/RDS/EDS), PassthroughCluster / BlackHoleCluster (registry에 매칭 안 되는 outbound를 각각 \"통과\" / \"차단\"으로 처리하는 두 합성 cluster). 이 세 가지가 아래 메커니즘의 부품이다. 2. 핵심 멘탈모델 — Sidecar는 'override 계층'이고, 그 위에 두 개의 독립 레버가 얹힌다 머릿속에 그릴 단 하나의 그림은 이것이다. Sidecar는 \"이 Pod가 받을 설정\"을 정하는 override 계층이다. 어느 Pod에든 적용되는 Sidecar는 항상 정확히 하나뿐이며 — 가장 좁은 범위가 이긴다 — 그 하나가 egress.hosts (무엇을 알게 할지=범위)와 outboundTrafficPolicy (모르는 곳을 어떻게 처리할지=차단)라는 서로 독립된 두 손잡이를 함께 든다. 이 한 문장에서 나머지가 전부 따라 나온다. 좁은 범위가 넓은 범위에 \"합쳐지는\" 게 아니라 통째로 갈아치운다 는 것, 그리고 범위(성능)와 차단(거버넌스)이 같은 리소스 안의 다른 손잡이 라는 것 — 이 둘이 거의 모든 오해의 진원지다. 2-1. 두 개의 레버: egress.hosts vs outboundTrafficPolicy 가장 흔한 오해는 \"egress.hosts에 안 적으면 차단된다\"는 것이다. 아니다. 둘은 독립된 레버 이고, 답하는 질문 자체가 다르다. 레버 답하는 질문 빼면 일어나는 일 egress.hosts \"이 사이드카가 무엇을 알게 할까?\" (푸시할 cluster/listener의 범위) 범위 축소 안 됨 = 메시 전체 설정 유지(성능 이득 없음). 차단과는 무관 outboundTrafficPolicy.mode \"registry에 없는 호스트로 가는 트래픽을 어떻게 처리할까?\" 기본값 ALLOW_ANY → 미등록 외부 호출이 PassthroughCluster로 그냥 통과 왜 굳이 둘을 쪼갰나? \"범위를 줄이는 일\"과 \"모르는 목적지를 막는 일\"은 본질적으로 다른 결정이기 때문이다. 설정을 가볍게 하고 싶다고 해서 반드시 외부를 차단하고 싶은 건 아니다(그 반대도 마찬가지). 그래서 egress.hosts 만 좁히고 outboundTrafficPolicy 를 생략하면, 설정은 가벼워져도 \"기본 deny\" 거버넌스는 생기지 않는다 . zero-trust egress(\"등록 안 된 외부는 막는다\")를 원하면 반드시 mode: REGISTRY_ONLY 를 함께 둬야 한다. 미등록 목적지의 처리는 결국 어느 합성 cluster로 보내느냐로 갈린다. REGISTRY_ONLY : registry(Service / ServiceEntry)에 없는 목적지는 BlackHoleCluster 로 보내 차단 → 호출 측은 보통 502 . ALLOW_ANY (기본):"
    },
    {
      "title": "Egress route 스코핑 — metadata.namespace는 적용 범위가 아니다",
      "desc": "Istio traffic 리소스의 metadata.namespace는 \"어디에 저장했는가\"(소유 경계)이지 \"어느 proxy에 적용되는가\"(적용 범위)가 아니다. 적용 범위는 gateways · exportTo · sourceNamespace/sourceLabels · Sidecar.…",
      "url": "/public/istio-egress/mm__note-vs-scoping.html",
      "domain": "istio-egress",
      "text": "istio egress virtualservice exportTo sourceNamespace sidecar scoping xds Egress route 스코핑 — metadata.namespace 는 적용 범위가 아니다 NOTE Istio traffic 리소스의 metadata.namespace 는 \" 어디에 저장했는가 \"(소유 경계)이지 \" 어느 proxy에 적용되는가 \"(적용 범위)가 아니다. 적용 범위는 gateways · exportTo · sourceNamespace/sourceLabels · Sidecar.egress.hosts 네 레버의 직교 조합 으로 따로 결정된다. 이 분리를 모르면 egress 설정이 다른 namespace sidecar로 새거나(전역 누수), gateway가 필요한 route를 못 본다. egress gateway를 여러 개 (passthrough용 / mTLS용) 두는 순간 스코핑은 선택 이 아니라 전제 가 된다. 대상독자: 멀티-gateway egress를 구성하며 \"어느 client가 어느 gateway를 타는가\"를 안전하게 강제하려는 SRE. 선행개념: VirtualService/ServiceEntry/Sidecar CRD, xDS push 모델. 환경: homelab (k8s v1.30.6, Istio 1.30.0 , CNI Calico). 실측 사례는 §4. 구성 랩 연결: egress gateway HTTPS 가이드 · egress CRD 멘탈모델 . 1. 배경 — 왜 \"namespace에 담았으니 거기만 적용\"이 아닌가 처음 Istio를 쓰면 VirtualService 를 payments namespace에 만들면 그 route가 payments pod에만 적용된다고 가정하기 쉽다. Kubernetes의 다른 리소스( ConfigMap , Secret 등)는 namespace가 곧 작용 경계이기 때문이다. Istio traffic 리소스는 그렇지 않다. 이유는 Istio의 config 전파 모델에 있다. istiod는 mesh 안 모든 proxy(sidecar + gateway)를 향해 config를 push하는 단일 컨트롤 플레인이다. VirtualService 하나가 어느 namespace에 저장됐든, istiod는 그것을 mesh 전역 후보 config 로 보고, \"이 proxy가 이 route를 받아야 하나?\"를 별도의 규칙으로 계산한다. 즉 저장 위치( metadata.namespace )와 적용 대상(어느 proxy)은 설계상 분리 돼 있고, 그 둘을 잇는 게 아래 네 레버다. 이 분리가 존재하는 이유는 실용적이다 — 한 팀이 자기 namespace에 리소스를 두면서도(소유권), 그 route를 mesh 전체 또는 특정 gateway에 걸 수 있어야(공유) 하기 때문이다. 분리를 모르고 namespace만 믿으면 두 방향으로 사고가 난다: route가 의도보다 넓게 새거나(전역 누수 → §4의 NACK 전파), 의도한 gateway가 route를 못 보거나 (필요한 leg 누락). 2. 멘탈모델 — 한 리소스를 보는 네 개의 직교 축 핵심 그림 하나만 머리에 넣으면 된다. 한 traffic 리소스는 서로 다른 질문에 답하는 네 개의 독립 필드를 가진다. 어느 하나도 다른 것을 함의하지 않는다(직교). metadata.namespace = 어디에 저장했나 (소유/관리 경계) ← 적용 범위 아님 spec.gateways = 어느 proxy 종류에 붙나 (mesh=sidecars / <gw>=gateway) spec.exportTo = 어느 namespace에 보이나 (가시성) tls.match.sourceX = 그 중 어느 workload에 (applicability selector, 런타임 match 아님) 그림 1. 한 VirtualService, 네 직교 축. 같은 리소스의 서로 다른 필드가 \"어디 저장 / 어느 proxy / 어느 ns에 보임 / 어느 workload\"라는 독립된 네 질문에 각각 답한다 — 저장 위치(namespace)는 적용 범위와 무관하다는 게 핵심 함정. 각 축이 답하는 질문과 함정을 부품표로: 레버 답하는 질문 기본값 함정 spec.gateways 어느 proxy 종류? mesh (모든 sidecar) 생략 = 전 sidecar에 적용. namespace로 안 좁혀짐 exportTo 어느 namespace에 보이나? * (전체) 누락 = 전역 가시성 → 누수의 근원 sourceNamespace / sourceLabels 그 중 어느 workload? (전체) 런타임 packet match가 아니라 applicability filter Sidecar.egress.hosts sidecar가 import할 config 범위 (전체) 방화벽 아님 — config scoping일 뿐 mesh 는 VirtualService.spec.gateways 의 reserved word 로 \"메시 안 모든 sidecar\"를 뜻한다. gateways 를 생략하면 기본값이 mesh 다. 그래서 payments namespace의 VS가 기본적으로 전 sidecar에 적용될 수 있는 것이다. 좁히는 네 레버를 메커니즘으로: exportTo: [\".\"] — 리소스를 선언된 namespace 안에서만 보이게 한다. 기본값은 전체 export( * ). ServiceEntry·VirtualService·DestinationRule 모두 지원. 소유권 분리와 전역 누수 차단에 가장 직접적인 레버. sourceNamespace / sourceLabels ( tls.match ) — 어떤 workload에 이 route를 적용할지 거르는 selector. 패킷이 들어왔을 때 매칭하는 게 아니라, istiod가 \"이 proxy에 이 route를 줄까\"를 정할 때 쓰는 applicability filter다. Sidecar.egress.hosts — 해당 sidecar가 import할 config 범위를 줄인다(성능 + governance). 단 이는 방화벽이 아니라 config scoping 이다. scope 밖으로 보"
    },
    {
      "title": "Istio Sidecar CRD 적용 범위(scope) 설정 방법",
      "desc": "Sidecar 리소스는 한 워크로드 Envoy가 받는 설정을 좁혀 config를 경량화하고, outboundTrafficPolicy와 결합해 egress 거버넌스를 만든다. 멘탈모델 하나: scope는 Mesh / Namespace / Workload 세 단계지만 어느 Pod에든 적용…",
      "url": "/public/istio-egress/mm__src-sidecar-scope.html",
      "domain": "istio-egress",
      "text": "istio sidecar egress Istio Sidecar CRD 적용 범위(scope) 설정 방법 NOTE Sidecar 리소스는 한 워크로드 Envoy가 받는 설정을 좁혀 config를 경량화 하고, outboundTrafficPolicy 와 결합해 egress 거버넌스 를 만든다. 멘탈모델 하나: scope는 Mesh / Namespace / Workload 세 단계지만 어느 Pod에든 적용되는 Sidecar는 정확히 하나 — 가장 좁은 것이 통째로 이긴다 (merge 아님, override). 이 문서는 그 override 의미론을 세 scope의 완전한 YAML(주석 포함) 로 따라 만들고, REGISTRY_ONLY 차단을 실측으로 굳힌다. 개념 전반은 Sidecar scope 개념 노트 에. 대상독자: 메시 규모가 커져 istiod push 비용이 보이기 시작했거나, egress를 \"기본 deny\"로 잠그려는 SRE. 선행개념: xDS push 모델(CDS/LDS/RDS/EDS), service registry, ServiceEntry. 환경: Istio 1.30, Envoy. cluster 이름 규칙 direction|port|subset|fqdn . 범위: Sidecar의 3 scope YAML 전문 + 판정 규칙 + REGISTRY_ONLY 검증. 차단 메커니즘의 왜 는 개념 노트로 위임. 1. 배경 — Sidecar 리소스가 푸는 문제 Istio의 기본 가정은 \"메시 안 모든 서비스가 다른 모든 서비스에 접근 가능하다\"이다. 편의성의 산물이다 — 개발자가 아무 서비스나 이름으로 부르면 곧장 라우팅된다. 대가는 데이터 평면 쪽이 치른다: istiod 는 그 워크로드가 실제로 무엇을 부르는지 알 길이 없으니, 보수적으로 메시 전체 설정 (모든 cluster/endpoint/listener)을 각 Envoy에 푸시한다. 200개 워크로드 규모만 돼도 사이드카 하나당 설정이 수 MB에 이르고, 이것이 메모리·xDS push· istiod CPU를 동시에 압박한다. 핵심 질문은 \"왜 Envoy는 자기가 부르지도 않는 서비스의 설정까지 받아야 하나?\"이다. 받을 이유가 없다 — istiod가 의존 대상을 모르기 때문일 뿐이다. Sidecar 리소스(CRD)는 운영자가 바로 그 정보를 명시적으로 주입하는 통로이고, 그것으로 두 방향의 문제를 동시에 푼다: 설정 범위 축소(performance) — egress.hosts 로 \"이 워크로드가 실제로 호출하는 대상\"만 선언하면, istiod가 그 사이드카에 보내는 설정 크기가 극적으로 줄고( Istio in Action 11장: 2MB → 644KB), registry 변경이 scope 밖이면 push 자체가 발생하지 않는다. 실측은 §4. 컨트롤 플레인을 좌우하는 다른 요인은 컨트롤 플레인 성능 요인 . 트래픽 거버넌스(security) — outboundTrafficPolicy.mode: REGISTRY_ONLY 와 결합하면 메시 레지스트리에 등록되지 않은 외부 호출을 차단하는 zero-trust egress 기본값이 된다. ℹ Sidecar 리소스는 \"보안 정책\"이자 동시에 \"성능 최적화 도구\"다. `Istio in Action` 11장이 컨트롤 플레인 성능 튜닝의 첫 권고로 \"항상 Sidecar 리소스를 정의하라\"고 강조하는 이유가 이것이다. 2. 핵심 멘탈모델 — '가장 좁은 하나가 통째로 이긴다' 머릿속에 그릴 단 하나의 그림(ANCHOR): Sidecar는 \"이 Pod가 받을 설정\"을 정하는 override 계층이다. 어느 Pod에든 적용되는 Sidecar는 항상 정확히 하나 — 가장 좁은 범위가 이긴다 — 그리고 그 하나가 egress.hosts (무엇을 알게 할지=범위)와 outboundTrafficPolicy (모르는 곳을 어떻게 처리할지=차단)라는 독립된 두 손잡이를 함께 든다. 이 한 문장에서 나머지가 전부 따라 나온다. 좁은 범위가 넓은 범위에 \"합쳐지는\" 게 아니라 통째로 갈아치운다 는 것, 그리고 범위(성능)와 차단(거버넌스)이 같은 리소스 안의 다른 손잡이 라는 것 — 이 둘이 거의 모든 오해의 진원지다. 세 scope와 우선순위: 범위 우선순위 적용 대상 주된 활용 판정 키 Mesh-wide ① 가장 낮음 메시 안 모든 Pod egress 기본 차단, 공통 기본값 강제 rootNamespace + name: default + selector 없음 Namespace-wide ② 해당 NS 모든 Pod 팀별 규칙(내부 호출만 허용) 대상 NS + workloadSelector 없음 Workload-specific ③ 가장 높음 selector 일치 특정 Pod 민감 서비스만 추가 egress 허용 workloadSelector.labels 지정 좁은 범위가 넓은 범위를 덮어쓴다 (Workload > Namespace > Mesh). 직관적으로는 \"더 구체적인 규칙이 일반 규칙 위에 얹힌다(덮어쓴다)\"고 기대하기 쉬운데, 여기선 일반 규칙이 통째로 사라진다 . 한 Pod가 어떤 Sidecar를 적용받는지 결정 흐름: 그림 1. effective Sidecar 결정 흐름 — workloadSelector 일치 시 그 Sidecar만(병합 없음), 없으면 NS default, 그것도 없으면 rootNamespace mesh-wide default, 모두 없으면 full mesh config가 통째로 push되는 무거운 default로 떨어진다(override 의미론). 이 흐름도가 override의 실체다 — 매칭은 위에서 아래로 첫 hit 하나에서 멈춘다 . workload가 match하면 NS/mesh default는 아예 보지 않는다. §3-3의 누락 함정이 여기서 나온다. ℹ \"Mesh-wide는 `istio-system` 네임스페이스에 둔다\"는 규칙의 정확한 근거. mesh-wide로 동작하는 조건은 \" 메시 루트 네임스페이스(기본값 istio-system )에 있고, 이름이 default 이며, workloadSelector 가 없을 것 \"이다. 단순히 istio-system 에 둔"
    },
    {
      "title": "Istio Egress Gateway 도입 가이드 — Passthrough vs mTLS Passthrough",
      "desc": "대상: 플랫폼/인프라/보안 담당자 환경: on-prem Kubernetes, Istio 1.30.0 (sidecar mode), PCI-DSS 준수 IDC 결정 요약: mTLS Passthrough(ISTIO_MUTUAL) 패턴 채택. proxy 도입에 따른 TCP 운영 이슈는 방식과…",
      "url": "/public/istio-egress/cfg__guide-adoption-passthrough-vs-mtls.html",
      "domain": "istio-egress",
      "text": "istio egress mtls passthrough adoption decision tcp operations Istio Egress Gateway 도입 가이드 — Passthrough vs mTLS Passthrough 대상: 플랫폼/인프라/보안 담당자 환경: on-prem Kubernetes, Istio 1.30.0 (sidecar mode), PCI-DSS 준수 IDC 결정 요약: mTLS Passthrough(ISTIO_MUTUAL) 패턴 채택. proxy 도입에 따른 TCP 운영 이슈는 방식과 무관한 공통 비용이며 별도 완화 설정으로 대응함. 1. 배경 및 목표 모든 대외(외부 기관) 통신을 통제된 단일 경로(egress gateway)로 수렴 필요. 보안 요건: ① 외부행 경로 강제, ② 워크로드 단위 최소권한(어떤 앱이 어떤 목적지로 나가는지 차등 통제), ③ 주체 식별 가능한 감사 추적, ④ mesh 외부 클라이언트의 gateway 사용 차단. 제약: 앱은 HTTPS를 직접 발신(종단간 TLS 유지 필요). 앱 코드/프로토콜 변경 불가. [app + sidecar] ---> [egress gateway] ---> IDC FW ---> 대외기관 API (HTTPS 발신) (유일한 출구) (gw 대역만 허용) 2. 두 방안 비교 두 방식 모두 앱↔외부 서버의 종단간 TLS는 유지 됨(gateway는 inner TLS를 복호화하지 않음). 차이는 sidecar→gateway 구간을 mesh mTLS로 한 겹 더 감싸는지 여부. A. TLS Passthrough app --[원본 TLS]--> sidecar --[원본 TLS 그대로]--> gw --[원본 TLS]--> 외부 gw는 SNI만 보고 통과 (신원 정보 없음) B. mTLS Passthrough (ISTIO_MUTUAL) app --[원본 TLS]--> sidecar --[mesh mTLS[원본 TLS]]--> gw --[원본 TLS]--> 외부 gw가 outer만 종단 -> SPIFFE 신원 추출 항목 A. Passthrough B. mTLS Passthrough gateway에서 호출 주체 식별 불가 (pod IP뿐, IP는 휘발성) 가능 (SPIFFE: ns/sa 단위) AuthorizationPolicy 표현력 목적지(SNI)·포트만 주체(principal) × 목적지 워크로드별 차등 통제 불가 — gateway 도달 가능한 모든 pod가 allowlist 합집합 사용 가능 — \"app-a→A기관만\" 식 최소권한 mesh 외부 클라이언트 차단 불가 (gateway Svc는 클러스터 전역 도달 가능) TLS handshake 단계에서 거부 (mesh CA 인증서 요구) 감사 로그 src pod IP SPIFFE ID ( DOWNSTREAM_PEER_URI_SAN ) 사고 시 즉시 회수 목적지 단위 전체 차단만 해당 SA 정책 1건 삭제 설정 복잡성 낮음 (DR 불필요, VS 양쪽 tls 라우트) 높음 (Gateway hosts ↔ DR sni ↔ VS 3자 정합) 디버깅 와이어에 원본 TLS 한 겹 mesh 구간만 TLS 2겹 (gw→외부는 단일 TLS로 동일) 연결 수립 레이턴시 기준 신규 연결당 outer 핸드셰이크 1회 추가 CPU 기준 sidecar(암호화)·gw(복호화) 증가 학습 곡선 낮음 라우트 타입 규칙, 인증서 체계 등 학습 필요 3. 결정: mTLS Passthrough 채택 근거 Passthrough의 결손은 구조적이며 운영 중 메울 수 없음. 검문소(gateway)는 만들어지지만 \"누가\"를 판정할 재료가 없음. 한 목적지를 뚫는 순간 전사 모든 워크로드(비-mesh pod 포함)에 그 경로가 개방됨 — 최소권한 요건 미충족. 감사 시점에 pod IP → 워크로드 역추적은 IP 휘발성 때문에 신뢰 불가 — 주체 식별 요건 미충족. 추후 신원이 필요해지면 결국 ISTIO_MUTUAL 전환이 유일한 경로 → 지금의 구축 비용을 그 시점에 지불하게 됨. mTLS의 비용은 인지하고 감수함. 비용: 신규 연결 수립 레이턴시 증가(핸드셰이크 1회분), 암호화 CPU 부하, 초기 설정 복잡성과 학습 곡선, mesh 구간 TLS-in-TLS 디버깅. 감수 근거: 이 비용은 설계 시점에 1회 지불되고 템플릿에 동결 됨(목적지 추가 = 표준 1벌 복사). 반면 passthrough의 결손은 런타임에 상존하며 사고·심사 시점에 청구됨. 가시성과 트래픽 통제가 비용을 상회한다고 판단. 디버깅 부담의 실제 범위: 장애 다발 구간인 gw→외부는 단일 TLS로 passthrough와 동일. 이중 TLS는 mesh 내부 hop에 한정되며, 해당 구간 1차 진단 도구는 tcpdump가 아닌 access log / istioctl로 표준화(§7). 참고: 두 모드는 같은 gateway Deployment에서 포트 단위로 공존 가능(예: 8443=ISTIO_MUTUAL, 9443=PASSTHROUGH). 필요 시 목적지별 점진 전환 경로 존재. 4. 아키텍처 ns: app-namespace ns: istio-egress +------------------+ +-----------------------------------+ | app + sidecar | outer mTLS | egressgateway Deployment (3+) | | (sa: app-a) ----+--------------->| :8443 ISTIO_MUTUAL | +------------------+ Svc:443 | 1) tls_inspector: outer SNI로 | -> pod:8443 | filter chain 선택 | | 2) outer 종단 -> principal 추출 | | 3) AuthzPolicy(principal x sni) | | 4) tcp_proxy -> 외부:443 | +-----------------+-----------------+ | (inner TLS만, 단일 겹) v 전용 노드풀 -> IDC FW -> 대외기관 핵심 동작 규칙 (트러블슈팅의 기준) 규칙 내용 라우트 타입 = 종단 여부 미종단 hop(s"
    },
    {
      "title": "GSLB 뒤 DNS resolution 재현 랩 — \"세션이 끊길 수도 있다\"를 눈으로 보기",
      "desc": "ServiceEntry resolution 정본은 DNS(STRICT_DNS)와 DNS_ROUND_ROBIN(LOGICAL_DNS)의 차이를 이론으로 정리했다. 이 문서는 그 이론을 자기완결형 랩으로 라이브 재현한다 — 공유 인프라(egress gateway·cluster CoreDN…",
      "url": "/public/istio-egress/cfg__guide-dns-gslb-repro-lab.html",
      "domain": "istio-egress",
      "text": "istio egress serviceentry dns gslb resolution envoy mental-model GSLB 뒤 DNS resolution 재현 랩 — \"세션이 끊길 수도 있다\"를 눈으로 보기 NOTE ServiceEntry resolution 정본 은 DNS (STRICT_DNS)와 DNS_ROUND_ROBIN (LOGICAL_DNS)의 차이를 이론으로 정리했다. 이 문서는 그 이론을 자기완결형 랩으로 라이브 재현 한다 — 공유 인프라(egress gateway·cluster CoreDNS)를 한 줄도 안 건드리고, 사설 GSLB 시뮬레이터 하나로 \"IP가 매번 바뀌는 도메인\"을 통제해 ① 기존 세션이 끊기는 순간 ② 죽은 IP로 트래픽이 새는 순간 ③ LOGICAL_DNS가 그 대가로 stale IP를 뒤늦게 붙잡고 있는 순간을 각각 만든다. 결론 한 문장: resolution 필드는 \"GSLB의 변덕을 Envoy가 펼쳐서 매번 반영하느냐(STRICT), 접어서 기존 연결은 안 건드리느냐(LOGICAL)\"의 선택이고, 그 선택이 곧 세션 생존 여부다. 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico), Istio 1.30.0 대상 독자: STRICT_DNS/LOGICAL_DNS 차이를 이론으로는 아는데, \"진짜 세션이 끊기는 장면\"을 직접 보고 싶은 사람 선행 개념: ServiceEntry resolution 정본 (STRICT_DNS/LOGICAL_DNS, DNS refresh ≠ health check), TLS origination vs passthrough, outlier detection 다루는 것: ① 왜 자체 GSLB 시뮬레이터인가 ② 멘탈모델·부품표 ③ 실제 구성(전체 YAML + 이유) ④ 3가지 재현 모드 ⑤ 함정 1. 배경 — 이론은 있는데 왜 \"재현\"이 따로 필요한가 기존 runbook은 resolution 필드가 Envoy cluster type을 가른다는 것, DNS refresh가 liveness가 아니라는 것, GSLB 뒤엔 LOGICAL_DNS가 낫다는 것까지 전부 정리했다. 그런데 이 문서들은 전부 정적 조사 다 — \"endpoint가 몇 개 잡히는가\"는 dig+proxy-config로 스냅샷을 찍으면 끝나지만, \"GSLB가 IP를 바꾸는 그 순간 이미 맺혀 있던 연결이 끊기는가/유지되는가\"는 시간축이 있는 이벤트 라 스냅샷으로는 못 본다. 이걸 보려면: GSLB를 내가 통제 해야 한다 — 실제 GSLB는 언제 IP를 바꿀지 내가 못 정한다. 롱커넥션이 있어야 한다 — 매 요청 새 커넥션이면 애초에 \"끊길 기존 세션\"이 없어 STRICT든 LOGICAL이든 차이가 안 보인다. 공유 인프라를 안 건드려야 한다 — egress gateway나 cluster CoreDNS를 실험용으로 고치면 다른 시나리오 (10/20/30-*)에 영향이 번진다. 이 세 요구를 동시에 만족하려고 scenarios/50-dns-resolution/ 에 사설 GSLB 시뮬레이터 + keepalive 부하생성기 로 된 독립 ns( dns-lab )를 만들었다. 아래는 그 구성을 어떻게, 왜 그렇게 만들었는지다. 2. 멘탈모델 한 문장 GSLB는 \"같은 도메인, 매번 다른 IP\"를 주는 존재일 뿐이다. resolution 필드는 그 변화를 Envoy가 \"펼쳐서 매번 반영\"(STRICT_DNS)할지 \"접어서 기존 연결은 안 건드리고 신규만 반영\"(LOGICAL_DNS)할지를 정하고, 그 선택이 곧 \"GSLB가 IP를 바꾸는 순간 내 세션이 죽느냐 사느냐\"다. resolution: DNS -> Envoy STRICT_DNS host set = A record 전체. DNS refresh 시 사라진 IP의 host를 cluster에서 제거 -> 그 host로 물려있던 커넥션을 정리(drain) => 기존 세션 끊김 [Mode 1] -> refresh 전 \"죽었지만 아직 목록에 있는\" IP를 LB가 고르면 connect 실패 => 유실 [Mode 2] resolution: DNS_ROUND_ROBIN -> Envoy LOGICAL_DNS host = 논리적 endpoint 1개. 새 커넥션만 최신 DNS로 연결 기존 커넥션은 \"절대 건드리지 않음\" => 기존 세션 유지 [Mode 1 대조] 대가: 물고 있는 IP가 나중에 죽어도 Envoy는 DNS로는 못 알아챔 [Mode 3] 세 대괄호 표시(Mode 1/2/3)가 이 랩이 실제로 만드는 세 장면이다. 이 한 문장 + 세 장면만 머리에 있으면 나머지 구성은 전부 \"이 장면을 어떻게 세팅하나\"의 디테일이다. 3. 부품표 — 각 조각 = 그게 없으면 안 되는 이유 답해야 할 질문 부품 왜 이게 필요한가 \"GSLB를 어떻게 내 손으로 흉내내나?\" lab-dns (CoreDNS + writer 사이드카) gslb.lab.internal 1개 이름을 authoritative(ttl 5s)로 응답. writer 가 A record를 실시간 재작성 = \"GSLB가 IP를 바꾸는 순간\"을 내가 만든 버튼 \"그 도메인을 누가 질의하나?\" client dnsConfig sidecar Envoy(c-ares)가 cluster DNS 대신 lab-dns를 보게 강제 — egress gateway·cluster CoreDNS 무변경으로 GSLB를 격리 \"펼치나 접나?\" ServiceEntry.resolution DNS =STRICT_DNS(펼침) / DNS_ROUND_ROBIN =LOGICAL_DNS(접음) — 이 랩이 대조하는 유일한 독립변수 \"drain이 눈에 보이는 층(L7)에서 일어나게 하려면?\" VS(80→443) + DR(TLS origination) client가 평문 HTTP로 부르고 sidecar가 TLS로 승격해야 Envoy가 upstream을 HTTP conn pool(L7)로 관리 → host 제거 시 GOAWAY/close가 관측 가능 해짐. https 직접 호출이면 SNI passthrough(L4)라 drain이 안 보임 \"죽은 IP를 어"
    },
    {
      "title": "이중 egress gateway — passthrough와 mTLS를 별도 ns·별도 pod로 나란히",
      "desc": "단일 egress gateway를 멀티-gateway 토폴로지로 일반화하면서, 두 egress 패턴(① TLS PASSTHROUGH, ② outer 메시 mTLS + inner 앱 TLS)을 서로 다른 namespace의 서로 다른 gateway pod로 동시에 띄워 직접 비교한 ho…",
      "url": "/public/istio-egress/cfg__guide-dual-gateway.html",
      "domain": "istio-egress",
      "text": "istio egress gateway multi-gateway tls-passthrough istio-mutual namespace-isolation 이중 egress gateway — passthrough와 mTLS를 별도 ns·별도 pod로 나란히 NOTE 단일 egress gateway를 멀티-gateway 토폴로지로 일반화 하면서, 두 egress 패턴(① TLS PASSTHROUGH, ② outer 메시 mTLS + inner 앱 TLS)을 서로 다른 namespace의 서로 다른 gateway pod 로 동시에 띄워 직접 비교한 homelab 검증 랩. 결론: 진짜 격리는 물리 분리(pod/ns/label)와 논리 스코핑( exportTo / sourceLabels )을 함께 갖춰야 성립 한다 — 둘 중 하나라도 빠지면 \"분리된 것처럼 보이는\" 상태에 그친다. 본 문서는 그 둘을 어떻게 맞물리는지에 집중한다(패턴 자체의 구조·운영은 기존 문서로 링크). 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico, 3노드), Istio 1.30.0 (Helm gateway chart) 대상 독자: 단일 egress gateway는 띄워봤고, 이제 패턴/tier별로 gateway를 쪼개려는 SRE/DevOps 범위: \"왜 쪼개나\" → 격리의 두 축(물리·논리) → 경로별 Istio 객체 → 트래픽·Envoy 검증 → 실측 함정 2건 선행 개념: egress route 스코핑 (멀티-gateway의 전제) · egress CRD 멘탈모델 1. 배경 — 왜 egress gateway를 둘로 쪼개나 단일 egress gateway면 메시의 모든 외부 호출이 한 pod를 통과한다. 처음엔 단순해서 좋지만, 운영이 커지면 그 한 pod가 여러 책임을 한 몸에 떠안는 구조가 문제가 된다: 장애 격리 부재 — 한 tier의 graceful drain·재시작·crashloop이 모든 외부 트래픽을 흔든다. SNAT 포트 고갈 공유 — 한 워크로드가 source port를 다 써버리면 무관한 다른 트래픽도 외부 연결을 못 연다. 정책 충돌 — passthrough(SNI만 보고 통과)와 mTLS 종단(client cert 검증)은 listener의 TLS 동작이 정반대 다. 한 pod에 얹으면 포트를 갈라야 하고, 한쪽 정책 변경이 다른 쪽 listener를 건드릴 위험이 상존한다. 그래서 패턴/tier별로 gateway pod를 분리한다. 그런데 \"Deployment를 둘로 나눴다\"만으로 격리가 끝나지 않는다는 게 이 문서의 핵심 교훈이다. Istio에서 한 메시 안의 리소스는 기본적으로 전역 가시성 을 갖기 때문에, 물리적으로 떨어진 두 pod라도 잘못된 전역 리소스 하나가 양쪽 config를 동시에 망가뜨릴 수 있다(함정 ②). 진짜 격리에는 두 번째 축 이 필요하다. 선행 개념인 egress route 스코핑 을 먼저 잡아두면 이 문서의 exportTo / client-VS·gateway-VS 분리가 자연스럽게 읽힌다. 멀티-gateway는 그 스코핑 규칙을 gateway 개수만큼 곱한 것에 가깝다. 2. 멘탈모델 — 격리는 두 축의 곱(物理 × 論理) 머릿속에 담을 한 장면: 두 gateway는 pod·ns·label로 물리적으로 분리돼 있지만 여전히 하나의 메시 다. 그래서 격리는 다음 두 축이 둘 다 채워질 때만 성립한다 — 하나라도 비면 \"분리된 척\"에 그친다. isolation = PHYSICAL × LOGICAL (분리) (가시성·적용대상) PHYSICAL axis LOGICAL axis +--------------------+ +---------------------------+ | label (selector) | | exportTo (누가 이 리소스를 | | Gateway가 자기 | | 볼 수 있나) | | pod만 잡음 | | sourceLabels (어떤 client에 | | ns (소유권 경계) | | 라우트가 붙나) | | pod (장애·SNAT 경계)| +---------------------------+ +--------------------+ | | v v \"딴 pod의 변경이 \"전역 리소스 하나가 내 listener를 안 건드림\" 모든 gateway를 못 얼리게\" PHYSICAL — 각 ns의 Gateway 리소스가 자기 label만 selector로 잡으므로 한쪽 변경이 다른 pod로 새지 않는다. ns는 소유권/RBAC 경계, pod는 장애·SNAT 경계. PILOT_SCOPE_GATEWAY_TO_NAMESPACE=true 환경에서도 Gateway 리소스와 workload가 같은 ns라 안전하다. LOGICAL — exportTo: [\".\"] 로 SE/VS의 가시성을 자기 ns에 닫아, 전역 push에 끼어 남의 config를 얼리지 않게 한다 (함정 ②). sourceLabels 는 \"어떤 client가 어느 gateway로 가나\"를 명시한다(현재 랩은 host로 분기, §다음 작업 참고). 이 한 장면에서 나머지가 따라 나온다. 아래 토폴로지가 그 물리 축을 그림으로 보인 것이다 — 같은 client( sleep )가 두 외부 host를 호출하지만, host에 따라 다른 ns의 다른 gateway pod 로 갈린다. 그림 1. host별로 갈리는 dual egress — example.org는 egress-pt가 SNI만 보고 복호화 없이 PASSTHROUGH, www.wikipedia.org는 egress-mtls가 OUTER mesh mTLS를 종단·검증하고 INNER 앱 TLS만 tcp_proxy로 흘려보낸다. 왼쪽 경로는 gateway가 복호화하지 않는 passthrough, 오른쪽은 gateway가 outer mesh mTLS를 종단 하고 inner 앱 TLS만 흘려보내는 패턴이다. inner 앱 TLS는 두 경로 모두 client↔external 간 end-to-end로 유지 된다 — passthrough는 애초에 건드리지 않고, mTLS 경로는 outer mesh 레이어"
    },
    {
      "title": "Egress TCP 문제별 처방전 — 어떤 문제에, 어떤 설정을, 어디에, 어떻게",
      "desc": "TCP 병목 정본이 \"왜 병목이 생기는가\"의 정본이라면, 이 문서는 egressgateway에서 실제로 발생하는 TCP 문제 5가지를 하나씩 놓고 — 증상 → 어떤 설정을 → 어디에(리소스) → 어떻게(YAML) → 검증 — 순서로 처방하는 실행 문서다. 예시 채널 하나 (peak 동…",
      "url": "/public/istio-egress/cfg__guide-egress-tcp-tuning.html",
      "domain": "istio-egress",
      "text": "istio egress tcp connection-pool destinationrule helm sysctl keepalive Egress TCP 문제별 처방전 — 어떤 문제에, 어떤 설정을, 어디에, 어떻게 NOTE TCP 병목 정본 이 \"왜 병목이 생기는가\"의 정본이라면, 이 문서는 egressgateway에서 실제로 발생하는 TCP 문제 5가지를 하나씩 놓고 — 증상 → 어떤 설정을 → 어디에(리소스) → 어떻게(YAML) → 검증 — 순서로 처방하는 실행 문서 다. 예시 채널 하나 (peak 동시 연결 1,500 / 신규 250 conn/s / FW idle timeout 30분 / Calico natOutgoing on)의 실측값으로 모든 숫자를 도출하고, 마지막에 4개 레이어 전체 YAML 종합본 을 둔다. 값을 복사하지 말고 도출식을 복사할 것. 대상 환경 : Istio 1.30 sidecar mode, egress gateway = tcp_proxy (PASSTHROUGH 또는 ISTIO_MUTUAL). 선행 문서 : TCP 병목 정본 (산술·메커니즘), Pod 커널 파라미터 정본 (sysctl 관문), tcpKeepalive 필드 노트 (time/interval/probes). 00. 준비 — 문제 지도와 측정 입력 문제가 어디서 터지고, 설정이 어디에 붙는가 그림 1. 트래픽 경로와 문제 지도 — app pod → sidecar → egress gw pod → gw node → firewall → partner-a:443. P1(Envoy 1024→DR), P1'(sidecar subset), P2(포트 고갈→Helm+sysctl), P3(conntrack→노드 sysctl), P4(idle timeout→DR keepalive), P5(drain→Helm)가 각 hop에 대응한다. 문제 한 줄 증상 설정 위치 (레이어) P1 연결 거부 1,024개에서 즉시 거부, flag UO DR 외부 호스트 (1) + DR subset (2) P2 포트 고갈 connect 실패, flag UF , EADDRNOTAVAIL Helm replica·antiAffinity·pod sysctl (3) P3 무응답 timeout reset조차 없는 silent drop 노드 /etc/sysctl.d (4) P4 유휴 후 절단 half-open RST / 정확히 N초 절단 DR keepalive + idleTimeout (1) P5 배포 시 절단 재배포마다 long-lived 연결 일괄 사망 Helm drain + PDB (3) 측정 — 값은 감이 아니라 세 입력에서 나온다 # ① 채널별 peak 동시 연결 + 신규 연결률 — gateway 도입 \"전\" sidecar 메트릭에서 istio_tcp_connections_opened_total{destination_service=\"api.partner-a.example.com\"} # → peak 동시 연결 수, rate()로 conn/s # ② FW/중간장비 idle timeout — 네트워크팀 확인 (보통 30~60분) # ③ SNAT 여부 — 켜져 있으면 포트 산술의 전제가 바뀐다 (정본 §05) calicoctl get ippool -o yaml | grep natOutgoing 예시 시나리오 (이하 모든 값의 입력): api.partner-a.example.com:443 , peak 1,500 , 신규 250 conn/s , FW idle 1800s , natOutgoing: true . 그림 2. 예시 채널의 실측값 4개가 도출식을 거쳐 최종 설정값 3개로 수렴한다 — 250 conn/s와 natOutgoing: on 두 입력이 함께 replicaCount 산정에 들어간다. P1. 연결이 1,024개에서 거부된다 — Envoy cluster 상한 증상 : 클라이언트는 reset류( SSL_ERROR_SYSCALL ), gateway access log에 flag UO , envoy_cluster_upstream_cx_overflow 증가. sidecar 시절엔 절대 안 보이던 벽 — pod 하나가 한 목적지로 동시 1,024개를 열 일이 없다가, gateway에서 전사 트래픽이 cluster 하나로 합쳐지며 가장 먼저 부딪힌다. 어떤 설정 : connectionPool.tcp.maxConnections (+ connectTimeout ) 어디에 : ① 외부 호스트 DR — 채널당 1벌. ② sidecar→gw subset에도 같은 기본값 1,024가 숨어 있다 — 외부 DR만 고치면 병목이 안쪽으로 한 칸 이동할 뿐. # (1) 외부 호스트 DR — 핵심 완화 지점 apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: { name: partner-a-external, namespace: istio-egress } spec: host: api.partner-a.example.com trafficPolicy: connectionPool: tcp: maxConnections: 4096 # 측정 peak 1,500 x 2~3. 무한정 키우면 격벽 상실 — connectTimeout: 3s # 메모리 노출 = 동시연결 x 소켓2 x ~1MiB buffer # (2) 기존 egressgateway DR의 subset — 이 hop의 cluster에도 명시 subsets: - name: partner-a trafficPolicy: portLevelSettings: - port: { number: 443 } tls: { mode: ISTIO_MUTUAL, sni: api.partner-a.example.com } connectionPool: tcp: { maxConnections: 4096 } # 단, \"클라이언트 pod당\" 상한이라 보통 여유 검증 : rate(envoy_cluster_upstream_cx_overflow[5m]) == 0 # UO 거부 없어야 함 envoy_cluster_upstream_cx_active / 4096 < 0.8 # 0.8 초과 ="
    },
    {
      "title": "Istio 1.30 Egress Gateway — 외부 HTTPS 통신 설치·구성·테스트 가이드",
      "desc": "homelab(kubespray bare-metal, k8s v1.30.6, CNI Calico, Istio 1.30.0)에서 egress gateway를 Helm으로 구성하고, 앱이 직접 https://를 호출하는 TLS Passthrough(SNI 라우팅) 시나리오를 끝까지 구성·…",
      "url": "/public/istio-egress/cfg__guide-gateway-https.html",
      "domain": "istio-egress",
      "text": "istio egress gateway tls-passthrough sni Istio 1.30 Egress Gateway — 외부 HTTPS 통신 설치·구성·테스트 가이드 NOTE homelab(kubespray bare-metal, k8s v1.30.6, CNI Calico , Istio 1.30.0 )에서 egress gateway를 Helm으로 구성하고, 앱이 직접 https:// 를 호출하는 TLS Passthrough(SNI 라우팅) 시나리오를 끝까지 구성·검증한다. 머릿속에 담을 한 장의 그림: 메시의 모든 외부 송신을 egress gateway라는 단일 choke point로 모으되, TLS는 끝까지 암호화된 채로 두고 gateway는 SNI만 보고 라우팅한다(2-홉: mesh→gateway, gateway→external). 핵심 결론: egress의 \"완료\"는 200이 아니라 트래픽이 egress gateway를 실제로 경유했음을 증명 하는 것이며, 호출 결과 / proxy-config / access log 세 가지를 교차 확인한다. 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico, 3노드), Istio 1.30.0 (Helm chart) 범위: egress gateway Helm 설치/구성 → 외부 HTTPS 테스트 앱 구성 → 필요한 Istio 객체 → 테스트·검증 절차 난이도 전제: Istio sidecar/Gateway/VirtualService 기본 개념을 알고 있음. egress 특유의 동작에 초점. 0. 배경 지식 — 왜 egress gateway를 거치게 만드는가 기본 상태의 메시는 외부 송신을 막지 않는다 . sidecar의 outboundTrafficPolicy 가 ALLOW_ANY 이면 각 워크로드의 Envoy가 모르는 목적지를 PassthroughCluster 로 그냥 흘려보낸다 — pod마다 인터넷으로 나가는 구멍이 하나씩 생기는 셈이다. 이게 운영에서 곤란한 이유는 세 가지다: 방화벽 화이트리스트가 불가능 — 송신 출발 IP가 노드 수만큼, pod 수만큼 흩어진다. \"이 IP에서만 나간다\"를 외부 방화벽에 적을 수가 없다. 송신 감사가 불가능 — 누가 어디로 나갔는지 한 곳에 로그가 없다. egress 정책 강제 지점이 없다 — \"결제 워크로드만 PG사로 나갈 수 있다\" 같은 규칙을 걸 단일 지점이 없다. egress gateway는 이 흩어진 송신을 단일 지점(choke point) 으로 모으는 전용 Envoy다. 메시의 외부 송신이 모두 이 pod 한 종류를 통과하면 — 출발 IP가 하나로 고정(방화벽 화이트리스트 가능), 송신 로그가 한 곳에 모이고 (감사 가능), 정책을 이 한 지점에 걸 수 있다(강제 지점 확보). 이 문서는 그 choke point를 homelab에 실제로 세우고, 앱이 https:// 를 직접 호출하는 가장 흔한 케이스를 통과시킨 뒤, 트래픽이 정말 그 지점을 경유했는지 까지 증명한다. 선행 개념 한 줄씩 (모르면 먼저 채울 것): 개념 이 문서에서 왜 필요한가 sidecar 트래픽 캡처( :15001 outbound) 앱이 보낸 패킷을 Envoy가 가로채야 egress로 우회시킬 수 있다 outboundTrafficPolicy (ALLOW_ANY / REGISTRY_ONLY) 통제를 \"강제\"로 바꾸는 스위치 — §7.4의 차단 검증 핵심 TLS handshake의 SNI passthrough에서 gateway가 라우팅에 쓸 수 있는 유일한 평문 키 Gateway / VirtualService / DestinationRule / ServiceEntry 2-홉을 잇는 4객체 (§5에서 관계, §6에서 YAML) 왜/모드결정/2-leg 라우팅의 개념 정본 은 egress gateway 개념 정본 §01·§02·§04에 있다. 본 가이드는 그 개념을 homelab에서 실제로 구성·검증 하는 절차에 집중하므로 이론은 정본에 위임하고 여기서는 \"왜 이 객체가 이 모양인지\"만 짚는다. 1. 핵심 아키텍처 — 한 장의 그림과 그로부터 따라오는 모든 것 머릿속 앵커 한 문장 : choke point로 모으되 TLS는 절대 풀지 않는다 . 이 한 가지 제약이 이후 모든 설계를 결정한다. 그림 1. 2-홉 egress 경로 — sidecar가 :15001에서 outbound를 가로채지만 SNI만 읽고 payload는 암호화 유지. hop1(mesh→egress)·hop2(egress→external) 모두 PASSTHROUGH라 ciphertext가 외부 호스트까지 그대로 전달된다. 그림이 말하는 핵심은 2-홉 이다. 외부 호출이 sidecar에서 외부로 직접 가지 않고, 일부러 한 번 더 꺾여 egress gateway를 경유한다(hop1: mesh→gateway, hop2: gateway→external). 이 \"일부러 꺾기\"가 choke point를 만든다. 그리고 양쪽 홉 모두 암호문이 그대로 유지된다(end-to-end TLS) — gateway는 봉투를 뜯지 않는다. 여기서 핵심 긴장이 나온다. choke point로 모으면 보통은 \"거기서 트래픽을 들여다보겠다\"가 따라오는데, 앱이 이미 https:// 로 종단간 암호화 를 걸어 보냈으므로 gateway가 봉투를 뜯으면 그 암호화가 깨진다. 그래서 봉투를 안 뜯는다 — 이게 TLS Passthrough 다. 봉투를 안 뜯으니 gateway가 라우팅에 쓸 수 있는 정보는 평문 HTTP 헤더/경로가 아니라, TLS handshake 때 평문으로 노출되는 목적지 호스트명 — SNI 하나뿐이다. 이 SNI 제약이 §6의 모든 객체 모양을 한 줄로 설명한다. 왜 이 모양인가 를 미리 깔아두면 §6 YAML이 전부 \"당연\"해진다: 설계 선택 SNI 제약에서 따라오는 이유 ServiceEntry protocol: TLS (HTTP 아님) Envoy가 평문 헤더를 못 보니 L7 HTTP 로 등록할 수 없다 → L4 TLS Gateway server tls.mode: PASSTHROUGH 봉투를 뜯지 않고 그대"
    },
    {
      "title": "Pod 커널 파라미터 정본 — 호스트 sysctl은 왜 pod에 안 먹히고, unsafe sysctl은 어떻게 넣는가",
      "desc": "TCP 병목 정본 §06은 완화 운영값이 4개 레이어(DR / pod sysctl / 노드 sysctl / Helm)에 분산된다고 정리했다. 이 문서는 그중 pod sysctl 레이어 하나를 메커니즘 레벨로 전개한다 — 호스트 /etc/sysctl.d가 pod에 전파되지 않는 이유(n…",
      "url": "/public/istio-egress/cfg__guide-pod-sysctl-netns.html",
      "domain": "istio-egress",
      "text": "istio egress sysctl netns tcp-tw-reuse time-wait paws kubernetes Pod 커널 파라미터 정본 — 호스트 sysctl은 왜 pod에 안 먹히고, unsafe sysctl은 어떻게 넣는가 NOTE TCP 병목 정본 §06은 완화 운영값이 4개 레이어(DR / pod sysctl / 노드 sysctl / Helm)에 분산된다고 정리했다. 이 문서는 그중 pod sysctl 레이어 하나를 메커니즘 레벨로 전개 한다 — 호스트 /etc/sysctl.d 가 pod에 전파되지 않는 이유(netns 초기화), tcp_tw_reuse=1 이 안전한 근거(PAWS), 그리고 Kubernetes에서 unsafe sysctl이 통과해야 하는 3중 관문 (kubelet allowlist → PSA → securityContext)까지. 대상 환경 : Kubernetes ≥ 1.29 자체 구축(kubespray) 기준 — managed 환경 대안은 §06. 선행 문서 : TCP 병목 정본 §02(포트 산술)·§06-4(이 문서의 원형이 된 2단계 요약). 01. 멘탈모델 — sysctl은 상속되지 않고 \"초기화\"된다 sysctl은 두 부류로 나뉜다: netns 스코프 ( net.ipv4.* 대부분, net.core.somaxconn 등): network namespace마다 독립 값을 가짐 호스트 전역 ( fs.* , vm.* , 그리고 실질적으로 conntrack): netns로 격리되지 않음 핵심은 이것이다 — 새 netns는 부모(호스트) netns의 현재 값을 복사하지 않는다. 커널 코드에 하드코딩된 기본값으로 초기화된다. tcp_tw_reuse 는 커널의 netns 초기화 함수( tcp_sk_init() )에 2 로 박혀 있다. 그림 1. 새 netns는 호스트 값을 상속하지 않는다 — 커널 tcp_sk_init() 의 하드코딩 기본값(tw_reuse=2, loopback 한정)으로 초기화되고, 호스트 /etc/sysctl.d는 호스트 netns에만 적용된다. 그래서 정확한 그림은: 호스트에서 sysctl -w net.ipv4.tcp_tw_reuse=1 → 호스트 netns만 바뀜. pod에는 무관. pod 안의 값은 항상 기본값 2 — 0 (끔)이 아니라 \" loopback 목적지에 한해서만 재사용 \"(kernel 4.15+). loopback은 옛 중복 세그먼트가 되돌아올 물리 경로가 없어 무조건 안전하므로 커널이 기본으로 켜둔 것. egress gateway의 외부행 연결은 loopback이 아니므로 실질적으로는 꺼진 것과 같다 → pod netns 안에서 직접 1 로 바꿔야 한다. 같은 이유로 ip_local_port_range 도 pod netns 값(기본 32768 60999 )이 호스트와 별개로 존재한다. 02. TIME_WAIT는 왜 존재하고, tw_reuse는 왜 안전한가 값을 바꾸기 전에 그 값이 지키던 것부터. 먼저 close한 쪽(active closer — proxy는 대개 이쪽)은 두 가지 책임 때문에 4-tuple을 보존해야 한다: 상대의 마지막 FIN이 유실·재전송되면 다시 ACK 해줄 것 같은 4-tuple로 새 연결이 즉시 열렸을 때, 네트워크에 떠돌던 이전 연결의 늦은 세그먼트가 새 연결의 데이터로 오인되지 않게 할 것 리눅스는 이 보존 시간을 60초로 하드코딩했다( TCP_TIMEWAIT_LEN — sysctl로 못 바꾼다). TCP 병목 정본 §02 의 분수식 28,232 ÷ 60s ≈ 470 conn/s 의 분모가 바로 이것. 그림 2. TIME_WAIT 60초가 지키는 두 책임. 포트 하나가 \"사용 시간 + 60s\"를 점유하므로 분수식 28,232 ÷ 60s ≈ 470 conn/s 의 분모가 된다. tw_reuse는 이 중 ②(amber)를 PAWS로 대체해 우회한다 — 그림 3. tw_reuse=1 의 안전 근거 — PAWS. TCP timestamps 옵션(기본 on)이 켜져 있으면 모든 세그먼트에 단조 증가하는 시계값이 실린다. 커널은 새 연결에서 이전 연결의 늦은 세그먼트를 timestamp 비교로 식별·폐기할 수 있다(PAWS, Protection Against Wrapped Sequences). 위 책임 ②가 timestamps로 대체되므로, 1초만 경과하면 그 4-tuple을 outgoing 연결에 재사용해도 안전 하다. 수신(서버) 소켓에는 적용되지 않는다 — gateway는 외부행 발신자라 정확히 이 케이스에 해당한다. 그림 3. tw_reuse=1이 안전한 이유 — timestamps가 단조 증가하므로 이전 연결의 늦은 세그먼트(ts=950)는 새 연결의 기대값(≥2000)보다 작아 PAWS가 폐기한다. 그림 2의 보호 ②가 timestamps로 대체되는 지점. 03. 파라미터별 정리 — 무엇이 어디에 사는가 패킷 경로를 그리면 각 파라미터의 적용 위치가 저절로 정해진다. pod netns 안에는 netfilter 룰이 없다. 패킷이 veth를 빠져나와 호스트 netns의 iptables/Calico 룰을 통과할 때 conntrack tracking이 일어난다 — conntrack이 pod가 아닌 노드 설정인 이유. 그림 4. 파라미터 스코프 맵. pod netns 안에는 netfilter 룰이 없어 conntrack tracking은 패킷이 호스트 netns의 iptables/Calico를 통과할 때 일어난다 — conntrack이 pod securityContext가 아닌 노드 /etc/sysctl.d 설정인 이유. 파라미터 스코프 (k8s 분류) 권장값 왜 필요한가 net.ipv4.ip_local_port_range pod netns ( safe ) \"10240 60999\" 포트 풀 28k→50k 순수 확장 — 유일하게 부작용 없는 완화. 하한 10240은 특권 포트(<1024)·registered port 관행 회피. pod netns라 노드의 NodePort 범위(30000–32767, 노드 netns 리스너)와 충돌하지 않음 — 노드 호스트에서 같은 확장을 하면 충돌"
    },
    {
      "title": "Egress TCP 병목 재현 랩 — 한계를 줄여서 부딪힌다",
      "desc": "TCP 병목 정본의 한계 수치(Envoy 1024, 포트 28k, conntrack 26만)는 그대로 재현하려면 연결 수만 개가 필요하다. 이 랩의 기법은 반대다 — 한계를 5~20으로 줄여서, 같은 메커니즘을 연결 수십 개로 관찰한다. 재현 4종 각각이 운영에서 만날 실패 시그니처(…",
      "url": "/public/istio-egress/cfg__guide-tcp-failure-reproduction.html",
      "domain": "istio-egress",
      "text": "istio egress tcp reproduction lab connection-pool port-exhaustion conntrack Egress TCP 병목 재현 랩 — 한계를 줄여서 부딪힌다 NOTE TCP 병목 정본 의 한계 수치(Envoy 1024, 포트 28k, conntrack 26만)는 그대로 재현하려면 연결 수만 개가 필요하다. 이 랩의 기법은 반대다 — 한계를 5~20으로 줄여서, 같은 메커니즘을 연결 수십 개로 관찰한다. 재현 4종 각각이 운영에서 만날 실패 시그니처( UO / UF / 무응답 / 정시 절단) 하나씩을 직접 떠올리고, 그 시그니처 구분이 곧 reset 분기 런북이 된다. 대상 환경 : Istio 1.30.0, Egress 신원 기반 통제 구성 의 테스트 클러스터(ns istio-egress 의 egressgateway + ns egress-test 의 netshoot-a)가 떠 있는 상태. 대상 독자 : 도입 보고서의 병목 표를 \"봤다\"에서 \"겪었다\"로 바꾸고 싶은 사람. 범위 : 병목 1·2·3·4 재현 + 복구. 완화 운영값 자체는 정본 §06 으로 위임. 0. 원칙 — 왜 줄여서 부딪히나 28,000개 연결을 만들어 한계에 도달하는 게 아니라, 한계를 5~20으로 줄여 같은 메커니즘을 소규모로 관찰한다. [운영 한계] [랩 재현] maxConnections: 1024(기본) maxConnections: 5 ephemeral ports: 28,232 ip_local_port_range: 20개 nf_conntrack_max: ~260k nf_conntrack_max: 200 idle timeout(FW): 30~60min idleTimeout: 30s | | +--- 같은 메커니즘, 같은 시그니처 ---+ 공유 테스트 클러스터에서 안전하고, 외부 sandbox 엔드포인트에 가는 부하도 연결 수십 개 수준이라 무해하다. (그래도 대상 LB에 이상탐지가 있다면 사전 공유 권장.) 비유 하나: 둑의 높이를 1m로 낮추고 물 한 양동이로 범람을 관찰하는 것. 한계: 비율이 다른 현상은 못 본다 — 예컨대 연결 수만 개 규모에서만 나타나는 Envoy 메모리 압박·FD 고갈은 이 기법으로 재현되지 않는다. 1. 관찰 도구 준비 # gw pod에 디버그 컨테이너 부착 (ss, conntrack 등 사용) GW=$(kubectl get pod -n istio-egress -l istio=egressgateway -o jsonpath='{.items[0].metadata.name}') kubectl debug -n istio-egress $GW -it --image=registry.example.com/netshoot:latest \\ --target=istio-proxy -- bash # 이 셸에서: ss -tan state time-wait | wc -l 등 실행 # Envoy 통계 조회 (별도 터미널, 반복 사용할 함수) stats() { kubectl exec -n istio-egress deploy/egressgateway -c istio-proxy -- \\ pilot-agent request GET stats | grep \"api-a\" | grep -E \"$1\" } # 부하 발생용 alias A=\"kubectl exec -n egress-test deploy/netshoot-a -c netshoot --\" kubectl debug --target=istio-proxy 로 붙이는 이유: ephemeral 컨테이너가 istio-proxy와 같은 프로세스/네트워크 네임스페이스를 공유 해야 그 pod의 소켓( ss )이 보인다. 2. 재현 1 — Envoy cluster 연결 상한 (운영 기본 1024 → 5) 부딪히는 순서 1번, 가장 먼저 맞을 함정. 상한을 5로 줄이고 10개 연결을 시도한다. # dr-api-a-tiny.yaml — 외부 호스트용 DR. gw의 upstream cluster에 적용됨 apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: api-a-external namespace: istio-egress spec: host: api-a.example.com trafficPolicy: connectionPool: tcp: maxConnections: 5 # 재현용. 미설정 시 기본 1024 kubectl apply -f dr-api-a-tiny.yaml # 동시 연결 10개 보유 (sleep이 stdin을 잡아 5분간 연결 유지) $A bash -c 'for i in $(seq 1 10); do (sleep 300 | openssl s_client -connect api-a.example.com:443 \\ -servername api-a.example.com -quiet > /tmp/c$i.log 2>&1) & done; sleep 15; grep -l \"BEGIN CERT\" /tmp/c*.log | wc -l' 관찰 명령 기대 성공 연결 수 위 마지막 출력 5 (6번째부터 handshake 실패) 거부 카운터 stats upstream_cx_overflow 5 이상 증가 활성 연결 stats upstream_cx_active 5에서 고정 access log gw 로그의 response flag UO (Upstream Overflow) 핵심 체감 : 클라이언트 증상이 AuthorizationPolicy 거부와 똑같은 \"TLS 실패/reset\"이다. flag( UO vs rbac 로그)로만 구분 가능 — 런북에 들어갈 내용이 바로 이것. 복구: kubectl delete dr api-a-external -n istio-egress (운영에서는 정본 §06-1 의 운영값으로 재생성). 3. 재현 2 — Ephemeral 포트 고갈 + TIME_WAIT (28,232개 → 20개) ip_local_port_range 는 K8s safe sysctl 이라 pod 단위로 바로 설정 가능하다(kubelet allowlist 불필요). # values-egre"
    },
    {
      "title": "Egress 신원 기반 통제 — mTLS 신원이 공용 gateway에 멀티테넌시를 만든다",
      "desc": "\"egress gateway만 세우면 외부 통신이 통제된다\"는 직관의 빈칸을 메우는 문서. 방화벽·NetworkPolicy는 \"외부로 나가는 유일한 경로가 gateway인가\"(Q1) 에만 답하고, \"그 gateway를 누가, 어느 목적지로 쓸 수 있나\"(Q2) 에는 답하지 못한다. Q…",
      "url": "/public/istio-egress/id__guide-mtls-identity-control.html",
      "domain": "istio-egress",
      "text": "istio egress mtls spiffe identity authorizationpolicy security multi-tenancy Egress 신원 기반 통제 — mTLS 신원이 공용 gateway에 멀티테넌시를 만든다 NOTE \"egress gateway만 세우면 외부 통신이 통제된다\"는 직관의 빈칸을 메우는 문서. 방화벽·NetworkPolicy는 \"외부로 나가는 유일한 경로가 gateway인가\"(Q1) 에만 답하고, \"그 gateway를 누가, 어느 목적지로 쓸 수 있나\"(Q2) 에는 답하지 못한다. Q2의 판정 재료가 mesh mTLS가 운반하는 SPIFFE 신원 이고, 판정 장치가 gateway 위의 AuthorizationPolicy(principal × SNI) 다. 이 문서는 ① 그 논리를 통제 체인(경로 강제 → 검문소 → 신원 판정)으로 세우고 ② SA 2개가 서로 다른 목적지만 허용받는 테스트 클러스터 전체 구성 (YAML 주석 포함)과 검증·함정까지 따라간다. 이중 TLS 구조 자체의 해부는 HTTPS over mTLS 정본 , 4-CRD 직관은 Egress 4-CRD 멘탈모델 이 정본 — 본 문서는 그 구조 위에 올라가는 \"통제\" 를 다룬다. 대상 환경: Istio 1.30.0 , sidecar mesh, Helm gateway chart (테스트 클러스터) 대상 독자: egress gateway를 보안 요건(최소권한·감사 추적) 충족 수단으로 도입하려는 DevOps/SRE 범위: 신원 기반 통제의 논리 → 테스트 클러스터 구성 → 검증 매트릭스 → 설계 결정 노트(분리 전략·TCP·wildcard) 선행 개념: egress 2-leg 라우팅( 4-CRD 멘탈모델 ), SPIFFE 신원( 정본 ), AuthorizationPolicy 평가( 멘탈모델 ) 1. 멘탈모델 한 문장 — 통제는 체인이고, 신원은 그 마지막 고리다 강제(enforcement)는 경로가 하고(NetworkPolicy·방화벽), 판정은 검문소(gateway)가 하며, 판정에 \"누가\"를 공급하는 유일한 장치가 sidecar↔gateway 구간의 mesh mTLS(SPIFFE 인증서)다. NetworkPolicy / IDC firewall : \"egress gw 경유 외 외부행 차단\" (경로 강제, L3/L4) | v egress gateway : 유일한 검문소가 됨 | v 검문소에서 \"누가 -> 어디로\" 판정 필요 <- 여기서 신원(SPIFFE)이 등장 이 체인에서 어느 고리 하나만 빠져도 통제가 성립하지 않는다. 경로 강제 없이 gateway만 → 침해된 pod가 sidecar를 우회(iptables 조작, UID 1337)해 직접 나간다. Istio 레이어는 우회를 못 막는다. 검문소까지 만들고 신원 없이 → gateway가 보는 건 source pod IP(휘발)와 SNI(클라이언트 제공 값)뿐. gateway에 도달 가능한 모든 pod가 gateway에 설정된 모든 경로를 쓸 수 있다. PG사 경로를 하나 뚫는 순간 전사 모든 워크로드가 PG사로 나갈 수 있는 상태가 된다. 신원 기반 authz는 결국 \"전용 gateway N개를 정책 N줄로 치환\" 하는 장치다. 물리 분리 없이 공용 gateway 위에 논리적 멀티테넌시를 만드는 것 — 이게 이 패턴의 본질이다. 2. 왜 신원인가 — 흔한 두 반론을 메커니즘으로 해소 반론 1: \"ServiceEntry로 특정 namespace에서 외부 주소를 못 보게 막으면 되지 않나\" ServiceEntry(+exportTo)·REGISTRY_ONLY는 차단 장치가 아니라 설정 배포 범위 제어다. \"team-a namespace에는 이 외부 호스트 설정을 안 내려준다\"일 뿐이고, 집행 주체가 클라이언트 자신의 sidecar 라는 게 구조적 약점이다. sidecar는 pod와 같은 network namespace에 있어서, root 권한을 가진 침해 pod는 sidecar를 건너뛰고 직접 나갈 수 있다. Istio 공식 Security Best Practices 가 REGISTRY_ONLY를 보안 경계가 아닌 best-effort 로 간주하라고 명시하는 이유다. 그래서 집행은 공격자가 통제할 수 없는 지점 (CNI NetworkPolicy, IDC 방화벽, gateway)으로 옮겨야 한다. 반론 2: \"어차피 egress 노드가 아니면 방화벽에서 막히는데, 그걸로 충분하지 않나\" 절반은 맞다. 방화벽과 신원은 다른 질문에 답하는 장치 라서 분리해야 한다. Q1. 외부로 나가는 유일한 경로가 gateway인가? -> 방화벽이 답함 (O) Q2. gateway를 \"누가\" \"어느 목적지로\" 쓸 수 있나? -> 방화벽은 못 답함 방화벽이 보는 건 gateway 노드 CIDR → 대외기관 IP 뿐이다. 모든 워크로드가 같은 SNAT IP로 나가니 app-a와 app-b를 구분할 수 없고, gateway Service는 ClusterIP라 클러스터 내 모든 pod가 도달 가능하다. 보안 요건이 \"모든 외부 통신은 통제된 경로 + 전사 공통 allowlist\"까지라면 passthrough + 방화벽으로 충분 하다. 요건에 워크로드 단위 최소권한(least privilege)과 주체 식별 가능한 감사 추적 이 포함될 때 비로소 신원이 필요해진다 — 그 판정을 할 수 있는 유일한 지점이 gateway이고, 판정 재료가 신원이기 때문이다. ✓ A/B 선택의 실제 분기점 \"passthrough(A)냐 이중 TLS(B)냐\"는 기술 선호가 아니라 사내 보안 심사 요건 문서에 \"워크로드별 차등 통제·주체 식별\"이 명시되어 있는지 로 판가름한다. 없으면 A로 시작하고, 요건이 생기면 같은 토폴로지에서 Gateway tls.mode + DR trafficPolicy + AuthorizationPolicy만 추가 하는 증분 적용이 가능하다( 델타 3곳 ). 같은 환경에서 신원을 CNI pod-selector로 대신 식별하는 반대 선택의 근거는 이중 TLS 없는 egress 신원 . 신원이 있을 때만 가능해지는 통제 제어 내용 신원 없이 가능? 워크로드별 목적"
    },
    {
      "title": "Egress 신원, 이중 TLS 없이 — passthrough + Calico가 HTTPS over mTLS를 대체하는 근거",
      "desc": "\"egress에서 호출 워크로드 신원을 통제하려면 HTTPS over mTLS(이중 TLS)가 필요하다\"는 통념에 대한 반론. 멘탈모델 한 줄: 강제(enforcement)는 어차피 chokepoint(egress gateway)에서 일어난다 — 패턴 선택은 \"그 앞에서 호출자를 어떻…",
      "url": "/public/istio-egress/id__note-identity-without-mtls.html",
      "domain": "istio-egress",
      "text": "istio egress mtls passthrough calico networkpolicy decision spiffe Egress 신원, 이중 TLS 없이 — passthrough + Calico가 HTTPS over mTLS를 대체하는 근거 NOTE \"egress에서 호출 워크로드 신원을 통제하려면 HTTPS over mTLS(이중 TLS)가 필요하다\"는 통념에 대한 반론. 멘탈모델 한 줄: 강제(enforcement)는 어차피 chokepoint(egress gateway)에서 일어난다 — 패턴 선택은 \"그 앞에서 호출자를 어떻게 식별 하느냐\"의 문제일 뿐이다. 식별을 메시 mTLS handshake로 할지, CNI(Calico)가 커널에서 이미 하는 pod-selector 로 할지. 단일 클러스터·노드 신뢰·Calico 단독관리 환경에선 후자가 동일 결과를 더 싸게 낸다. 이중 TLS의 추가 비용(handshake 2회·포트분리 함정·L7 사각)은 그 위협모델에서 보안 이득이 0에 가깝다. 대상 구조의 상세는 HTTPS over mTLS 구조 정본 참조 — 본 문서는 그 반대 선택의 근거 다. 맥락: 사내 egress 도입에서 \"HTTPS over mTLS냐 passthrough냐\" 결정 토론용 근거 문서. 환경 전제: 단일 클러스터, CNI Calico(플랫폼팀 단독관리) , 노드 신뢰 위협모델, 외부 파트너 감사는 source IP로 충분. 대상 독자: egress 패턴을 고르는 DevOps/SRE. 선행 개념: egress gateway가 chokepoint라는 것, mTLS=SPIFFE cert, NetworkPolicy의 selector. 1. 배경 — 애초에 \"egress 신원\"이 왜 문제인가 메시 내부 트래픽은 mTLS로 자동 식별된다. 문제는 밖으로 나가는 트래픽 이다. payment pod가 partner-bank.example.com:443 을 부를 때, 클러스터 운영자는 두 가지를 보장하고 싶다. 누가 나가는지 통제 — payment 만 partner-bank로 나가고 나머지는 차단. 누가 나갔는지 귀속 — 외부 파트너 감사·내부 로그에서 \"이 호출은 payments tier가 한 것\"을 증빙. 그런데 외부 목적지는 메시 밖이라 응답 측이 SPIFFE를 검증해 주지 않는다. 식별은 전적으로 나가는 경계(egress gateway)에서, 우리 손으로 해야 한다. 그래서 \"egress 신원\"은 본질적으로 경계 통과자를 어떻게 식별하느냐 의 문제로 환원된다. 표준 처방은 HTTPS over mTLS(이중 TLS) 다: 앱→egress gateway 구간에 메시 mTLS를 한 겹 더 씌워, gateway가 outer handshake의 client cert로 호출자 SPIFFE ID를 확정하고 AuthorizationPolicy 를 건다. 본 문서는 그게 정말 필요한 비용인가 를 따진다. 결론부터: 위 환경 전제에선 CNI가 이미 공짜로 하는 식별로 동일 결과를 낸다. 여기서 먼저 통념의 정체를 정확히 분해해야 토론에서 못 진다. 이중 TLS가 passthrough 대비 추가로 사는 것 은 딱 하나 다. egress gateway가 outer mTLS handshake에서 받은 client cert로 호출 워크로드의 SPIFFE ID ( cluster.local/ns/payments/sa/payment )를 암호학적으로 확정 하고, 그걸 키로 AuthorizationPolicy 를 건다. 즉 사는 물건은 \"egress chokepoint를 통과하는 호출자를 SA 단위로 식별·인가\" 다. 그 이상도 이하도 아니다(L7은 여전히 못 봄 — outer/inner TLS 모두 불투명). 2. 핵심 — 식별과 강제를 분리하면 대안이 보인다 멘탈모델 anchor: egress 보안 = 식별(누구인가) + 강제(못 나가게 막기) , 이 둘은 별개 축이다. 강제는 어느 패턴이든 똑같이 chokepoint(egress gateway + 그 앞의 NetworkPolicy)에서 일어난다. 그러니 패턴 비교는 오직 식별 한 축 으로 좁혀진다 — chokepoint 앞에서 호출자를 무엇으로 식별하느냐. 그림 1. 강제는 chokepoint에서 일어난다 — 패턴 선택은 그 앞에서 호출자를 어떻게 식별하느냐의 문제일 뿐. 왜 이 분리가 결정적인가. 식별 방법 A(mTLS handshake)는 이중 TLS·L7 사각·포트 머지 충돌·핸드셰이크 2회라는 비용을 동반한다. 식별 방법 B(Calico pod-selector)는 CNI가 패킷 라우팅을 위해 어차피 유지하는 endpoint(IP↔pod 매핑) 를 재사용하므로 추가 비용이 0이다. 두 방법이 식별 이라는 같은 일을 하면서 가격표만 다르다면, 강제가 공유되는 한 더 싼 쪽이 합리적이다. 이 anchor에서 \"더 싸게 근사\"가 막연한 말이 아니라 구체적 대안 3개 로 떨어진다. 식별 메커니즘의 핵심 — pod-selector는 \"비-핸드셰이크 신원\"이다 방법 B가 단순 ACL이 아니라 신원 메커니즘 이라는 게 논거의 심장이다. app == 'payment' 는 pod가 헤더로 주장 하는 값이 아니라 Calico가 endpoint(IP↔pod 매핑)에 묶어 커널(iptables/eBPF)에서 강제 하는 라벨이다. 옆 pod가 payment 를 사칭하려면 그 pod의 네트워크 네임스페이스 자체를 위조 해야 하고, 그건 노드 컴프로마이즈 수준이다. → 즉 \"pod-selector NetworkPolicy는 그 자체로 비-핸드셰이크 신원 메커니즘\" 이다. mTLS가 cert로 하는 일(검증된 호출자 식별)을 Calico는 커널 endpoint로 한다 — handshake 비용 0. 이것이 §3·§4 두 논증(중복 구매·노드 신뢰)의 토대다. 3. 근사 3종 — 구체 시나리오 + 실제 YAML 시나리오 고정: \" payment 워크로드만 partner-bank.example.com:443 으로 나갈 수 있다. 나머지는 차단.\" (전형적 사내 egress 요구) 세 근사는 §2의 식별 축을 config 분배 / 커널 강제 / IP 귀속"
    },
    {
      "title": "Egress \"HTTPS over mTLS\" (ISTIO_MUTUAL) — 구조·CRD·장단점·활용·운영",
      "desc": "머릿속에 넣을 한 장의 그림: 이 패턴은 \"end-to-end 암호화 보존\"과 \"egress에서 호출자 신원 식별\"이라는 서로 독립인 두 축의 교집합 칸이다 — 둘 다 Yes일 때만 정답인 좁은 셀. 구현은 이중 TLS: 앱은 그대로 https://(inner end-to-end TL…",
      "url": "/public/istio-egress/id__src-https-over-mtls.html",
      "domain": "istio-egress",
      "text": "istio egress mtls istio-mutual spiffe double-tls gateway Egress \"HTTPS over mTLS\" (ISTIO_MUTUAL) — 구조·CRD·장단점·활용·운영 NOTE 머릿속에 넣을 한 장의 그림: 이 패턴은 \" end-to-end 암호화 보존 \"과 \" egress에서 호출자 신원 식별 \"이라는 서로 독립인 두 축의 교집합 칸이다 — 둘 다 Yes일 때만 정답인 좁은 셀. 구현은 이중 TLS : 앱은 그대로 https:// (inner end-to-end TLS)로 호출하고, sidecar↔egress 구간만 Istio 메시 mTLS( ISTIO_MUTUAL )로 한 겹 더 감싸 (outer) gateway가 그 outer를 종단 하며 호출 워크로드의 SPIFFE 신원을 암호학적으로 검증 하되, 안쪽 앱 TLS는 풀지 않고 tcp_proxy 로 외부까지 그대로 흘린다. passthrough(신원 없음)와 TLS origination(앱 평문화)의 사각지대를 메우는 패턴이며, 실측 검증은 Egress mTLS 리포트 , 운영 정본은 Egress 운영 , 개념 정본은 Egress Gateway 정본 , 이 신원 위에 올라가는 통제 (AuthorizationPolicy·테스트 매트릭스)는 Egress 신원 기반 통제 가이드 참조. 대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico), Istio 1.30.0 검증 도메인: edition.cnn.com (외부 HTTPS) — 기존 httpbin.org passthrough를 control로 보존하고 직접 비교 전제 지식: sidecar/Gateway/VirtualService/DestinationRule 기본 + egress 2-leg 라우팅( 정본 §04) 1. 배경 — 왜 이 패턴이 존재하나 (passthrough와 origination 사이의 빈칸) egress gateway로 외부 HTTPS를 내보내는 방식은, 단 두 개의 독립된 질문으로 완전히 좌표가 잡힌다. 이 두 질문이 이 문서 전체의 출발점이다. gateway가 앱 payload를 복호화해도 되는가? (= end-to-end 암호화를 포기할 수 있나) egress에서 \"누가 나가는지\"를 식별해야 하는가? (= 호출 워크로드 신원이 필요한가) 표준 egress 패턴 두 개는 이 좌표의 대각선 두 칸 만 채운다. PASSTHROUGH : 가장 단순·흔함. gateway는 SNI만 보고 앱 TLS를 그대로 흘린다 → end-to-end는 지켜지지만, in-mesh leg가 평문 TCP라 호출자가 누군지 암호학적으로 모른다 . 상세 정본 , 가이드 HTTPS passthrough 가이드 . TLS origination : gateway가 외부로 TLS를 시작 하므로 앱은 평문 HTTP를 메시에 보내야 한다 → gateway가 L7(method/path/status)을 보고 정책·감사·중앙 cert 관리가 가능하지만, end-to-end 암호화가 깨진다 (payload가 gateway 메모리에 평문으로 존재). 비교 HTTP vs HTTPS . 여기서 빈 칸 이 하나 남는다: \"payload는 절대 못 보게 하면서(end-to-end) + 그래도 누가 나가는지는 알아야 한다.\" passthrough는 신원이 없어서, origination은 복호화를 해서 둘 다 이 칸을 못 채운다. 그 빈 칸을 메우려고 등장한 게 바로 이 \"HTTPS over mTLS\" 패턴 이다. 이 한 문장이 이 패턴이 존재하는 유일한 이유이고, 이후 모든 설계 결정(이중 TLS, ISTIO_MUTUAL, leg-2가 tcp인 것)은 전부 이 칸을 채우기 위한 귀결이다. end-to-end 암호화 보존? Yes No +----------------+----------------+ egress No | PASSTHROUGH | (의미 없음) | 신원 | 가장 흔함 | | 식별? +----------------+----------------+ Yes | HTTPS over mTLS | TLS origination| | <-- 본 문서 | gw L7+cert중앙 | +----------------+----------------+ 그림 1. egress 패턴 선택 결정 트리 — payload 복호화 허용/egress 신원 필요 두 축으로 세 패턴이 갈린다(본 문서는 HTTPS over mTLS). 세 패턴을 한 표로 정렬하면 \"무엇을 누가 푸느냐\"가 한눈에 보인다. gateway가 앱 TLS를 in-mesh leg(sidecar->gw)가 gateway가 본 패턴 복호화하는가 암호+신원검증인가 L7(method/path/status) --------------- ------------------- -------------------------- --------------------- PASSTHROUGH No (SNI만 봄) No (평문 TCP, 앱 TLS만) No (L4 only) HTTPS over mTLS No (이중 TLS) Yes (메시 mTLS 종단+SPIFFE) No (L4 only) <-- 본 문서 TLS origination Yes (gw가 종단) 선택(메시 mTLS는 별개) Yes (L7 visible) 여기서 이 패턴이 niche인 이유까지 미리 못 박아두자 — 뒤의 모든 설계 함정을 받아들이는 마음가짐이 된다. 본 패턴은 위 좌표의 한 칸일 뿐이고, 그 칸에 드는 시나리오 자체가 드물다. 대부분의 \"단순 외부 HTTPS\"는 passthrough로 충분하고(신원 불필요·cert 관리 없음·4객체 중 최단순), 신원·정책이 필요한 조직은 보통 origination을 택한다(egress gateway를 두는 주된 이유 인 L7 감사/per-URL 정책/중앙 cert를 다 주므로). 본 패턴은 payload는 절대 노출 불가 + 신원은 필요 라는 교집합에서만 최적이고, 결정타로 이 패턴을 써도 L7 egress 정책은 여전히 불가 하다(이중 TLS라 gateway가 L7을 못 봄). 추가"
    },
    {
      "title": "tcpKeepalive 필드 노트 — time / interval / probes는 각각 무엇을 제어하는가",
      "desc": "DR connectionPool.tcp.tcpKeepalive의 세 필드는 Envoy가 만든 개념이 아니라 리눅스 커널의 TCP keepalive 소켓 옵션 3개에 1:1 매핑된다. Envoy는 upstream 소켓에 옵션을 설정만 하고, probe를 보내는 주체는 커널이다. 이 한…",
      "url": "/public/istio-egress/ref__note-tcp-keepalive-fields.html",
      "domain": "istio-egress",
      "text": "istio egress tcp keepalive destinationrule connection-pool envoy kernel tcpKeepalive 필드 노트 — time / interval / probes는 각각 무엇을 제어하는가 NOTE DR connectionPool.tcp.tcpKeepalive 의 세 필드는 Envoy가 만든 개념이 아니라 리눅스 커널의 TCP keepalive 소켓 옵션 3개에 1:1 매핑 된다. Envoy는 upstream 소켓에 옵션을 설정만 하고, probe를 보내는 주체는 커널이다. 이 한 설정이 서로 다른 두 역할 (중간장비 세션 유지 / 죽은 상대 감지)을 겸한다는 것, 그리고 각 역할을 결정하는 필드가 다르다는 것이 이 노트의 본체다. 선행 문서 : TCP 병목 정본 §04(병목 4: 중간장비 idle timeout)·§06-1(권장값 YAML). 01. 커널 소켓 옵션 매핑 DR 필드 소켓 옵션 의미 커널 기본값 time TCP_KEEPIDLE 마지막 데이터 송수신 이후 이만큼 유휴하면 첫 probe 발사 7200s (2시간) interval TCP_KEEPINTVL probe에 응답이 없을 때 다음 probe까지의 재시도 간격 75s probes TCP_KEEPCNT 연속 무응답 probe가 이 횟수에 도달하면 연결을 죽은 것으로 판정·폐기 9 probe의 정체는 데이터가 아니라 빈 ACK 세그먼트 다(시퀀스 번호를 일부러 1 뒤로 물려 보내 상대의 ACK를 유도). 상대가 살아 있으면 즉시 ACK가 돌아오고, 그 왕복이 경로상 중간장비의 세션 타이머를 갱신한다. 02. 타임라인 — 권장값 (time=300, interval=30, probes=3)의 두 시나리오 그림 1. time=300 / interval=30 / probes=3의 두 시나리오. 상대가 살아있으면(A) 첫 probe에 즉시 ACK가 와서 interval/probes는 발동하지 않고, 죽어 있으면(B) 30초 간격 3회 무응답 후 t=390s에 소켓이 폐기된다. 역할 분리가 핵심이다: 중간장비 세션 유지 ( 병목 4 의 처방) — 상대가 살아있는 정상 케이스. probe/ACK 왕복이 FW 세션 idle 타이머를 갱신한다. 결정 규칙은 time < FW idle timeout (예: 300s < 1800s)이며, interval / probes 는 이 역할과 무관 하다 — 살아있는 상대는 첫 probe에 즉시 ACK하므로 재시도가 발생하지 않는다. 죽은 상대 감지 — 상대 서버가 소리 없이 사라진 경우(전원 단절, 경로 단절). interval × probes 가 감지 지연을 결정한다. 커널 기본(75s × 9 ≈ 11분)은 너무 느려서, 30s × 3 = 90초 안에 죽은 연결을 정리하도록 줄인다. 이게 없으면 죽은 연결이 Envoy 풀에 유령으로 남아 maxConnections 슬롯을 점유한다. 한 줄 요약: time 은 \"얼마나 자주 안부를 묻나\"(FW 대응), interval + probes 는 \"무응답을 얼마나 참고 사망 선고하나\"(유령 연결 정리) . 03. 값 결정 규칙 필드 규칙 근거 time FW idle timeout의 1/3 이하 (예: FW 1800s → 300) probe 유실·지연 1~2회를 견디고도 세션 갱신 보장 interval 수십 초 (예: 30) 짧을수록 감지 빠르지만 일시적 경로 flap에 과민해짐 probes 3 안팎 interval × probes = 사망 판정 지연이자 오판 방어 횟수 04. 적용 검증 — 설정은 sysctl이 아니라 소켓에 박힌다 KEY DR tcpKeepalive 는 커널 파라미터( net.ipv4.tcp_keepalive_* )를 바꾸지 않는다 . istiod가 cluster 설정으로 컴파일하고, Envoy가 연결을 새로 열 때마다 그 소켓에 setsockopt(TCP_KEEPIDLE/KEEPINTVL/KEEPCNT) 를 호출 하는 방식이다. 소켓 단위 옵션은 netns 기본값(sysctl)을 소켓별로 덮어쓸 뿐, 기본값 자체는 영원히 그대로다. sysctl 로 확인하는 것은 검증 지점 자체가 틀린 것이지 미적용의 증거가 아니다. [sysctl 경로] net.ipv4.tcp_keepalive_time = 7200 (netns-wide DEFAULT) | v applies only when socket has NO explicit option [DR 경로] Envoy --- setsockopt(fd, TCP_KEEPIDLE=300) ---> per-SOCKET option (overrides the netns default, socket by socket) 설정은 DR → istiod(xDS) → Envoy cluster → 소켓 옵션 → 와이어 probe 순으로 흐른다. 단계 순서대로 확인하면 실패 지점을 이분탐색할 수 있다. 확인할 pod에 주의 : 외부 호스트 DR(레이어 1)은 gw→외부 연결이므로 egress gateway pod 에서, sidecar→gw subset DR(레이어 2)은 앱 sidecar 에서 확인한다. ① xDS — Envoy에 설정이 도달했는가 (default + subset cluster 나란히) istioctl proxy-config cluster deploy/istio-egressgateway -n istio-egress \\ --fqdn api.partner-a.example.com -o json | \\ jq '.[] | {name, dr: .metadata.filterMetadata.istio.config, max: .circuitBreakers.thresholds[0].maxConnections, ka: .upstreamConnectionOptions.tcpKeepalive}' 기대 출력 — DR host의 모든 cluster가 한 줄씩 나온다. name의 세 번째 칸이 subset 이름(비어 있으면 default): { \"name\": \"outbound|443||api.partner-a.example.com\", \"dr\": \".../destination-ru"
    },
    {
      "title": "Egress Gateway 필드 매뉴얼 — HTTPS passthrough 중심 (출처: LLM 답변 + Istio 1.30 공식 문서)",
      "desc": "머릿속에 둘 단 하나의 그림(ANCHOR): egress gateway는 외부 트래픽을 유도하는 라우팅 수렴점일 뿐 강제 장치가 아니며, 외부 구간의 TLS를 누가 종단하느냐가 게이트웨이의 가시성·앱 변경·암호화 범위를 한꺼번에 결정한다. 앱이 끝까지 TLS를 쥐면(https) 게이트…",
      "url": "/public/istio-egress/ref__src-egress-gateway.html",
      "domain": "istio-egress",
      "text": "istio egress passthrough mtls tls-origination service-mesh Egress Gateway 필드 매뉴얼 — HTTPS passthrough 중심 (출처: LLM 답변 + Istio 1.30 공식 문서) ℹ 이 문서가 다루는 것 머릿속에 둘 단 하나의 그림(ANCHOR): egress gateway는 외부 트래픽을 유도하는 라우팅 수렴점 일 뿐 강제 장치가 아니며, 외부 구간의 TLS를 누가 종단하느냐 가 게이트웨이의 가시성·앱 변경·암호화 범위를 한꺼번에 결정한다. 앱이 끝까지 TLS를 쥐면( https ) 게이트웨이는 SNI만 보는 passthrough (주류), 앱이 평문을 보내면( http ) 게이트웨이가 TLS를 대신 맺는 origination (특수) — 이 한 갈래에서 나머지(가시성·앱 변경·암호화·인증서 위치)가 전부 따라 나온다. - Sidecar 모드 (Ambient 아님) · CRD는 Istio 1.30 / networking.istio.io/v1 기준 (공식 문서 검증, 출처는 문서 끝) - 주류 경로 = 앱이 https:// 로 직접 호출 → 게이트웨이 PASSTHROUGH (섹션 04, 가장 비중 큼) - 특수 경로 = 앱을 http:// 로 바꿔 게이트웨이가 TLS/mTLS를 대신 맺는 origination (섹션 06, 필요할 때만) 대상환경: Istio 1.30 sidecar mesh · on-prem/홈랩(Calico CNI 전제) · 대상독자: egress 경로를 설계·운영하는 DevOps/SRE · 범위: 설계·TLS 모델 정본(운영 deep-dive는 egress 운영 정본 에 위임) · 선행개념: 아래 박스. ℹ 선행 개념 — 이 7개만 손에 쥐면 본문이 풀린다 본문은 이 단어들 위에서 돈다. 모르는 것만 집어 읽고 넘어가라. - TLS / 종단(termination) : 주고받는 내용을 암호화하는 규약( https 의 's'). 암호를 푸는 지점 이 종단인데, 한 연결에서 종단은 단 한 번뿐 이다 — 이 제약이 passthrough vs origination을 가르는 물리 법칙이다. - SNI : TLS를 열 때 평문으로 같이 실리는 목적지 도메인(예: api.partner.example.com). 암호를 못 풀어도 \"어느 호스트로 가는 연결인지\"는 보인다 → passthrough 라우팅의 유일한 단서. - passthrough vs origination : 전자는 암호를 안 풀고 SNI만 보고 통과, 후자는 게이트웨이/사이드카가 외부행 TLS를 직접 새로 맺음 . - L4 / L7 : L4 = 연결(TCP) 수준(\"어디로 얼마나\"), L7 = 요청(HTTP) 수준(\"어떤 요청이 200이었나\"). 게이트웨이가 L7을 보려면 암호를 풀어야 한다. - 사이드카 vs egress gateway : 사이드카 = 앱마다 붙는 Envoy(모든 트래픽 통과). egress gateway = 외부행만 모으는 전용 Envoy. - 메시 자동 암호화 (mesh mTLS, ISTIO_MUTUAL) : 클러스터 안 사이드카끼리를 Istio가 자동 양방향 암호화하는 것. 외부 파트너와의 mTLS와는 다른 구간, 다른 인증서 다(혼동이 모든 오해의 출발점). - 설정 리소스 4종 : ServiceEntry (어떤 외부를 허용·등록) · Gateway (게이트웨이가 받는 포트) · VirtualService (어디로 보낼지 규칙) · DestinationRule (보낸 뒤 TLS·묶음 정책). 01. 배경 — 왜 egress gateway가 존재하나 (해결하는 문제) 사이드카를 주입하면 기본값( outboundTrafficPolicy: ALLOW_ANY )에서 각 Pod는 자기 사이드카를 거쳐 외부로 직접 나간다 — 출구가 노드 수만큼 흩어지고, 누가 어디로 나갔는지 한 곳에 안 남으며, 파트너에게 등록할 출구 IP도 고정되지 않는다. egress gateway가 푸는 문제는 이 흩어진 외부행을 하나의 제어점(choke point)으로 수렴 시키는 것이고, 거기서 네 가지가 파생된다. 동기 무엇을 얻는가 노드 고정 (가장 흔한 목표) 외부 NAT·방화벽 통로를 특정 노드에만 부여. 앱 노드는 외부 라우트가 없고 egress 노드만 인터넷에 도달. 출구 IP를 고정해 파트너 IP allowlist에 등록 가능 관측·감사 외부 접근이 단일 지점을 통과 → 누가·어디로·얼마나 나갔는지 한 곳에서 수집. PCI-DSS 감사 추적에 직접 연결 인증서 집중 파트너가 client 인증서(mTLS)를 요구할 때, 모든 앱이 각자 들고 있는 대신 게이트웨이 한 곳에 모음. 단, 이건 origination(섹션 06)에서만 공격면 축소 침해된 Pod의 데이터 유출 경로를 게이트웨이로 한정. 단, 강제 계층이 따로 받쳐줄 때만 — 섹션 02 대부분의 도입 목적은 첫 줄 — \"외부행을 특정 노드로만\" — 이다. 그리고 그 경우 앱은 보통 이미 https:// 로 외부를 부르고 있어서, 앱을 건드리지 않고 게이트웨이만 얹는 passthrough 가 자연스러운 선택이 된다. ℹ What you might be missing egress gateway는 ingress gateway의 거울상이 아니다. Ingress는 외부 트래픽이 물리적으로 그 LoadBalancer IP를 거칠 수밖에 없어 자연히 강제된다. Egress엔 그런 물리적 강제가 없다 — Pod는 여전히 자기 사이드카에서 임의 목적지로 나갈 수 있다. 공식 문서도 명시한다: \"egress Gateway를 정의하는 것 자체는 그 게이트웨이가 도는 노드에 어떤 특별한 취급도 부여하지 않는다.\" 이 비대칭성이 egress 운영 난이도의 근원이다. 02. 전제 교정 — 게이트웨이는 강제 장치가 아니다 도입 사고가 가장 많이 나는 지점이라 먼저 못을 박는다. \"트래픽을 게이트웨이로 라우팅\" 과 \"트래픽이 게이트웨이를 거치도록 강제\" 는 완전히 다른 레이어의 일이다. VirtualService로 \"이 호스트는 egress gateway를 거쳐라\"라고 쓰면, 그 규칙을 따르는 사이드카는 게이"
    },
    {
      "title": "Egress 외부 endpoint 프로토콜별 Istio 설정 — HTTP vs HTTPS",
      "desc": "외부로 나가는 트래픽의 endpoint가 평문 HTTP일 때와 HTTPS일 때 ServiceEntry / Gateway / VirtualService / DestinationRule을 어떻게 설정하고, 왜 한쪽 설정을 다른 쪽에 그대로 쓰면 깨지는지를 Envoy 필터 체인 수준에서 정…",
      "url": "/public/istio-egress/ref__src-http-vs-https.html",
      "domain": "istio-egress",
      "text": "istio egress http https tls-origination service-entry mental-model Egress 외부 endpoint 프로토콜별 Istio 설정 — HTTP vs HTTPS NOTE 외부로 나가는 트래픽의 endpoint가 평문 HTTP일 때 와 HTTPS일 때 ServiceEntry / Gateway / VirtualService / DestinationRule을 어떻게 설정하고, 왜 한쪽 설정을 다른 쪽에 그대로 쓰면 깨지는지 를 Envoy 필터 체인 수준에서 정리한다. 결론은 한 문장: 외부 endpoint가 첫 바이트에 실제로 말하는 프로토콜 과 ServiceEntry port protocol 이 일치해야 하고, 평문에 TLS 설정을 쓰면 istiod가 그 listener에 거는 필터 체인 자체가 달라져 connection reset·핸드셰이크 실패로 깨진다. (검증 기준 Istio 1.30 / networking.istio.io/v1 , sidecar 모드) 1. 배경 — 왜 \"외부가 무엇을 받느냐\"가 모든 설정을 가른다 egress 설정을 처음 만지면 \"TLS 옵션 몇 개를 켜고 끄는 문제\"처럼 보인다. 그래서 HTTPS 예제에서 동작하던 매니페스트를 평문 endpoint에 복붙하고, protocol: TLS / sniHosts 만 남겨둔 채 connection reset에 빠진다. 이 문서가 풀려는 혼란이 바로 이것이다. 핵심은 egress에서 두 개의 독립된 사실 이 자꾸 한 단어(\"https\")로 뭉뚱그려진다는 점이다. 외부 endpoint가 받는 것 — 파트너 서버가 80에서 평문을 받나, 443에서 TLS를 받나. 이건 상대가 정한 고정값 이다. 앱이 보내는 것 — 우리 앱 코드가 http:// 를 부르나 https:// 를 부르나. 이건 우리가 바꿀 수 있는 값 이다. 이 둘이 같을 필요는 없다. 앱이 http:// 를 보내도 Istio가 중간에서 외부로 새 TLS를 맺어줄 수 있다(origination). 그래서 \"https로 나가야 하나요?\"라는 질문은 사실 \"외부 endpoint가 TLS를 받아주는가?\" 라는, 우리가 못 바꾸는 쪽 사실로 환원된다. 받아주면 TLS를 쓸 수 있고(권장), 안 받아주면 평문 외엔 선택지가 없다. NOTE 선행 개념: ServiceEntry(외부 서비스를 mesh 레지스트리에 등록), TLS origination(앱은 평문, Istio가 외부로 TLS를 새로 맺음), passthrough(앱의 TLS를 Istio가 안 건드리고 SNI로만 라우팅). 이 셋의 CRD 골격은 Egress gateway 매뉴얼 에 정본이 있고, 여기서는 HTTP vs HTTPS 경계 만 깊게 판다. 네 가지 경로로 좌표를 잡기 \"외부 endpoint가 무엇을 받느냐 × 앱이 무엇을 보내느냐\"의 조합이 실무 패턴 네 가지를 만든다. ①②③은 모두 외부가 HTTPS 라 \"TLS를 누가·어디서 종단하느냐\"의 차이일 뿐이고, ④만 외부가 평문 HTTP 라 TLS가 아예 없다 — 이 문서가 추가로 채우는 칸이 ④다. # 외부 endpoint 앱 호출 패턴 외부 구간 암호화 GW L7 가시성 앱 변경 ① HTTPS https:// passthrough end-to-end 유지 ❌ (SNI/L4만) 없음 ② HTTPS http:// mTLS origination (MUTUAL) GW부터 새 TLS ✅ 전부 필요 ③ HTTPS http:// TLS origination (SIMPLE) GW부터 새 TLS ✅ 전부 필요 ④ HTTP (평문) http:// plain HTTP ❌ 없음(평문 노출) ✅ 전부 없음 ②③이 앱을 http:// 로 바꾼다고 ④와 같아지지 않는다 — ②③의 외부 구간은 여전히 TLS다. 이 착각이 평문 노출 사고의 단골 원인이다(→ §6 What you might be missing). 2. 핵심 — protocol 선언이 Envoy 필터 체인을 고른다 멘탈 모델 앵커 한 문장: ServiceEntry/Gateway 포트의 protocol 값은 옵션이 아니라 스위치 다 — istiod가 그 listener(LDS)에 어떤 필터 체인을 박을지 를 고르고, 필터 체인은 입력 첫 바이트의 형태 를 전제한다. 그래서 선언한 protocol과 실제로 들어오는 첫 바이트가 어긋나면, 옵션이 안 맞는 게 아니라 체인 자체가 못 맞물려 깨진다. CR→xDS 멘탈 모델 의 원리 그대로다: CR은 입력, 진실은 Envoy config. protocol 을 바꾸면 같은 포트라도 전혀 다른 listener가 생성된다. protocol 입력 첫 바이트 핵심 필터 체인 라우팅 키 보이는 메트릭 HTTP GET / HTTP/1.1... (평문) HTTP Connection Manager Host 헤더 (RDS) istio_requests_total (method/path/status) TLS TLS ClientHello tls_inspector → tcp_proxy SNI istio_tcp_* 만 (복호화 안 함) HTTPS TLS ClientHello TLS 종단(복호화) → HTTP Connection Manager 복호 후 Host 헤더 istio_requests_total (주로 origination 외부 leg) 이 표가 왜 호환 불가를 만드는지는 첫 바이트 를 보면 즉시 드러난다. 평문 HTTP가 와이어에 처음 흘리는 것은: GET /v1/foo HTTP/1.1\\r\\nHost: api.partner.example.com\\r\\n... 이건 TLS ClientHello가 아니다. 그런데 이 endpoint를 protocol: TLS 로 등록하면 Envoy는 그 포트에 tls_inspector 를 걸고 첫 바이트에서 SNI를 뽑으려 한다 → GET ... 에는 ClientHello 구조가 없으니 SNI 추출 실패 → 매칭되는 filter chain 없음 → connection reset (또는 SNI 없는 filter chain으로 떨어져 미스매치). protocol: HTTPS 로 쓰면 Envoy가 TLS 핸드셰"
    },
    {
      "title": "Egress Gateway 운영 가이드 — passthrough + ISTIO_MUTUAL (출처: LLM 답변 + Istio 1.30 공식 문서)",
      "desc": "앱이 HTTPS로 직접 호출(end-to-end TLS 유지) + 게이트웨이는 PASSTHROUGH(복호화 안 함) + proxy→egress 구간은 ISTIO_MUTUAL로 mTLS. 결론부터: 이 게이트웨이가 보는 것은 \"암호화된 장기 TCP 스트림\" 하나뿐이고, 모니터링(L4+S…",
      "url": "/public/istio-egress/ref__src-operations.html",
      "domain": "istio-egress",
      "text": "istio egress passthrough monitoring graceful-shutdown operations Egress Gateway 운영 가이드 — passthrough + ISTIO_MUTUAL (출처: LLM 답변 + Istio 1.30 공식 문서) ℹ 대상 구조 앱이 HTTPS로 직접 호출(end-to-end TLS 유지) + 게이트웨이는 PASSTHROUGH(복호화 안 함) + proxy→egress 구간은 ISTIO_MUTUAL로 mTLS. 결론부터: 이 게이트웨이가 보는 것은 \"암호화된 장기 TCP 스트림\" 하나뿐이고, 모니터링(L4+SNI만)·운영 함정(연결을 소수 노드에 모음)·graceful shutdown(요청이 아니라 연결이 닫히길 기다림)이 전부 이 한 사실에서 연역된다. 이 문서는 그 연역을 따라간다. 대상환경 Istio 1.30 / networking.istio.io/v1 · 대상독자 egress를 운영하는 DevOps/SRE · 범위 관측·운영함정·종료. 구조의 조립 은 구조 정본 에 위임 · 선행개념 egress gateway 기본 구성 00. 배경 — 왜 이 게이트웨이는 운영이 다른가 일반 게이트웨이(ingress, 또는 TLS를 종단하는 egress)는 트래픽을 복호화해서 안을 본다. HTTP method·path·status code가 다 보이니, 모니터링은 istio_requests_total 로 에러율을 보고, 정책은 path 기반 라우팅을 걸고, graceful shutdown은 \"in-flight 요청 이 끝나길\" 기다린다. 이 모든 운영 상식이 L7이 보인다는 전제 위에 서 있다. 그런데 이 구조는 그 전제를 의도적으로 깬다. 앱이 외부와 end-to-end TLS 를 맺고(게이트웨이가 중간에서 복호화하면 그 보장이 깨지므로), 게이트웨이는 PASSTHROUGH — 암호문 봉투를 뜯지 않고 그대로 중계한다. 왜 그렇게 하나: 외부 파트너에 대한 기밀성을 게이트웨이가 신뢰 경계에서 빼앗지 않으면서도, egress를 소수 노드로 강제 경유 시켜 출구 IP를 고정(파트너 방화벽 allowlist)하고 중앙에서 관측·통제하려는 것이다. proxy→egress 안쪽 한 구간만 ISTIO_MUTUAL로 감싸 메시 내부 신원을 입힌다. 이 절충의 대가가 바로 운영의 차이다. 게이트웨이는 자기가 나르는 트래픽의 내용을 원리적으로 알 수 없다 — 복호화를 안 하기로 설계했으니까. 그래서 L7에 기대던 운영 상식이 전부 한 단계 내려가야 한다. 무엇이 어떻게 내려가는지가 이 문서의 전부다. 01. 핵심 — \"암호화된 장기 TCP 스트림\" 하나에서 모든 게 따라 나온다 ℹ 머릿속 앵커 (이거 하나만) 게이트웨이 입장에서 트래픽은 요청이 아니라 연결이다. 외부 TLS를 안 풀기 때문에 L7(HTTP)이 없고, 한 번 맺힌 암호화 TCP 스트림이 오래 살아 있다. 이 그림에서 세 가지 운영 특성이 기계적으로 연역된다 — ①관측은 L4+SNI만 ②연결을 소수 노드에 모으니 연결 단위 자원이 병목 ③종료는 연결이 닫히길 기다림. 세 갈래로 펼쳐 보면: 그림 1. 앵커 — 게이트웨이가 보는 것은 암호화된 장기 TCP 스트림 하나뿐. ①관측 ②운영 함정 ③종료가 전부 여기서 연역된다 세 갈래의 공통 뿌리 가 하나(앵커)라는 게 핵심이다. 아래 섹션은 각 갈래를 \"앵커에서 왜 이렇게 되는가\"로 전개한다. 갈래마다 디테일이 다르지만, 막힐 때마다 앵커로 돌아오면 답이 나온다. 02. 갈래 1 — 모니터링: L7이 없으니 관측이 한 단계 내려간다 앵커에서 곧장 따라 나오는 제약을 한 줄로: 게이트웨이에서는 HTTP 메트릭(요청 수, 상태 코드, 지연)이 나오지 않는다. 외부 TLS를 종단하지 않으니 Envoy가 그 트래픽을 TCP로만 인식해서다. 그래서 게이트웨이 메트릭은 전부 istio_tcp_* 계열이다. \"요청이 없고 연결만 있다\"가 메트릭 이름표에 그대로 박힌다. 게이트웨이에서 나오는 메트릭 (L4) 메트릭 의미 istio_tcp_connections_opened_total 열린 연결 수 — \"어느 외부로 몇 번 연결했나\" istio_tcp_connections_closed_total 닫힌 연결 수 — opened와의 차이로 현재 활성 연결 추정 istio_tcp_sent_bytes_total / istio_tcp_received_bytes_total 송수신 바이트 — 트래픽 양·이상 급증 탐지 비-HTTP egress 트래픽의 표준 관측이 바로 이 TCP 메트릭이다. 게이트웨이 워크로드를 기준으로 집계하면 \"egress 게이트웨이를 통과한 전체 외부 연결\"을 중앙에서 볼 수 있다. # 외부 호스트별 신규 연결률 (source = egress 게이트웨이) sum(rate(istio_tcp_connections_opened_total{ source_workload=\"istio-egressgateway\" }[5m])) by (destination_service_name) # 외부 호스트별 송신 바이트율 sum(rate(istio_tcp_sent_bytes_total{ source_workload=\"istio-egressgateway\" }[5m])) by (destination_service_name) # 현재 활성 연결 추정 (opened - closed 누적) sum(istio_tcp_connections_opened_total{source_workload=\"istio-egressgateway\"}) - sum(istio_tcp_connections_closed_total{source_workload=\"istio-egressgateway\"}) ⚠ opened−closed의 한계 *_opened_total / *_closed_total 은 monotonic counter 라 Pod 재시작·카운터 리셋 시 두 합의 차가 음수나 과대값으로 튄다. 또 두 합을 다른 라벨 집합으로 묶으면 매칭이 어긋난다. 정밀한 활성 연결 은 게이지를 직접 보는 편이 정확하다 — Prometheus(15090)에서는 envoy_cluster_upstream_cx_activ"
    },
    {
      "title": "Egress TCP 병목 정본 — L4 proxy가 연결을 모으면 무엇이 고갈되는가",
      "desc": "egress gateway 도입은 mTLS 여부와 무관하게 L4 proxy 하나를 전사 외부행 경로에 끼워 넣는 일이다. 이 문서는 그때 발생하는 TCP 병목 5종을 — Envoy 연결 상한 → ephemeral port/TIME_WAIT → conntrack → 중간장비 half-o…",
      "url": "/public/istio-egress/ref__src-tcp-bottlenecks.html",
      "domain": "istio-egress",
      "text": "istio egress tcp connection-pool port-exhaustion conntrack keepalive snat Egress TCP 병목 정본 — L4 proxy가 연결을 모으면 무엇이 고갈되는가 NOTE egress gateway 도입은 mTLS 여부와 무관하게 L4 proxy 하나를 전사 외부행 경로에 끼워 넣는 일 이다. 이 문서는 그때 발생하는 TCP 병목 5종을 — Envoy 연결 상한 → ephemeral port/TIME_WAIT → conntrack → 중간장비 half-open → drain — 산술적 한계 수치와 부딪히는 순서 까지 박아서 정리하고, 완화 운영값 전체(YAML), reset 원인 분기 런북, Prometheus 알람까지 한 곳에 둔다. 모니터링 일반론·graceful shutdown 상세는 Egress 운영 정본 에 있고, 이 문서는 그 §03(연결 단위 자원 병목)을 수치·처방 레벨로 전개한 심화다. 대상 환경 : Istio 1.30 sidecar mode, egress gateway = tcp_proxy (PASSTHROUGH 또는 ISTIO_MUTUAL — 어느 쪽이든 동일 적용). 대상 독자 : gateway를 운영하게 될 사람, 도입 전 캐파 산정을 해야 하는 사람. 선행 개념 : Egress 4-CRD 멘탈모델 . 01. 멘탈모델 — 문제의 뿌리는 단 하나 L4 proxy는 암호화된 바이트 파이프다. 앱 연결 1개 = gateway 소켓 2개(1:1, 재사용 불가)이고, gateway 도입은 전사에 분산되어 있던 출발지 IP 다양성을 의도적으로 소수 pod로 붕괴시킨다. 이 한 문장에서 병목 전부가 연역된다. \"1:1 + 집중\"이라는 구조가 연결 단위로 세는 모든 자원(Envoy 풀, ephemeral port, conntrack 엔트리, FW 세션 테이블)을 동시에 압박한다. [before] [after] pod1(IP1) ----\\ pod1 --\\ pod2(IP2) -----> partner-a:443 pod2 ---> gw1(IPa) --> partner-a:443 ... ----/ ... --/ gw2(IPb) --> pod500(IP500) pod500-/ port space: 28k x 500 port space: 28k x 2 포트 고갈은 새로 생기는 문제가 아니다. 원래 전사에 분산돼 있던 자원(src IP 다양성)을 gateway가 보안상 이득(방화벽 등록 IP 축소)과 맞바꿔 포기 하는 구조라서 따라오는 청구서다. 동전의 양면이라는 점이 도입 보고서에 적혀야 할 문장이다. 02. 기본 산술 — 4-tuple에서 한계 수치가 나온다 TCP 연결 하나는 커널 입장에서 (srcIP, srcPort, dstIP, dstPort) 4-tuple로 식별된다. 4개가 모두 같은 연결은 동시에 2개 존재할 수 없다. 1 connection = ( srcIP, srcPort, dstIP, dstPort ) | | | | gw pod 29,xxx partner-a 443 (fixed) (variable) (fixed) (fixed) 외부 API 호출에서 dstIP:443은 고정, srcIP도 그 pod의 IP로 고정 — 변수는 srcPort 하나뿐 이다. 커널이 발신 연결에 자동으로 빌려주는 이 포트(ephemeral port)의 리눅스 기본 범위는 32768–60999 , 약 28,232개 . usable srcPort pool (per srcIP, per destination) |--------------- 28,232 ports ----------------| 32768 60999 한계 ① 동시 연결 : gw pod 1개 → 목적지 1개의 동시 연결 상한 ≈ 28,000 . 설정이 아니라 산술이다. 한계 ② 신규 연결률 : 연결이 닫혀도 포트는 즉시 반환되지 않는다. 먼저 close한 쪽이 그 4-tuple을 TIME_WAIT 상태로 60초 (커널 고정) 보존하고, proxy는 보통 먼저 닫는 쪽이라 TIME_WAIT는 gateway에 쌓인다. t=0s t=5s t=65s connect --- close ---------- port returned |--- TIME_WAIT (60s) ---| port 29314 locked for this dst 포트 하나가 \"사용 시간 + 60초\"를 점유하므로, short-lived 연결이 계속 생기면: 28,232 ports ÷ 60s ≈ 470 new connections/sec (gw pod 1개 -> 목적지 1개) 구체 예: pod 500개가 keep-alive 없이 각자 초당 1회 호출하면 gw에 500 conn/s가 모인다. gw pod 1개면 470/s를 넘어 수십 초 안에 EADDRNOTAVAIL (빌려줄 포트 없음)이 나기 시작한다. 처방의 우선순위가 ① 앱 keep-alive(분자 줄이기) ② replica 증설(분모 늘리기) ③ tcp_tw_reuse (60초 규칙 완화) 순인 이유가 이 분수식이다. 03. 왜 연결을 재사용 못 하나 — L7 vs L4의 본질 차이 \"gateway가 외부행 연결을 모아서 재사용(pooling)하면 되지 않나\"가 자연스러운 반론인데, 여기서 L7/L4 proxy의 근본 차이가 갈린다. [L7 proxy (HTTP)] [L4 proxy (tcp_proxy) = egress gw] client A --\\ client A ===[encrypted bytes]===> upstream A client B ---> [pool: conn x2] --> client B ===[encrypted bytes]===> upstream B client C --/ upstream client C ===[encrypted bytes]===> upstream C (요청 경계를 아니까 섞어 태움) (바이트 파이프. 섞으면 TLS 세션 깨짐) L7 proxy는 요청의 경계를 알기 때문에 여러 클라이언트의 요청을 적은 수의 upstream 연결에 다중화할 수 있다. 그러나 egress gateway가 다루는 것은 앱↔외부 서버가 종단간으로 맺은"
    },
    {
      "title": "Runbook — ServiceEntry resolution: DNS 동작과 진단 (STRICT_DNS)",
      "desc": "외부 도메인(httpbin.org)을 ServiceEntry(resolution: DNS)로 등록하면 istiod가 이를 Envoy의 STRICT_DNS cluster로 변환한다. 이 문서는 그 변환·DNS 갱신 메커니즘, \"죽은 IP\" 처리, ambient DNS 질의 경로를 홈랩…",
      "url": "/public/istio-egress/rpt__report-2026-06-07_dns-resolution.html",
      "domain": "istio-egress",
      "text": "istio egress dns serviceentry envoy Runbook — ServiceEntry resolution: DNS 동작과 진단 (STRICT_DNS) NOTE 외부 도메인( httpbin.org )을 ServiceEntry(resolution: DNS) 로 등록하면 istiod가 이를 Envoy의 STRICT_DNS cluster 로 변환한다. 이 문서는 그 변환·DNS 갱신 메커니즘, \"죽은 IP\" 처리, ambient DNS 질의 경로를 홈랩 실측·재현 명령과 함께 한 줄의 멘탈모델로 꿴다. 결론 한 문장: DNS refresh는 health check가 아니다 — DNS는 \"목록\"만 줄 뿐 IP의 생사를 모르므로, 죽은 IP 회피는 outlier detection으로 명시 해야 한다. 환경: cluster homelab (k8s v1.30.6, istiod 1.30.0 / istioctl 1.27.0 client·skew) / egress gateway 경유 / 관련: ingress·egress 통합 리포트 1. 배경 — 왜 mesh가 외부 도메인의 IP를 직접 들어야 하나 mesh 안 트래픽은 전부 Envoy가 가로채 라우팅한다(sidecar/gateway). 그런데 Envoy는 \"IP:port의 묶음\"인 cluster 단위로만 upstream을 안다 — Envoy의 세계에는 호스트네임이 없고 endpoint 집합만 있다. mesh 내부 서비스는 istiod가 k8s Endpoints / EndpointSlice 를 watch해서 cluster를 자동으로 채워준다. 하지만 httpbin.org 같은 mesh 밖 외부 도메인 은 k8s에 Endpoints가 없다. istiod가 그 IP를 알 길이 없다. ServiceEntry 는 바로 이 공백을 메우는 CRD다 — \"이 호스트는 mesh의 일부로 취급하라, IP는 이렇게 알아내라\"를 선언한다. 그 \"IP를 알아내는 방식\"이 spec.resolution 필드이고, 이 한 필드가 Envoy cluster의 종류를 결정 한다. 즉 외부 도메인 한 줄을 등록할 때 진짜로 고르는 것은 \"Envoy가 IP를 들고 LB하는 전략\"이다. 여기서 자연스럽게 따라오는 질문 4개가 이 문서의 뼈대다 — 이 순서대로 읽으면 길을 잃지 않는다: resolution -> cluster type -> 갱신 주기 -> 죽은 IP -> 회피 구성 (어떻게 (STRICT_DNS (DNS TTL을 (DNS는 (outlier IP를 안다) / LOGICAL) 따라 재질의) 생사 모름) detection+retry) 선행 개념: Envoy cluster = endpoint(IP:port) 집합 + LB 정책 / cluster 이름 규칙 direction|port|subset|fqdn (예: outbound|443||httpbin.org ) / egress gateway = mesh 밖으로 나가는 트래픽을 한 노드로 모으는 전용 Envoy. cluster 구조 자체는 Cluster 해부 참조. 2. 머릿속에 둘 그림 — DNS는 \"목록\", 생사는 별도 책임 이 문서 전체를 꿰는 앵커 한 문장 : resolution 이 Envoy cluster type을 정하고, cluster type이 \"IP를 어떻게 들고 LB할지\"를 정한다. 그러나 어느 type이든 DNS는 IP \"목록\"만 줄 뿐 그 IP가 살아있는지는 모른다 — liveness는 outlier detection의 별도 책임이다. 이 한 문장에서 나머지가 전부 파생된다. resolution을 STRICT_DNS로 두면 A record의 모든 IP를 펼쳐 Envoy가 직접 LB하고(§3), 갱신 주기는 DNS TTL을 따른다(§4). 하지만 TTL 만료 전에 IP가 죽으면 Envoy는 그 IP를 여전히 HEALTHY로 들고 있어 연결이 실패한다(§5) — DNS refresh는 단지 \"목록 다시 받기\"이지 \"이 IP 살아있냐\"를 묻는 게 아니기 때문 이다. 그래서 죽은 IP 회피는 outlier detection + retry로 명시 해야 하고(§7), GSLB 뒤라면 LB 권한을 DNS에 위임하는 LOGICAL_DNS가 더 맞는다(§6). 마지막으로 \"그 DNS 질의를 실제로 누가 쏘는가\"가 진단의 출발점이다 — egress 경유 시 그 주체는 egress gateway의 Envoy(c-ares)다(§8). 왜 이렇게 책임을 쪼갰는가? DNS와 health는 원래 다른 시스템 이기 때문이다. DNS 서버는 \"이 이름에 어떤 IP들이 매핑되는가\"의 권위자일 뿐, 그 IP 뒤 프로세스가 지금 요청을 받을 수 있는지는 모른다(특히 같은 A record 안에서 일부만 죽은 경우). Envoy는 이 둘을 의도적으로 분리한다 — DNS는 endpoint 발견(discovery) , outlier detection은 endpoint 축출(ejection) . 이 분리를 모르면 \"DNS만 믿으면 알아서 죽은 IP 빠지겠지\"라는 가장 흔한 오해에 빠진다. 3. 변환 구조 — resolution 이 cluster type을 결정한다 ServiceEntry.spec.resolution 필드가 Envoy cluster type을 결정한다. 이 매핑을 먼저 확립해야 이후 갱신 주기(§4)·죽은 IP(§5)를 STRICT/LOGICAL 대비로 읽을 수 있다. resolution Envoy type endpoint 적재 적합 대상 DNS STRICT_DNS A record 모든 IP 를 펼침 개별 IP를 Envoy가 직접 LB하고 싶을 때 DNS_ROUND_ROBIN LOGICAL_DNS 첫 IP 1개 만, 연결 재사용 CDN/GSLB/대형 LB 뒤 \"논리적 단일 endpoint\" 차이의 본질은 \"endpoint를 펼치느냐(STRICT) vs 한 점으로 접느냐(LOGICAL)\" 한 곳이다 — 나머지(갱신·LB·연결 재사용)는 전부 이 한 델타에서 따라온다. resolution: DNS (STRICT_DNS) 변환 결과: ServiceEntry(resolution: DNS, hosts: httpbin.or"
    },
    {
      "title": "Test Report — Ingress / Egress Gateway 동작 검증",
      "desc": "홈랩 클러스터에서 Ingress·Egress gateway 동작을 실측 검증한 리포트. 하나의 그림: gateway는 메시 경계에 선 전용 Envoy proxy다. ingress는 자기가 TLS를 끝내므로(termination) L7 전체가 보여 host/path로 분기하고, egre…",
      "url": "/public/istio-egress/rpt__report-2026-06-07_ingress-egress.html",
      "domain": "istio-egress",
      "text": "istio ingress egress gateway report Test Report — Ingress / Egress Gateway 동작 검증 NOTE 홈랩 클러스터에서 Ingress·Egress gateway 동작을 실측 검증한 리포트. 하나의 그림: gateway는 메시 경계에 선 전용 Envoy proxy다. ingress는 자기가 TLS를 끝내므로(termination) L7 전체가 보여 host/path로 분기 하고, egress는 나가는 HTTPS를 복호화하지 않고 SNI만 보고 L4로 한 chokepoint를 거치게 강제 한다 — 이 L7 vs L4 비대칭이 두 gateway 검증법까지 갈라놓는다. 결론 — Ingress : host/path 라우팅 + TLS termination PASS. Egress : HTTPS SNI PASSTHROUGH로 외부 호출이 egress gateway를 강제 경유함을 access log로 입증(200). REGISTRY_ONLY 미등록 차단은 메시 전역 영향 탓에 의도적 보류. Date: 2026-06-07 Cluster: homelab (kubespray bare-metal, k8s v1.30.6, Calico) Istio: 1.30.0 (istiod + ingress/egress gateway, Helm 설치) Scenario: 10-ingress, 20-egress NS: mesh-test (istio-injection=enabled) 0. 배경 — 왜 gateway가 따로 있고, 왜 둘의 검증법이 다른가 sidecar가 이미 모든 pod에 붙어 mTLS·라우팅·관측을 한다면, 경계에 또 하나의 Envoy(gateway)를 세우는 이유는 무엇인가. 답은 경계 트래픽은 sidecar가 다루기 곤란한 특성 을 갖기 때문이다. Ingress 쪽 문제: 외부에서 오는 클라이언트는 mesh identity(SPIFFE cert)가 없다. 그냥 공개 HTTPS로 들어온다. 누군가는 그 외부 TLS를 종료(termination) 하고, 복호화한 Host/path를 보고 어느 내부 서비스로 보낼지 정해야 한다. 이걸 모든 pod에 흩뿌릴 수 없으니 단일 진입점 이 필요하다. Egress 쪽 문제: 기본값( outboundTrafficPolicy: ALLOW_ANY )에선 각 sidecar가 외부로 제멋대로 나간다. 그러면 egress IP가 노드마다 흩어지고, 방화벽 화이트리스트가 복잡해지고, 외부 호출 로그가 흩어진다. 그래서 나가는 트래픽을 한 pod(chokepoint)로 모아 단일 통제·관측 지점을 만든다. 그래서 ingress와 egress는 같은 \"경계 gateway\"지만 푸는 문제가 정반대 다. ingress는 외부 TLS를 끝내고 안을 들여다봐야 하고(그래야 path로 분기), egress는 나가는 TLS를 그대로 둔 채 어디로 가는지만 알면 된다. 이 한 줄의 차이가 이 리포트의 핵심 — ingress = L7 termination, egress = L4 SNI passthrough — 으로 굳는다. 그림 1. ingress/egress 비대칭. ingress GW는 TLS를 종료해 L7(Host/path)로 라우팅하므로 status code로 검증한다. egress GW는 payload를 복호화하지 않고 SNI만 보고 L4 passthrough하므로 status를 못 보고 access log로 \"경유했는가\"를 검증한다. 이 비대칭의 직접적 귀결: 검증법이 다르다. ingress는 \"복호화가 됐나 + path가 맞게 갈렸나\"를 status code(200/418/404)로 본다. egress는 payload가 암호화돼 gateway가 status를 볼 수 없으니, \"200이 떴나\"가 아니라 \"정말 그 pod를 경유 했나\" 를 access log로 본다. 이 차이를 모르면 egress의 200 을 \"gateway가 반환한 200\"으로 오독한다(→ §2, What you might be missing). 선행 개념: Envoy listener/route/cluster, cluster 이름 규칙 direction|port|subset|fqdn , SNI(TLS handshake에 평문 노출되는 목적지 호스트명), ServiceEntry/Gateway/VirtualService/DestinationRule 4종 CRD. 깊은 레퍼런스는 Egress Gateway 정본 , Cluster 해부 . 1. 사전 확인 — 출발선이 깨끗한가 검증 전에 메시 자체가 정상이어야 결과를 믿을 수 있다. 메시 상태 정상: istiod/ingress/egress 모두 1/1 Running , proxy-status 2 proxies SYNCED. ⚠️ 초기 \"비정상\" 의심은 istioctl 1.27 클라이언트로 1.30 컨트롤플레인을 조회 한 버전 불일치 착시였음. 실제 이상 없음 — 진단 도구의 버전부터 의심하라 는 교훈. sample app( httpbin , sleep ) READY 2/2 → sidecar 주입 정상. baseline: 내부(sleep→httpbin) 200, 외부(httpbin.org) 200 (기본 outboundTrafficPolicy: ALLOW_ANY — 이 시점엔 egress gateway를 안 거치고도 외부가 뚫린다는 뜻. §2의 \"경유 강제\"는 이 baseline 위에 길을 새로 까는 작업이다). 2. Ingress Gateway — L7 termination 검증 메커니즘: ingress gateway는 외부에서 받은 HTTPS를 자기가 termination (복호화)한다. 일단 복호화하면 L7 전체(Host 헤더·path·method)가 보이므로, 그 정보로 내부 서비스에 분기할 수 있다. egress가 SNI만 보는 L4 라우팅인 것과 정확히 대비되는 지점이다 — 자기가 TLS를 끝내므로 L7 전체가 보인다. 그래서 검증 포인트는 둘로 갈린다: (1) TLS termination이 동작하는가, (2) 복호화된 L7로 host/path 분기가 올바른가(매칭 안 되면 404). 적용 manifest"
    },
    {
      "title": "Test Report — Egress Gateway \"HTTPS over mTLS\" (ISTIO_MUTUAL)",
      "desc": "egress gateway에서 sidecar↔gw 구간만 Istio mTLS(ISTIO_MUTUAL)로 감싸 게이트웨이가 호출자의 SPIFFE 신원을 검증하면서, 앱이 보낸 HTTPS는 외부까지 end-to-end로 보존되는 \"이중 TLS\" 패턴을 홈랩에서 실측 검증한 리포트. 결론:…",
      "url": "/public/istio-egress/rpt__report-2026-06-08_egress-mtls.html",
      "domain": "istio-egress",
      "text": "istio egress mtls istio-mutual spiffe gateway report Test Report — Egress Gateway \"HTTPS over mTLS\" (ISTIO_MUTUAL) NOTE egress gateway에서 sidecar↔gw 구간만 Istio mTLS( ISTIO_MUTUAL )로 감싸 게이트웨이가 호출자의 SPIFFE 신원을 검증 하면서, 앱이 보낸 HTTPS는 외부까지 end-to-end로 보존 되는 \"이중 TLS\" 패턴을 홈랩에서 실측 검증한 리포트. 결론: 동작하지만(200), 처음 manifest 그대로는 깨졌고 그 실패 두 개가 이 패턴의 핵심 원리를 그대로 드러낸다 — 종단하면 SNI가 소비된다 는 한 문장에서 모든 설정 결정과 두 함정이 따라 나온다. Date: 2026-06-08 · Cluster: homelab (kubespray bare-metal, k8s v1.30.6, Calico) · Istio: 1.30.0 (istiod + egress gateway, Helm) Scenario: scenarios/20-egress/ — *-cnn-mtls 세트 · NS: mesh-test (istio-injection=enabled) / 외부: edition.cnn.com 대상독자: egress 패턴은 한 번 봤지만 ISTIO_MUTUAL과 PASSTHROUGH가 왜 다른 라우트 타입을 강요하는지 궁금한 SRE · 선행: egress gateway 개념, mesh mTLS/SPIFFE, TLS SNI 0. 배경 지식 — PASSTHROUGH는 \"누가 나가는지\"를 모른다 egress gateway의 기본형은 HTTPS PASSTHROUGH 다(이 아카이브의 control: httpbin.org). 게이트웨이가 TLS를 풀지 않고 단일 TLS 레이어의 SNI만 읽어 외부로 포워딩한다. 구현은 단순하고 앱 TLS가 end-to-end로 보존되는 장점이 있지만, 보안상 빈 칸이 하나 있다: 메시 내부 leg(sidecar→gw)가 평문 TCP (그 위에 앱 TLS만 얹힘)다. 게이트웨이 입장에서 들어온 바이트에는 호출 워크로드의 신원 이 없다. 즉 \"누가 외부로 나가는가\"를 암호학적으로 식별하지 못한다. egress를 보안 경계로 쓰려면 이게 치명적이다 — 신원 없이는 그 위에 인가(authorization)도 못 건다. 이 빈 칸을 메우는 변형이 \"HTTPS over mTLS\"다. in-mesh leg를 메시 mTLS( ISTIO_MUTUAL )로 묶어 게이트웨이가 SPIFFE 신원을 검증하게 하되, 앱은 평문 HTTP가 아니라 HTTPS를 그대로 보낸다. 결과는 두 개의 TLS 레이어가 중첩된 구조 — 바깥은 mesh mTLS(신원), 안쪽은 앱 HTTPS(기밀성). 전제 개념 셋만 잡고 가면 된다. 개념 한 줄 정의 이 리포트에서의 역할 TLS termination vs passthrough 게이트웨이가 TLS를 푸는가 (terminate) 그냥 흘리는가 (passthrough) 둘의 차이가 라우트 타입( tcp vs tls )을 강제 — 함정 2의 뿌리 SNI TLS handshake가 평문으로 싣는 목적지 호스트명 passthrough면 라우팅 키, terminate면 풀리는 순간 소비 되어 다음 leg에선 못 씀 SPIFFE / mesh mTLS sidecar가 제시하는 워크로드 신원 인증서( spiffe://…/sa/… ) egress-gw가 누가 호출했는지 검증하는 근거 → mTLS/SPIFFE 신원 비교 기준선: Ingress/Egress 검증 리포트 (PASSTHROUGH control) · 운영 주의점 Egress 운영 가이드 · 필드 매뉴얼 src-egress-gateway 1. 핵심 아키텍처 — \"종단하면 SNI가 소비된다\" 머릿속에 둘 그림 하나: egress-gw에서 바깥 TLS는 종단되고 안쪽 TLS는 통과한다 . 바깥(mesh mTLS)을 푸는 순간 그 handshake가 싣고 온 SNI는 그 자리에서 소비 된다. 그래서 그 뒤 leg-2는 더 이상 SNI로 라우팅할 수 없고, 불투명한 바이트( tcp proxy) 로만 흘려야 한다. 이 한 문장이 아래 모든 설정과 두 함정의 원천이다. 그림 1. 이중 TLS — 바깥 메시 mTLS는 egress-gw에서 종단(신원 검증)되지만, 안쪽 앱 TLS는 app부터 cnn까지 end-to-end로 보존된다. 두 레이어를 각각 보면: outer (sidecar↔egress) = Istio mTLS. sidecar가 SPIFFE client cert를 제시하고, egress-gw가 그걸 강제 (requireClientCertificate)하며 mesh CA로 검증한다. 여기서 누가 나가는지가 결정된다. inner (app↔cnn) = 앱 HTTPS. egress-gw는 바깥 mesh TLS만 풀고 안쪽 앱 TLS는 복호화하지 않은 채 tcp_proxy로 cnn까지 전달. 그래서 게이트웨이는 끝까지 평문 payload를 못 본다. 왜 PASSTHROUGH와 ISTIO_MUTUAL이 라우트 타입을 가르나 본질은 \"종단(terminate) 여부\" 한 축이다. 이게 SNI의 운명을 정하고, SNI의 운명이 leg-2의 라우트 타입을 정한다. outer TLS를 푸는가 leg-1에서 SNI는 leg-2 라우팅 키 leg-2 라우트 타입 PASSTHROUGH 안 푼다 (그냥 흘림) 끝까지 살아 있음 SNI( sniHosts ) tls ISTIO_MUTUAL 푼다 (mesh mTLS 종단·SPIFFE 검증) 푸는 순간 소비됨 더 못 씀 → 포트 매칭 tcp (tcp_proxy) PASSTHROUGH는 게이트웨이가 SNI를 끝까지 들고 갈 수 있으니 leg-2도 tls /sniHosts로 받는다. ISTIO_MUTUAL은 종단하는 순간 SNI가 사라지므로, 종단된 listener에는 SNI 기반 network filter가 하나도 안 생긴다. Envoy listener는 filter chain이 0개면 통째로 omit된다(함정 2). 그래서 leg-2는 SNI를 포기하고"
    },
    {
      "title": "KIND Lab Usage Guide",
      "desc": "Verified: 2026-03-02 환경: Ubuntu 24.04 LTS, Docker 29.2.1, KIND v0.31.0",
      "url": "/public/k8s/k8s__guide-kind-usage.html",
      "domain": "k8s",
      "text": "KIND Lab Usage Guide Verified: 2026-03-02 환경: Ubuntu 24.04 LTS, Docker 29.2.1, KIND v0.31.0 도구 요약 도구 경로 용도 kind /usr/local/bin/kind 클러스터 생성/삭제/관리 kubectl /usr/local/bin/kubectl (alias: k ) K8s 리소스 조작 helm /usr/local/bin/helm 차트 기반 앱 배포 kubectl krew ~/.krew/bin/ kubectl 플러그인 매니저 kubectl ctx krew 플러그인 컨텍스트 전환 kubectl ns krew 플러그인 네임스페이스 전환 k9s ~/.local/bin/k9s 터미널 기반 K8s 대시보드 클러스터 라이프사이클 클러스터 생성 구성 파일 위치: ~/kind-configs/ # 단일 노드 (기본) kind create cluster --config ~/kind-configs/single-node.yaml --image kindest/node:v1.34.3 # 멀티 워커 (CP1 + Worker3) kind create cluster --config ~/kind-configs/multi-worker.yaml --image kindest/node:v1.34.3 # HA 구성 (CP3 + Worker3) kind create cluster --config ~/kind-configs/ha-control-plane.yaml --image kindest/node:v1.34.3 --image 태그를 변경하면 다른 K8s 버전으로 생성할 수 있다. 사용 가능한 이미지 목록: https://github.com/kubernetes-sigs/kind/releases 클러스터 목록 확인 kind get clusters 클러스터 삭제 # 특정 클러스터 삭제 kind delete cluster --name lab # 전체 삭제 kind delete clusters --all 클러스터 재생성 (초기화) KIND는 상태 초기화 기능이 없으므로, 삭제 후 재생성한다: kind delete cluster --name lab && \\ kind create cluster --config ~/kind-configs/single-node.yaml --image kindest/node:v1.34.3 컨텍스트/네임스페이스 전환 KIND 클러스터의 컨텍스트 이름은 kind-<클러스터명> 형식이다. # 컨텍스트 목록 및 전환 kubectl ctx # 목록 표시 (fzf 설치 시 인터랙티브 선택) kubectl ctx kind-lab # kind-lab으로 전환 # 네임스페이스 전환 kubectl ns # 목록 표시 kubectl ns kube-system # kube-system으로 전환 kubectl ns - # 이전 네임스페이스로 복귀 Helm 사용 # 리포지토리 추가 helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update # 차트 검색 helm search repo nginx # 설치 helm install my-nginx bitnami/nginx -n default # 목록 확인 helm list -A # 삭제 helm uninstall my-nginx -n default k9s 사용 # 현재 컨텍스트로 실행 k9s # 특정 컨텍스트 지정 k9s --context kind-lab # 특정 네임스페이스로 시작 k9s -n kube-system k9s 핵심 단축키 키 동작 : 리소스 타입 입력 ( :pod , :svc , :deploy 등) / 필터링 (이름 검색) d describe l 로그 보기 s 셸 접속 (exec) e YAML 편집 ctrl-d 삭제 ctrl-k kill (강제 삭제) esc 뒤로 가기 ctrl-c 종료 Docker 이미지를 KIND로 로드 로컬에서 빌드한 이미지를 KIND 클러스터에서 사용하려면: # 로컬 이미지를 KIND 노드로 로드 kind load docker-image my-app:latest --name lab # 로드된 이미지 확인 docker exec lab-control-plane crictl images | grep my-app Pod에서 사용할 때 imagePullPolicy: Never 또는 IfNotPresent 로 설정해야 한다. 포트 포워딩 KIND 노드는 Docker 컨테이너이므로 외부 접근에는 포트 포워딩이 필요하다. 방법 1: kubectl port-forward (임시) kubectl port-forward svc/my-service 8080:80 # localhost:8080으로 접근 가능 방법 2: KIND 구성에서 extraPortMappings (영구) 클러스터 생성 시 구성 파일에 포트 매핑을 추가한다: kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 name: lab nodes: - role: control-plane extraPortMappings: - containerPort: 30000 hostPort: 30000 protocol: TCP networking: podSubnet: \"10.244.0.0/16\" serviceSubnet: \"10.96.0.0/12\" NodePort 서비스(30000번)를 호스트에서 직접 접근할 수 있다. 이미 생성된 클러스터에는 적용할 수 없으므로 재생성이 필요하다. 트러블슈팅 노드가 NotReady # 원인 확인 kubectl describe node lab-control-plane | grep -A5 Conditions # 시스템 Pod 상태 확인 kubectl get pods -n kube-system 보통 CNI(kindnet)나 CoreDNS가 아직 초기화 중일 때 발생하며, 1-2분 대기하면 해결된다. Docker 리소스 부족 # Docker가 사용 중인 리소스 확인 docker system df # 사용하지 않는 이미지/컨테이너 정리 docker system prune -f 클러스터 접근 불가 # Docker"
    },
    {
      "title": "virsh + cloud-init VM 생성 가이드",
      "desc": "작성일: 2026-03-05 환경: Ubuntu 24.04 호스트, libvirt/KVM, cloud-init",
      "url": "/public/k8s/k8s__guide-virsh-vm-creation.html",
      "domain": "k8s",
      "text": "virsh + cloud-init VM 생성 가이드 작성일: 2026-03-05 환경: Ubuntu 24.04 호스트, libvirt/KVM, cloud-init 사전 조건 호스트에 br-host 브릿지가 구성되어 있어야 함 (203.0.113.0/24 대역) 원본 클라우드 이미지: /var/lib/libvirt/images/noble-server-cloudimg-amd64.img cloud-init 설정 디렉토리: /var/lib/libvirt/cloud-init/ 디렉토리 구조 /var/lib/libvirt/ ├── images/ │ ├── noble-server-cloudimg-amd64.img ← 원본 (수정 금지) │ ├── k8s-cp1.qcow2 ← VM별 디스크 │ └── ... └── cloud-init/ ├── k8s-cp1-user-data.yaml ├── k8s-cp1-network-config.yaml ├── k8s-cp1-cidata.iso └── ... 생성 절차 1. 변수 설정 NAME=k8s-worker1 # VM 이름 IP=203.0.113.71 # 할당할 IP MEM=2048 # 메모리 (MB) VCPU=2 # vCPU 수 DISK_SIZE=20G # 디스크 크기 BASE_IMG=/var/lib/libvirt/images/noble-server-cloudimg-amd64.img IMG=/var/lib/libvirt/images/${NAME}.qcow2 CLOUD_INIT_DIR=/var/lib/libvirt/cloud-init 2. user-data.yaml 작성 sudo tee ${CLOUD_INIT_DIR}/${NAME}-user-data.yaml > /dev/null <<'EOF' #cloud-config hostname: k8s-worker1 manage_etc_hosts: true users: - name: kube sudo: ALL=(ALL) NOPASSWD:ALL shell: /bin/bash ssh_authorized_keys: - ssh-ed25519 AAAA...REPLACE-WITH-YOUR-PUBLIC-KEY... user@host package_update: true packages: - curl - wget - vim - net-tools - jq - tree - tcpdump - socat - conntrack - ipset EOF 주의: 첫 줄의 #cloud-config 은 반드시 포함해야 한다. 없으면 cloud-init이 동작하지 않는다. 3. network-config.yaml 작성 sudo tee ${CLOUD_INIT_DIR}/${NAME}-network-config.yaml > /dev/null <<EOF version: 2 ethernets: enp1s0: dhcp4: false addresses: - ${IP}/24 gateway4: 203.0.113.1 nameservers: addresses: - 1.1.1.1 - 8.8.8.8 EOF 주의: IP 대역은 연결할 브릿지 대역(br-host = 203.0.113.0/24)과 일치해야 한다. 4. cloud-init ISO 생성 sudo cloud-localds \"${CLOUD_INIT_DIR}/${NAME}-cidata.iso\" \\ \"${CLOUD_INIT_DIR}/${NAME}-user-data.yaml\" \\ --network-config \"${CLOUD_INIT_DIR}/${NAME}-network-config.yaml\" 5. 디스크 이미지 준비 sudo cp \"$BASE_IMG\" \"$IMG\" sudo qemu-img resize \"$IMG\" \"$DISK_SIZE\" 주의: 반드시 원본 클라우드 이미지( noble-server-cloudimg-amd64.img )에서 복사해야 한다. 이미 cloud-init이 실행된 이미지를 복사하면 새 설정이 적용되지 않는다. 6. VM 생성 sudo virt-install \\ --name \"$NAME\" \\ --memory \"$MEM\" \\ --vcpus \"$VCPU\" \\ --disk path=\"$IMG\",format=qcow2 \\ --disk path=\"${CLOUD_INIT_DIR}/${NAME}-cidata.iso\",device=cdrom \\ --os-variant ubuntu24.04 \\ --network bridge=br-host,model=virtio \\ --graphics none \\ --console pty,target_type=serial \\ --noautoconsole \\ --import 7. 접속 확인 # 30초 정도 대기 후 ping -c 3 ${IP} ssh kube@${IP} 전체 스크립트 (복사-붙여넣기용) #!/bin/bash set -e NAME=k8s-worker1 IP=203.0.113.71 MEM=2048 VCPU=2 DISK_SIZE=20G BASE_IMG=/var/lib/libvirt/images/noble-server-cloudimg-amd64.img IMG=/var/lib/libvirt/images/${NAME}.qcow2 CLOUD_INIT_DIR=/var/lib/libvirt/cloud-init # user-data sudo tee ${CLOUD_INIT_DIR}/${NAME}-user-data.yaml > /dev/null <<EOF #cloud-config hostname: ${NAME} manage_etc_hosts: true users: - name: kube sudo: ALL=(ALL) NOPASSWD:ALL shell: /bin/bash ssh_authorized_keys: - ssh-ed25519 AAAA...REPLACE-WITH-YOUR-PUBLIC-KEY... user@host package_update: true packages: - curl - wget - vim - net-tools - jq - tree - tcpdump - socat - conntrack - ipset EOF # network-config sudo tee ${CLOUD_"
    },
    {
      "title": "ArgoCD Application 네임스페이스 Watch 범위",
      "desc": "작성일: 2026-07-02 | 컴포넌트: argocd-server, argocd-application-controller, argocd-applicationset-controller",
      "url": "/public/k8s/k8s__ref-argocd-application-namespace-scope.html",
      "domain": "k8s",
      "text": "ArgoCD Application 네임스페이스 Watch 범위 작성일: 2026-07-02 | 컴포넌트: argocd-server, argocd-application-controller, argocd-applicationset-controller 1. 핵심 사실 기본 설치 상태에서 ArgoCD는 자신이 설치된 네임스페이스(보통 argocd )에 있는 Application 커스텀 리소스만 list/watch함. 여러 네임스페이스(또는 전체)의 Application을 다루려면 argocd-cmd-params-cm ConfigMap의 application.namespaces 키를 명시적으로 설정해야 함. \"Applications in any namespace\" 기능 (v2.5+에서 지원). AppProject 는 이 설정과 무관하게 항상 ArgoCD 설치 네임스페이스에만 존재 가능. AppProject에는 네임스페이스 확장 기능 자체가 없음. 2. 설정 확인 방법 kubectl -n argocd get configmap argocd-cmd-params-cm -o jsonpath='{.data.application\\.namespaces}'; echo data.application.namespaces 상태 의미 키 없음 기본값 — 설치 네임스페이스만 watch \"argocd\" (설치 네임스페이스와 동일 값) 명시적 지정, 결과는 기본값과 동일 \"*\" 전체 네임스페이스 watch \"ns1,ns2\" , \"team-*\" (와일드카드 지원) 지정된 네임스페이스(들)만 watch 주의: ConfigMap 자체의 metadata.namespace: argocd 는 이 ConfigMap 리소스가 어디 위치하는지를 나타낼 뿐 watch 범위와 무관함. 반드시 data 블록 안의 application.namespaces 키를 확인해야 함 — 헷갈리기 쉬운 포인트. 3. 확장 시 함께 필요한 설정 application.namespaces 에 네임스페이스를 추가해도, 각 AppProject 의 spec.sourceNamespaces 에 해당 네임스페이스가 명시되어 있지 않으면 그 네임스페이스의 Application이 permission 에러로 거부됨. 두 설정은 항상 세트로 관리. 4. 설정 반영 방법 application.namespaces 는 컨트롤러 기동 시 읽는 정적 파라미터라 ConfigMap 수정만으로는 반영되지 않음. 변경 후 재기동 필요: kubectl -n argocd rollout restart deployment argocd-server kubectl -n argocd rollout restart statefulset argocd-application-controller kubectl -n argocd rollout restart deployment argocd-applicationset-controller # 사용 시 5. 관련 트러블슈팅: SyncFailed / \"Too long\" Application의 sync 결과가 Too long: may not be more than 262144 bytes 로 실패하는 것은 이 네임스페이스 설정과는 무관한 별개 이슈. Kubernetes API server의 annotation 총 크기 제한(256KiB)에 걸리는 것으로, 주로 client-side apply가 kubectl.kubernetes.io/last-applied-configuration 어노테이션에 전체 manifest를 복사해 넣으면서 발생. CRD처럼 spec이 큰 리소스에서 흔함. 해결은 해당 Application/리소스에 ServerSideApply=true sync option 적용. 6. 참조 공식 문서: Applications in any namespace — Argo CD"
    },
    {
      "title": "k8s-cp1 VM 네트워크 접속 불가 트러블슈팅",
      "desc": "작성일: 2026-03-05",
      "url": "/public/k8s/k8s__runbook-cp1-vm-network-troubleshooting.html",
      "domain": "k8s",
      "text": "k8s-cp1 VM 네트워크 접속 불가 트러블슈팅 작성일: 2026-03-05 증상 virsh domifaddr k8s-cp1 결과에 IP가 표시되지 않음 VM에 SSH 접속 불가 원인 분석 원인 1: 네트워크 대역 불일치 VM은 virbr0 (libvirt 기본 NAT 네트워크, 198.51.100.0/24 )에 연결되어 있었지만, cloud-init network-config에서는 203.0.113.70/24 를 할당하고 있었다. # virt-install 시 사용된 네트워크 --network bridge=virbr0 ← 198.51.100.0/24 대역 (libvirt 기본 NAT) # cloud-init network-config에 설정된 IP addresses: 203.0.113.70/24 ← 203.0.113.0/24 대역 (호스트 LAN) 브릿지 대역과 VM IP 대역이 다르므로 통신 자체가 불가능했다. 원인 2: user-data.yaml #cloud-config 헤더 누락 # 잘못된 형식 (헤더 없음) hostname: k8s-cp1 users: - name: kube ... # 올바른 형식 #cloud-config ← 이 줄이 반드시 있어야 함 hostname: k8s-cp1 users: - name: kube ... #cloud-config 헤더가 없으면 cloud-init이 파일을 cloud-config 형식으로 인식하지 못해, 사용자 생성 및 SSH 키 주입이 수행되지 않았다. ping은 되지만 SSH 인증이 실패하는 현상의 원인이었다. 원인 3: 베이스 이미지 오염 ubuntu2404-base.qcow2 는 이전에 이미 cloud-init이 한 번 실행된 이미지였다. cloud-init은 기본적으로 첫 번째 부팅에서만 실행되므로, 이 이미지를 복사해서 VM을 만들면 새로운 cloud-init ISO를 넣어도 무시된다. 이미지 용도 noble-server-cloudimg-amd64.img 원본 클라우드 이미지 (cloud-init 미실행) ubuntu2404-base.qcow2 이전에 사용된 이미지 (cloud-init 이미 실행됨) 해결 과정 1단계: 네트워크를 virbr0 → br-host (호스트 브릿지)로 변경 호스트에 이미 br-host 브릿지가 구성되어 있었다 (eth0 → br-host, 203.0.113.2/24). VM을 br-host 에 연결하면 로컬 네트워크(203.0.113.0/24) 대역을 직접 사용할 수 있다. --network bridge=virbr0 → --network bridge=br-host 2단계: user-data.yaml에 #cloud-config 헤더 추가 sudo vi /var/lib/libvirt/cloud-init/k8s-cp1-user-data.yaml # 첫 줄에 #cloud-config 추가 3단계: cloud-init ISO 재생성 sudo cloud-localds /var/lib/libvirt/cloud-init/k8s-cp1-cidata.iso \\ /var/lib/libvirt/cloud-init/k8s-cp1-user-data.yaml \\ --network-config /var/lib/libvirt/cloud-init/k8s-cp1-network-config.yaml 4단계: 원본 이미지에서 디스크 새로 생성 후 VM 재생성 # 기존 VM 삭제 sudo virsh destroy k8s-cp1 sudo virsh undefine k8s-cp1 # 원본 클라우드 이미지에서 복사 (cloud-init 미실행 상태) sudo cp /var/lib/libvirt/images/noble-server-cloudimg-amd64.img \\ /var/lib/libvirt/images/k8s-cp1.qcow2 sudo qemu-img resize /var/lib/libvirt/images/k8s-cp1.qcow2 20G # br-host 브릿지로 VM 생성 sudo virt-install \\ --name k8s-cp1 \\ --memory 2048 \\ --vcpus 2 \\ --disk path=/var/lib/libvirt/images/k8s-cp1.qcow2,format=qcow2 \\ --disk path=/var/lib/libvirt/cloud-init/k8s-cp1-cidata.iso,device=cdrom \\ --os-variant ubuntu24.04 \\ --network bridge=br-host,model=virtio \\ --graphics none \\ --console pty,target_type=serial \\ --noautoconsole \\ --import 5단계: 접속 확인 ssh kube@203.0.113.70 # 성공 교훈 cloud-init network-config의 IP 대역은 연결할 브릿지 대역과 반드시 일치해야 한다 user-data.yaml 첫 줄에 #cloud-config 이 없으면 cloud-init이 동작하지 않는다 cloud-init은 첫 부팅에서만 실행된다 — 설정 변경 시 반드시 원본(미사용) 이미지에서 디스크를 새로 복사해야 한다"
    },
    {
      "title": "Runbook: KIND-Based Local Kubernetes Lab Setup",
      "desc": "Date: 2026-03-02 KIND: v0.31.0 Kubernetes: v1.34.3 (single-node, control-plane only) Cluster Name: lab (context: kind-lab)",
      "url": "/public/k8s/k8s__runbook-kind-lab-setup.html",
      "domain": "k8s",
      "text": "Runbook: KIND-Based Local Kubernetes Lab Setup Date: 2026-03-02 KIND: v0.31.0 Kubernetes: v1.34.3 (single-node, control-plane only) Cluster Name: lab (context: kind-lab) Verified Environment Component Detail OS Ubuntu 24.04.4 LTS, Linux 6.17.0-14-generic Docker 29.2.1 (cgroup v2, systemd) kubectl v1.35.1 KIND v0.31.0 Helm v3.20.0 krew v0.4.5 (플러그인 매니저) k9s v0.50.18 Shell zsh + oh-my-zsh + Powerlevel10k 1. KIND 설치 GitHub releases에서 바이너리를 직접 다운로드하여 설치. curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64 chmod +x ./kind sudo mv ./kind /usr/local/bin/kind 설치 경로: /usr/local/bin/kind 2. 클러스터 구성 파일 생성 ~/kind-configs/ 디렉토리에 3개의 구성 파일을 생성했다. 디렉토리 구조 ~/kind-configs/ ├── single-node.yaml # 기본 단일노드 (현재 사용 중) ├── multi-worker.yaml # CP1 + Worker3 (레퍼런스) └── ha-control-plane.yaml # CP3 + Worker3 (레퍼런스) single-node.yaml (현재 사용) kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 name: lab nodes: - role: control-plane networking: podSubnet: \"10.244.0.0/16\" serviceSubnet: \"10.96.0.0/12\" control-plane 1대가 워커 역할을 겸한다 (KIND 기본 동작). taint가 없으므로 별도 toleration 없이 워크로드 스케줄링 가능. multi-worker.yaml (레퍼런스, 미생성) kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 name: lab-multi nodes: - role: control-plane - role: worker - role: worker - role: worker networking: podSubnet: \"10.244.0.0/16\" serviceSubnet: \"10.96.0.0/12\" Gateway API, CNI 교체, Pod 분산 스케줄링 실험 등에 적합. ha-control-plane.yaml (레퍼런스, 미생성) kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 name: lab-ha nodes: - role: control-plane - role: control-plane - role: control-plane - role: worker - role: worker - role: worker networking: podSubnet: \"10.244.0.0/16\" serviceSubnet: \"10.96.0.0/12\" etcd 클러스터링, leader election, control-plane HA 스터디용. 3. 클러스터 생성 kind create cluster --config ~/kind-configs/single-node.yaml --image kindest/node:v1.34.3 생성 후 kubectl context 가 자동으로 kind-lab 으로 설정된다. 4. Helm v3.20.0 설치 공식 설치 스크립트 사용: curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | DESIRED_VERSION=v3.20.0 bash 설치 경로: /usr/local/bin/helm 5. krew 설치 ( set -x; cd \"$(mktemp -d)\" && OS=\"$(uname | tr '[:upper:]' '[:lower:]')\" && ARCH=\"$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')\" && KREW=\"krew-${OS}_${ARCH}\" && curl -fsSLO \"https://github.com/kubernetes-sigs/krew/releases/download/v0.5.0/${KREW}.tar.gz\" && tar zxvf \"${KREW}.tar.gz\" && ./\"${KREW}\" install krew ) 설치 경로: ~/.krew/bin/kubectl-krew krew 플러그인 설치 kubectl krew install ctx # kubectx — 컨텍스트 전환 kubectl krew install ns # kubens — 네임스페이스 전환 6. k9s 설치 webi.sh를 통해 설치: curl -sS https://webi.sh/k9s | sh 설치 경로: ~/.local/bin/k9s (v0.50.18) 7. Shell 설정 (~/.zshrc) 이번 작업에서 추가한 항목: # alias k에 자동완성 연결 compdef k=kubectl # KIND / Helm 자동완성 source <(kind completion zsh) source <(helm completion zsh) 기존에 이미 설정되어 있던 항목: 항목 출처 kubectl 자동완성 oh-my-zsh kubectl 플러그인 alias k=kubectl ~/.zshrc 직접 설정 alias kns=kubens , alias kctx=kubectx ~/.zshrc 직접 설정 krew PATH ( ~/.krew/bin ) ~/.zshrc PATH 설정 k9s PATH ( ~/.local/bin ) envman ( ~/.config/envman/load.sh ) 8. 검증 결과 ki"
    },
    {
      "title": "Linux CFS Scheduler 완전 정리 (Kernel 6.x 기준)",
      "desc": "작성일: 2026-02-21 커널 버전: 6.17.0-14-generic (Ubuntu 24.04.4 LTS) 목적: CFS 동작 원리, 한계, EEVDF 전환 배경까지 이해",
      "url": "/public/kernel/sched__note-cfs-eevdf-scheduler.html",
      "domain": "kernel",
      "text": "Linux CFS Scheduler 완전 정리 (Kernel 6.x 기준) 작성일: 2026-02-21 커널 버전: 6.17.0-14-generic (Ubuntu 24.04.4 LTS) 목적: CFS 동작 원리, 한계, EEVDF 전환 배경까지 이해 목차 타임라인 CFS 핵심 철학 vruntime (Virtual Runtime) Red-Black Tree 기반 스케줄링 주요 Tunable 파라미터 (6.0~6.5) 커널 소스 내부 구조 Preemption 모델 CFS의 한계 1: Sleeper Fairness 문제 CFS의 한계 2: Latency 제어 한계 EEVDF로의 전환 현재 커널(6.17) 실측값 1. 타임라인 Kernel 2.6.23 (2007) ─── CFS 도입 (Con Kolivas의 아이디어 → Ingo Molnár 구현) │ Kernel 2.6 ~ 5.x ─── CFS가 기본 스케줄러로 약 16년간 사용 │ Kernel 6.6 (2023.10) ─── EEVDF로 교체, CFS tunable 제거 │ Kernel 6.17 (현재) ─── 현재 커널, EEVDF 기반 2. CFS 핵심 철학 \"이상적인 멀티태스킹 CPU\"를 소프트웨어로 근사 이상적인 CPU가 있다면 N개 태스크에 각각 1/N 의 CPU 시간을 동시에 줄 수 있다. 현실에서는 불가능하므로, CFS는 누가 가장 적게 실행됐는지 추적해서 그 태스크를 다음에 실행한다. 3. vruntime (Virtual Runtime) 계산 공식 vruntime += delta_exec × (NICE_0_WEIGHT / task_weight) 요소 의미 delta_exec 실제로 CPU를 사용한 물리적 시간 (ns) NICE_0_WEIGHT nice 0의 가중치 = 1024 task_weight 해당 태스크의 nice에 따른 가중치 동작 원리 nice가 낮을수록 (우선순위 높음) → weight 큼 → vruntime이 느리게 증가 → 더 오래 실행됨 nice가 높을수록 (우선순위 낮음) → weight 작음 → vruntime이 빠르게 증가 → 빨리 양보 계산 예시 Task A (nice 0, weight 1024): 1ms 실행 → vruntime += 1ms Task B (nice 5, weight 335): 1ms 실행 → vruntime += 3.05ms Task C (nice -5, weight 3121): 1ms 실행 → vruntime += 0.33ms → Task C는 같은 시간을 써도 vruntime이 천천히 오르므로 CPU를 더 많이 받음 4. Red-Black Tree 기반 스케줄링 [vruntime 기준 정렬된 RB-Tree] (50ms) / \\ (30ms) (80ms) / \\ \\ (10ms) (40ms) (100ms) ↑ leftmost = 다음 실행 대상 모든 runnable 태스크를 vruntime 오름차순 으로 RB-Tree에 삽입 스케줄러는 항상 leftmost 노드 (가장 작은 vruntime)를 O(1) 로 pick 삽입/삭제는 O(log N) 5. 주요 Tunable 파라미터 (6.0~6.5) 주의: 커널 6.6 이후(EEVDF)에서는 이 파라미터들이 제거 되었다. /proc/sys/kernel/sched_latency_ns = 6000000 (6ms) /proc/sys/kernel/sched_min_granularity_ns = 750000 (0.75ms) /proc/sys/kernel/sched_wakeup_granularity_ns = 1000000 (1ms) /proc/sys/kernel/sched_nr_migrate = 32 파라미터 의미 sched_latency_ns 스케줄링 주기 . 이 시간 안에 모든 runnable 태스크가 최소 1번 실행됨 sched_min_granularity_ns 태스크당 최소 실행 시간 . 이보다 짧게 preempt하지 않음 sched_wakeup_granularity_ns wakeup preemption 임계값. 깨어난 태스크의 vruntime이 현재 태스크보다 이만큼 작아야 preempt sched_nr_migrate 로드 밸런싱 시 한 번에 옮기는 최대 태스크 수 타임슬라이스 계산 태스크 수가 적을 때 (N ≤ sched_latency / sched_min_granularity = 8): timeslice = sched_latency × (task_weight / total_weight) 태스크 수가 많을 때 (N > 8): timeslice = sched_min_granularity × (task_weight / total_weight) (실제 주기 = sched_min_granularity × N 으로 확장) 예: nice 0인 태스크 4개, sched_latency = 6ms 각 태스크 timeslice = 6ms × (1024/4096) = 1.5ms → 6ms 주기 안에 4개가 1.5ms씩 실행 예: nice 0인 태스크 12개 (> 8) 각 태스크 timeslice = 0.75ms (min_granularity) → 실제 주기 = 0.75ms × 12 = 9ms로 확장 6. 커널 소스 내부 구조 핵심 파일 kernel/sched/fair.c ← CFS 메인 로직 kernel/sched/core.c ← 스케줄러 코어 include/linux/sched.h ← task_struct 정의 주요 자료구조 struct sched_entity { struct rb_node run_node; // RB-Tree 노드 u64 vruntime; // 가상 런타임 u64 sum_exec_runtime; // 누적 실행 시간 struct load_weight load; // nice 기반 가중치 ... }; struct cfs_rq { struct rb_root_cached tasks_timeline; // RB-Tree (cached = leftmost 포인터) u64 min_vruntime; // 현재 큐의 최소 vruntime struct sched_entity *curr; // 현재 실행 중인 엔티티 unsigned int nr"
    },
    {
      "title": "HTTP Tunneling",
      "desc": "HTTP Tunneling은 HTTP 프로토콜을 통로(터널)로 사용하여, 원래 HTTP가 아닌 트래픽을 전달하는 기법이다.",
      "url": "/public/networking/tunnel__note-http-tunneling-basics.html",
      "domain": "networking",
      "text": "HTTP Tunneling HTTP Tunneling이란? HTTP Tunneling은 HTTP 프로토콜을 통로(터널)로 사용하여, 원래 HTTP가 아닌 트래픽을 전달하는 기법 이다. 많은 기업 네트워크/방화벽은 HTTP(80)와 HTTPS(443) 포트만 허용한다. HTTP tunneling을 사용하면 이 제한된 환경에서도 다른 프로토콜의 트래픽을 주고받을 수 있다. 주요 방식 1. HTTP CONNECT (가장 대표적) 클라이언트가 프록시 서버에 CONNECT 메서드를 전송 프록시가 목적지와 TCP 연결을 수립하면, 이후 양방향 바이트 스트림 이 됨 HTTPS 통신의 기본 동작 방식 Client → Proxy: CONNECT example.com:443 HTTP/1.1 Proxy → Client: HTTP/1.1 200 Connection Established (이후 TLS 핸드셰이크 및 암호화된 데이터가 그대로 통과) 2. HTTP Encapsulation 비-HTTP 데이터를 HTTP 요청/응답 body에 감싸서 전달 예: SSH-over-HTTP, DNS-over-HTTPS (DoH) 각 메시지가 일반 HTTP 요청처럼 보이므로 방화벽 통과가 용이 3. WebSocket Upgrade 초기 HTTP 핸드셰이크 후 풀-듀플렉스 TCP 연결 로 전환 실시간 양방향 통신에 사용 (채팅, 스트리밍 등) 일반 프록시 vs HTTP Tunnel [일반 HTTP 프록시] Client → Proxy → Server (프록시가 HTTP 내용을 읽고 중계) [HTTP Tunnel (CONNECT)] Client → Proxy → Server (프록시는 바이트를 그대로 전달, 내용 못 봄) ════════════════ ← 투명한 TCP 파이프 핵심 차이는 프록시가 payload를 해석하느냐 여부 이다. 터널에서는 프록시가 단순 relay 역할만 하므로 TLS 등 암호화된 트래픽도 통과시킬 수 있다. 실제 사용 사례 사례 설명 HTTPS 브라우저가 프록시 환경에서 CONNECT 로 TLS 터널 생성 Corporate proxy 우회 방화벽 뒤에서 SSH, VPN 등을 HTTP로 감싸서 사용 ngrok / Cloudflare Tunnel 로컬 서비스를 외부에 노출할 때 HTTP 터널 활용 DNS-over-HTTPS DNS 쿼리를 HTTPS 요청으로 캡슐화 실습: Squid 프록시 + CLI 도구 환경 준비 # Squid 설치 sudo apt install -y squid # 기본 포트: 3128 # 설정 파일: /etc/squid/squid.conf Step 1: HTTPS 트래픽 터널링 (curl + CONNECT) # CONNECT 터널 경유 HTTPS 요청 curl -x http://localhost:3128 https://httpbin.org/ip # -v 옵션으로 CONNECT 핸드셰이크 과정 확인 curl -v -x http://localhost:3128 https://httpbin.org/ip curl -v 출력에서 다음과 같은 흐름이 보인다: * Establish HTTP proxy tunnel to httpbin.org:443 > CONNECT httpbin.org:443 HTTP/1.1 > Host: httpbin.org:443 > < HTTP/1.1 200 Connection established < * CONNECT tunnel established, response 200 * TLS handshake starts... Squid 로그 확인: sudo tail -f /var/log/squid/access.log # TCP_TUNNEL/200 CONNECT httpbin.org:443 형태의 로그가 기록됨 Step 2: 패킷 캡처로 CONNECT 동작 관찰 # 터미널 1: loopback 트래픽 캡처 sudo tcpdump -i lo port 3128 -A # 터미널 2: curl 요청 curl -x http://localhost:3128 https://httpbin.org/ip 관찰 포인트: - CONNECT 요청/응답 헤더는 평문 으로 보임 - 이후 TLS 핸드셰이크 데이터는 암호화 되어 읽을 수 없음 - 프록시는 목적지 호스트:포트만 알 수 있고, 실제 전송 내용은 알 수 없음 Step 3: Raw CONNECT 핸드셰이크 직접 해보기 (nc) # netcat으로 Squid에 직접 접속 nc localhost 3128 수동으로 아래 내용을 입력: CONNECT httpbin.org:443 HTTP/1.1 Host: httpbin.org:443 응답: HTTP/1.1 200 Connection established 이 시점에서 터널이 열린 상태이다. 이후 입력하는 모든 바이트는 httpbin.org:443 으로 그대로 전달된다. Step 4: SSH over HTTP Tunnel Squid 설정에서 포트 22에 대한 CONNECT를 허용해야 한다: # /etc/squid/squid.conf에 추가 # acl Safe_ports port 22 # acl SSL_ports port 22 # 설정 변경 후: sudo systemctl reload squid SSH 터널링 실행: # nc를 ProxyCommand로 사용하여 SSH를 HTTP CONNECT 터널로 통과 ssh -o ProxyCommand='nc -X connect -x localhost:3128 %h %p' localhost Squid 로그에서 SSH 연결이 CONNECT로 터널링된 것을 확인할 수 있다. 정리 # 실습 후 정리 sudo apt remove --purge squid sudo apt autoremove 보안 관점 정당한 용도 : HTTPS 통신, 기업 VPN, 보안 DNS 리스크 : 방화벽/DLP를 우회할 수 있어 데이터 유출 경로가 될 수 있음 대응 : 프록시에서 CONNECT 대상 도메인/포트를 화이트리스트로 제한 핵심 정리 CONNECT 메서드 — 프록시에게 \"이 호스트:포트로 TCP 파이프를 열어달라\"는 요청 터널 vs 프록시 — 터널은 프록시가 payload를 해석하지 않음 ("
    },
    {
      "title": "TLS 1.3 over HTTP Tunneling via Squid — 무엇이 문제인가",
      "desc": "기업 환경에서 Squid 같은 Forward Proxy는 보안·감사·캐싱 목적으로 HTTPS 트래픽을 관찰하거나 검사한다. 그런데 TLS 1.3이 도입되면서, 기존에 프록시가 의존하던 여러 관찰 포인트가 사라졌다.",
      "url": "/public/networking/tunnel__note-tls13-over-squid.html",
      "domain": "networking",
      "text": "TLS 1.3 over HTTP Tunneling via Squid — 무엇이 문제인가 배경 기업 환경에서 Squid 같은 Forward Proxy는 보안·감사·캐싱 목적으로 HTTPS 트래픽을 관찰하거나 검사한다. 그런데 TLS 1.3이 도입되면서, 기존에 프록시가 의존하던 여러 관찰 포인트가 사라졌다. 이 문서는 Squid를 통한 HTTP CONNECT 터널 위에서 TLS 1.3이 동작할 때 발생하는 문제 들을 정리한다. 먼저: CONNECT 터널에서 Squid가 할 수 있는 일 Client ──CONNECT──▶ Squid ──TCP──▶ Server (평문) │ │ 터널 수립 후: Client ◀═══════════TLS═══════════▶ Server │ Squid는 바이트를 그대로 릴레이만 함 CONNECT 터널이 수립되면 Squid는 기본적으로 바이트 릴레이 만 한다. 하지만 Squid는 두 가지 방법으로 추가 가시성을 확보해왔다: 방법 설명 Peek TLS ClientHello의 SNI를 엿봐서 접속 대상 도메인 파악 SslBump (MITM) TLS를 중간에서 종단하여 복호화된 트래픽을 검사 TLS 1.3은 이 두 가지 모두에 심각한 영향을 미친다. 문제 1: Encrypted ClientHello (ECH)로 SNI 은닉 TLS 1.2에서 ClientHello (평문): - SNI: www.example.com ← Squid가 읽을 수 있음 - Supported Ciphers - ... Squid는 peek 모드로 ClientHello의 SNI 필드를 읽어 어떤 도메인에 접속하는지 판단할 수 있었다. 이를 기반으로 URL 필터링, 도메인 기반 ACL 적용이 가능했다. TLS 1.3 + ECH에서 ClientHello (outer, 평문): - SNI: cloudflare-ech.com ← 실제 대상이 아닌 fronting 도메인 - ECH extension (암호화된 inner ClientHello 포함) Inner ClientHello (암호화): - SNI: real-target.com ← Squid가 읽을 수 없음 TLS 1.3 자체만으로도 핸드셰이크 메시지 대부분이 암호화됨 ECH (Encrypted Client Hello) 가 적용되면 SNI까지 암호화 Squid의 peek 방식은 실제 접속 대상을 파악할 수 없게 됨 도메인 기반 ACL, URL 필터링이 무력화 문제 2: SslBump (MITM 검사)의 난이도 증가 Squid의 ssl_bump 은 클라이언트-서버 사이에서 TLS를 종단하여 트래픽을 복호화·검사하는 기능이다. TLS 1.2에서의 SslBump 동작 Client ←TLS→ Squid(가짜 인증서) ←TLS→ Server │ 복호화된 HTTP를 검사·로깅·필터링 Squid가 서버 인증서를 먼저 peek 동일 CN/SAN으로 가짜 인증서 생성 (자체 CA 서명) 클라이언트에게 가짜 인증서 제시 양쪽 TLS 세션을 독립적으로 유지하며 중계 TLS 1.3에서의 문제 변경사항 영향 서버 인증서 암호화 TLS 1.3에서는 Certificate 메시지가 암호화됨. Squid가 서버 인증서를 peek만으로는 볼 수 없음 → 가짜 인증서 생성 전에 full MITM이 필요 PFS 필수 RSA 키 교환 제거, (EC)DHE만 허용. 사전 공유키(pre-master secret) 기반 passive 복호화 불가능 핸드셰이크 라운드트립 감소 1-RTT 핸드셰이크로 서버 응답이 빨라져 프록시의 개입 타이밍이 촉박 0-RTT Early Data 재연결 시 핸드셰이크 완료 전에 데이터 전송 가능. 프록시가 검사하기 전에 데이터가 이미 서버에 도달 문제 3: Peek-and-Splice의 한계 Squid의 peek-and-splice 는 SslBump의 경량 버전이다: peek: ClientHello의 SNI를 엿봄 (MITM 없이) splice: 판단 후 해당 연결을 그대로 통과시킴 (터널링) bump: 판단 후 해당 연결을 MITM으로 검사 TLS 1.3에서의 문제 서버 인증서를 peek할 수 없음 - TLS 1.2: ServerHello → Certificate (평문) → Squid가 서버 인증서 확인 가능 - TLS 1.3: ServerHello → {EncryptedExtensions, Certificate, ...} (전부 암호화) → 볼 수 없음 splice 후 재개입 불가 - 한번 splice(통과)하면 해당 연결에 대해 더 이상 개입 불가 - TLS 1.3에서는 peek 단계에서 얻을 수 있는 정보가 적어 splice/bump 결정이 부정확 서버의 TLS 버전 다운그레이드 감지 - TLS 1.3은 다운그레이드 방지 메커니즘 내장 - 프록시가 중간에서 TLS 1.2로 다운그레이드하려 하면 클라이언트가 감지하고 연결 거부 문제 4: 0-RTT (Early Data) 검사 불가 [TLS 1.3 세션 재개 with 0-RTT] Client → Server: ClientHello + Early Data (HTTP GET /secret) ↑ 핸드셰이크 완료 전에 이미 데이터가 전송됨 0-RTT 데이터는 이전 세션의 PSK로 암호화됨 Squid는 이전 세션 키를 모르므로 Early Data를 복호화할 수 없음 검사 없이 서버에 도달하는 데이터 경로 가 생김 리플레이 공격 가능성도 추가 리스크 문제 5: Squid 자체의 TLS 1.3 지원 미성숙 이슈 상세 OpenSSL 의존 Squid의 TLS 처리는 OpenSSL에 의존. OpenSSL 버전에 따라 TLS 1.3 지원 수준이 다름 ssl_bump + TLS 1.3 버그 다수의 알려진 버그 존재 (핸드셰이크 실패, 연결 끊김 등) 설정 복잡도 TLS 1.3 환경에서 ssl_bump을 안정적으로 동작시키려면 세밀한 설정 필요 성능 저하 MITM 시 TLS 1.3의 (EC)DHE 연산이 RSA보다 CPU 부하가 큼 정리: TLS 1.3이 프록시 가시성에 미치는 영향 TLS 1.2 TLS 1.3 ────────── ────────── SNI 관찰 (peek) ✅ 가능 ⚠️ ECH 적용 시 불가 서버 인"
    },
    {
      "title": "virsh를 이용한 KVM/QEMU VM 관리 가이드",
      "desc": "session vs system 모드: qemu:///session은 일반 사용자 권한으로 동작하며 네트워크 기능이 제한됩니다. 브릿지 네트워크 등 고급 네트워킹이 필요하면 qemu:///system을 사용해야 합니다.",
      "url": "/public/virsh/kvm__guide-virsh.html",
      "domain": "virsh",
      "text": "virsh를 이용한 KVM/QEMU VM 관리 가이드 현재 환경 상태 항목 값 libvirt 10.0.0 QEMU 8.2.2 연결 URI qemu:///session (사용자 세션 모드) 사용자 그룹 kvm , libvirt 포함 스토리지 풀 default → ~/.local/share/libvirt/images/ OS Ubuntu 24.04.4 LTS (Kernel 6.17) CPU AMD Ryzen 9 5900X (12c/24t) RAM 40 GB session vs system 모드 : qemu:///session 은 일반 사용자 권한으로 동작하며 네트워크 기능이 제한됩니다. 브릿지 네트워크 등 고급 네트워킹이 필요하면 qemu:///system 을 사용해야 합니다. # system 모드로 연결 virsh -c qemu:///system list --all # 환경변수로 기본 URI 변경 export LIBVIRT_DEFAULT_URI=\"qemu:///system\" 1. ISO 다운로드 & 디스크 이미지 생성 # ISO 다운로드 (예: Ubuntu 24.04 Server) wget -P ~/.local/share/libvirt/images/ \\ https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso # qcow2 디스크 이미지 생성 (씬 프로비저닝) qemu-img create -f qcow2 ~/.local/share/libvirt/images/myvm.qcow2 20G # 디스크 이미지 정보 확인 qemu-img info ~/.local/share/libvirt/images/myvm.qcow2 2. VM 생성 — virt-install virsh 자체는 이미 정의된 VM을 관리하는 도구이고, 최초 생성 은 virt-install 이 편합니다. 기본 VM 생성 (GUI 콘솔) virt-install \\ --name ubuntu-lab01 \\ --ram 4096 \\ --vcpus 2 \\ --disk path=~/.local/share/libvirt/images/ubuntu-lab01.qcow2,size=20,format=qcow2 \\ --cdrom ~/.local/share/libvirt/images/ubuntu-24.04-live-server-amd64.iso \\ --os-variant ubuntu24.04 \\ --network default \\ --graphics vnc,listen=0.0.0.0 \\ --noautoconsole 헤드리스 (시리얼 콘솔만) SSH 접근용으로 가볍게 사용할 때 적합합니다. virt-install \\ --name ubuntu-lab01 \\ --ram 4096 \\ --vcpus 2 \\ --disk path=~/.local/share/libvirt/images/ubuntu-lab01.qcow2,size=20,format=qcow2 \\ --cdrom ~/.local/share/libvirt/images/ubuntu-24.04-live-server-amd64.iso \\ --os-variant ubuntu24.04 \\ --network default \\ --graphics none \\ --console pty,target_type=serial \\ --extra-args 'console=ttyS0,115200n8' system 모드 + 브릿지 네트워크 sudo virt-install \\ --connect qemu:///system \\ --name ubuntu-lab01 \\ --ram 4096 \\ --vcpus 4 \\ --cpu host-passthrough \\ --disk path=/var/lib/libvirt/images/ubuntu-lab01.qcow2,size=30,format=qcow2 \\ --cdrom /var/lib/libvirt/images/ubuntu-24.04-live-server-amd64.iso \\ --os-variant ubuntu24.04 \\ --network bridge=virbr0 \\ --graphics vnc \\ --noautoconsole os-variant 확인 osinfo-query os | grep ubuntu 3. VM 라이프사이클 관리 상태 조회 virsh list # 실행 중인 VM만 virsh list --all # 모든 VM (정지 포함) virsh dominfo ubuntu-lab01 # VM 상세 정보 virsh domstate ubuntu-lab01 # 상태만 (running/shut off 등) virsh vcpuinfo ubuntu-lab01 # vCPU 배치 정보 virsh domifaddr ubuntu-lab01 # VM IP 주소 확인 virsh dumpxml ubuntu-lab01 # 전체 XML 설정 덤프 시작 / 종료 / 재부팅 virsh start ubuntu-lab01 # 시작 virsh shutdown ubuntu-lab01 # 정상 종료 (ACPI shutdown) virsh reboot ubuntu-lab01 # 정상 재부팅 virsh destroy ubuntu-lab01 # 강제 종료 (= 전원 뽑기) virsh reset ubuntu-lab01 # 강제 리셋 virsh suspend ubuntu-lab01 # 일시정지 (메모리 유지) virsh resume ubuntu-lab01 # 일시정지 해제 자동 시작 설정 virsh autostart ubuntu-lab01 # 호스트 부팅 시 자동 시작 virsh autostart --disable ubuntu-lab01 # 자동 시작 해제 4. VM 설정 변경 CPU / 메모리 # VM 정지 상태에서 변경 (다음 부팅 시 적용) virsh setvcpus ubuntu-lab01 4 --config virsh setmaxmem ubuntu-lab01 8G --config virsh setmem ubuntu-lab01 8G --config # 라이브 변경 (핫플러그 — 게스트 지원 필요) virsh setvcpus ubuntu-lab01 4 --live virs"
    }
  ]
}