K8s 二次开发漫游录
目录
- 1、针对APIServer的开发
- 1)声明式API
- 2)认证阶段
- 3)Mutating Webhook
- 4)Validating Webhook
- 5)两种Webhook差异
- 6)Aggregated API Servers
- 2、针对Operator的开发
- 1)CRD/CR
- 2)Operator
- 3)Operator Framework
- 4)举例说明
- 3、推荐阅读
1、针对APIServer的开发
整体的APIServer总览图如下:
上图每个组件的简单说明:
AuthN (Authentication)
认证:验证用户身份(比如 kubeconfig 里的证书、token、OIDC)。
如果需要,会调用外部 AuthService来完成认证。
通过后,API Server 知道这是哪个用户。
Rate Limit
限速:避免 API Server 被单一用户/客户端请求打爆。
Auditing
审计:把 API 请求相关的信息记录下来,写到审计日志。
AuthZ (Authorization)
鉴权:确认用户是否有权限操作资源,结合 Kubernetes RBAC(角色访问控制)做权限判断。
Aggregator
聚合层:如果目标资源不是由核心 API Server 提供,而是某个 扩展的 Aggregated API Server 提供,就会转发过去,比如一些自定义资源 (CRD) 或扩展 API。
Mutating Webhook
可变更准入控制器:在资源落库之前,可以修改对象。例如:自动为 Pod 注入 sidecar 容器(如 Istio 注入 Envoy),通过调用外部的 Mutating Webhook Service。
Schema Validation
内建 schema 校验:比如必填字段、数据类型、CRD 定义里的 OpenAPI schema 校验。
Validating Webhook
验证型准入控制器:用于拦截非法请求,但不修改对象。例如:禁止没有资源配额的 Pod 创建。,会调用外部 Validating Webhook Service。
1)声明式API
问题:
把一个 YAML 文件提交给 k8s之后,是如何创建出一个 API 对象的呢?
在 K8s 项目中,一个 API 对象在 Etcd 里的完整资源路径,是由:Group(API
组)、Version(API 版本)和 Resource(API 资源类型)三个部分组成的。
Kubernetes 里 API 对象的组织方式,其实是层层递进的:
apiVersion: batch/v2alpha1
kind: CronJobCronJob 就是这个 API 对象的资源类型(Resource),
batch 就是它的组(Group),
v2alpha1 就是它的版本(Version)。
解析过程:
首先,Kubernetes 会匹配 API 对象的组,而对于 Kubernetes 里的核心 API 对象,比如:Pod、Node 等,是不需要Group 的(它们 Group 是“”)。所以,对于这些 API 对象来说,Kubernetes 会直接在 /api 这个层级进行下一步的匹配过程。而对于 CronJob 等非核心 API 对象来说,Kubernetes 就必须在 /apis 这个层级里查找它对应的 Group,进而根据“batch”这个 Group 的名字,找到 /apis/batch。然后,Kubernetes 会进一步匹配到 API 对象的版本号。在 Kubernetes 中,同一种 API 对象可以有多个版本;这样,比如在 CronJob 的开发过程中,对于会影响到用户的变更就可以通过升级新版本来处理,从而保证了向后兼容。最后,Kubernetes 会匹配 API 对象的资源类型。
举例:
首先,当发起了创建 CronJob 的 POST 请求之后,编写 YAML 的信息就被提交给了 APIServer。而 APIServer 的第一个功能,就是过滤这个请求,并完成一些前置性的工作,比如授权、超时处理、审计等。然后,请求会进入 MUX 和 Routes 流程。MUX 和 Routes 是 APIServer 完成 URL 和 Handler 绑定的场所。而 APIServer 的Handler 要做的事情,按照路径匹配找到对应的 CronJob 类型定义。
接着,APIServer 最重要的职责就来了:根据这个 CronJob 类型定义,使用用户提交的YAML 文件里的字段,创建一个 CronJob 对象。而在这个过程中,APIServer 会进行一个 Convert 工作,即:把用户提交的 YAML 文件,转换成一个叫作 Super Version 的对象,它正是该 API 资源类型所有版本的字段全集。
这样用户提交的不同版本的 YAML 文件,就都可以用这个 Super Version 对象来进行处理了。接下来,APIServer 会先后进行 Admission() 和 Validation() 操作。Validation,则负责验证这个对象里的各个字段是否合法。这个被验证过的 API 对象,都保存在了 APIServer 里一个叫作 Registry 的数据结构中。最后,APIServer 会把验证过的 API 对象转换成用户最初提交的版本,进行序列化操作,并调用Etcd 的 API 把它保存起来
2)认证阶段
对于开发AuthService来说,就是认证这个工作给外包出去。
举个例子:
1)旅客(用户)出示登机牌(Token)。
2)安检员(API Server)自己不认登机牌真假。
3)安检员把登机牌拿去问航空公司柜台(外部认证服务 Webhook)。
4)请求内容 = TokenReview(“这个登机牌号对不对?属于谁?”)
5)航空公司柜台(Webhook 服务)查数据库后说:
6)“这个登机牌有效,乘客是张三,VIP 用户。”
7)安检员(API Server)确认后,允许张三进入候机区。
8)后续是否能进 VIP 贵宾厅,则要看 RBAC(授权规则)。
Kubernetes 本身不负责验证用户提供的 Token 是否有效,它可以“外包”给一个外部服务。当用户带着 Token 调 API Server 时:
API Server 把这个 Token 封装成 TokenReview 请求。
发送到外部认证服务(Webhook)。
认证服务 验证 Token 是否有效,并返回结果(用户名、UID、群组)。
API Server 收到结果后,认为该用户身份就是认证服务返回的身份,再走 授权(RBAC)。
细化一下流程:
1)用户请求 API Server,比如用户运行
kubectl get pods --token=ABC123
//解释
这里 ABC123 就是用户凭证(Token)。
API Server 自己不会直接判断这个 Token 对不对,
而是打包成一个 TokenReview 请求发给外部服务。
2)API Server 构造请求(TokenReview)
API Server → 外部服务(Webhook):
POST https://authn.example.com/authenticate
Content-Type: application/json
{"apiVersion": "authentication.k8s.io/v1beta1","kind": "TokenReview","spec": {"token": "ABC123"}
}
3)Webhook 服务处理(需要自己开发)
...
decoder := json.NewDecoder(r.Body)
var tr authentication.TokenReview
err := decoder.Decode(&tr)
if err != nil {// 解码失败 → 直接返回认证失败
}
...
然后它会验证这个 tr.Spec.Token。比如:
去 GitHub/GitLab API 验证 OAuth Token。或者去公司内部 SSO 系统查。
...
如果验证成功:
trs := authentication.TokenReviewStatus{Authenticated: true,User: authentication.UserInfo{Username: "janedoe@example.com",UID: "42",Groups: []string{"developers", "qa"},},
}
返回给 API Server:
{"apiVersion": "authentication.k8s.io/v1beta1","kind": "TokenReview","status": {"authenticated": true,"user": {"username": "xiaomi@example.com","uid": "42","groups": ["developers", "qa"]}}
}
4)配置认证服务(kube-apiserver 启动参数)
API Server 必须知道外部服务的地址,用一个 kubeconfig 文件描述。
apiVersion: v1
kind: Config
clusters:
- name: github-authncluster:server: http://xxxxxx:3000/authenticate # 外部服务地址
users:
- name: authn-apiserveruser:token: secret
contexts:
- name: webhookcontext:cluster: github-authnuser: authn-apiserver
current-context: webhook
API Server 启动时指定:
kube-apiserver \--authentication-token-webhook-config-file=/etc/kubernetes/authn-webhook.kubeconfig
【注意】
Kubernetes 集成外部认证时,如果没有“熔断 + 限流”,一旦外部服务出问题,K8S 的海量请求会让对方彻底恢复不了。
比如基于Keystone的认证插件导致Keystone故障且无法恢复的场景:
(1) 正常时
Kubernetes API Server 配置了 Keystone 认证插件。
每个用户请求(kubectl、Controller 调 API)都会拿着 Token 去 Keystone 验证。
Keystone 响应 200 OK → Kubernetes 继续处理。
(2) Keystone 出现故障
比如 Keystone 自己挂了 / 连接 DB 失败 / 内部服务抖动。
Keystone 开始返回 401 Unauthorized 或响应超时。
(3) Kubernetes 的反应
API Server / Controller 每个请求都去 Keystone 校验 Token。
收到 401 → 认为是 Token 过期或无效 → 再去 Keystone 重试认证。
控制器里还可能有指数退避(backoff),但外层库(gophercloud)针对 token 过期会 一直 retry。
结果:重试逻辑把请求雪崩式推到 Keystone,Keystone 压力更大 → 彻底宕机。
正反馈循环:
Keystone 出错 → K8S 认为 Token 过期 → 更多重试。
更多请求 → Keystone 压力更大 → 错误率更高。
最终 = 双边系统都死锁。
解决办法:
1)Circuit Breaker(熔断)
类似于电路里的保险丝:电流过大就断开,防止烧坏设备。
当检测到外部依赖(Keystone)持续失败,就临时熔断对它的调用。连续几次失败 → 打开熔断器 → 不再把请求发给 Keystone,而是立即返回错误或降级结果。一段时间后再尝试少量请求探测,如果 Keystone 恢复 → 关闭熔断器。
2)Rate Limit(限流)
类似于高速公路的匝道限流:控制车流进入,避免主干道完全堵死。
每秒最多 100 个请求,多余的直接丢弃或排队,保护 Keystone 不被瞬间压垮。
3)Mutating Webhook
举例:为 Pod 自动注入一个 sidecar
“Sidecar”原指摩托车侧边的副车厢,在 Kubernetes 架构里则代表一种设计模式:
把额外的功能模块“挂载”到主应用容器旁边,与主容器共用 Pod 内的网络和存储空间。
它们通常以一个独立的容器运行。
比如日志收集,主应用写日志,Sidecar 可以实时读取并转发到Fluentd、Elasticsearch等平台。
比如你有一个运行 Web Server 的应用容器 app,它产生日志写到 /var/log/app.log。
你希望这些日志被收集到集中的日志系统。但你不想在主应用中嵌入日志 SDK
或者列表代码,这时候引入一个 Sidecar 是典型方案。从yaml文件角度来说则这个例子:
====你自己提交的yaml====
apiVersion: v1
kind: Pod
metadata:name: webapp
spec:containers:- name: appimage: my-web-server:latestvolumeMounts:- name: log-volumemountPath: /var/logvolumes:- name: log-volumeemptyDir: {}====Sidecar Webhook 自动注入后====
spec:containers:- name: appimage: my-web-server:latestvolumeMounts:- name: log-volumemountPath: /var/log- name: sidecar-filebeatimage: filebeat:latestvolumeMounts:- name: log-volumemountPath: /var/log
volumes:
- name: log-volumeemptyDir: {}看了上面的例子,你可能会说那我自己写好不行吗?为什么非要注入?
用户自己写可以,但问题是:繁琐、容易出错。
用户写 Pod/Deployment YAML 时,本来只想关心业务容器,
比如 nginx、app-server。如果每个 Pod 都要手工加上 日志收集 sidecar、
安全代理 sidecar、服务网格 sidecar…写 YAML 的复杂度会大大增加,
也会出现容易出现配置不一致的情况。
主要的步骤如下:
1)注册 Webhook(MutatingWebhookConfiguration)
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:name: sidecar-injectorannotations:cert-manager.io/inject-ca-from: default/sidecar-injector-serving-cert
webhooks:
- name: sidecar.mutate.example.comadmissionReviewVersions: ["v1"]sideEffects: NonefailurePolicy: FailclientConfig:service:name: sidecar-injector-svcnamespace: defaultpath: /mutate #请求的路径 caBundle: <自动注入或手工填入CA>rules:- operations: ["CREATE"]apiGroups: [""]apiVersions: ["v1"]resources: ["pods"]namespaceSelector:matchLabels:sidecar-injection: "enabled"
2)编写Mutating Webhook
// POST /mutate
func handleMutate(w http.ResponseWriter, r *http.Request) {var ar admissionv1.AdmissionReviewjson.NewDecoder(r.Body).Decode(&ar)// 反序列化出 Podvar pod corev1.Podjson.Unmarshal(ar.Request.Object.Raw, &pod)// 若已注入则跳过,保证幂等for _, c := range pod.Spec.Containers {if c.Name == "sidecar-logger" { returnAllow(ar, w, nil) }}// JSONPatch:追加一个容器/或追加 labelpatch := []map[string]interface{}{{"op": "add","path": "/spec/containers/-","value": map[string]interface{}{"name":"sidecar-logger","image":"busybox","args":[]string{"sh","-c","tail -f /var/log/app.log"},},},// 给 metadata.labels 增加一个键等逻辑}patchBytes,_ := json.Marshal(patch)returnAllow(ar, w, patchBytes) // AdmissionResponse.Patch = patchBytes, PatchType=JSONPatch
}
4)Validating Webhook
举例:禁止没有安全设置的 Pod
1)注册 Webhook(ValidatingWebhookConfiguration)
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:name: pod-security-guardannotations:cert-manager.io/inject-ca-from: default/pod-guard-serving-cert
webhooks:
- name: pod.guard.example.comadmissionReviewVersions: ["v1"]sideEffects: NonefailurePolicy: FailclientConfig:service:name: pod-guard-svcnamespace: defaultpath: /validate #请求路径rules:- operations: ["CREATE","UPDATE"]apiGroups: [""]apiVersions: ["v1"]resources: ["pods"]
2)编写Validating Webhook
// POST /validate
func handleValidate(w http.ResponseWriter, r *http.Request) {var ar admissionv1.AdmissionReviewjson.NewDecoder(r.Body).Decode(&ar)var pod corev1.Podjson.Unmarshal(ar.Request.Object.Raw, &pod)// 校验:所有容器必须 runAsNonRootfor _, c := range pod.Spec.Containers {if c.SecurityContext == nil || c.SecurityContext.RunAsNonRoot == nil || !*c.SecurityContext.RunAsNonRoot {returnDeny(ar, w, "all containers must set securityContext.runAsNonRoot=true")return}}returnAllow(ar, w, nil)
}
5)两种Webhook差异
6)Aggregated API Servers
Aggregated API Servers解释:
Kubernetes 的 API 是通过 APIServer 提供的,所有资源都走 /api 和 /apis 路径。
默认 APIServer 支持内置资源(Pod/Service/Deployment 等)。如果想扩展资源类型,最简单的是 CRD:写一个 CustomResourceDefinition,API Server 自动识别并存 etcd。但是,有时候 CRD 不够用(性能/存储/功能限制),就可以自己实现一个独立的 API Server,然后把它 聚合到主 APIServer 的 API 路径下。
主 APIServer 就像一个“反向代理”,收到请求会转发到你自定义的 APIServer。
CRD 适合大部分声明式配置型资源,但有一些场景 CRD 不行:
1)复杂存储需求
CRD 的存储必须用 etcd,而你可能需要 PostgreSQL、Elasticsearch、时序数据库。
例如:存储上百万 IoT 设备的实时数据,etcd 会崩溃。
2)高频读写
CRD 每次变更都会触发 etcd 写入 + watch 广播,性能瓶颈明显。
3)自定义协议/逻辑
CRD 只能做基本的 CRUD 校验,复杂逻辑要靠 Admission Webhook 或 Operator 补充。自定义 APIServer 具备完全的控制权:
自己定义资源的 API(Group/Version/Kind)
自己决定存储方式(DB/文件/外部系统)
实现自己的逻辑//流程图
用户 → kubectl get foos|v主 APIServer (/apis/...)|vAPI Aggregator (代理层)|v你的自定义 APIServer (实现 foos 资源)|v你定义的存储逻辑 (MySQL, Redis, etc.)
举例:
假设你想在 Kubernetes 里管理一本书的清单(Book),但你不想把数据放到 etcd,而是放到内存/外部数据库。这就很适合用自定义 APIServer。因为CRD是最常见扩展 Kubernetes API的方式,很适合存声明式配置,但只能存到 etcd,逻辑受限。
1)执行获取清单的命令
kubectl get books.mycompany.com
#返回结果
NAME TITLE AUTHOR
book1 Kubernetes Patterns Bilgin Ibryam
book2 Programming Kubernetes Michael Hausenblas
2)注册 Aggregated API Server
首先需要一个 APIService,让主 APIServer 把请求转发到我们的服务。
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:name: v1.mycompany.com
spec:group: mycompany.comversion: v1service:name: book-apiservernamespace: defaultgroupPriorityMinimum: 2000versionPriority: 15
3)编写自定义 APIServer代码
package mainimport ("encoding/json""fmt""net/http"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)// Book 类型
type Book struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec BookSpec `json:"spec,omitempty"`
}type BookSpec struct {Title string `json:"title"`Author string `json:"author"`
}// BookList 类型
type BookList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata,omitempty"`Items []Book `json:"items"`
}// 内存存储
var books = []Book{{TypeMeta: metav1.TypeMeta{Kind: "Book",APIVersion: "mycompany.com/v1",},ObjectMeta: metav1.ObjectMeta{Name: "book1"},Spec: BookSpec{Title: "Kubernetes Patterns", Author: "Bilgin Ibryam"},},{TypeMeta: metav1.TypeMeta{Kind: "Book",APIVersion: "mycompany.com/v1",},ObjectMeta: metav1.ObjectMeta{Name: "book2"},Spec: BookSpec{Title: "Programming Kubernetes", Author: "Michael Hausenblas"},},
}// Handler
func booksHandler(w http.ResponseWriter, r *http.Request) {if r.Method == http.MethodGet {// List Booksif r.URL.Path == "/apis/mycompany.com/v1/books" {list := BookList{TypeMeta: metav1.TypeMeta{Kind: "BookList",APIVersion: "mycompany.com/v1",},Items: books,}json.NewEncoder(w).Encode(list)return}// Get Bookfor _, b := range books {if r.URL.Path == fmt.Sprintf("/apis/mycompany.com/v1/books/%s", b.Name) {json.NewEncoder(w).Encode(b)return}}http.NotFound(w, r)}
}func main() {http.HandleFunc("/apis/mycompany.com/v1/books", booksHandler)http.HandleFunc("/apis/mycompany.com/v1/books/", booksHandler)fmt.Println("Starting Book APIServer at :8443 ...")
}
4)整理链路
kubectl get books.mycompany.com│▼
主 APIServer (/apis/mycompany.com/v1/books)-> 查路由表│▼
API Aggregation Layer 发现这是一个聚合 API│▼
转发请求到你的自定义 APIServer Service (book-apiserver.default.svc:443)│▼
你的自定义 APIServer 处理请求,返回 JSON 格式的资源│▼
主 APIServer 把响应回传给 kubectl路由解释:
1)执行kubectl get books.mycompany.com
2)主 APIServer 首先看 “mycompany.com/v1” 这个 API group 有没有内置实现。
发现没有 → 再查 APIService 对象,找到一个叫 v1.mycompany.com 的 APIService,
定义了“这个 APIGroup 的处理逻辑 = 转发到一个后端 Service”。
2、针对Operator的开发
1)CRD/CR
Custom Resource Define 简称 CRD,是 Kubernetes(v1.7+)为提高可扩展性,让开发者去自定义资源的一种方式。CRD 资源可以动态注册到集群中,注册完毕后,用户可以通过 kubectl 来创建访问这个自定义的资源对象,类似于操作 Pod 一样。不过需要注意的是 CRD 仅仅是资源的定义而已,需要一个 Controller 去监听 CRD 的各种事件来添加自定义的业务逻辑。
简单来说:
- CRD (Custom Resource Definition,自定义资源定义)
→ 定义一种新的 API 对象(比如 MySQLCluster、RedisCluster)。 - Controller (控制器)
→ 监听这种自定义对象的变化,并采取行动(比如创建 StatefulSet、Service)。 - Operator = CRD + Controller
→ 本质就是 把人的运维知识固化为程序,交给 K8S 来自动化运维。
CRD = 类(class)
定义了有哪些字段、什么格式、可以做什么。换句话说,就是让k8s认识这个自定义的资源而已。
CR = 对象(instance)
具体的数据实例,比如 my-nginx。
定义一个CRD资源:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition <-类型
metadata:name: nginxdeployments.example.com
spec:group: example.comnames:kind: NginxDeployment <-类型plural: nginxdeploymentssingular: nginxdeploymentscope: Namespacedversions:- name: v1served: truestorage: trueschema:openAPIV3Schema:type: objectproperties:spec:type: objectproperties:replicas:type: integer
定义CR资源:
apiVersion: example.com/v1
kind: NginxDeployment <-类型
metadata:name: my-nginx
spec:replicas: 2
2)Operator
Operator = 用代码扩展 Kubernetes API,把日常人工运维工作自动化。
Deployment 保证“容器在跑”,但不保证“服务是好的”。你用 Deployment 部署 MySQL,只能确保 Pod 一直在跑。但 MySQL 需要建库、初始化数据、做主从复制、监控存活性。这些 Deployment 并不会帮你做。Operator 就是在 Deployment 基础上,加入“领域知识(Domain Knowledge)”。Operator 把人类 SRE/运维工程师的经验,固化成 Controller 逻辑,让 Kubernetes 自动运维。Operator 不是 Kubernetes 内置的“魔法”,它之所以能 自动创建,是因为 我们在 Operator 的控制器(Controller)代码里写了这些动作Operator 会自动创建,是因为:
- 控制器代码里写了“收到 CR → 调用 Kubernetes API → 创建资源”。
- Operator 一直在 watch CR 对象,一旦变化就执行这些动作。
“自动创建” = Operator 控制器代码里定义的行为,而不是 K8S 自己做的魔法。
3)Operator Framework
Operator Framework 是 CoreOS 开源的一个用于快速开发 Operator 的工具包,该框架包含两个主要的部分:
1)Operator SDK: 无需了解复杂的 Kubernetes API 特性,即可让你根据你自己的专业知识构建一个 Operator 应用。
2)Operator Lifecycle Manager(OLM): 帮助你安装、更新和管理跨集群的运行中的所有 Operator
Operator SDK 提供以下工作流来开发一个新的 Operator:
- 使用 SDK 创建一个新的 Operator 项目
- 通过添加自定义资源(CRD)定义新的资源 API
- 指定使用 SDK API 来 watch 的资源
- 定义 Operator 的协调(reconcile)逻辑
- 使用 Operator SDK 构建并生成 Operator 部署清单文件
4)举例说明
在 Kubernetes 里,部署一个 Web 应用通常需要:
a)Deployment(定义镜像、副本、环境变量等)。
b)Service(把 Pod 暴露成一个稳定的访问入口,ClusterIP / NodePort / LoadBalancer)。
c)Ingress(如果要 HTTP 域名访问,还需要写 Ingress 规则)。
也就是说,用户要写 三份 YAML,每份 YAML 里还要保持一些字段的一致性(比如 labels)。
===Deployment.yaml===
apiVersion: apps/v1
kind: Deployment
metadata:name: myblog
spec:replicas: 2selector:matchLabels:app: myblogtemplate:metadata:labels:app: myblogspec:containers:- name: myblogimage: myrepo/myblog:1.0ports:- containerPort: 8080env:- name: BLOG_DB_HOSTvalue: mysql.default.svc.cluster.local===Service.yaml===
apiVersion: v1
kind: Service
metadata:name: myblog-svc
spec:selector:app: myblogports:- port: 80targetPort: 8080type: NodePort===Ingress.yaml===
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:name: myblog-ing
spec:rules:- host: blog.example.comhttp:paths:- path: /pathType: Prefixbackend:service:name: myblog-svcport:number: 80
看了上面的过程是不是觉得特特特别麻烦?! 我们就可以创建一个自定义的资源对象,通过我们的 CRD 来描述我们要部署的应用信息,比如镜像、服务端口、环境变量等等,然后创建我们的自定义类型的资源对象的时候,通过控制器去创建对应的 Deployment 和 Service,是不是就方便很多了,相当于我们用一个资源清单去描述了 Deployment 和 Service 要做的两件事情。
我们定义一个 CRD:WebApp,用来一次性描述应用:
# webapp-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:name: webapps.mycompany.com
spec:group: mycompany.comnames:kind: WebAppplural: webappssingular: webappshortNames: ["wa"]scope: Namespacedversions:- name: v1served: truestorage: trueschema: # OpenAPI v3 schemaopenAPIV3Schema:type: objectproperties:spec:type: objectrequired: ["image","port","replicas"]properties:image: { type: string }replicas: { type: integer, minimum: 1 }port: { type: integer, minimum: 1, maximum: 65535 }host: { type: string } status:type: objectproperties:readyReplicas: { type: integer }# my-webapp.yaml
apiVersion: mycompany.com/v1
kind: WebApp
metadata:name: myblognamespace: demo
spec:image: nginx:1.25replicas: 2port: 8080host: blog.example.com
这个 YAML 只关心业务信息(镜像、副本、端口、域名),不用管 Deployment/Service/Ingress 的细节。然后我们写一个 Controller(Operator):
监听 WebApp CR 的变化;自动创建对应的 Deployment、Service、Ingress。这样只写一个资源,系统自动完成三件事。
开发过程:
使用 operator-sdk new 命令创建新的 Operator 项目后,项目目录就包含了很多生成的文件夹和文件。还需要添加 api 的定义以及对应的 controller 实现,这样整个 Operator 项目的脚手架就已经搭建完成了,接下来就是具体的实现了(搭建项目的详细过程省略)。
1)Go 类型定义
// api/v1/webapp_types.go 《-路径位置
package v1import (metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)# 自定义对象
type WebAppSpec struct {Image string `json:"image"`Replicas int32 `json:"replicas"`Port int32 `json:"port"`Host string `json:"host,omitempty"`
}type WebAppStatus struct {ReadyReplicas int32 `json:"readyReplicas,omitempty"`
}// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type WebApp struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec WebAppSpec `json:"spec,omitempty"`Status WebAppStatus `json:"status,omitempty"`
}// +kubebuilder:object:root=true
type WebAppList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata,omitempty"`Items []WebApp `json:"items"`
}
2)Reconciler(核心逻辑代码)
主要逻辑:监听 WebApp 资源,确保对应的 Deployment、Service、Ingress 存在;如果不存在则创建。
// controllers/webapp_controller.go
package controllers
import ("context"appsv1 "k8s.io/api/apps/v1"corev1 "k8s.io/api/core/v1"networkingv1 "k8s.io/api/networking/v1""k8s.io/apimachinery/pkg/api/errors""k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/types"ctrl "sigs.k8s.io/controller-runtime""sigs.k8s.io/controller-runtime/pkg/client""sigs.k8s.io/controller-runtime/pkg/controller/controllerutil""sigs.k8s.io/controller-runtime/pkg/log"myv1 "example.com/webapp/api/v1"
)type WebAppReconciler struct {client.ClientScheme *runtime.Scheme
}// RBAC:允许操作子资源(Deployment/Service/Ingress)
/*
+kubebuilder:rbac:groups=mycompany.com,resources=webapps,verbs=get;list;watch
+kubebuilder:rbac:groups=mycompany.com,resources=webapps/status,verbs=get;update;patch
+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete
*/func (r *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {log := log.FromContext(ctx)// 1) 读取 WebAppvar wa myv1.WebAppif err := r.Get(ctx, req.NamespacedName, &wa); err != nil {// 被删了就结束return ctrl.Result{}, client.IgnoreNotFound(err)}labels := map[string]string{"app": wa.Name,"webapp": wa.Name,"managed-by": "webapp-operator",}// 2) 确保 Deployment 存在(不存在则创建)depName := wa.Namevar dep appsv1.Deploymentif err := r.Get(ctx, types.NamespacedName{Name: depName, Namespace: wa.Namespace}, &dep); err != nil {if errors.IsNotFound(err) {dep = buildDeployment(&wa, labels)// 让 Deployment/Service 成为 CR 的子资源(OwnerReference),便于级联删除_ = controllerutil.SetControllerReference(&wa, &dep, r.Scheme)if err := r.Create(ctx, &dep); err != nil {log.Error(err, "create Deployment failed")return ctrl.Result{}, err}log.Info("Deployment created", "name", dep.Name)} else {return ctrl.Result{}, err}}// 3) 确保 Service 存在(不存在则创建)svcName := wa.Namevar svc corev1.Serviceif err := r.Get(ctx, types.NamespacedName{Name: svcName, Namespace: wa.Namespace}, &svc); err != nil {if errors.IsNotFound(err) {svc = buildService(&wa, labels)_ = controllerutil.SetControllerReference(&wa, &svc, r.Scheme)if err := r.Create(ctx, &svc); err != nil {log.Error(err, "create Service failed")return ctrl.Result{}, err}log.Info("Service created", "name", svc.Name)} else {return ctrl.Result{}, err}}// 4) 如果指定了 host,则确保 Ingress 存在if wa.Spec.Host != "" {var ing networkingv1.Ingressif err := r.Get(ctx, types.NamespacedName{Name: wa.Name, Namespace: wa.Namespace}, &ing); err != nil {if errors.IsNotFound(err) {ing = buildIngress(&wa, labels)_ = controllerutil.SetControllerReference(&wa, &ing, r.Scheme)if err := r.Create(ctx, &ing); err != nil {log.Error(err, "create Ingress failed")return ctrl.Result{}, err}log.Info("Ingress created", "name", ing.Name)} else {return ctrl.Result{}, err}}}return ctrl.Result{}, nil
}func (r *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error {return ctrl.NewControllerManagedBy(mgr).For(&myv1.WebApp{}).Owns(&appsv1.Deployment{}).Owns(&corev1.Service{}).Owns(&networkingv1.Ingress{}).Complete(r)
}// ------- 构造期望对象 -------func int32p(v int32) *int32 { return &v }func buildDeployment(wa *myv1.WebApp, labels map[string]string) appsv1.Deployment {return appsv1.Deployment{ObjectMeta: v1.ObjectMeta{Name: wa.Name,Namespace: wa.Namespace,Labels: labels,},Spec: appsv1.DeploymentSpec{Replicas: int32p(wa.Spec.Replicas),Selector: &v1.LabelSelector{MatchLabels: map[string]string{"app": wa.Name},},Template: corev1.PodTemplateSpec{ObjectMeta: v1.ObjectMeta{Labels: labels},Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "app",Image: wa.Spec.Image,Ports: []corev1.ContainerPort{{ContainerPort: wa.Spec.Port}},Env: []corev1.EnvVar{}, // 可扩展}},},},},}
}func buildService(wa *myv1.WebApp, labels map[string]string) corev1.Service {return corev1.Service{ObjectMeta: v1.ObjectMeta{Name: wa.Name,Namespace: wa.Namespace,Labels: labels,},Spec: corev1.ServiceSpec{Selector: map[string]string{"app": wa.Name},Ports: []corev1.ServicePort{{Port: wa.Spec.Port, TargetPort: intstr.FromInt(int(wa.Spec.Port)),}},Type: corev1.ServiceTypeClusterIP,},}
}func buildIngress(wa *myv1.WebApp, labels map[string]string) networkingv1.Ingress {pathType := networkingv1.PathTypePrefixreturn networkingv1.Ingress{ObjectMeta: v1.ObjectMeta{Name: wa.Name,Namespace: wa.Namespace,Labels: labels,},Spec: networkingv1.IngressSpec{Rules: []networkingv1.IngressRule{{Host: wa.Spec.Host,IngressRuleValue: networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{Paths: []networkingv1.HTTPIngressPath{{Path: "/",PathType: &pathType,Backend: networkingv1.IngressBackend{Service: &networkingv1.IngressServiceBackend{Name: wa.Name,Port: networkingv1.ServiceBackendPort{Number: wa.Spec.Port},},},}},},},}},},}
}
3)测试/部署
测试没问题之后,需要将自定义的controller部署集群中,主要步骤就是:
构建镜像并推送到远程仓库,部署控制器(Deployment)即可。
apiVersion: apps/v1
kind: Deployment
metadata:name: webapp-controller #自定义的控制器namespace: webapp-system labels: { app: webapp-controller }
spec:replicas: 1 selector:matchLabels: { app: webapp-controller }template:metadata:labels: { app: webapp-controller }spec:serviceAccountName: webapp-controllercontainers:- name: managerimage: your-registry.example.com/webapp-operator:v0.1.0 #远程仓库地址imagePullPolicy: IfNotPresentargs:- "--metrics-bind-address=:8080" - "--leader-elect" ports:- containerPort: 8080 resources:requests: { cpu: 50m, memory: 64Mi }limits: { cpu: 200m, memory: 256Mi }
创建 CRD → 编写Operator代码→ 部署 controller Deployment → 提交 WebApp CR → 控制器自动创建 Deployment/Service/Ingress。
3、推荐阅读
(1)藏在 K8s 幕后的记忆中枢(etcd)
(2)解锁 Docker:一场从入门到源码的趣味解谜之旅
(3)GO的启动流程(GMP模型/内存)
(4)文件系统(dcoker联合文件系统&linux文件系统)