Skip to content

client-go基础

ops-checker 已经能做 HTTP 巡检,但 Kubernetes 里的服务状态还会体现在资源对象上。接口返回 200 时,Deployment 可能已经少了副本;Pod 可能频繁重启;Node 可能 NotReady;Service 背后可能没有可用 Endpoint。

Kubernetes 资源都通过 API Server 读写。Go 程序要读取 Pod、Deployment、Node 这些对象,需要一个 Kubernetes Go 客户端。client-go 是 Kubernetes 官方客户端库,负责把 Go 里的调用变成对 API Server 的请求。

一、检查范围

HTTP 检查回答“入口能不能访问”。Kubernetes 检查回答“集群对象当前是不是健康”。

常见差异:

现象HTTP 巡检看到的结果Kubernetes 对象里能看到的信息
副本不足负载均衡还转到幸存 Pod,接口仍然 200Deployment 的 availableReplicas(就绪副本数)少于期望值
Pod 频繁重启请求刚好打到正常 Pod,接口仍然 200Pod 的 restartCount(重启次数)持续增加
节点异常入口层还有缓存或其他副本Node 的 condition 出现 Ready=False(节点未就绪)
Service 无后端入口可能直接 503EndpointSlice(Service 的后端地址列表)里没有 ready endpoint

这些字段都可以通过 kubectl getkubectl describe 看到,对应的 Go 客户端查询方式见第七节

ops-checker 接入 Kubernetes 后,原来的 HTTP 检查仍然保留,再增加 Kubernetes 检查类型。一个工具同时记录入口状态和集群对象状态。

二、连接链路

执行 kubectl get pod 时,kubectl 会读取 kubeconfig,找到当前集群地址、认证信息和 namespace,然后向 API Server 发请求。

Go 程序也是同样链路:

这几个对象各自处理不同问题:

对象解决的问题
kubeconfig配置文件,记录连哪个集群、用哪个用户、默认 namespace
RESTConfigGo 程序运行时使用的连接配置
ClientSet访问 Kubernetes 内置资源的客户端集合
Informer长时间监听资源变化并维护本地缓存

ops-checker 作为命令行巡检工具,使用 ClientSet 做一次性查询。Informer 属于长期运行的控制器场景。

三、配置变更

原来的 configs/targets.json 只有 HTTP 目标:

json
[
  {
    "type": "http",
    "name": "api",
    "url": "https://example.com/health",
    "timeout_seconds": 3
  }
]

增加 Kubernetes Pod 检查后,目标仍然放在同一个配置文件里。

修改文件:configs/targets.json

json
[
  {
    "type": "http",
    "name": "api",
    "url": "https://example.com/health",
    "timeout_seconds": 3
  },
  {
    "type": "kubernetes_pods",
    "name": "nginx-pods",
    "namespace": "default",
    "label_selector": "app=nginx",
    "min_ready": 2
  }
]

type 表示检查类型。HTTP 目标用 url,Kubernetes Pod 检查用 namespacelabel_selectormin_ready

修改文件:internal/config/target.go

Target 增加类型和 Kubernetes 字段:

go
type Target struct {
	Type           string `json:"type"`
	Name           string `json:"name"`
	URL            string `json:"url"`
	TimeoutSeconds int    `json:"timeout_seconds"`

	Namespace     string `json:"namespace"`
	LabelSelector string `json:"label_selector"`
	MinReady      int    `json:"min_ready"`
}

字段不是每种检查都会用到。type=http 时,Kubernetes 字段为空;type=kubernetes_pods 时,url 为空。检查类型继续增加后,再拆成更严格的配置结构。

四、依赖包

项目开始接入 Kubernetes 后,需要新增 client-go 相关依赖。

修改后的目录会多出一个 internal/kube/

text
ops-checker/
├── cmd/
│   └── ops-checker/
│       └── main.go
├── configs/
│   └── targets.json
└── internal/
    ├── checker/
    │   ├── http.go
    │   ├── kubernetes.go
    │   ├── result.go
    │   └── runner.go
    ├── config/
    │   └── target.go
    ├── kube/
    │   └── client.go
    └── output/
        ├── json.go
        └── text.go

添加依赖:

bash
go get k8s.io/client-go/kubernetes
go get k8s.io/client-go/tools/clientcmd
go get k8s.io/apimachinery/pkg/apis/meta/v1

client-go 版本通常跟 Kubernetes 主版本对齐,例如 Kubernetes 1.30 对应 client-go v0.30.x。版本差太多时,代码可能仍然能编译,但某些新字段、新资源类型或认证插件行为会不一致。

五、RESTConfig

kubeconfig 是 YAML 文件,常见位置:

