注入了代理,应用为何不必改 orders 的地址

第 03 篇把 orders 的库存地址显式改成了 Envoy listener。sidecar 模式希望应用继续访问原有 Service 地址,由同 Pod 的代理接管特定出入站流量。这里的“接管”不是 Istiod 替应用转发请求,而是 Pod 生命周期中添加代理并建立网络重定向规则;应用、代理和真正的目标 Pod 都需要从实际运行状态里核对。

sidecar 出站与入站流量分别从 Pod 网络命名空间中的重定向规则进入代理

图的左半段是 orders 的出站请求,右半段是 inventory 的入站请求。两个代理均在各自 Pod 附近工作;控制面向代理提供配置但通常不在业务连接中间。读者需先理解 Pod 多容器共享网络命名空间、Service 地址与 Envoy listener/cluster 的区别。

从 Pod 创建到一条连接被代理处理

Pod 被创建时,已配置且适用的注入机制可能修改新 Pod 的定义,加入代理容器以及代理启动所需配置。只给现存 namespace 打标签不会凭空改写已经运行的 Pod;必须检查新 Pod的容器列表和注入状态。控制面安装、webhook 匹配、Pod/namespace 标签、revision 及显式不注入设置都会影响结果。不能用“部署了 istiod”代替注入证据。

在典型的透明接管方式中,Pod 网络命名空间内的重定向规则把应用发起的出站连接送到代理的出站 listener,把面向应用端口的入站连接送到代理的入站 listener。规则可以由初始化容器或 Istio CNI 负责配置;具体组件、端口和链名依安装版本/模式而定。之后出站 Envoy 按其路由/cluster 配置寻找后端,后端 Pod 的入站 Envoy 再按规则把请求交给 inventory。网络可达与网格能识别为 HTTP不是同一命题:未识别协议时的治理能力须另行核验。

这条路径有两个容易误判的地方。第一,应用依旧向 inventory 的 Service 地址发送请求,不代表连接一定绕过代理:捕获发生在应用的 socket 与目标网络之间。第二,看见两个容器或一条重定向规则也不等于测试流量实际经过了 Envoy;必须用请求 ID 同时关联应用、代理访问日志与后端响应。istioctl proxy-config 可检查 listener/cluster 等当前配置,但静态快照不能代替这条请求的日志。

排除规则不是功能验证

Istio 的流量排除注解可以按端口或地址改变捕获范围;其名称和具体支持范围要与部署版本的注解参考核对。设合成 external-stub 在某端口监听:对该端口应用出站排除后,相同输入可能仍拿到相同业务响应。但这只能说明服务还可达,不能独自证明绕过。应比较改变前后的 Pod 捕获规则、代理访问日志/连接计数、进程 socket 的目标,以及 external-stub 的请求 ID;并排除其他调用也打到该端口的混淆。排除传输捕获不等于获得业务授权,也不自动建立端到端 TLS。

注入失败、代理尚未就绪或应用先发连接时,具体失败表现取决于启动策略和版本,不能把某次 503 全部归因为重定向。主线下一篇专门拆解应用与代理就绪及终止顺序。

在相同应用地址下验证代理参与

本实验固定 Istio 1.27.0,使用专用 kind sidecar 集群;从 examples/service-mesh/ 执行:

1
2
SM_RUNTIME=podman bash scripts/setup-kind.sh sidecar
SM_CONTEXT=kind-service-mesh-sidecar bash scripts/cluster-case.sh 04

生成器仍给 frontend 配置 http://orders:8080,给 orders 配置 http://inventory:8080。注入改变的是 Pod 内连接处理路径,不要求把业务 URL 改成代理监听端口。脚本检查每个 Pod 有 istio-proxy,再发送 sm-04-injected,从 orders 代理访问日志查同一 ID。数据路径应结合实际代理配置读取,不能仅由容器列表推导所有流量均被截获。

