CRDは単なるデータベースのテーブル

この記事は [Kubernetes2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kubernetes2) の 2 日目です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/5444e87f-3ed3-56cb-5916-26e367117568.png)

本記事は、全く CRD を知らないところからスタートし、ざっくり CRD を理解して少し好きになることを目的にしています。

([ITNEXT投稿記事](https://itnext.io/crd-is-just-a-table-in-kubernetes-13e15367bbe4)を翻訳/改変したものです)

# CRD ってなんだか難しそう

CRD... Custom Resource Definition は Kubernetes 内で作れる特殊なオブジェクト (リソース) です。

Kubernetes の普通の使い方、例えばコンテナを起動するというだけであれば、ユーザは CRD のような特殊なリソースを使う必要はありません。したがって、CRDとは、ほとんどの人にとっては重要ではないものです。

しかし、たまに最近の技術ブログなどでは CRD を目にすることがあります。また、kubernetes.ioやKubernetes界隈の議論スレッドでも見ることがあります。こういう場面に出くわすと、CRD を知らなかった場合は理解に少し時間をかけなくてはいけません。私もそういう一人でした。

CRD とは何なのでしょうか。どうやって使うのでしょうか。もし必要になったら、どうやって作ればいいのでしょうか。

心配はありません。実はさほど難しいものではありません。今回は、CRD とそれに関連する CR の概念について、できる限りかんたんに説明したいと思います。

# CRD は端的に言うと

CRD とは、**単なるデータベースのテーブル**です。この記事で説明したいことは実はそれだけです。

例えば、データベースの世界のことを考えてみます。まず、以下のようなテーブルを作ることを考えましょう。

- "Fruit" という名のデータベースのテーブルを作るとします。
- テーブルには "apple", "banana", "orange"という3つのフルーツを表すレコード3行を登録します。
- これらのレコードは、 "name", "sweetness", "weight"という3つの列を持つとします。この3つの列は、フルーツの「名前」「甘さ」「重さ」という特徴を表しています。

すでに、データベースに馴染みがある方は、`CREATE TABLE` 文がぱっと頭をよぎったのではないでしょうか。上記テーブルは非常にかんたんなテーブル実装の例です。

ここからが本題ですが、**CRD はまさにこれと同じテーブル定義を含んだ情報です**。CRD のかんたんな模式図を以下に示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/68a902cc-98ec-882c-3c28-c706345f1123.png)

次に、**CR (Custom Resource) とは、"apple"などのそれぞれのレコードです**。CRD がテーブルなら、その中のレコードが CR というわけです。CR のかんたんな模式図を以下に示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/685dce61-83ce-f5f0-ee6b-c3bac2f9149c.png)

# どうやって CRD を作るの？

すでに説明したとおり、 CRD はテーブルです。テーブルを作るとき、**列名や列の型**などのテーブルのフォーマットの情報を入力する必要があります。フォーマットの情報は CRD の定義ファイル (YAMLやJSONデータ)の中に書き込まれます。

また、レコードごとの情報は、CR 定義ファイルのYAMLやJSONデータで定義されます。

CRD, CR のそれぞれの定義ファイルとそれが指すテーブルの対応関係は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/d3400ece-8631-0572-8be5-82d2c03204fa.png)

では、 CRD の定義ファイルの中身を見ていきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/f4c94508-d64c-9012-1304-cba75bf2888d.png)

CRD の定義ファイルはは大きく以下の3つのパートに別れます。

- 一般的な情報のパート
    - Kubernetes内の他のリソースと同じように、CRD リソースの名前などの共通的なメタデータを書く部分です。
    - 例: `name: "fruit"`など。
- テーブルレベルの情報を書くパート
    - テーブルの名称やその複数形など、Kubernetesでテーブルを扱うために必要な情報を書く部分です。
    - 例: `kind: Fruit`, `simpler: fruit`, `plural: fruits` など。
- 列レベルの情報を書くパート
    - 列名 ( `"sweetness"`など)や列の型 (`"boolean"`, `"string"`, `"integer"`, `"object"`), ネストされたオブジェクトの定義など (`props: <子要素の名前や列>`)
    - これらの定義は、Open API Specification の version 3の記法で定義されます。

