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中两个比较重要的概念, GVK
与GVR
.
- 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 }
代码大概包括这几部分
- 设置kubeconfig的命令行参数, 即kubeconfig配置文件的路径
- 创建kubeconfig对象并创建k8s客户端集合(clientset), 从这个集合中获取对应资源的客户端
- 构造deployment对象,一般来说不会手动构造,而是解析yaml文件
- 创建
- 更新
- 列出
- 删除
前面三个部分一般来说差别不大,特别是第一部分,无论如何我们都是要读取kubeconfig文件已获得连接k8s集群的凭证的,除非是在集群内部。
后面四个部分其实大多数资源都差不多,不同的点在于不同类型定义的字段不同。
总的来说,要操作k8s的资源大致需要三步,
- 配置加载, 获取凭证(通过kubeconfig或者service account token)
- 基于上一步创建客户端(动态或者静态)
- 使用上一步客户端提供的接口, 如
Create,Update,Delete,List
,进行资源的相关操作
总结
k8s的接口遵循RESTFul规范,所以熟悉RESTFul风格的接口使用起来还是很顺手的,client-go暴露出的各种方法也是比较一致的。