k8s源码
APIServer提供了 k8s各类资源对象的CURD/watch、认证授权、准入控制等众多核心功能,在k8s中定位类似于大脑和心脏,它的功能包括:
如图所示,这是创建一个资源(Pod)实例过程中,控制层面所经过的调用过程
因此,APIServer无疑是各模块中 最复杂、定位最核心、涉及面最广、代码量最大 的模块。
APIServer的工作主要围绕着对各类资源对象的管控,因此,在开始阅读APIServer的源码之前,有必要笼统地列举一下它在运行中所用到的核心数据结构等基础性信息
APIServer的工作主要围绕着对各类资源对象的管控,因此,在开始阅读APIServer的源码之前,有必要笼统地列举一下它在运行中所用到的核心数据结构等基础性信息
Group/Version/Kind/Resource
在K8s的设计中,resource是其最基础、最重要的概念,也是最小的管理单位,所有的管理对象都承载在一个个的resource实例上,为了实现这些resource的复杂管理逻辑,又进一步地将他们分组化、版本化,依照逻辑层次,形成了Group、Version、Kind、Resource核心数据结构:
锚定形式
概念层面,在K8s中,常见的资源路径锚定形式为:///,例如deployment对应的路径是:apps/v1/deployments/status
官方通常通过缩写词GVR(GroupVersionKind)来描述一个资源的明确锚定位置(类似于绝对路径?),同理,GVK(GroupVersionKind)锚定资源的明确所属类型,在项目代码中也经常用到,例如
资源结构体
而落实到代码中,每一种资源的结构体定义文件都位于其Group下的的types.go文件中,例如,Deployment资源的结构体定义在这里pkg/apis/apps/types.go:268
资源操作方法
概念层面,每种resource都有对应的管理操作方法,目前支持的有这几种
使用[]string结构来描述资源所对应的操作,而[]string终归只是描述,需要与实际的存储资源CRUD操作关联,因此,不难猜测,每种string描述的方法会map到具体的方法上去,结构类似于: map[string]Function
内部和外部Version
在k8s的设计中,资源版本分外部版本(external)和内部版本(internal)之分,外部版本(例如v1/v1beta1/v1beta2)提供给外部使用,而对应的内部版本仅在APIServer内部使用
区分内外版本的作用:
Schema注册表
每一种Resource都有对应的Kind,为了更便于分类管理这些资源,APIServer设计了一种名为scheme的结构体,类似于注册表,运行时数据存放内存中,提供给各种资源进行注册,scheme有如下作用:
Scheme支持注册两种类型的资源:
注册方法
scheme表提供两个注册方法:AddKnownTypes
| AddKnownTypeWithName
,使用reflect反射的方式获取type obj的gvk然后进行注册
序列化和反序列化
APIServer对资源的描述支持yaml和json格式,分别对应不同的Serializer,Serializer内置有bool类型的yaml字段,来辨别是否是yaml Serializer。
序列化代码位于:vendor/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go:223
可以得知,默认以json格式响应,而对于yaml格式,先将其转换为json格式,再转换回yaml格式响应
反序列化代码:vendor/k8s.io/apimachinery/pkg/runtime/serializer/json/json.go:86
go-restful
k8s选用的Restful框架是go-restful,简单说明一下go-restful的结构,辅助后面对于APIServer工作流程的理解。
go-restful层级结构概念自顶上下依次有:
资源注册
scheme是一种内存型的注册表,提供给各类gvk进行注册。在APIServer http服务启动前的第一步,就是将所支持的gvk注册到scheme中,后面的步骤会依赖scheme注册表信息。
值得注意的是,并没有函数方法来显示地注册scheme,而是通过go语言的包导入init机制来初始化注册的
认证配置
APIServer支持如下的认证策略:
所有 Kubernetes 集群都有两类用户:由 Kubernetes 管理的服务账号和普通用户。
其中服务账号(ServiceAccount)是提供给集群中的程序使用,以Secret资源保存凭据,挂载到pod中,从而允许集群内的服务调用k8s API。
而普通用户,尚不支持使用API创建,一般由证书创建,Kubernetes 使用证书中的 'subject' 的通用名称(Common Name)字段(例如,"/CN=bob")来 确定用户名。
Kubernetes 使用身份认证插件利用客户端证书、持有者令牌(Bearer Token)、身份认证代理(Proxy) 或者 HTTP 基本认证机制来认证 API 请求的身份。HTTP 请求发给 API 服务器时, 插件会将以下属性关联到请求本身:
kube-admin
或 jane@example.com
。system:masters
或者 devops-team
等。与其它身份认证协议(LDAP、SAML、Kerberos、X509 的替代模式等等)都可以通过 使用一个身份认证代理或 身份认证 Webhoook来实现。
认证流程
RequestHeader认证
是一种代理认证方式,需要再apiserver启动时以参数形式配置,来看看官方的介绍:
API 服务器可以配置成从请求的头部字段值(如 X-Remote-User
)中辩识用户。 这一设计是用来与某身份认证代理一起使用 API 服务器,代理负责设置请求的头部字段值。
--requestheader-username-headers
必需字段,大小写不敏感。用来设置要获得用户身份所要检查的头部字段名称列表(有序)。第一个包含数值的字段会被用来提取用户名。--requestheader-group-headers
可选字段,在 Kubernetes 1.6 版本以后支持,大小写不敏感。 建议设置为 "X-Remote-Group"。用来指定一组头部字段名称列表,以供检查用户所属的组名称。 所找到的全部头部字段的取值都会被用作用户组名。--requestheader-extra-headers-prefix
可选字段,在 Kubernetes 1.6 版本以后支持,大小写不敏感。 建议设置为 "X-Remote-Extra-"。用来设置一个头部字段的前缀字符串,API 服务器会基于所给 前缀来查找与用户有关的一些额外信息。这些额外信息通常用于所配置的鉴权插件。 API 服务器会将与所给前缀匹配的头部字段过滤出来,去掉其前缀部分,将剩余部分 转换为小写字符串并在必要时执行百分号解码 后,构造新的附加信息字段键名。原来的头部字段值直接作为附加信息字段的值。BasicAuth认证
BasicAuth是一种简单的基础http认证,用户名、密码写入http请求头中,用base64编码,防君子不防小人,安全性较低,因此很少使用。快速略过
启动apiserver时,使用--basic-auth-file参数指定csv文件,csv里面以逗号切割,存放用户名、密码、uid
X509 CA认证
又称TLS双向认证,APIServer启动时使用--client-ca-file指定客户端的证书文件,用作请求的认证
BearerToken认证
这种认证方式是专为k8s节点准备的,避免每个节点都要手动配置TLS证书,在apiserver启动时指定--enable-bootstrap-token-auth
参数来启用这种认证方式
bearertoken的认证方式是在请求头里放入bearer令牌,令牌的格式为 [a-z0-9]{6}.[a-z0-9]{16}
。第一个部分是令牌的 ID;第二个部分 是令牌的 Secret。对应http请求头的格式是:
Authorization: Bearer xxxxxx.xxxxxxxxxxxxxxxx
ServiceAccount 认证
启用方式为APIServer命令使用--service-account-key-file
参数指定一个为token签名的PEM秘钥文件。
SA认证是jwt形式的认证,使用方式与bearer token类似,也是放在请求头里,内容为Base64编码,header格式为:
Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo0NTk0LCJ1c2VybmFtZSI6InlpbndlbnFpbiIsImV4cCI6MTU3MDY3NDEyNywiZW1haWwiOiIifQ.djC2w5l3IiXYv7slZtGzlMzLc3_oPuR1M0dM9FwoaUU
token哪里来呢?答案就是ServiceAccount。
SA是一种面向集群内部应用需要调用APIServer的场景所设计的认证方式。在创建ServiceAccount资源时,可以显示地设置标签将ServiceAccount绑定给某Deploy/sts/pod,也可以在Deploy/sts/pod的声明文件里显示指定ServiceAccount。ServiceAccount会自动创建Secret资源,token秘钥存放其中。
在相应的容器层面,token信息会被挂载进容器中,包含3个文件:
WebhookToken认证
Webhook 身份认证是一种用来验证持有者令牌的回调机制。
--authentication-token-webhook-config-file
指向一个配置文件,其中描述 如何访问远程的 Webhook 服务。--authentication-token-webhook-cache-ttl
用来设定身份认证决定的缓存时间。 默认时长为 2 分钟。当客户端尝试在 API 服务器上使用持有者令牌完成身份认证( 如前所述)时, 身份认证 Webhook 会用 POST 请求发送一个 JSON 序列化的对象到远程服务。 该对象是 authentication.k8s.io/v1beta1
组的 TokenReview
对象, 其中包含持有者令牌。 Kubernetes 不会强制请求提供此 HTTP 头部。
要注意的是,Webhook API 对象和其他 Kubernetes API 对象一样,也要受到同一 版本兼容规则约束。 实现者要了解对 Beta 阶段对象的兼容性承诺,并检查请求的 apiVersion
字段, 以确保数据结构能够正常反序列化解析。此外,API 服务器必须启用 authentication.k8s.io/v1beta1
API 扩展组 (--runtime-config=authentication.k8s.io/v1beta1=true
)。
Anonymous认证
启用匿名请求支持之后,如果请求没有被已配置的其他身份认证方法拒绝,则被视作 匿名请求(Anonymous Requests)。这类请求获得用户名 system:anonymous
和 对应的用户组 system:unauthenticated
。
例如,在一个配置了令牌身份认证且启用了匿名访问的服务器上,如果请求提供了非法的 持有者令牌,则会返回 401 Unauthorized
错误。 如果请求没有提供持有者令牌,则被视为匿名请求。
在 1.5.1-1.5.x 版本中,匿名访问默认情况下是被禁用的,可以通过为 API 服务器设定 --anonymous-auth=true
来启用。
在 1.6 及之后版本中,如果所使用的鉴权模式不是 AlwaysAllow
,则匿名访问默认是被启用的。 从 1.6 版本开始,ABAC 和 RBAC 鉴权模块要求对 system:anonymous
用户或者 system:unauthenticated
用户组执行显式的权限判定,所以之前的为 *
用户或 *
用户组赋予访问权限的策略规则都不再包含匿名用户。
请求在通过认证之后,请求将进入鉴权环节
审查请求属性
Kubernetes 仅审查以下 API 请求属性:
user
字符串。/api
或 /healthz
。get
、list
、create
、update
、patch
、watch
、 proxy
、redirect
、delete
和 deletecollection
用于资源请求。 要确定资源 API 端点的请求动词,请参阅 确定请求动词。get
、post
、put
和 delete
用于非资源请求。get
、update
、patch
和 delete
动词的资源请求,你必须提供资源名称。鉴权策略
目前支持6种鉴权策略,每种鉴权策略对应一个鉴权器,使用的鉴权策略需要在APIServer启动时以参数--authorization-mode
的形式指定,多种策略同时指定时使用','号连接:
策略分类有:
--authorization-mode=ABAC
基于属性的访问控制(ABAC)模式允许你 使用本地文件配置策略。--authorization-mode=RBAC
基于角色的访问控制(RBAC)模式允许你使用 Kubernetes API 创建和存储策略。--authorization-mode=Webhook
WebHook 是一种 HTTP 回调模式,允许你使用远程 REST 端点管理鉴权。--authorization-mode=Node
节点鉴权是一种特殊用途的鉴权模式,专门对 kubelet 发出的 API 请求执行鉴权。--authorization-mode=AlwaysDeny
该标志阻止所有请求。仅将此标志用于测试。--authorization-mode=AlwaysAllow
此标志允许所有请求。仅在你不需要 API 请求 的鉴权时才使用此标志。与上一篇的认证模块不同的是,当配置多个鉴权模块时,鉴权模块按顺序检查,靠前的模块具有更高的优先级来允许或拒绝请求。
鉴权结果
Kubelet作为k8s核心组件中的daemon端运行在集群中的每一个节点上,承接着控制平面的指令向数据平面传达。不像scheduler、controller组件只负责相对单一的功能,kubelet除了管理自身的运行时外,还需要和宿主系统(linux)、CRI、CNI、CSI等外部组件对接,无疑是一个复杂度很高的组件
Kubelet的主要功能
本篇开始进入数据交互平面的daemon组件kubelet部分,看看kubelet是如何在控制平面和数据平面中以承上启下的模式工作的
https://www.zhihu.com/topic/21216319/top-answers
client-go 是 kubernetes 中比较重要的一个组件,从我上一篇文章中梳理的图中可以看出来,apiserver 是一个核心,其它组件都要和这个核心模块交互,所以 client-go 的出现就是为了统一封装对 apiserver 的交互访问。
client-go 这种设计思路还是不错的,当然是适合 kubernetes 这样的项目,几乎所有的模块都在围绕 apiserver,那么和 apiserver 的交互就显的尤为重要,那么这部分代码的抽象封装也就顺理成章了。这种解偶方式也是挺特别的,在看了书,走读了这部分的源码之后也才发现,同样的 client 在使用方式,使用对象不一样,就需要不一样的封装方式
目录名 | 用途 |
---|---|
discovery | 这个是 discovery client 的代码,是对 rest 客户端的进一步封装,用于发现 apiserver 所支持的能力和信息 |
dynamic | 这个是 dynamic client 的代码,是对 rest 客户端的进一步封装,动态客户端,面向处理 CRD |
examples | 这里面有一些例子,比如对 deployment 创建、修改,如何选主,workqueue 如何使用等等 |
informers | 这就是 client-go 中非常有名的 informer 机制的核心代码 |
kubernetes | clientset 的代码,也是对 rest 客户端的进一步封装,提供复杂的资源访问和管理能力 |
listers | 为每个 k8s 资源提供 lister 功能,提供了只读缓存功能 |
metadata | |
pkg | 主要是一些功能函数,比如版本函数 |
rest | 这是最基础的 client,其它的 client 都是基于此派生的 |
scale | scale client 的代码 |
tools | 工具函数库,主要是和 k8s 相关的工具函数 |
util | 通用的一些工具函数 |
transport | 提供安全 tcp 链接 |
核心数据结构
type RESTClient struct {
// 这个初始化的 apiserver 的地址,下面我也贴了一个 kubeconfig 文件的内容,这个地址就是 cluster 的 server。
base *url.URL
// 这个是 apiVersion
versionedAPIPath string
// 对客户端编解码的设置
content ClientContentConfig
// creates BackoffManager that is passed to requests.
createBackoffMgr func() BackoffManager
// 限流控制,是针对这个客户端的所有请求的。这个也是非常好的一个设计,一般 sdk 的设计很少考虑这个,大多数只考虑功能
rateLimiter flowcontrol.RateLimiter
// warningHandler is shared among all requests created by this client.
// If not set, defaultWarningHandler is used.
warningHandler WarningHandler
// http 请求客户端
Client *http.Client
}
Informer (就是 SharedInformer)是 client-go 的重要组成部分,在了解 client-go 之前,了解一下 Informer 的实现是很有必要的
主要使用到 Informer 和 workqueue 两个核心组件。Controller 可以有一个或多个 informer 来跟踪某一个 resource。Informter 跟 API server 保持通讯获取资源的最新状态并更新到本地的 cache 中,一旦跟踪的资源有变化,informer 就会调用 callback。把关心的变更的 Object 放到 workqueue 里面。然后 woker 执行真正的业务逻辑,计算和比较 workerqueue 里 items 的当前状态和期望状态的差别,然后通过 client-go 向 API server 发送请求,直到驱动这个集群向用户要求的状态演化
代码路径: vendor/k8s.io/apimachinery/pkg/util/wait/wait.go
wait库内的各种function,大体来说都是以轮询的形式,根据时间间隔、条件判断,来确定工具执行函数是否应被继续执行。按代码中呈现,按触发形式再细化一下,各function则可以分为这几类
条件类型 | 说明 |
---|---|
Until类 | 用得最多的类型,一般以一条chan struct{} 或context Done接收done信号作为终止轮询的依据 |
Backoff类 | 每间隔一定的时长执行一次回溯函数,一般情况下,间隔时长随着回溯次数递增而倍数级延长,但间隔时长也会有上限值 |
poll类 | 两条channel,一条用作传递单次执行信号用来轮询,一条用作传递done信号 |
Untile类型有两个具体实现,分别是Until和UntilWithContext
func Until(f func(), period time.Duration, stopCh <-chan struct{}) {
JitterUntil(f, period, 0.0, true, stopCh)
}
JitterUntil函数可谓是把条件考虑得很细致,参数上有执行周期、抖动因子、窗口期(是否包含函数执行时间),另外在stopCh信号处理上也做到了预防超期执行,JitterUntil函数已经足以应对各类以时间间隔维度的轮询场景了
UntilWithContext
func UntilWithContext(ctx context.Context, f func(context.Context), period time.Duration) {
JitterUntilWithContext(ctx, f, period, 0.0, true)
}
Backoff类
源码剖析:https://helight.cn/blog/2020/kube-controller-manager-code-1/
源码剖析:https://github.com/cloudnativeto/sig-kubernetes/blob/master/docs/event/code-club.md