场景路径
Linux 普通用户~/.kube/config
kubeadm 管理节点/etc/kubernetes/admin.conf
多配置合并KUBECONFIG 环境变量指定

kubectl 用来验证当前配置:

bash
kubectl config current-context
kubectl get ns

kubectl get ns 能验证三件事:API Server 地址能连通,认证信息能通过,当前身份至少有读取 namespace 的权限。这个命令失败时,Go 程序里创建 ClientSet 也很难成功。

新增文件:internal/kube/client.go

go
package kube

import (
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
)

func BuildConfig(kubeconfigPath string) (*rest.Config, error) {
	if kubeconfigPath == "" {
		// 本机运行时默认读取 ~/.kube/config,行为接近 kubectl。
		return clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
	}

	return clientcmd.BuildConfigFromFlags("", kubeconfigPath)
}

func NewClientSet(kubeconfigPath string) (*kubernetes.Clientset, error) {
	config, err := BuildConfig(kubeconfigPath)
	if err != nil {
		return nil, err
	}

	return kubernetes.NewForConfig(config)
}

RESTConfig 还不是客户端。它只是“怎么连接 API Server”的运行时配置,里面包含地址、证书、token、QPS、Burst、UserAgent 等信息。NewForConfig 才会基于这份配置创建 ClientSet。

Pod 内运行的程序通常不用本机 kubeconfig,而是使用 ServiceAccount。Kubernetes 会自动把 ServiceAccount 的 token 挂载到 /var/run/secrets/kubernetes.io/serviceaccount/token,把 CA 证书挂载到同目录下的 ca.crt,把当前 namespace 写入同目录的 namespace 文件。rest.InClusterConfig() 会读取这些挂载路径,自动构造连接配置:

go
import "k8s.io/client-go/rest"

config, err := rest.InClusterConfig()
if err != nil {
    // 不在 Pod 内运行时(比如本地开发),会因为没有挂载路径而报错
    panic(err.Error())
}
clientset, err := kubernetes.NewForConfig(config)

本机开发时用 BuildConfigFromFlags(读 kubeconfig),部署到集群内时用 InClusterConfig()。两种方式可以按优先级封装:先尝试集群内配置,失败时回退到 kubeconfig。

六、ClientSet

ClientSet 是访问 Kubernetes 内置资源的类型化客户端。Pod 属于 core group 的 v1,所以通过 CoreV1() 访问。

修改文件:cmd/ops-checker/main.go

先给入口加 kubeconfig 参数:

go
kubeconfigPath := flag.String("kubeconfig", "", "kubeconfig path")

再创建 ClientSet:

go
clientset, err := kube.NewClientSet(*kubeconfigPath)
if err != nil {
	fmt.Fprintln(os.Stderr, "create kubernetes client:", err)
	os.Exit(2)
}

外部运行时显式传 kubeconfig:

bash
go run ./cmd/ops-checker \
  -config configs/targets.json \
  -kubeconfig ~/.kube/config

ClientSet 创建失败属于工具自身错误,用退出码 2。常见原因是 kubeconfig 路径错误、认证插件不可用、证书不匹配、API Server 地址不可达。

七、Pod 检查

查询 Pod 对应的 kubectl 命令大概是:

bash
kubectl get pod -n default -l app=nginx

client-go 里同一件事写成:

go
pods, err := clientset.CoreV1().Pods(target.Namespace).List(ctx, metav1.ListOptions{
	LabelSelector: target.LabelSelector,
})

target.Namespace 对应 -n defaultLabelSelector 对应 -l app=nginxListOptions 里还能放字段选择器、分页参数和 resourceVersion;当前 Pod 检查只用标签选择器。

新增文件:internal/checker/kubernetes.go

go
package checker

import (
	"context"
	"fmt"

	"example.com/ops-checker/internal/config"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
)

func CheckKubernetesPods(ctx context.Context, clientset *kubernetes.Clientset, target config.Target) Result {
	pods, err := clientset.CoreV1().Pods(target.Namespace).List(ctx, metav1.ListOptions{
		LabelSelector: target.LabelSelector, // 和 kubectl -l 的含义一致
	})
	if err != nil {
		return Result{Name: target.Name, OK: false, Message: err.Error()}
	}

	ready := 0
	for _, pod := range pods.Items {
		if isPodReady(pod) {
			ready++
		}
	}

	minReady := target.MinReady
	if minReady <= 0 {
		minReady = 1 // 不写 min_ready 时,至少要求有 1 个 Ready Pod
	}

	ok := ready >= minReady
	return Result{
		Name:    target.Name,
		OK:      ok,
		Message: fmt.Sprintf("ready=%d total=%d min_ready=%d", ready, len(pods.Items), minReady),
	}
}

