为什么如今所有API都开始模仿k8s? — 声明式资源模型成为事实标准背后的真正原因

“告诉服务器’它应该是什么’,而不是’去做什么’。”

— 这一句话在过去十年中改变了API设计的格局。

>

>

无数曾经分道扬镳的API设计方式(河流)最终汇聚成一个巨大的标准(海洋)。

本文涵盖内容

  • 传统REST与k8s风格API的根本区别
  • apiVersion / kind / metadata / spec / status 这五个骨架的力量
  • 声明式(Declarative)模型和协调循环(Reconciliation Loop)的原理
  • Crossplane、ArgoCD、Istio、Knative等项目为何全部采用k8s风格
  • 以这种风格设计API时必须注意的实战要点

引言 — REST设计者的长期困扰

任何设计过REST API的人都可能遇到过这样的困扰:

  • 用户创建用POST /users很简洁,但激活用户应该放在哪里?
  • POST /users/{id}/activate?这是一个动词,是否与REST哲学相悖?
  • 添加角色时,是POST /users/{id}/roles,还是PUT /users/{id}?

这种困扰反复出现的原因很简单。传统REST始于“对资源的CRUD”,但实际的业务逻辑大多是“状态转换”的连续过程。 激活、批准、取消、重试——所有这些都被强行塞进了REST的语法中。

然而,自2015年以来,随着云原生生态系统的爆炸式增长,一场悄然但强大的变革发生了。Kubernetes所展示的API设计方式已成为事实上的标准。 Crossplane、ArgoCD、Istio、Knative、Tekton、KubeVirt——这些耳熟能详的项目都采用了相同的API模式。甚至连AWS Controllers for Kubernetes (ACK)、GCP Config Connector等云服务提供商也开始将自己的资源封装成k8s风格的API

为什么会这样?

命令式 vs 声明式 — 最大的范式转变

传统REST是“命令式(Imperative)”的

POST   /users                  → 생성해라
PUT    /users/{id}/activate    → 활성화해라
POST   /users/{id}/roles       → 역할을 추가해라
DELETE /users/{id}             → 삭제해라

客户端指示服务器“去做什么”。每个请求都是一个动作,如果失败,客户端必须自己编写重试逻辑。两次调用可能会导致两次执行(幂等性问题)。

k8s风格是“声明式(Declarative)”的

apiVersion: v1
kind: User
metadata:
  name: dohyeon
spec:
  active: true
  roles:
    - admin
    - developer

这个YAML通过PUT /apis/v1/users/dohyeon一次性发送。服务器会比较当前状态(status)期望状态(spec),并自行弥补差异。这就是协调循环(Reconciliation Loop)

客户端只需声明“它应该是什么”。激活、添加角色、删除——所有这些都集成到一个端点、一个资源文档中。

为什么这很重要?

分布式系统中最困难的问题是部分失败(partial failure)。网络中断、请求重复、顺序错乱。在命令式API中,要应对这些问题,客户端必须直接管理复杂的有限状态机。然而,声明式API是基于收敛(convergence)设计的,因此即使发送相同的请求多次,结果也是一致的。这就是GitOps、基础设施即代码与k8s风格API完美契合的原因。

k8s API的5个骨架

k8s风格的资源总是包含以下5个字段:

apiVersion: apps/v1        # (1) 哪个API组/版本 (GVK)
kind: Deployment           # (2) 是什么类型的资源
metadata:                  # (3) 通用元数据
  name: web-server
  namespace: production
  labels:
    app: frontend
  annotations:
    team: platform
spec:                      # (4) 期望状态 (用户编写)
  replicas: 3
  selector:
    matchLabels:
      app: frontend
status:                    # (5) 实际状态 (控制器编写)
  replicas: 3
  availableReplicas: 3
  conditions:
    - type: Available
      status: "True"

这个结构蕴含着深刻的哲学。

  • apiVersion + kind — 通过Group/Version/Kind (GVK)唯一标识资源。即使版本升级,现有API也不会被破坏。
  • metadata — 名称、命名空间、标签、注解等所有资源共享的元数据。可以统一进行搜索、过滤和所有权追踪。
  • spec / status 分离 — 将用户编写的区域(spec)和系统编写的区域(status)物理分离。明确了谁对什么负责。

只要遵循这5个骨架,kubectl、Helm、ArgoCD、Crossplane等现有工具就能直接工作。这就是标准化的复利效应。

协调循环(Reconciliation Loop) — 简单却最强大的思想

