Telepresence 是一个开源工具,可让您在本地运行单个服务,同时将该服务连接到远程 Kubernetes 集群

https://www.telepresence.io/about/

为什么需要 Telepresence

k8s pod IP 由 CNI 分配, 通信是走 overlay 网络, 容器之间的通信都是基于cluser IP.

cluser IP 并不像我们平常所用的 IP. 它只能在 k8s 集群内部使用.

虽然我们可以通过配置 overlay 网络的网段 跟 k8s node 的网段在一个大的子网段, 然后通过 vpn 把对应网段的流量路由到overlay 网络, 我们完全可以通过 kubectl get po -o wide 获取到 pod IP, 然后访问服务.

也就是说, 如果我们在本机想运行一个服务, 不依赖 Telepresence 这种工具是完全可行的.

但是, 其实我们每个服务都有配置, 配置里面写的grpc 服务地址可能像这样: xxxx.svc.cluster.local:1234, 如果我们想要服务的配置不经过任何修改, 直接在本机运行.

有没有办法呢? 答案当然是有的, 设置 hosts 啊. kubectl get po -o wide 获取到 pod IP 和 pod name, (假设 pod name 和 service name 命名是一致的,即从 pod name 我们可以字面上得到 service name) 然后拼接成k8s里面的 DNS name xxxx.svc.cluster.local, 将这个域名映射到 pod IP 即可. 假设我们写了这样一个脚本, 但是当 pod 被调试到不同的node, 或者 pod 重建之后, 其 pod IP 必然会改变, 这个时候我们又要手动去重新生成这个 hosts 文件. 总体操作来说, 还是挺麻烦的.

另一个问题是, 团队内部有很多人, 要让所有人都都学会这一招操作, 可能会有些困难, 或者说, 这种方式, 对用户不太好友.

这个时候, Telepresence 横空出世.

在 k8s 官方文档中, “ 本地开发和调试服务” 一节, Telepresence 是唯一介绍的工具.

对于用户来说, Telepresence 提供了3个非常重要的功能:

  1. cluster 域名解析
  2. cluster 流量代理
  3. cluster 流量拦截

域名解析 可以使我们在本地开发机器直接解析如 xxxx.svc.cluster.local 这种域名.

(注: mac 下面机制不太一样, 不可以使用dig测试,但是可以用curl)

你可以像查询其它域名一样使用命令行 dig 工具进行查询, 如 dig kubernetes.default

光有域名解析还不够, 我们还是没法连接集群内的其它服务, 因此 流量代理 功能的作用就体验出来了.

在 Linux 下面, Telepresence 会建立一个名叫 tel0 的 tun 设备. 然后通过 systemd-resolved 服务将集群命令空间的 cluster domain 添加到这个设备. 通过resolvectl status tel0可以查看到当前有哪些命令空间被添加进来了:

resolvectl status tel0
Link 66 (tel0)
    Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6
         Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 10.0.165.145
       DNS Servers: 10.0.165.145
        DNS Domain: ~ambassador ~argocd ~cluster.local ~db ~default ~devops ~istio-system ~jaeger ~kube-public ~kube-system ~nacos ~observability

流量拦截 可以将集群里指定服务的流量拦截并转发到本地开发机器, 比如调试复杂的 app 业务接口时,非常方便.

这个非常有用, 但是老灯平常一般都不用这个. 因为我们的服务都有注入 istio side car. 而 Telepresence 的拦截原理其实也跟 istio 类似, 也是利用 side car 注入然后代理流量. 但是同一个 pod 的流量, 不能同时被 istio 接管, 然后又被 Telepresence 接管. 这一点我后面再详细说怎么解决.

日常使用

telepresence connect 连接

telepresence status 查看连接状态

curl -ik https://kubernetes.default 测试连接成功与否( 有任意响应就是成功)

telepresence quit -u -r 断开连接并退出user 和 root daemon

DNS 解析原理

这部分主要参考 https://www.telepresence.io/docs/latest/reference/routing/

Linux systemd-resolved resolver

以 Linux 上面的实现为例, 简单来说, Telepresence 就是新建一 tun 设备, 这个设备的流量会代理到 k8s 里的Telepresence traffic manager ( 负责流量转发等). tun 设备的 DNS 被设置成了 k8s 集群的 DNS (一般是 coredns 啦). Telepresence 会 watch 整个集群的所有命名空间, 因此, 当发生变量的时候, DNS link domain 会被自动更新.

然后, 设置哪此命令空间后缀要通过这个 DNS 查询 是通过 systemd-resolved 的服务的 dbus 接口 SetLinkDomains 操作的. 设置 DNS 是通过 SetLinkDNS

这种操作, 其实就相当于在 /etc/systemd/resolved.conf.d 下面新建一 k8s.conf 文件, 内容类似于:

