2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Honoで作る Extension API Server 入門

2
Posted at

はじめに

KubernetesのAggregation Layerを使うと、独自のAPIリソースをkube-apiserverに統合できます。本記事では、Honoを使って、シンプルなダミーメールAPI(mail.example.com/v1)を実装し、kubectl get mailsでメール一覧を取得できる環境を構築します。

この記事で学べること

  • Kubernetes Aggregation Layerの基本的な使い方
  • HonoでのカスタムAPIサーバー実装
  • APIServiceリソースによるAPI拡張の仕組み
  • kindクラスタでの動作確認手順

全体の流れ

  1. HonoでMail API(mail.example.com/v1)を実装
  2. Dockerイメージを作成
  3. kindクラスタ作成 & イメージロード
  4. Deployment + Service + APIService をデプロイ
  5. kubectl get mails でメール一覧を取得できることを確認

1. プロジェクトのセットアップ

まず作業ディレクトリを作成します。

mkdir mail-hono-apiserver
cd mail-hono-apiserver

package.jsonの作成

{
  "name": "mail-hono-apiserver",
  "type": "module",
  "dependencies": {
    "hono": "^4.10.7"
  }
}

依存関係をインストール:

pnpm install

2. HonoでAPIサーバーを実装

server.mjsの作成

Kubernetes APIの仕様に準拠したエンドポイントを実装します。

// server.mjs
import { Hono } from 'hono'
import https from 'node:https'
import fs from 'node:fs'

const PORT = 8443

// 自己署名証明書の読み込み(Dockerfileで生成)
const tlsOptions = {
  key: fs.readFileSync('/certs/tls.key'),
  cert: fs.readFileSync('/certs/tls.crt'),
}

const app = new Hono()

// Mailリソースのファクトリー関数
const newMail = ({ name, namespace, subject, from, body, labels }) => {
  const now = new Date().toISOString()
  return {
    apiVersion: 'mail.example.com/v1',
    kind: 'Mail',
    metadata: {
      name,
      namespace,
      resourceVersion: '1',
      uid: `${namespace}-${name}`,
      creationTimestamp: now,
      labels: labels ?? {},
    },
    spec: {
      subject,
      from,
      body,
    },
    status: {
      delivered: true,
      receivedAt: now,
    },
  }
}

// サンプルメールデータ
const listMails = (namespace) => [
  newMail({
    name: 'hello-from-alice',
    namespace,
    subject: 'Hello from Alice',
    from: 'alice@example.com',
    body: 'Hey, how are you?',
    labels: { box: 'inbox' },
  }),
  newMail({
    name: 'k8s-weekly',
    namespace,
    subject: 'Kubernetes Weekly',
    from: 'news@k8s.example.com',
    body: 'This week in Kubernetes...',
    labels: { box: 'inbox' },
  }),
]

// Discovery API: リソース定義を返す
app.get('/apis/mail.example.com/v1', (c) =>
  c.json({
    kind: 'APIResourceList',
    apiVersion: 'v1',
    groupVersion: 'mail.example.com/v1',
    resources: [
      {
        name: 'mails',
        singularName: 'mail',
        namespaced: true,
        kind: 'Mail',
        verbs: ['get', 'list'],
      },
    ],
  })
)

// List API: メール一覧を取得
app.get('/apis/mail.example.com/v1/namespaces/:namespace/mails', (c) => {
  const ns = c.req.param('namespace')
  return c.json({
    kind: 'MailList',
    apiVersion: 'mail.example.com/v1',
    metadata: {},
    items: listMails(ns),
  })
})

// Get API: 単一メールを取得
app.get('/apis/mail.example.com/v1/namespaces/:namespace/mails/:name', (c) => {
  const ns = c.req.param('namespace')
  const name = c.req.param('name')
  return c.json(
    newMail({
      name,
      namespace: ns,
      subject: `Detail of ${name}`,
      from: 'system@example.com',
      body: `Generated Mail for ${name} in ${ns}`,
      labels: { box: 'detail' },
    })
  )
})