k8s的核心是控制器(Controller)。控制器会不断地运行这样的循环:

  1. 观察当前状态(status)。
  2. 与期望状态(spec)进行比较。
  3. 如果存在差异,则采取行动来缩小差异。
  4. 返回步骤1。

由于这个循环,三个惊人的特性随之而来:

  • 自我修复(Self-healing) — 如果Pod宕机,控制器会自动重新创建。客户端无需重试。
  • 事件驱动 — 通过Watch API (GET /apis/…/pods?watch=true)实时接收变更流。摆脱了轮询地狱。
  • 幂等性 — 即使发送相同的spec 100次,循环也只会收敛一次。

为何成为标准 — 5个决定性原因

1. CRD让任何人都能创建相同结构的API

自定义资源定义(Custom Resource Definition, CRD)通过一个YAML文件解决了“我们公司的资源也想像k8s资源一样使用”的问题。OpenAPI schema、验证、版本管理、Watch——所有这些都免费提供。

2. kubectl这一统一的客户端

kubectl get, kubectl apply, kubectl describe, kubectl logs。无论资源是什么,命令体系都是相同的。学习新工具的成本几乎趋近于零。

3. 与GitOps的完美契合

由于是声明式的,可以将YAML文件上传到Git仓库,ArgoCD/Flux会直接将“期望状态”同步到集群。Git成为单一事实来源(Single Source of Truth)

4. 通过Operator模式将运维经验代码化

数据库备份、Kafka重新平衡、Redis集群故障转移等运维经验可以封装到控制器中。无需人工在凌晨3点起床进行SSH操作。

5. 生态系统的复利效应

当Crossplane以k8s风格抽象AWS资源后,ArgoCD也能通过GitOps管理AWS S3存储桶。工具调用工具的网络效应。这已是不可逆转的趋势。

亲手构建k8s风格API

即使没有Kubernetes,一般的后端系统也可以采用这种风格。让我们通过FastAPI看一个非常简单的例子。

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional, List

app = FastAPI()

class Metadata(BaseModel):
    name: str
    labels: Optional[dict] = {}

class UserSpec(BaseModel):
    active: bool = True
    roles: List[str] = []

class UserStatus(BaseModel):
    phase: str = "Pending"            # Pending | Ready | Failed
    observed_generation: int = 0

class User(BaseModel):
    apiVersion: str = "v1"
    kind: str = "User"
    metadata: Metadata
    spec: UserSpec
    status: Optional[UserStatus] = None

# 用户只发送spec。status由控制器填充。
@app.put("/apis/v1/users/{name}")
def apply_user(name: str, user: User):
    # 1) 保存spec
    # 2) 独立的worker(控制器)观察实际状态并更新status
    return {"applied": user.metadata.name}

# Watch通过SSE/WebSocket提供变更流。

核心是遵守“用户只编写spec,status由系统编写”的边界。只要很好地遵守这个边界,API的职责归属就会清晰得多。

⚠️ 注意事项 / 常见错误

  • 不要让用户写入status。 如果spec/status分离原则被打破,协调循环的基础就会动摇。必须在服务器端阻止对status的写入。
  • 不要混用命令式端点。 一旦添加POST /users/{id}/activate之类的东西,声明式方法的优势就会减半。首先考虑激活是否可以用spec.active: true来表达。
  • 版本兼容性通过GVK管理。 从一开始就考虑v1beta1 → v1的转换过程,并设计转换webhook或等效机制。
  • 协调并非免费。 如果循环周期、退避策略和错误处理设计不当,可能会导致无限重试炸弹。指数退避和死信队列是必需的
  • 并非所有API都需要采用k8s风格。 简单的查询性CRUD(如博客文章列表)仍然更适合传统REST。当存在状态转换和长期运行(long-running)任务时,k8s风格才能大放异彩。

✅ 总结 / 结束语

k8s风格API成为标准的原因可以一言以蔽之:

“它在API设计层面以声明式方式解决了分布式系统的本质难题。”

  • 命令式 → 声明式转变不仅仅是偏好差异,而是处理部分失败和收敛的工程优势
  • apiVersion / kind / metadata / spec / status 这五个骨架是可扩展性和兼容性的基础
  • CRD、kubectl、GitOps和Operator模式的网络效应使生态系统进入自我强化(self-reinforcing)阶段。
  • 如果正在构建新的云平台或工具,遵循这种风格所获得的免费兼容性是巨大的。

下一步,建议尝试使用Operator SDK / Kubebuilder直接创建CRD和Controller,或者使用Crossplane以声明式方式管理云资源。一旦熟悉了这种模型,你会发现很难再回到传统的REST。



Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注