负对照使用第 02 篇独立 bare 集群相同生成器:相同业务请求可以成功,但不会有 istio-proxy 容器或该代理日志。两个集群分别执行,避免在同一 namespace 内混用旧 Pod 与注入后新 Pod。若 sidecar 组缺代理,先查 namespace 注入标签、Pod 创建时刻和 admission 事件,再重建实验 Deployment;只补标签不会改造已经创建的 Pod。

1
2
3
kubectl --context kind-service-mesh-sidecar -n service-mesh-lab rollout restart deployment
kubectl --context kind-service-mesh-sidecar -n service-mesh-lab rollout status deployment/orders --timeout=90s
SM_CONTEXT=kind-service-mesh-sidecar bash scripts/cluster-case.sh 04

只排除 external-stub 的 8081 出站连接

累计工程的库存和 external-stub 原本共用 8080;直接排除 8080 会同时影响两个依赖。这个对照先把 external-stub 的监听与 Service 端口改为 8081,保持 inventory 为 8080,再只排除 orders 的出站 8081。注解作用于新建 Pod,需写在 Deployment 的 Pod 模板上并等待滚动完成。Istio 注解参考将该字段定义为出站端口排除列表。

仅在 setup-kind 创建的独占 sidecar 实验集群运行,不与其他章节同时修改工作负载。本段要求默认 PERMISSIVE,且没有其他认证、授权或 DestinationRule TLS 策略;下面故意拒绝存在这些对象的集群。STRICT 下,绕过 orders 代理后的明文连接可能被目标代理拒绝,此时业务失败不是“排除没有生效”。排除只改变源端出站路径,external-stub 的入站代理仍可能参与。

以下命令在 examples/service-mesh/ 下的同一个 Bash 会话执行,每条命令占一行。先验证 context 和 namespace 所有权,备份三个对象;若 orders 模板已有 excludeOutboundPorts,即使值为空也拒绝覆盖。证据目录使用独立时间戳。

1
2
3
4
5
6
7
8
9
10
11
12
13
set -euo pipefail
context=kind-service-mesh-sidecar
k=(kubectl --context "$context" -n service-mesh-lab)
test "$(kubectl config current-context)" = "$context"
test "$("${k[@]}" get ns service-mesh-lab -o 'jsonpath={.metadata.labels.service-mesh\.example/lab}')" = true
out="evidence/04/exclude-8081-$(date -u +%Y%m%dT%H%M%SZ)-$$"
mkdir -p "$out"
printf '%s\n' "$out"
"${k[@]}" get deployment external-stub -o json > "$out/external-deployment.before.json"
"${k[@]}" get service external-stub -o json > "$out/external-service.before.json"
"${k[@]}" get deployment orders -o json > "$out/orders.before.json"
kubectl --context "$context" get peerauthentications.security.istio.io,authorizationpolicies.security.istio.io,destinationrules.networking.istio.io --all-namespaces -o json > "$out/security.before.json"
node -e 'const fs=require("fs"),a=require("assert"),d=process.argv[1],read=n=>JSON.parse(fs.readFileSync(d+"/"+n));a.equal(read("security.before.json").items.length,0,"requires an isolated default-PERMISSIVE lab without extra security/TLS policies");const o=read("orders.before.json"),e=read("external-deployment.before.json"),s=read("external-service.before.json");a(!Object.hasOwn(o.spec.template.metadata.annotations||{},"traffic.sidecar.istio.io/excludeOutboundPorts"),"refuse existing exclusion");a.equal(o.spec.template.spec.containers[0].name,"app");a.equal(e.spec.template.spec.containers[0].name,"app");a.deepEqual(e.spec.template.spec.containers[0].args.slice(0,4),["-listen","0.0.0.0:8080","-role","external-stub"]);a.deepEqual(o.spec.template.spec.containers[0].args.slice(4),["-inventory-url","http://inventory:8080","-external-url","http://external-stub:8080"]);a.equal(s.spec.ports[0].port,8080);for(const n of ["external-deployment","external-service","orders"]){const b=read(n+".before.json");fs.writeFileSync(d+"/"+n+".restore.json",JSON.stringify([{op:"test",path:"/metadata/uid",value:b.metadata.uid},{op:"replace",path:"/spec",value:b.spec}]))}' "$out"