# https://learn.hashicorp.com/tutorials/consul/dns-forwarding#systemd-resolved-setup
[Resolve]
DNS=10.0.165.145:53
DNSSEC=false
Domains=~ambassador ~argocd ~cluster.local ~db ~default ~devops ~istio-system ~jaeger ~kube-public ~kube-system ~nacos ~observability

只不过, 通过 dbus 可以动态的修改, 更加方便.

Linux overriding resolver

Linux systems that aren’t configured with systemd-resolved will use this resolver. A Typical case is when running Telepresence  inside a docker container. During initialization, the resolver will first establish a fallback connection to the IP passed as --dns, the one configured as local-ip in the  local DNS configuration, or the primary nameserver registered in /etc/resolv.conf. It will then use iptables to actually override that IP so that requests to it instead end up in the overriding resolver, which unless it succeeds on its own, will use the fallback.

即, 对于不支持 systemd-resolved 的 Linux 系统(一般是非常老的那种), Telepresence 会自己起一个 DNS 代理服务, 一般是监听随机端口, 然后再将系统 DNS 设置成 TelepresenceDNS 代理服务的地址. 即解析的时候会先查集群, 没有结果会 fallback 到本机原来的 DNS, 比如 Google public DNS 等解析其它域名.这个会影响其它应用的使用, 这种实现方式不太好, 以老灯的使用经验来看, 这种方式也不太稳定. 容易造成问题.

macOS resolver

在 macOS 下面是通过 resolver hooks 实现的.

This resolver hooks into the macOS DNS system by creating files under /etc/resolver. Those files correspond to some domain and contain the port number of the Telepresence resolver. Telepresence creates one such file for each of the currently mapped namespaces and include-suffixes option. The file telepresence.local contains a search path that is configured based on current intercepts so that single label names can be resolved correctly.

troubleshooting

1. 流量拦截不生效

测试过程中发现 流量拦截 与 注入的 istio-proxy 容器存在冲突,即当 istio-proxy 存在时,流量全部被 istio-proxy 接管了,traffic-agent 没有成功拦截到流量。

目前我暂时的一个 hack 方法是取消 istio-proxy sidecar注入:

diff --git a/develop/overlays/my_app/deployment.yaml b/develop/overlays/my_app/deployment.yaml
index 1049d335..26ee38d4 100644
--- a/develop/overlays/my_app/deployment.yaml
+++ b/develop/overlays/my_app/deployment.yaml
@@ -4,6 +4,9 @@ metadata:
   name: ttys3
 spec:
   template:
+    metadata:
+      annotations:
+        sidecar.istio.io/inject: "false"
     spec:
       containers:
       - name : my-app

traffic-agent 日志查看: stern --tail 100 ttys3-my-app -c traffic-agent

如果是采用 argo cd rollout:

diff --git a/develop/overlays/my-app/rollout.yaml b/develop/overlays/my-app/rollout.yaml
index 263eab87c..bbc44c378 100644
--- a/develop/overlays/my-app/rollout.yaml
+++ b/develop/overlays/my-app/rollout.yaml
@@ -6,6 +6,9 @@ metadata:
 
 spec:
   template:
+    metadata:
+      annotations:
+        sidecar.istio.io/inject: "false"
     spec:
       securityContext:
         runAsUser: 1000

2. 连接不上

使用新版本的 telepresence v2.x.x 如果“重复” 出现 (偶尔一次可能是意外)以下错误:

⁣telepresence: error: the port-forward connection to the traffic manager timed out. The current timeout 20s can be configured as timeouts.trafficManagerConnect

⁣ ⁣或 ⁣

⁣telepresence: error: the traffic manager gRPC API timed out. The current timeout 20s can be configured as timeouts.trafficManagerAPI in /Users/tomeee/Library/Application Support/telepresence/config.yml ⁣

⁣类似错误, ⁣说明同一集群里有多个人使用不同版本 v2 的客户端互相在打架。 当 telepresence 连接的时候, 如果与当前版本匹配的 traffic manager不存在, 则会自动安装与当前版本匹配的 traffic manager. 当不同的人, 从不同的地方, 下载了不同的版本, 都在连接同一个集群的时候, 问题就发生了.

解决方案: 同一集群所有人统一使用相同版本的客户端 (版本号要完全相同,对于 nightly 版, 小版本号和commit hash 都要相同) sys op 对整个集群的 rbac 做更加安全地配置, 禁止除 devops 组之外的其它开发人员拥有可以 ambassador 命名空间下资源的更新权限, 这样就可以阻止开发人员在使用telepresence 连接的时候无意中错误地在不停地安装各种版本的 traffic manager. 但是为了保证开发人员可以正常使用, list resource “namespaces“权限一定要给, 然后就是 create resource “pods/portforward” in API group "” in the namespace “ambassador” 的权限. ⁣ Client / Root Daemon / User Daemon 3 个版本号一定要完全一致:

