K8s 二次开发漫游录

目录

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 对象的组织方式,其实是层层递进的:

bash 复制代码
apiVersion: batch/v2alpha1
kind: CronJob

CronJob 就是这个 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 时:

bash 复制代码
API Server 把这个 Token 封装成 TokenReview 请求。
发送到外部认证服务(Webhook)。
认证服务 验证 Token 是否有效,并返回结果(用户名、UID、群组)。
API Server 收到结果后,认为该用户身份就是认证服务返回的身份,再走 授权(RBAC)。

细化一下流程:

1)用户请求 API Server,比如用户运行

bash 复制代码
kubectl get pods --token=ABC123
//解释
这里 ABC123 就是用户凭证(Token)。
API Server 自己不会直接判断这个 Token 对不对,
而是打包成一个 TokenReview 请求发给外部服务。

2)API Server 构造请求(TokenReview)

API Server → 外部服务(Webhook):

bash 复制代码
POST https://authn.example.com/authenticate
Content-Type: application/json
{
  "apiVersion": "authentication.k8s.io/v1beta1",
  "kind": "TokenReview",
  "spec": {
    "token": "ABC123"
  }
}

3)Webhook 服务处理(需要自己开发)

bash 复制代码
...
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 文件描述。

bash 复制代码
apiVersion: v1
kind: Config
clusters:
- name: github-authn
  cluster:
    server: http://xxxxxx:3000/authenticate   # 外部服务地址
users:
- name: authn-apiserver
  user:
    token: secret
contexts:
- name: webhook
  context:
    cluster: github-authn
    user: authn-apiserver
current-context: webhook

API Server 启动时指定:

bash 复制代码
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

bash 复制代码
"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: app
    image: my-web-server:latest
    volumeMounts:
    - name: log-volume
      mountPath: /var/log
  volumes:
  - name: log-volume
    emptyDir: {}
    
====Sidecar Webhook 自动注入后====
spec:
  containers:
  - name: app
    image: my-web-server:latest
    volumeMounts:
    - name: log-volume
      mountPath: /var/log
  - name: sidecar-filebeat
    image: filebeat:latest
    volumeMounts:
    - name: log-volume
      mountPath: /var/log
volumes:
- name: log-volume
  emptyDir: {}

看了上面的例子,你可能会说那我自己写好不行吗?为什么非要注入?
用户自己写可以,但问题是:繁琐、容易出错。
用户写 Pod/Deployment YAML 时,本来只想关心业务容器,
比如 nginx、app-server。如果每个 Pod 都要手工加上 日志收集 sidecar、
安全代理 sidecar、服务网格 sidecar...写 YAML 的复杂度会大大增加,
也会出现容易出现配置不一致的情况。

主要的步骤如下:

1)注册 Webhook(MutatingWebhookConfiguration)

bash 复制代码
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: sidecar-injector
  annotations:
    cert-manager.io/inject-ca-from: default/sidecar-injector-serving-cert
webhooks:
- name: sidecar.mutate.example.com
  admissionReviewVersions: ["v1"]
  sideEffects: None
  failurePolicy: Fail
  clientConfig:
    service:
      name: sidecar-injector-svc
      namespace: default
      path: /mutate  #请求的路径 
    caBundle: <自动注入或手工填入CA>
  rules:
  - operations: ["CREATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
  namespaceSelector:
    matchLabels:
      sidecar-injection: "enabled"

2)编写Mutating Webhook

bash 复制代码
// POST /mutate
func handleMutate(w http.ResponseWriter, r *http.Request) {
    var ar admissionv1.AdmissionReview
    json.NewDecoder(r.Body).Decode(&ar)

    // 反序列化出 Pod
    var pod corev1.Pod
    json.Unmarshal(ar.Request.Object.Raw, &pod)

    // 若已注入则跳过,保证幂等
    for _, c := range pod.Spec.Containers {
        if c.Name == "sidecar-logger" { returnAllow(ar, w, nil) }
    }

    // JSONPatch:追加一个容器/或追加 label
    patch := []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)