预先设置退出恢复:先移除本段新增的模板注解,再按原始 spec 恢复这三个对象,保留其他对象及所有原始日志。UID 测试防止把备份写到后来同名重建的对象。恢复失败必须查看 restore.log,不能把它算成实验通过。

1
2
3
4
5
6
7
8
9
10
11
restore_04() { local status=$?; trap - EXIT; set +e; "${k[@]}" patch deployment orders --type=merge -p '{"spec":{"template":{"metadata":{"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":null}}}}}' >> "$out/restore.log" 2>&1 || status=1; "${k[@]}" patch deployment external-stub --type=json --patch-file "$out/external-deployment.restore.json" >> "$out/restore.log" 2>&1 || status=1; "${k[@]}" patch service external-stub --type=json --patch-file "$out/external-service.restore.json" >> "$out/restore.log" 2>&1 || status=1; "${k[@]}" patch deployment orders --type=json --patch-file "$out/orders.restore.json" >> "$out/restore.log" 2>&1 || status=1; "${k[@]}" rollout status deployment/external-stub --timeout=90s >> "$out/restore.log" 2>&1 || status=1; "${k[@]}" rollout status deployment/orders --timeout=90s >> "$out/restore.log" 2>&1 || status=1; printf '%s\n' "$status" > "$out/run.exit"; return "$status"; }
trap restore_04 EXIT
"${k[@]}" patch deployment external-stub --type=json -p '[{"op":"replace","path":"/spec/template/spec/containers/0/args/1","value":"0.0.0.0:8081"},{"op":"replace","path":"/spec/template/spec/containers/0/ports/0/containerPort","value":8081}]' > "$out/move-listener.log" 2>&1
"${k[@]}" patch service external-stub --type=json -p '[{"op":"replace","path":"/spec/ports/0/port","value":8081}]' > "$out/move-service.log" 2>&1
"${k[@]}" patch deployment orders --type=json -p '[{"op":"replace","path":"/spec/template/spec/containers/0/args/7","value":"http://external-stub:8081"}]' > "$out/move-client.log" 2>&1
"${k[@]}" rollout status deployment/external-stub --timeout=90s > "$out/external-ready.log" 2>&1
"${k[@]}" rollout status deployment/orders --timeout=90s > "$out/orders-ready.log" 2>&1
"${k[@]}" exec deploy/frontend -c app -- /mesh-demo -role loadgen -frontend-url http://frontend:8080 -request-id sm-04-before > "$out/before.json" 2> "$out/before.stderr"
sleep 2
"${k[@]}" logs deploy/orders -c istio-proxy > "$out/orders-proxy.before.log"
grep -F sm-04-before "$out/orders-proxy.before.log" | grep -F 'outbound|8081||external-stub.service-mesh-lab.svc.cluster.local' > "$out/before-outbound-8081.log"