同一个文件里再加 Pod Ready 判断:

go
func isPodReady(pod corev1.Pod) bool {
	if pod.Status.Phase != corev1.PodRunning {
		return false
	}

	for _, condition := range pod.Status.Conditions {
		if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue {
			return true
		}
	}

	return false
}

代码没有把 Running 直接当作健康。Pod 进入 Running 只说明容器已经启动,Ready condition 才说明它已经通过探针并能接流量。

八、检查分发

HTTP 和 Kubernetes 检查按 target.Type 分发。原来的 worker 只调用 CheckHTTP,现在改成统一入口。

修改文件:internal/checker/runner.go

新增函数:RunOne

go
func RunOne(ctx context.Context, clientset *kubernetes.Clientset, target config.Target) Result {
	switch target.Type {
	case "http":
		return CheckHTTP(ctx, target)
	case "kubernetes_pods":
		return CheckKubernetesPods(ctx, clientset, target)
	default:
		return Result{Name: target.Name, OK: false, Message: "unknown target type: " + target.Type}
	}
}

继续修改文件:internal/checker/runner.go

worker 里把原来的 CheckHTTP 替换成 RunOne

go
for target := range jobs {
	results <- RunOne(ctx, clientset, target)
}

函数签名也要带上 ClientSet:

go
func Run(ctx context.Context, clientset *kubernetes.Clientset, targets []config.Target, concurrency int) []Result

这样 HTTP 检查、Kubernetes 检查仍然走同一个结果模型、同一套输出和同一套退出码。新增检查类型时,主要是增加一个检查函数和一个 case

九、RBAC

client-go 最终仍然受 Kubernetes RBAC 控制。程序是否能 list Pod,取决于 kubeconfig 里的用户或 Pod 里的 ServiceAccount 有没有权限。

本机 kubeconfig 验证:

bash
kubectl auth can-i list pods -n default
kubectl auth can-i list pods -A

Pod 内 ServiceAccount 验证:

bash
kubectl auth can-i list pods \
  --as=system:serviceaccount:ops-system:ops-checker \
  -n default

常见错误:

错误读到时的含义排查入口
connection refusedAPI Server 地址不通或端口错kubeconfig、网络、代理
x509: certificate signed by unknown authorityCA 证书不匹配kubeconfig 里的 cluster CA
Unauthorized认证信息无效或过期token、证书、exec 登录插件
forbidden认证通过,但 RBAC 不允许kubectl auth can-i
context deadline exceeded请求超时网络、API Server 负载、DNS

forbiddenUnauthorized 不一样。Unauthorized 是身份没通过;forbidden 是身份通过了,但权限不够。

十、Informer

ops-checker 当前是一次性巡检命令:启动、读取配置、查询 API Server、输出结果、退出。这种模式用 ClientSet 的 List 就够了。

Informer 属于长期运行的程序。它会先 List 当前对象,再 Watch 后续变化,把对象放到本地缓存里:

只 Watch 不 List,会漏掉程序启动前已经存在的对象;只 List 不 Watch,又需要反复全量查询。Informer 把这两个动作合在一起,并处理断线重连和本地缓存。

ops-checker 处在命令行巡检形态时,ClientSet 的一次性查询已经够用。常驻进程或控制器形态再进入 Informer。

十一、kubectl 对照

client-go 代码里的问题,常用 kubectl 复现同一件事:

Go 程序动作kubectl 对照
读取 kubeconfigkubectl config view --minify
测试连接kubectl get ns
查询 Podkubectl get pod -n default -l app=nginx
看 Pod Readykubectl get pod -n default -o wide
看完整字段kubectl get pod <name> -n default -o yaml
查权限kubectl auth can-i list pods -n default

kubectl 使用同一份 kubeconfig、同一个 namespace、同一个 label selector 能查到对象,再回到程序里看 RESTConfig、ClientSet 调用和错误处理,能缩小问题范围。

十二、目录结构

接入 client-go 后,目录比上一版多了 internal/kube/client.gointernal/checker/kubernetes.go

text
ops-checker/
├── cmd/
│   └── ops-checker/
│       └── main.go
├── configs/
│   └── targets.json
└── internal/
    ├── checker/
    │   ├── http.go
    │   ├── kubernetes.go
    │   ├── result.go
    │   └── runner.go
    ├── config/
    │   └── target.go
    ├── kube/
    │   └── client.go
    └── output/
        ├── json.go
        └── text.go

internal/kube/client.go 负责把 kubeconfig 变成 ClientSet;internal/checker/kubernetes.go 负责具体检查逻辑。连接集群和判断对象状态分开后,增加 Deployment、Node、EndpointSlice 检查时,main.go 仍然只串流程。