なぜ最近のAPIはすべてk8sに似てきているのか? — 宣言型リソースモデルが事実上の標準となった本当の理由

「サーバーに『何をすべきか』ではなく、『どうあるべきか』を伝えよ。」

— この一文が、過去10年間でAPI設計の風景を一変させた。

>

>

互いに異なる流れを持っていた数多くのAPI設計方式(川)が、最終的に一つの巨大な標準(海)へと収束。

この記事で扱うこと

  • 伝統的なRESTとk8sスタイルAPIの根本的な違い
  • apiVersion / kind / metadata / spec / status の5つの骨格が持つ力
  • 宣言型(Declarative)モデルとReconciliation Loopの原理
  • Crossplane、ArgoCD、Istio、Knativeまで、なぜすべてk8sスタイルを採用したのか
  • 自分のAPIをこのスタイルで設計する際に必ず押さえるべき実践ポイント

導入 — REST設計者の長年の悩み

REST APIを設計したことがある人なら、一度はこのような悩みを抱えたことがあるだろう。

  • ユーザー作成はPOST /usersでシンプルだが、ユーザーをアクティブ化するのはどこに置くべきか?
  • POST /users/{id}/activate?これは行為(verb)であり、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}             → 삭제해라

クライアントがサーバーに「何をせよ」と指示する。各リクエストは一つの行為(Action)であり、失敗すればクライアントが再試行ロジックを直接組む必要がある。二度呼び出されれば二度実行される可能性もある(冪等性の問題)。

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、Infrastructure as Codeと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に戻る。

このループのおかげで、3つの素晴らしい属性が無料で手に入る。

  • 自己修復(Self-healing) — Podが死んだらコントローラーが自動的に再作成する。クライアントが再試行する必要がない。
  • イベント駆動 — Watch API(GET /apis/…/pods?watch=true)で変更事項をリアルタイムストリーミングで受け取る。ポーリング地獄から解放される。
  • 冪等性 — 同じspecを100回送っても、ループが一度だけ収束させる。

なぜ標準になったのか — 5つの決定的な理由

1. CRDで誰もが同じ形のAPIを作成できる

Custom Resource Definition(CRD)は、「自社のリソースもk8sリソースのように使いたい」という要望を一枚のYAMLで解決してくれる。OpenAPIスキーマ、検証、バージョン管理、Watch — これらすべてが無料で付いてくる。

2. kubectlという統一されたクライアント

kubectl get, kubectl apply, kubectl describe, kubectl logs。リソースが何であれ、コマンド体系は同じだ。新しいツールを学ぶコストはほぼゼロに収束する。

3. GitOpsとの完璧な相性

宣言型であるため、GitリポジトリにYAMLをアップロードし、ArgoCD/Fluxが「望ましい状態」をそのままクラスターに同期する。Gitが単一の真実供給源(Single Source of Truth)となる。

4. Operatorパターンで運用ノウハウをコード化

データベースのバックアップ、Kafkaのリバランス、Redisクラスターのフェイルオーバーといった運用ノウハウをコントローラー内に組み込むことができる。人が深夜3時に起きてSSHする必要がない。

5. エコシステムの複利効果

CrossplaneがAWSリソースをk8sスタイルで抽象化した結果、ArgoCDでAWS S3バケットもGitOpsで管理できるようになった。ツールがツールを呼ぶネットワーク効果。もはや後戻りできない流れだ。

自分で作ってみる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) 別途ワーカー(コントローラー)が実際の状態を監視し、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への移行プロセスを最初から念頭に置き、conversion webhookまたは同等のメカニズムを設計すること。
  • Reconciliationは無料ではない。 ループ周期、バックオフ、エラーハンドリングを誤って実装すると、無限再試行爆弾になる。指数バックオフとデッドレターキューは必須だ。
  • すべてのAPIをk8sスタイルにする必要はない。 単純な参照系CRUD(ブログ投稿リストなど)は、依然として伝統的なRESTの方が適している。状態遷移と長期実行(long-running)タスクがある場合にk8sスタイルが輝く。

✅ まとめ / 締めくくり

k8sスタイルAPIが標準になった理由は、一言で要約できる。

「分散システムの本来的な困難さを、API設計レベルで宣言型に解決したからだ。」

  • 命令型 → 宣言型への転換は、単純な好みの違いではなく、部分的な失敗と収束を扱う工学的な優位性である。
  • apiVersion / kind / metadata / spec / status の5つの骨格は、拡張性と互換性の基盤である。
  • CRD、kubectl、GitOps、Operatorパターンのネットワーク効果により、エコシステムが自己強化(self-reinforcing)段階に突入した。
  • 新しいクラウドプラットフォームやツールを作成する場合、このスタイルに従うことで得られる無料の互換性は計り知れない。

次のステップとしては、Operator SDK / Kubebuilderで直接CRDとControllerを作成してみるか、Crossplaneでクラウドのリソースを宣言型で扱ってみることをお勧めする。一度このモデルに慣れると、伝統的なRESTに戻るのは容易ではないことを体感するだろう。



Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です