より詳細な情報は[公式ドキュメント](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#create-a-customresourcedefinition)を参照ください。

# CRD の機能を試してみる

CRD は本当にテーブルとして操作できるのでしょうか。確かめてみましょう。

> 実際の確認結果の説明の前に少しだけ前置きさせてください。通常はCRD の定義ファイルを手動で作成するのが普通だと思いますが、今回はCRDの定義ファイルをよりかんたんなYAMLから自動生成する方法を使います。詳細は後で紹介します。その点をご了承いただければ幸いです。

今回作るテーブルは単純です。先に少し例で説明した「フルーツ」用のCRDを作りたいと思います。"Fruit"というCRDは以下の3つの列を持ちます。

| 列名 | 列の方 |
|---|---|
| `sweetness` | `boolean` (甘いか甘くないか) |
| `weight` | `integer` (何グラムかを数字で表す) |
| `comment` | `string` (参考情報を文字列で記載する) |

これを端的なYAMLで記載すると以下のようになります (これはまだ正式な CRD 定義ファイルではありません)。列名や型がそれぞれ定義されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/669bf83f-6db6-ba2b-cee7-158b7052df5f.png)

ここから CRD の定義ファイルを作ります。先の端的なYAMLをKubernetesに導入する際に[ある変換処理としてOperator](https://github.com/onelittlenightmusic/k8sasdb)を用いて CRD の定義ファイルを自動生成します。生成した CRD 定義ファイルは以下のようになります。直接手動でCRDを作成する場合には、不要な`ownerReferences`や`creationTimestamp`を外しコピーし、`kubectl apply -f <ファイル名>`を実行すれば問題ありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/8f57bbcd-2f31-963b-8c92-ae7e9264cdf2.png)

<details><summary> 実際に生成された CRD の定義ファイルはここを開いて参照ください (Operatorを使わない場合はこれを直接ファイルに保存して`kubectl apply -f <ファイル名>`してください。)</summary><div>

- [Githubの格納場所](https://github.com/onelittlenightmusic/k8sasdb/blob/master/test/generated/fruit.yaml)

```
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: fruits.user.k8sasdb.org
spec:
  conversion:
    strategy: None
  group: user.k8sasdb.org
  names:
    kind: Fruit
    listKind: FruitList
    plural: fruits
    singular: fruit
  scope: Namespaced
  versions:
  - additionalPrinterColumns:
    - jsonPath: .spec.sweetness
      name: sweetness
      type: boolean
    - jsonPath: .spec.weight
      name: weight
      type: integer
    - jsonPath: .spec.comment
      name: comment
      type: string
    name: v1
    schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              comment:
                type: string
              sweetness:
                type: boolean
              weight:
                type: integer
            type: object
        type: object
    served: true
    storage: true
```
</div>
</details>

この CRD こそが Kubernetes における自作テーブル定義方法です。この中には、先ほど定義した「`"sweetness"`という列が`boolean`である、、」といった情報が埋め込まれていることがわかります。
これを入力されたタイミングから、Kubernetes は「あ、フルーツというリソースがあるんだな」と理解し、これに対応する「レコード追加」や「削除」などのAPIを稼働させます。Swaggerドキュメントも自動生成し公開します。
これは、データベースにおいて "CREATE TABLE"をしたあとにそのテーブルの処理ができるようになるのと同じです。

さて、本当にデータベースと同じように振る舞うのか、試します。

## レコードを追加してみる

Kubernetesにおけるレコードは、さきに説明したとおり CR でした。CRを追加するには、まずCRの定義ファイルを以下のように記載します。

```apple.yaml
apiVersion: user.k8sasdb.org/v1
kind: Fruit
metadata:
  name: apple
spec:
  sweetness: false
  weight: 100
  comment: little bit rotten
```

この CR には、`apple`というフルーツがあり、甘さはそれほど甘くなく、重さが100グラムで、コメントとしては「ちょっと腐っている」ということが書いてあります。

どのテーブルに追加するレコードかということを、上の`kind: Fruit`で指定しています。これを見て、 Kubernetes は「あ、Fruitというテーブルに追加するレコードね」と理解します。

では実際に CR を追加してみます。

```sh
kubectl create -f apple.yaml
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/97808c34-427d-85d3-aa6f-f5706aa8b784.png)

エラーが出ずに実行完了したことを表示しています。

このコマンドはまさにデータベースのクエリで言うところの以下と同じと言えます。

```sql
INSERT INTO fruits values('apple', ...);
```

## レコードのリストを取得する

CR のリストを取得するには以下のコマンドが使えます。

```sh
kubectl get fruits
```

Kubernetes を使っている方なら、この馴染みのある `kubectl get`コマンドで自作のCRDの操作ができることが楽しいと感じられると思います。このコマンドにより、以下の結果を得られます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/27fa3086-9b31-7ed8-e4a2-9c3613b958a1.png)

この結果は、私が`"apple"`と`"banana"`という２つのレコードを追加した際の状態を示しています。

これは、データベースにおける以下のクエリと同じです。

```sql
SELECT * FROM fruits;
```

## 単一レコードを取得する

一つだけ CR を取得するには以下のコマンドが使えます。

```sh
kubectl get fruit apple
```

単に、前のコマンドの後ろに`apple`という名前をつけただけになっています。Kubernetes で Pod などを取得するときと同じです。このコマンドにより、以下の結果を得られます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/d8b2c211-8d8b-ecc9-d274-0a161c989262.png)


私がすでに `"apple"`と`"banana"`という2つのCRを追加したにもかかわらず、コマンドは単一のレコードのみを取得していることがわかります。

これは、データベースにおける以下のクエリと同じです。

```sql
SELECT * FROM fruits WHERE name = 'apple';
```

## レコードを削除する

```sh
kubectl delete fruit apple
```

上記のコマンドで、レコードを削除できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/f6da03c5-864f-6cf8-4cda-2e85463978e0.png)

この結果、`Fruit`の全レコードの中にはすでに`apple`というレコードは削除され、`banana`しか残っていないこがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/5d4d5cbc-a5f3-2115-383f-2aeef2d44866.png)

これは、データベースにおける以下のクエリと同じです。

```sql
DELETE FROM fruits WHERE name = 'banana';
```

# なぜ CRD が必要なのか

なぜ CRD が必要とされるようになったのでしょうか。私は、この理由をこう考えています。

Kubernetes は近年さまざまなユーザが幅広い用途に使うようになりました。APIやコマンドも非常に使いやすく便利です。そうなると、ユーザはもっと自分が便利になるよう、いろんなデータをKubernetesに登録したくなります。そのデータに応じて Kubernetes の振る舞いを変えさせたい、とも考えるようになります。

ここから、ユーザごとに違うフォーマットのデータを登録したい、列名やデータ型が異なるデータを登録したい、というニーズが生まれます。しかし、Kubernetes にはそのようなユーザごとのデータを事前に登録しておくということはできません。ニーズを実現するには、Kubernetes 構築後の事後でもユーザごとのデータを登録できるように、柔軟なスキーマ定義をさせる必要があります。

このインターフェースが CRD という形で実現されたのだと考えます。

CRD を使う一例として、会社の部署ごとの予算、といった情報を CRD で Kubernetes に登録しておき、予算消化状況に合わせて起動できる Pod 数の上限をコントロールするといった使い方が考えられます。

# まとめ

本記事では、 CRD がデータベースのテーブルと同じであることを説明しました。

- CRD を作成することで、Kubernetes内にテーブルを作成できます。
- CR を作成することで、テーブル内にレコードを追加できます。
- テーブルの定義に必要な情報 (列名や型)はCRDの定義ファイルの中に書きます。
- CRD や CR を Kubernetes に登録するには、`kubectl` コマンドを使用します。
- `kubectl apply -f <定義ファイル>`でレコードを追加し、`kubectl get`にてレコードを表示し、`kubectl delete`でレコードを削除できます。

# 最後に補足 (かんたんなYAMLからの CRD の自動生成)

今回 `Fruit` という CRD を作成した際に使用した自動生成ツールは以下のレポジトリに公開しています。

https://github.com/onelittlenightmusic/k8sasdb

最初に定義した端的な YAML ファイル自体も `Table` という独自の CRD です。以下のように `Table` を登録することで CRD が生成されます。`Table` から正式な `CRD` を自動生成するために Operator という仕掛けを使います。この手法を適用するには、事前にこの Operator を Kubrnetes クラスタに導入しておく必要があります。導入方法などは先のレポジトリのREADMEをごらんいただき、フィードバックをいただければ幸いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96277/ee8bff34-565f-9bbf-86ec-74f623035b91.png)