「サーバーに『何をすべきか』ではなく、『どうあるべきか』を伝えよ。」
— この一文が、過去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)だ。コントローラーはこのようなループを無限に回る。
- 現在の状態(status)を観察する。
- 望ましい状態(spec)と比較する。
- 違いがあれば、その違いを減らす行動をする。
- 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に戻るのは容易ではないことを体感するだろう。

コメントを残す