❯ telepresence version
Client: v2.5.4 (api v3)
Root Daemon: v2.5.4 (api v3)
User Daemon: v2.5.4 (api v3)

参考官方 issue: https://github.com/telepresenceio/telepresence/issues/1652

https://github.com/telepresenceio/telepresence/issues/1689

3. 如何彻底卸载

一般情况下可以直接 telepresence uninstall --everything 搞定.

如果需要手动卸载可以这样操作:

k delete deploy -n ambassador traffic-manager
k delete secrets sh.helm.release.v1.traffic-manager.v1 -n ambassador

注意它并不会真正检查 pod 是否存在, 如果检查到 sh.helm.release.v1.traffic-manager.v1 这个 secrets 就会直接跳过安装了. 所以你要是删除了 traffic manger 的 deployment, 但是忘记删除了这个 secrets, 会导致下次连接的时候, traffic manger 不会被安装.

4. 调试问题

k8s 端问题查看:

先检查 pod 是不是正常:

k get po -n ambassador

k get deploy -n ambassador

查看 pod 日志:

k logs -n ambassador -f traffic-manager

5. 编译和构建容器镜像

traffic manager 的版本一定要匹配客户端的版本.

对于 nightly 版本, 其形式如 v2.5.4-2-g8ccf3c9d

对于正式版, 其形式如 v2.5.3

不同版本不能连接, 会提示错误. 即使是客户端, 不同版本的 daemon 也是不兼容的, 如:

version mismatch. Client v2.5.3 != User Daemon v2.5.4-2-g8ccf3c9d, please run ’telepresence quit -u’ and reconnect

计算当前 nightly 版本号: git describe --tags --match='v*'

build 的时候,必须通过 env 指定 TELEPRESENCE_VERSION, 不然 Makefile 会自动运行 go run build/genversion.go 生成一个带 unix timestamp 的版本号,这样客户端和 agent docker image 的版本号便没办法对应上了。

同时还要指定 TELEPRESENCE_REGISTRY , 这个主要是在构建时打docker tag用的,主要用于 docker push, 真正程序运行的时候,取的还是 env:"TELEPRESENCE_REGISTRY,default=docker.io/datawire" 因此,如果要防止客户端安装官方镜像, 这一行硬编码的代码必须修改.

# 构建本地bin和容器镜像并 push:
make build image push-image TELEPRESENCE_VERSION=v2.5.4 TELEPRESENCE_REGISTRY=docker.io/ttys3

# 只构建本地bin
make build TELEPRESENCE_VERSION=v2.5.4 TELEPRESENCE_REGISTRY=docker.io/ttys3

6. 突然无法访问的Rust文档站点docs.rs

连接 telepresence后, 发现 https://docs.rs/ 怎么也打不开了. dig docs.rs 发现超时, 并没有解析成功.

然后我想到了原因, 有人在集群里创建了一个名为 rs 的命名空间, 连接 telepresence后, 导致所有 .rs 域名都无法解析了(除了 k8s 集群里面的 ).

经过查看源码, 老灯发现可以通过patch updateLinkDomains 这个方法搞定. 思路就是, 如果一个命名空间, 是ICANN 管理的通用 TLD 后缀, 则直接 skip 它, 不设置.

直接修改 telepresence 源码然后自己编译客户端即可:

