k8s client-go快速入门教程及源代码阅读之总纲

文章目录

client-go是kubernetes官方维护的一个go语言客户端,用于与k8s集群交互,使用client-go可以很方便的完成k8s的二次开发(似乎也必不可少),无论是稳定性还是健壮性都有充分的保障。

client-go代码版本: v0.20.2

个人水平有些,一定会出现不严谨或者错误的地方,如有错误麻烦评论指正,谢谢

版本选择

kubernetes差不多是6个月左右发布一个版本,然后持续维护一年,之后除非有重大安全问题,不然一般不会再更新对应的版本,因此,作为kubernetes的官方客户端client-go也遵循着差不多的版本语义,为了获得与集群最佳的交互,应该使用语义完全相同的版本, 但是, 由于一些历史原因,版本语义的对应有一定的变化,kubernetes 1.17.0之后对应的版本语义是v0.x.y, 而kubernetes 1.17.0 之前的版本对应的是kubernetes-1.x.y.

举例来说,kubernetes 1.20.2对应的client-go版本应该使用v0.20.2,kubernetes1.16.2 对应的client-go版本是kubernetes-1.16.2

GVK/GVR

在操作使用client-go之前首先应该了解一下k8s中两个比较重要的概念, GVKGVR.

  • GVK Group Version Kind的缩写
  • GVK Group Version Resource的缩写

两者有两部分相同,区别在于Kind和Resource, 在讲解之前可以先看看下面命令执行的结果

kubectl api-resources
# 输出结果
NAME                              SHORTNAMES   APIVERSION                             NAMESPACED   KIND
configmaps                        cm           v1                                     true         ConfigMap
deployments                       deploy       apps/v1                                true         Deployment
...省略后续输出...

kubectl api-resources可以列出当前集群中所有资源的版本信息,其中包括Name(资源名), SHORTNAMES(资源名缩写), APIVERSION(推荐版本, 包括组名和版本, 即Group Version), NAMESPACED(是否是命名空间级别的资源), KIND(资源类型)。

以deployments资源举例,其对应的GVK是apps v1 Deployment, GVR对应的是apps v1 deployments。

细心的读者可能会发现,configmaps似乎没有组名,这是因为k8s在早期没有group这个概念,所以核心资源如configmaps, pods, endpoints等都没有组名, 代码中其实它们被分在了core组,这个以后会说到。

其实通过GVK和GVR都能定位到资源,为啥需要两个?

个人的看法是, GVK中的K(Kind)是为了直接使用资源在Go语言中定义的类型名, 通过反射可以直接得到,因为需要导出,所以是大写,而这样的大写如果在命令行中使用并不方便,所以弄了一个额外的映射规则,比如,deployments -> Deployment, deployment和它的deploy缩写通过另外的方式定义,即GVR,而为啥Deployment对应的资源名为啥是复数形式,应该是为了符合RESTFul规范。

总的来说,k8s中有许多的资源,GVK和GVR都是用于定位资源的标识方式,就像URL一样。

增删改查

似乎所有业务最终都会抽象成增删改查,之所以这样,我想这是因为大多数业务都需要持久化,持久化自然要保存到数据库,虽然数据库不同,但是各式各样的数据库提供的操作基本都是会包含增删改查的。

值得注意的是: k8s的API叫做声明式API, 比如,修改副本数应该传一个预期的副本数,而不是让k8s增加多少副本或者减少多少副本数。

下面贴一个官方的增删改查代码

完整代码请参考: https://github.com/kubernetes/client-go/tree/v0.20.2/examples/create-update-delete-deployment

func main() {
    // 1.
	var kubeconfig *string
	if home := homedir.HomeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}
	flag.Parse()
	
	// 2.
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	clientset, err := kubernetes.NewForConfig(config)
	deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)
	
    // 3.
	deployment := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name: "demo-deployment",
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: int32Ptr(2),
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"app": "demo",
				},
			},
			Template: apiv1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						"app": "demo",
					},
				},
				Spec: apiv1.PodSpec{
					Containers: []apiv1.Container{
						{
							Name:  "web",
							Image: "nginx:1.12",
							Ports: []apiv1.ContainerPort{
								{
									Name:          "http",
									Protocol:      apiv1.ProtocolTCP,
									ContainerPort: 80,
								},
							},
						},
					},
				},
			},
		},
	}

	// 4.
	fmt.Println("Creating deployment...")
	result, err := deploymentsClient.Create(context.TODO(), deployment, metav1.CreateOptions{})
	fmt.Printf("Created deployment %q.\n", result.GetObjectMeta().GetName())

	// 5. 
	prompt()
	fmt.Println("Updating deployment...")
	retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		result, getErr := deploymentsClient.Get(context.TODO(), "demo-deployment", metav1.GetOptions{})
		if getErr != nil {
			panic(fmt.Errorf("Failed to get latest version of Deployment: %v", getErr))
		}

		result.Spec.Replicas = int32Ptr(1)                           // reduce replica count
		result.Spec.Template.Spec.Containers[0].Image = "nginx:1.13" // change nginx version
		_, updateErr := deploymentsClient.Update(context.TODO(), result, metav1.UpdateOptions{})
		return updateErr
	})
	fmt.Println("Updated deployment...")

	// 6.
	prompt()
	fmt.Printf("Listing deployments in namespace %q:\n", apiv1.NamespaceDefault)
	list, err := deploymentsClient.List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		panic(err)
	}
	for _, d := range list.Items {
		fmt.Printf(" * %s (%d replicas)\n", d.Name, *d.Spec.Replicas)
	}

	// 7
	prompt()
	fmt.Println("Deleting deployment...")
	deletePolicy := metav1.DeletePropagationForeground
	if err := deploymentsClient.Delete(context.TODO(), "demo-deployment", metav1.DeleteOptions{
		PropagationPolicy: &deletePolicy,
	}); err != nil {
		panic(err)
	}
	fmt.Println("Deleted deployment.")
}

func prompt() {
	fmt.Printf("-> Press Return key to continue.")
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		break
	}
	if err := scanner.Err(); err != nil {
		panic(err)
	}
	fmt.Println()
}

func int32Ptr(i int32) *int32 { return &i }

代码大概包括这几部分

  1. 设置kubeconfig的命令行参数, 即kubeconfig配置文件的路径
  2. 创建kubeconfig对象并创建k8s客户端集合(clientset), 从这个集合中获取对应资源的客户端
  3. 构造deployment对象,一般来说不会手动构造,而是解析yaml文件
  4. 创建
  5. 更新
  6. 列出
  7. 删除

前面三个部分一般来说差别不大,特别是第一部分,无论如何我们都是要读取kubeconfig文件已获得连接k8s集群的凭证的,除非是在集群内部。

后面四个部分其实大多数资源都差不多,不同的点在于不同类型定义的字段不同。

总的来说,要操作k8s的资源大致需要三步,

  1. 配置加载, 获取凭证(通过kubeconfig或者service account token)
  2. 基于上一步创建客户端(动态或者静态)
  3. 使用上一步客户端提供的接口, 如Create,Update,Delete,List,进行资源的相关操作

总结

k8s的接口遵循RESTFul规范,所以熟悉RESTFul风格的接口使用起来还是很顺手的,client-go暴露出的各种方法也是比较一致的。

参考链接