// HTTPSサーバーの起動
https
  .createServer(tlsOptions, async (req, res) => {
    try {
      const host = req.headers.host ?? `mail-apiserver`
      const url = `https://${host}${req.url}`

      const request = new Request(url, {
        method: req.method,
        headers: req.headers,
      })

      const honoResponse = await app.fetch(request)

      res.statusCode = honoResponse.status
      honoResponse.headers.forEach((value, key) => {
        res.setHeader(key, value)
      })

      const body = await honoResponse.arrayBuffer()
      res.end(Buffer.from(body))
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      res.end('internal error')
    }
  })
  .listen(process.env.PORT || 8443, () => {
    console.log('Server running on port 443')
  })

実装のポイント

  • Discovery API (/apis/mail.example.com/v1): kubectlがリソース情報を取得するために使用
  • List API: namespace内の全メールを取得
  • Get API: 特定のメールを名前で取得
  • KubernetesのAPIの仕様に準拠したレスポンス構造(metadata、spec、status)

3. Dockerイメージのビルド

Dockerfileの作成

FROM node:24-alpine

WORKDIR /app

RUN apk add --no-cache openssl

COPY package.json ./
RUN npm install --omit=dev

COPY server.mjs .

# デモ用の自己署名証明書を生成
RUN mkdir -p /certs && \
    openssl req -x509 -newkey rsa:2048 \
      -keyout /certs/tls.key \
      -out /certs/tls.crt \
      -days 3650 -nodes \
      -subj "/CN=mail-apiserver"

EXPOSE 8443

CMD ["node", "server.mjs"]

イメージのビルド

docker build -t mail-hono-apiserver:latest .

本番環境では、CA署名済み証明書を使用してください。今回は検証目的のため自己署名証明書を使用しています。

4. kindクラスタのセットアップ

クラスタ作成とイメージロード

# kindクラスタの作成
kind create cluster --name mail-demo

# ビルドしたイメージをクラスタにロード
kind load docker-image mail-hono-apiserver:latest --name mail-demo

5. Kubernetesリソースのデプロイ

マニフェストの作成

# mail-apiserver.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mail-apiserver
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mail-apiserver
  template:
    metadata:
      labels:
        app: mail-apiserver
    spec:
      containers:
        - name: mail-apiserver
          image: mail-hono-apiserver:latest
          imagePullPolicy: Never
          ports:
            - name: https
              containerPort: 8443
---
apiVersion: v1
kind: Service
metadata:
  name: mail-apiserver
  namespace: default
spec:
  selector:
    app: mail-apiserver
  ports:
    - name: https
      port: 443
      targetPort: 8443
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1.mail.example.com
spec:
  group: mail.example.com
  version: v1
  service:
    name: mail-apiserver
    namespace: default
    port: 443
  # デモ用に証明書検証をスキップ
  insecureSkipTLSVerify: true
  groupPriorityMinimum: 1000
  versionPriority: 10

APIServiceについて

APIServiceリソースを作成すると、Aggregation Layerが/apis/mail.example.com/v1/...へのすべてのリクエストを登録されたサービスにプロキシします。

  • insecureSkipTLSVerify: true: 自己署名証明書の検証をスキップ(本番環境では非推奨)
  • groupPriorityMinimumversionPriority: API検出時の優先順位

デプロイ

kubectl apply -f mail-apiserver.yaml

状態確認

# Podの起動確認
kubectl get pods

# APIServiceの登録確認
kubectl get apiservices | grep mail

v1.mail.example.comAVAILABLEカラムがTrueになればOKです。

6. 動作確認

kubectlでメール一覧を取得

kubectl get mails -o yaml

単一リソースの取得

kubectl get mail hello-from-alice -o yaml

Discovery APIの確認

kubectl get --raw "/apis/mail.example.com/v1" | jq .

期待される出力:

{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "mail.example.com/v1",
  "resources": [
    {
      "name": "mails",
      "singularName": "mail",
      "namespaced": true,
      "kind": "Mail",
      "verbs": ["get", "list"]
    }
  ]
}

api-resourcesコマンドでの確認

kubectl api-resources | grep mail

出力例:

mails   mail.example.com/v1   true   Mail

まとめ

HonoとKubernetes Aggregation Layerを使って、カスタムAPIサーバーを実装しました。

参考リンク

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?