diff --git a/pkg/client/rootd/dns/resolved_linux.go b/pkg/client/rootd/dns/resolved_linux.go
index b2e8897bbeb5405170359f5318a7ae40dfc6e949..d90c2735ef9d4d421738fea533f7bfb244172b61 100644
--- a/pkg/client/rootd/dns/resolved_linux.go
+++ b/pkg/client/rootd/dns/resolved_linux.go
@@ -13,6 +13,7 @@ import (
 	"github.com/datawire/dlib/dtime"
 	"github.com/telepresenceio/telepresence/v2/pkg/client/rootd/dbus"
 	"github.com/telepresenceio/telepresence/v2/pkg/vif"
+	"golang.org/x/net/publicsuffix"
 )
 
 func (s *Server) tryResolveD(c context.Context, dev *vif.Device, configureDNS func(net.IP, *net.UDPAddr)) error {
@@ -102,28 +103,36 @@ func (s *Server) tryResolveD(c context.Context, dev *vif.Device, configureDNS fu
 func (s *Server) updateLinkDomains(c context.Context, paths []string, dev *vif.Device) error {
 	namespaces := make(map[string]struct{})
 	search := make([]string, 0)
-	for i, path := range paths {
+	pathsFiltered := make([]string, 0)
+	for _, path := range paths {
 		if strings.ContainsRune(path, '.') {
 			search = append(search, path)
 		} else {
+			// skip namespace which conflict with eTLD like `im`, `rs` to avoid pollute client normal DNS query
+			if eTLD, icann := publicsuffix.PublicSuffix(path); icann && path == eTLD {
+				dlog.Infof(c, "Skip set Link domains on device %q for [%s] due to conflict with ICANN eTLD [%s]",
+					dev.Name(), path, eTLD)
+				continue
+			}
 			namespaces[path] = struct{}{}
 			// Turn namespace into a route
-			paths[i] = "~" + path
+			// paths[i] = "~" + path
+			pathsFiltered = append(pathsFiltered, "~"+path)
 		}
 	}
 	for _, sfx := range s.config.IncludeSuffixes {
-		paths = append(paths, "~"+strings.TrimPrefix(sfx, "."))
+		pathsFiltered = append(pathsFiltered, "~"+strings.TrimPrefix(sfx, "."))
 	}
-	paths = append(paths, "~"+s.clusterDomain)
+	pathsFiltered = append(pathsFiltered, "~"+s.clusterDomain)
 	namespaces[tel2SubDomain] = struct{}{}
 
 	s.domainsLock.Lock()
 	s.namespaces = namespaces
 	s.search = search
 	s.domainsLock.Unlock()
-	if err := dbus.SetLinkDomains(dcontext.HardContext(c), int(dev.Index()), paths...); err != nil {
+	if err := dbus.SetLinkDomains(dcontext.HardContext(c), int(dev.Index()), pathsFiltered...); err != nil {
 		return fmt.Errorf("failed to set link domains on %q: %w", dev.Name(), err)
 	}
-	dlog.Debugf(c, "Link domains on device %q set to [%s]", dev.Name(), strings.Join(paths, ","))
+	dlog.Debugf(c, "Link domains on device %q set to [%s]", dev.Name(), strings.Join(pathsFiltered, ","))
 	return nil
 }

注意: telepresence 的 exclude-suffixes 选项并不能解决我们这里的问题.

https://www.telepresence.io/docs/latest/reference/config/#dns

其默认值为 [".arpa", ".com", ".io", ".net", ".org", ".ru"]

exclude-suffixes Suffixes for which the DNS resolver will always fail (or fallback in case of the overriding resolver)

由于我们用的都是基于systemd-resolved 的 resover, 因此, 如果把 `.rs` 加入这个列表, 则会导致 `.rs` 域名总是解析失败(NXDomain)

所以其实这个默认列表也是有问题的, 为什么默认有 `.com`, 而 `google.com` 不会解析失败呢? 那是因为我们没有名为 `com` 的命名空间. 所以, 假设你有名为 `com` 的命名空间, 就要小心了.

7. DNS 解析特别慢或超时

我们同时在开发和测试环境部署了 telepresence (traffic manager).

使用中发现, 测试环境丝滑无比, 开发环境总是 DNS 解析超时.

通过查看log可发现类似日志:

LookupHost: lookup kubernetes.default on 10.0.165.145:53: dial udp 10.0.165.145:53: i/o timeout

暂时没排查出什么原因, 大概率是 coredns 出问题了.

干了 coredns pod 之后, 等 coredns pod 重新 ruuning 了, 再把 traffic manager 的 pod干了,

重新测试, 发现正常了.

8. 某些网段或IP的流量不想走 tel0 tun 出去

这个也是通过k8s 扩展配置实现的, 之所以放在这里, 我想 telepresence 是考虑到对于不同人集群, 可以方便支持不同的配置.

修改 ~/.kube/config 文件, 增加配置到 never-proxy 数组即可, 如要将 10.10.8.9 单个IP排除:

- cluster:
    certificate-authority-data: xxxxxx
    server: https://example.com
    extensions:
    - name: telepresence.io
      extension:
        never-proxy:
          - 10.10.8.9/32
  name: cluster-001

k8s 集群 api server 的 IP telepresence是会默认排除的(通过 telepresence status 你可以看到这一点), 除此之外 , 如果你有些外部服务, 因为网段跟 k8s cluster IP 段重合, 导致流量错误地走了 tel0 tun 设备, 就可以通过这个配置来修正.

Refs

在本地开发和调试服务 https://kubernetes.io/zh/docs/tasks/debug-application-cluster/local-debugging/

https://www.telepresence.io/docs/latest/reference/routing/

https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html

https://www.telepresence.io/docs/latest/reference/config/#dns

https://www.getambassador.io/docs/telepresence/latest/howtos/intercepts/

https://github.com/telepresenceio/telepresence/tree/release/v2

https://www.getambassador.io/docs/telepresence/latest/quick-start/qs-go/