bash 复制代码
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: pod-security-guard
  annotations:
    cert-manager.io/inject-ca-from: default/pod-guard-serving-cert
webhooks:
- name: pod.guard.example.com
  admissionReviewVersions: ["v1"]
  sideEffects: None
  failurePolicy: Fail
  clientConfig:
    service:
      name: pod-guard-svc
      namespace: default
      path: /validate  #请求路径
  rules:
  - operations: ["CREATE","UPDATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]

2)编写Validating Webhook

bash 复制代码
// POST /validate
func handleValidate(w http.ResponseWriter, r *http.Request) {
    var ar admissionv1.AdmissionReview
    json.NewDecoder(r.Body).Decode(&ar)
    
    var pod corev1.Pod
    json.Unmarshal(ar.Request.Object.Raw, &pod)

    // 校验:所有容器必须 runAsNonRoot
    for _, 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。

bash 复制代码
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/...)
       |
       v
  API Aggregator (代理层)
       |
       v
  你的自定义 APIServer (实现 foos 资源)
       |
       v
  你定义的存储逻辑 (MySQL, Redis, etc.)

举例:

假设你想在 Kubernetes 里管理一本书的清单(Book),但你不想把数据放到 etcd,而是放到内存/外部数据库。这就很适合用自定义 APIServer。因为CRD是最常见扩展 Kubernetes API的方式,很适合存声明式配置,但只能存到 etcd,逻辑受限。

1)执行获取清单的命令

bash 复制代码
kubectl get books.mycompany.com
#返回结果
NAME     TITLE                    AUTHOR
book1    Kubernetes Patterns      Bilgin Ibryam
book2    Programming Kubernetes   Michael Hausenblas

2)注册 Aggregated API Server

首先需要一个 APIService,让主 APIServer 把请求转发到我们的服务。

bash 复制代码
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1.mycompany.com
spec:
  group: mycompany.com
  version: v1
  service:
    name: book-apiserver
    namespace: default
  groupPriorityMinimum: 2000
  versionPriority: 15

3)编写自定义 APIServer代码

bash 复制代码
package main

import (
	"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 Books
		if 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 Book
		for _, 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)整理链路

bash 复制代码
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资源:

bash 复制代码
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition <-类型
metadata:
  name: nginxdeployments.example.com
spec:
  group: example.com
  names:
    kind: NginxDeployment <-类型
    plural: nginxdeployments
    singular: nginxdeployment
  scope: Namespaced
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                replicas:
                  type: integer

定义CR资源:

bash 复制代码
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 会自动创建,是因为:

  1. 控制器代码里写了"收到 CR → 调用 Kubernetes API → 创建资源"。
  2. 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:

  1. 使用 SDK 创建一个新的 Operator 项目
  2. 通过添加自定义资源(CRD)定义新的资源 API
  3. 指定使用 SDK API 来 watch 的资源
  4. 定义 Operator 的协调(reconcile)逻辑
  5. 使用 Operator SDK 构建并生成 Operator 部署清单文件
4)举例说明

在 Kubernetes 里,部署一个 Web 应用通常需要:

a)Deployment(定义镜像、副本、环境变量等)。

b)Service(把 Pod 暴露成一个稳定的访问入口,ClusterIP / NodePort / LoadBalancer)。

c)Ingress(如果要 HTTP 域名访问,还需要写 Ingress 规则)。

也就是说,用户要写 三份 YAML,每份 YAML 里还要保持一些字段的一致性(比如 labels)。

bash 复制代码
===Deployment.yaml===
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myblog
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myblog
  template:
    metadata:
      labels:
        app: myblog
    spec:
      containers:
      - name: myblog
        image: myrepo/myblog:1.0
        ports:
        - containerPort: 8080
        env:
        - name: BLOG_DB_HOST
          value: mysql.default.svc.cluster.local

===Service.yaml===
apiVersion: v1
kind: Service
metadata:
  name: myblog-svc
spec:
  selector:
    app: myblog
  ports:
  - port: 80
    targetPort: 8080
  type: NodePort