先确认改端口后的正常响应含 external-stub,再修改模板并产生新 Pod。代理日志可能延后刷新,下面保留两秒采集窗口;若日志尚未出现,应保留失败记录并补采,不能跳过正对照。第二组请求仍必须成功,且 orders 的代理有该请求的 inventory 8080 出站记录、没有 external-stub 8081 出站记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"${k[@]}" patch deployment orders --type=merge -p '{"spec":{"template":{"metadata":{"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8081"}}}}}' > "$out/exclude.log" 2>&1
"${k[@]}" rollout status deployment/orders --timeout=90s > "$out/excluded-ready.log" 2>&1
"${k[@]}" get pods -l app=orders -o json > "$out/orders-pods.after.json"
"${k[@]}" exec deploy/frontend -c app -- /mesh-demo -role loadgen -frontend-url http://frontend:8080 -request-id sm-04-after > "$out/after.json" 2> "$out/after.stderr"
sleep 2
"${k[@]}" logs deploy/orders -c istio-proxy > "$out/orders-proxy.after.log"
"${k[@]}" logs deploy/external-stub -c app > "$out/external-app.after.log"
"${k[@]}" logs deploy/external-stub -c istio-proxy > "$out/external-proxy.after.log"
grep -F sm-04-after "$out/orders-proxy.after.log" | grep -F 'outbound|8080||inventory.service-mesh-lab.svc.cluster.local' > "$out/after-outbound-8080.log"
if grep -F sm-04-after "$out/orders-proxy.after.log" | grep -F 'outbound|8081||external-stub.service-mesh-lab.svc.cluster.local' > "$out/after-unexpected-outbound-8081.log"; then printf 'FAIL: excluded port still appears in orders outbound log\n' >&2; exit 1; fi
grep -F sm-04-after "$out/external-app.after.log" > "$out/after-external-handler.log"
node -e 'const fs=require("fs"),a=require("assert"),d=process.argv[1];for(const stage of ["before","after"]){const r=JSON.parse(fs.readFileSync(d+"/"+stage+".json"));a.equal(r.request_id,"sm-04-"+stage);a.equal(r.external,"external-stub");a(["v1","v2"].includes(r.inventory_version))}console.log("PASS response and dependency identity checks; inspect positive and negative proxy records together")' "$out" > "$out/assertions.txt"
restore_04
if "${k[@]}" exec deploy/frontend -c app -- /mesh-demo -role loadgen -frontend-url http://frontend:8080 -request-id sm-04-restored > "$out/restored.json" 2> "$out/restored.stderr"; then printf '0\n' > "$out/run.exit"; else printf '%s\n' "$?" > "$out/run.exit"; exit 1; fi

日志缺席本身不能证明绕过;这里同时要求排除前同一采集方式看到 8081、排除后看到 inventory 的 8080、external-stub handler 确认处理请求,以及 Pod 模板注解已生效。若访问日志格式或采样被改变,应先恢复观测条件。最后的 restored.json 验证回到备份配置后业务仍成功;它不能把一次失败的排除对照改判为成功。本段完整集群执行仍为 NOT_RUN,不预填代理日志或 HTTP 状态。

[PATTERN] 透明代理依赖环境建立连接拦截;装饰调用路径不等于改写业务语义。

原则 具体约束
生命周期影响配置生效 注入设置在新 Pod 创建时处理
最小范围变更 共享 8080 的排除影响多个依赖
正负路径对照 bare 与 sidecar 用相同业务输入

练习与参考解答

  1. 对现有 namespace 打开自动注入后,原 Pod 仍只有应用容器,另一新 Pod 有代理。哪个更可能被透明捕获?参考解答:首先按 Pod 实际容器、状态和捕获规则确认;注入通常影响创建阶段,原 Pod 不会因为标签变化被原地加入容器。仍需对新 Pod 发带标识请求并比对代理与后端记录,不能仅凭容器数宣布流量已接管。
  2. 给外部依赖端口加排除配置后,业务仍返回 200。如何构造反例说明“200 不是绕过证据”?参考解答:让同一合成请求分别走被捕获端口与排除端口,记录各自的请求 ID、代理访问日志/连接统计及目标进程日志,并检查捕获规则;若排除前后业务均为 200,却只有前者出现在对应代理处理记录中,则说明仅看响应无法判断路径。必须防止日志采样或端口复用造成误读。

官方资料与系列导航

系列导航:00 一次服务调用需要哪些基础条件 → 01 SDK、网关与服务网格分别处理什么 → 02 Service 地址怎样到达 Pod → 03 Envoy 怎样转发请求 → 04 Sidecar 怎样接管流量(本文)→ 05 应用与代理如何启动和退出。