Istio Deep Dive — Why It Flows That Way
Insert a patch and tell istioctl's verdict from Envoy's
한국어 원문으로 표시합니다.
목표
EnvoyFilter 로 fault 필터를 router 앞에 끼우고, 그 패치가 Envoy 설정의 어디가 되는지 손으로 재현한다. router 뒤에 끼운 것과 값이 틀린 것을 만들어 istioctl 과 Envoy 의 판정이 갈리는 것을 보고, 판 묶기와 적용 범위를 확인한다.
왜 중요한가
EnvoyFilter 는 메시에서 가장 힘센 리소스이면서 가장 쉽게 조용히 깨지는 리소스다. istioctl 이 통과시켜도 Envoy 가 거절하면 운영에서는 '설정이 안 바뀐다' 로만 보인다. 패치가 닿는 자리와 검사의 한계를 알면 머지하기 전에 걸러 낼 수 있다.
단계
/root/ist2-ef를 만들고/root/ist2-ef/ef.yaml에 EnvoyFilter 를 쓰세요 —apiVersion: networking.istio.io/v1alpha3, 이름reviews-fault, 네임스페이스default,workloadSelector는app: reviews. 패치는 하나로,applyTo: HTTP_FILTER,match.context: SIDECAR_INBOUND,match.listener.filterChain.filter.name은envoy.filters.network.http_connection_manager, 그subFilter.name은envoy.filters.http.router,patch.operation: INSERT_BEFORE. 끼울 값은 이름envoy.filters.http.fault에typed_config(@type은type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault)로abort.http_status: 418,abort.percentage는numerator: 100·denominator: HUNDRED입니다.istioctl validate -f /root/ist2-ef/ef.yaml의 출력(표준 오류 포함)과 종료 코드를/root/ist2-ef/01-validate.txt에 담으세요(마지막 줄rc=)./root/ist2-ef/ef.yaml의 첫 패치에서 다섯 값을yq로 뽑아/root/ist2-ef/02-fields.txt에 적으세요 —applyTo=,context=(match.context),operation=(patch.operation),anchor=(match가 기준으로 삼는 HTTP 필터, 곧subFilter.name),filter=(끼워 넣는 값의name)./root/ist2-ef/envoy-before.yaml에 Envoy 설정을 쓰세요 — 관리 포트9989, 리스너virtualInbound가127.0.0.1:10089에서 듣고, HTTP 연결 관리자의stat_prefix는inbound_0.0.0.0_9080, 모든 경로를 클러스터inbound|9080||(127.0.0.1:8112)로 보냅니다.http_filters는 ef.yaml 의 패치 값과 똑같은 fault 필터 항목, 그다음 router 순서입니다. 업스트림을8112에ok로 띄우고 Envoy 를 띄운 뒤curl localhost:10089/reviews를 세 번 보내,/root/ist2-ef/03-before.txt에 세 줄을 적으세요 —codes=(세 응답 코드를 쉼표로),stat_name=(fault 가 중단시킨 요청 수를 세는 통계의 전체 이름),aborts_injected=(그 통계의 값)./root/ist2-ef/ef.yaml을/root/ist2-ef/ef-after.yaml로 복사해patch.operation만INSERT_AFTER로 바꾸세요. 그리고/root/ist2-ef/envoy-before.yaml을/root/ist2-ef/envoy-after.yaml로 복사해http_filters의 순서만 router, fault 로 뒤집으세요(그 패치가 적용된 결과). 두 파일을 각각istioctl validate -f와envoy --mode validate -c로 검사해/root/ist2-ef/04-after.txt에 적으세요 — 첫 줄istioctl_rc=, 둘째 줄envoy_rc=, 그 아래에 Envoy 가 거절한 이유가 담긴 출력 줄을 그대로./root/ist2-ef/ef.yaml을/root/ist2-ef/ef-badfield.yaml로,/root/ist2-ef/envoy-before.yaml을/root/ist2-ef/envoy-badfield.yaml로 복사한 뒤 두 파일의 faultabort.percentage를 VirtualService 에서 쓰던 모양{ value: 100 }으로 바꾸세요(나머지는 그대로).istioctl validate -f와envoy --mode validate -c로 각각 검사해/root/ist2-ef/05-gap.txt에 적으세요 — 첫 줄istioctl_rc=, 둘째 줄envoy_rc=, 그 아래에 istioctl 이 이 필드를 문제 삼은 경고 줄과 Envoy 가 거절한 이유 줄을 그대로./root/ist2-ef/ef.yaml을/root/ist2-ef/ef-pinned.yaml로 복사해 첫 패치의match.proxy.proxyVersion에 정규식^1\.24.*을 더하세요(나머지는 그대로).istioctl validate로 통과하는지 확인하고, 이 실습의 프록시 판을 알아내기 위해/root/ist2-ef에서istioctl kube-inject로/opt/lab/fixtures/istio/inject-target.yaml을 주입해/root/ist2-ef/inject.yaml로 저장하세요(주입 설정 세 파일/opt/istio/inject-config.yaml·mesh-config.yaml·values-config.yaml을 모두 넘깁니다)./root/ist2-ef/06-version.txt에 네 줄을 적으세요 —regex=(ef-pinned.yaml 에 적은 정규식 그대로),proxy_version=(istio-proxy이미지의 태그),matches_proxy=(그 판이 정규식에 맞으면yes, 아니면no),matches_1_25_0=(가상의 판1.25.0이 맞으면yes, 아니면no)./root/ist2-ef/ef-ratelimit.yaml에 EnvoyFilter 를 쓰세요 — 이름inbound-ratelimit, 네임스페이스default,workloadSelector없이, 패치 모양은 ef.yaml 과 같되(HTTP_FILTER · SIDECAR_INBOUND · router 앞 INSERT_BEFORE) 값은 이름envoy.filters.http.local_ratelimit,@typetype.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit,stat_prefix: http_local_rate_limiter,token_bucket은max_tokens: 1·tokens_per_fill: 1·fill_interval: 300s,filter_enabled와filter_enforced는 둘 다default_value가numerator: 100·denominator: HUNDRED입니다.istioctl validate로 값 경고가 없는지 확인한 뒤, 그 값을 router 앞에 둔 Envoy 설정을/root/ist2-ef/envoy-rl.yaml에 쓰세요(관리 포트·리스너·클러스터는 3단계와 같습니다). 띄우고curl localhost:10089/reviews를 세 번 보낸 뒤/root/ist2-ef/07-ratelimit.txt에 네 줄을 적으세요 —codes=(세 응답 코드를 쉼표로),rate_limited=(통계http_local_rate_limit.rate_limited의 값),scope=(이 EnvoyFilter 가 닿는 범위:workload·namespace·mesh중 하나),mesh_wide_namespace=(같은 파일을 옮겨 두면 메시 전체에 닿는 네임스페이스,/opt/istio/mesh-config.yaml에서)./root/ist2-ef/08-report.md에before_status=,after_envoy_rc=,badfield_istioctl_rc=,ratelimit_codes=네 줄을 적고(각각 3단계에서 fault 가 돌려준 상태 코드, 4단계에서 Envoy 의 종료 코드, 5단계에서 istioctl 의 종료 코드, 7단계의 세 응답 코드), 그 아래-로 시작하는 설명을 네 줄 이상 적으세요.
참고
- 이 파드에는 진짜 istiod 도 진짜 사이드카도 없습니다. 그래서
istioctl proxy-config로 실제 생성물을 볼 수 없고, 번역 규칙을 알고 손으로 등가 Envoy 설정을 만들어 동작을 확인합니다. 같은 규칙이 운영 클러스터의proxy-config출력에 그대로 보입니다. istioctl validate는 올바른 EnvoyFilter 에도 'exposes internal implementation details' 경고를 냅니다. 이 경고는 정상이며, 값 안의 필드를 문제 삼는 경고가 따로 나오는지를 보세요. 경고는 표준 오류로도 나오니 파일에 담을 때2>&1을 붙입니다.- Envoy 의 오류 문구는 단어 사이 공백이 일부러 흔들립니다(
no such field). 눈으로 읽을 때 놀라지 마세요. - 단계마다 설정 파일 이름이 다릅니다. 앞 단계의 파일은 복사해서 쓰고 고치지 마세요 — 앞 단계 채점이 그 파일을 봅니다.
- Envoy 를 띄울 때는
setsid --fork nohup envoy -c <파일> --log-level warn > <로그> 2>&1 </dev/null로 셸에서 완전히 떼어 놓으세요. 다시 띄우기 전에는pkill -x envoy로 정리합니다 (pkill -f 'envoy -c'는 그 문자열이 든 셸 자신까지 죽입니다). - 업스트림 흉내용 서버가 이미지에 있습니다:
python3 /opt/lab/envoy/upstream.py <포트> ok|fail|slow. 응답 본문은<모드>:<포트> <경로>입니다. - 설정을 고친 뒤에는 띄우기 전에
envoy --mode validate -c <파일>로 먼저 거르세요. 클러스터 이름에|가 들어가므로 YAML 에서는 반드시 따옴표로 감쌉니다.
router 앞에 fault 를 끼우는 EnvoyFilter 를 쓴다
/root/ist2-ef 를 만들고 /root/ist2-ef/ef.yaml 에 EnvoyFilter 를 쓰세요 — apiVersion: networking.istio.io/v1alpha3, 이름 reviews-fault, 네임스페이스 default, workloadSelector 는 app: reviews. 패치는 하나로, applyTo: HTTP_FILTER, match.context: SIDECAR_INBOUND, match.listener.filterChain.filter.name 은 envoy.filters.network.http_connection_manager, 그 subFilter.name 은 envoy.filters.http.router, patch.operation: INSERT_BEFORE. 끼울 값은 이름 envoy.filters.http.fault 에 typed_config(@type 은 type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault)로 abort.http_status: 418, abort.percentage 는 numerator: 100·denominator: HUNDRED 입니다. istioctl validate -f /root/ist2-ef/ef.yaml 의 출력(표준 오류 포함)과 종료 코드를 /root/ist2-ef/01-validate.txt 에 담으세요(마지막 줄 rc=).
EnvoyFilter 의 패치 하나는 '어디에'(applyTo·match)와 '무엇을'(operation·value)의 짝입니다. HTTP_FILTER 에 subFilter 로 기준 필터를 고르면, 그 필터가 든 HTTP 연결 관리자의 필터 목록이 무대가 됩니다. value 는 Istio 문법이 아니라 Envoy 설정 그대로라서, 퍼센트도 Envoy 의 FractionalPercent (분자·분모)로 적어야 합니다. 올바른 파일이어도 istioctl 은 EnvoyFilter 자체에 대한 경고 한 줄을 냅니다 — 그 경고는 정상이고, 값 안의 필드를 문제 삼는 경고가 더 나오면 고쳐야 합니다.
패치의 '어디에' 와 '무엇을' 을 네 칸으로 읽는다
/root/ist2-ef/ef.yaml 의 첫 패치에서 다섯 값을 yq 로 뽑아 /root/ist2-ef/02-fields.txt 에 적으세요 — applyTo=, context=(match.context), operation=(patch.operation), anchor=(match 가 기준으로 삼는 HTTP 필터, 곧 subFilter.name), filter=(끼워 넣는 값의 name).
applyTo 는 패치가 닿는 Envoy 객체의 종류(리스너·필터 체인·네트워크 필터·HTTP 필터·클러스터·라우트…), context 는 어느 방향의 프록시인가(사이드카의 들어오는 쪽·나가는 쪽, 게이트웨이), match 는 그 종류 안에서 어느 하나를 고르는 조건입니다. INSERT_BEFORE 같은 상대 위치 연산은 이 기준 필터가 있어야 뜻이 있습니다. filter.name 과 subFilter.name 을 헷갈리지 마세요 — 앞의 것은 네트워크 필터, 뒤의 것이 HTTP 필터입니다.
패치가 적용된 모습을 Envoy 로 세워 418 을 받는다
/root/ist2-ef/envoy-before.yaml 에 Envoy 설정을 쓰세요 — 관리 포트 9989, 리스너 virtualInbound 가 127.0.0.1:10089 에서 듣고, HTTP 연결 관리자의 stat_prefix 는 inbound_0.0.0.0_9080, 모든 경로를 클러스터 inbound|9080||(127.0.0.1:8112)로 보냅니다. http_filters 는 ef.yaml 의 패치 값과 똑같은 fault 필터 항목, 그다음 router 순서입니다. 업스트림을 8112 에 ok 로 띄우고 Envoy 를 띄운 뒤 curl localhost:10089/reviews 를 세 번 보내, /root/ist2-ef/03-before.txt 에 세 줄을 적으세요 — codes=(세 응답 코드를 쉼표로), stat_name=(fault 가 중단시킨 요청 수를 세는 통계의 전체 이름), aborts_injected=(그 통계의 값).
istiod 가 EnvoyFilter 를 적용하면 이 사이드카의 들어오는 쪽 리스너에서 HTTP 필터 목록이 [..., fault, router] 가 됩니다. 이 파드에는 istiod 가 없으니 그 결과를 손으로 만드는 것입니다. 패치의 value 가 그대로 목록의 항목 하나가 된다는 점이 핵심입니다 — 옮기면서 값을 바꾸지 마세요. HTTP 필터의 통계는 http.<stat_prefix>. 아래에 쌓입니다. 관리 포트의 /stats?filter= 로 좁혀 보세요.
router 뒤에 끼우면 istioctl 은 통과, Envoy 는 거절
/root/ist2-ef/ef.yaml 을 /root/ist2-ef/ef-after.yaml 로 복사해 patch.operation 만 INSERT_AFTER 로 바꾸세요. 그리고 /root/ist2-ef/envoy-before.yaml 을 /root/ist2-ef/envoy-after.yaml 로 복사해 http_filters 의 순서만 router, fault 로 뒤집으세요(그 패치가 적용된 결과). 두 파일을 각각 istioctl validate -f 와 envoy --mode validate -c 로 검사해 /root/ist2-ef/04-after.txt 에 적으세요 — 첫 줄 istioctl_rc=, 둘째 줄 envoy_rc=, 그 아래에 Envoy 가 거절한 이유가 담긴 출력 줄을 그대로.
router 는 요청을 업스트림으로 보내고 끝내는 필터라서 Envoy 는 그 뒤에 필터가 오는 것을 설정 단계에서 막습니다. istioctl 은 EnvoyFilter 의 겉 구조만 알고 Envoy 의 필터 순서 규칙은 모르므로 이 패치를 통과시킵니다. 운영에서는 istiod 가 그대로 밀어 넣고 Envoy 가 그 리스너 갱신을 거절(NACK)합니다 — 파드는 멀쩡해 보이는데 설정이 안 바뀌는 모습입니다. 종료 코드는 명령 바로 뒤의 $? 로 받으세요.
VirtualService 식 퍼센트를 옮겨 적으면 경고만 나온다
/root/ist2-ef/ef.yaml 을 /root/ist2-ef/ef-badfield.yaml 로, /root/ist2-ef/envoy-before.yaml 을 /root/ist2-ef/envoy-badfield.yaml 로 복사한 뒤 두 파일의 fault abort.percentage 를 VirtualService 에서 쓰던 모양 { value: 100 } 으로 바꾸세요(나머지는 그대로). istioctl validate -f 와 envoy --mode validate -c 로 각각 검사해 /root/ist2-ef/05-gap.txt 에 적으세요 — 첫 줄 istioctl_rc=, 둘째 줄 envoy_rc=, 그 아래에 istioctl 이 이 필드를 문제 삼은 경고 줄과 Envoy 가 거절한 이유 줄을 그대로.
VirtualService 의 fault.abort.percentage.value 는 Istio 의 퍼센트 타입이고, Envoy 의 fault 필터는 분자·분모로 된 FractionalPercent 를 씁니다. 사람이 흔히 옮겨 적는 실수입니다. istioctl 은 값을 Envoy 타입으로 풀어 보기는 하지만 모르는 필드를 경고로만 알리고 종료 코드는 0 입니다 — CI 가 종료 코드만 본다면 그대로 배포됩니다. 반면 @type 이름 자체가 틀리면 istioctl 도 오류로 막습니다. 어느 쪽을 믿을지 기준이 여기서 갈립니다. yq 로 한 필드만 바꾸면 나머지를 건드리지 않습니다.
proxyVersion 으로 패치를 한 판에 묶는다
/root/ist2-ef/ef.yaml 을 /root/ist2-ef/ef-pinned.yaml 로 복사해 첫 패치의 match.proxy.proxyVersion 에 정규식 ^1\.24.* 을 더하세요(나머지는 그대로). istioctl validate 로 통과하는지 확인하고, 이 실습의 프록시 판을 알아내기 위해 /root/ist2-ef 에서 istioctl kube-inject 로 /opt/lab/fixtures/istio/inject-target.yaml 을 주입해 /root/ist2-ef/inject.yaml 로 저장하세요(주입 설정 세 파일 /opt/istio/inject-config.yaml·mesh-config.yaml·values-config.yaml 을 모두 넘깁니다). /root/ist2-ef/06-version.txt 에 네 줄을 적으세요 — regex=(ef-pinned.yaml 에 적은 정규식 그대로), proxy_version=(istio-proxy 이미지의 태그), matches_proxy=(그 판이 정규식에 맞으면 yes, 아니면 no), matches_1_25_0=(가상의 판 1.25.0 이 맞으면 yes, 아니면 no).
istiod 는 프록시가 접속할 때 알려 온 자기 판(ISTIO_VERSION 메타데이터)을 이 정규식에 대 보고, 맞을 때만 패치를 붙입니다. 업그레이드로 Envoy 의 필터 이름이나 설정 모양이 바뀌면 옛 패치가 새 프록시를 깨뜨릴 수 있는데, 판을 묶어 두면 새 프록시에는 패치가 아예 붙지 않습니다. 점은 정규식에서 아무 글자이므로 \. 로 막고, 앞을 ^ 로 고정하세요. 판별은 grep -E 로 해 볼 수 있습니다. 주입 산출물 끝의 빈 문서 때문에 yq 는 select(.kind=="Deployment") 로 거르세요.
선택자 없는 EnvoyFilter 로 네임스페이스 전체에 요청 제한을 건다
/root/ist2-ef/ef-ratelimit.yaml 에 EnvoyFilter 를 쓰세요 — 이름 inbound-ratelimit, 네임스페이스 default, workloadSelector 없이, 패치 모양은 ef.yaml 과 같되(HTTP_FILTER · SIDECAR_INBOUND · router 앞 INSERT_BEFORE) 값은 이름 envoy.filters.http.local_ratelimit, @type type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit, stat_prefix: http_local_rate_limiter, token_bucket 은 max_tokens: 1·tokens_per_fill: 1·fill_interval: 300s, filter_enabled 와 filter_enforced 는 둘 다 default_value 가 numerator: 100·denominator: HUNDRED 입니다. istioctl validate 로 값 경고가 없는지 확인한 뒤, 그 값을 router 앞에 둔 Envoy 설정을 /root/ist2-ef/envoy-rl.yaml 에 쓰세요(관리 포트·리스너·클러스터는 3단계와 같습니다). 띄우고 curl localhost:10089/reviews 를 세 번 보낸 뒤 /root/ist2-ef/07-ratelimit.txt 에 네 줄을 적으세요 — codes=(세 응답 코드를 쉼표로), rate_limited=(통계 http_local_rate_limit.rate_limited 의 값), scope=(이 EnvoyFilter 가 닿는 범위: workload·namespace·mesh 중 하나), mesh_wide_namespace=(같은 파일을 옮겨 두면 메시 전체에 닿는 네임스페이스, /opt/istio/mesh-config.yaml 에서).
workloadSelector 가 있으면 라벨이 맞는 워크로드에만, 없으면 그 네임스페이스의 모든 워크로드에, 그리고 메시 설정의 루트 네임스페이스에 두면 메시 전체에 붙습니다. 루트 네임스페이스의 것이 먼저, 워크로드 네임스페이스의 것이 나중에 적용됩니다. 요청 제한 값 안에 runtime_key 는 넣지 마세요 — 이 판의 Envoy 가 거절합니다. 토큰이 한 개이고 채워지는 간격이 길면 첫 요청만 지나갑니다. 제한된 응답의 코드가 무엇인지 직접 보세요.
EnvoyFilter 를 쓰기 전의 점검표로 정리한다
/root/ist2-ef/08-report.md 에 before_status=, after_envoy_rc=, badfield_istioctl_rc=, ratelimit_codes= 네 줄을 적고(각각 3단계에서 fault 가 돌려준 상태 코드, 4단계에서 Envoy 의 종료 코드, 5단계에서 istioctl 의 종료 코드, 7단계의 세 응답 코드), 그 아래 - 로 시작하는 설명을 네 줄 이상 적으세요.
값은 앞 단계 파일에서 옮기세요. 설명 줄에는 'EnvoyFilter 를 머지하기 전에 무엇을 확인하는가' 를 적으면 이 표가 리뷰 체크리스트가 됩니다 — 패치가 닿는 자리, router 의 위치, istioctl 이 못 잡는 것, 판 묶기, 범위.