===Ingress.yaml===
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myblog-ing
spec:
  rules:
  - host: blog.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myblog-svc
            port:
              number: 80

看了上面的过程是不是觉得特特特别麻烦?! 我们就可以创建一个自定义的资源对象,通过我们的 CRD 来描述我们要部署的应用信息,比如镜像、服务端口、环境变量等等,然后创建我们的自定义类型的资源对象的时候,通过控制器去创建对应的 Deployment 和 Service,是不是就方便很多了,相当于我们用一个资源清单去描述了 Deployment 和 Service 要做的两件事情。

我们定义一个 CRD:WebApp,用来一次性描述应用:

bash 复制代码
# webapp-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: webapps.mycompany.com
spec:
  group: mycompany.com
  names:
    kind: WebApp
    plural: webapps
    singular: webapp
    shortNames: ["wa"]
  scope: Namespaced
  versions:
  - name: v1
    served: true
    storage: true
    schema:            # OpenAPI v3 schema
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            required: ["image","port","replicas"]
            properties:
              image: { type: string }
              replicas: { type: integer, minimum: 1 }
              port: { type: integer, minimum: 1, maximum: 65535 }
              host: { type: string }          
          status:
            type: object
            properties:
              readyReplicas: { type: integer }

# my-webapp.yaml
apiVersion: mycompany.com/v1
kind: WebApp
metadata:
  name: myblog
  namespace: demo
spec:
  image: nginx:1.25
  replicas: 2
  port: 8080
  host: blog.example.com

这个 YAML 只关心业务信息(镜像、副本、端口、域名),不用管 Deployment/Service/Ingress 的细节。然后我们写一个 Controller(Operator):

监听 WebApp CR 的变化;自动创建对应的 Deployment、Service、Ingress。这样只写一个资源,系统自动完成三件事。
开发过程:

使用 operator-sdk new 命令创建新的 Operator 项目后,项目目录就包含了很多生成的文件夹和文件。还需要添加 api 的定义以及对应的 controller 实现,这样整个 Operator 项目的脚手架就已经搭建完成了,接下来就是具体的实现了(搭建项目的详细过程省略)。

1)Go 类型定义

bash 复制代码
// api/v1/webapp_types.go  《-路径位置
package v1

import (
  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 存在;如果不存在则创建。

bash 复制代码
// 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.Client
  Scheme *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) 读取 WebApp
  var wa myv1.WebApp
  if 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.Name
  var dep appsv1.Deployment
  if 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.Name
  var svc corev1.Service
  if 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.Ingress
    if 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.PathTypePrefix
  return 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)即可。

bash 复制代码
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-controller
      containers:
      - name: manager
        image: your-registry.example.com/webapp-operator:v0.1.0 #远程仓库地址
        imagePullPolicy: IfNotPresent
        args:
          - "--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文件系统)

相关推荐
运维开发王义杰4 小时前
Kubernetes: 解构Karpenter NodePool, 云原生时代的弹性节点管理艺术
云原生·容器·kubernetes
FreeBuf_4 小时前
无恶意软件勒索:Storm-0501如何转向云原生攻击
大数据·云原生·storm
Clownseven4 小时前
Prometheus+Grafana入门教程:从零搭建云原生服务器监控系统
云原生·grafana·prometheus
上邪o_O4 小时前
从零开始部署 Kubernetes Dashboard:可视化管理你的集群
云原生·kubernetes
草履虫建模4 小时前
若依微服务一键部署(RuoYi-Cloud):Nacos/Redis/MySQL + Gateway + Robot 接入(踩坑与修复全记录)
redis·mysql·docker·微服务·云原生·nacos·持续部署
Light606 小时前
领码前瞻|国产操作系统闯关之路:从创新到应用
云原生·国产操作系统·ai赋能·自主可控·生态建设
草莓田田圈~6 小时前
kubernetes-ubuntu24.04操作系统部署k8s集群
云原生·容器·kubernetes
chenglin0167 小时前
架构设计——云原生与分布式系统架构
云原生·架构
007php0078 小时前
Go 语言常用命令使用与总结
java·linux·服务器·前端·数据库·docker·容器