KubernetesのCRD(Custom Resource Definition)とカスタムコントローラーの作成

CRDと、それを操作するカスタムコントローラーを一から作る個人メモです。

# ここでの目標
1.以下のようなCRDリソースを作成できる

```yaml
apiVersion: "workflow.com/v1beta"
kind: Workflow
metadata:
  name: my-new-workflow
spec:
  name: my-new-workflow-name
  status:
    name: init
```

2.作成後、status.nameがspec.nameと同じ値に更新されるようなカスタムコントローラーをDeploymentとして作る。

## 前提条件
* kubernetes-1.9.6のKubernetesクラスタを準備する（Docker for Mac Edgeを使いました）
* カスタムコントローラーはGo1.10で実装する

## この記事でのソースコード
以下に、この記事作成のために作ったプロジェクトがあります。

https://github.com/Attsun1031/k8s-crd-sample

# はじめに
## CRDって？
Custom Resource Definitionです。DeploymentやPodといったものがリソースですが、ユーザー独自のリソースを定義することもでき、それがCRDです。

Kubernetesのリソースは内部のcontrollerを介して、リソースの状態の変化に応じて操作されます。例えばDeploymentが作成されたら、それを満たすReplicaSetやPodを作るなど。

## カスタムコントローラーって？
カスタムコントローラーはユーザー独自に作成するコントローラーです。DeploymentなどデフォルトのリソースやCRDの変化に応じて何かアクションをとるということもできます。通常は、[client-go](https://github.com/kubernetes/client-go)などを使ってプログラミングを行い、DeploymentとしてKubernetes上に展開するようです。

詳しくは[公式ドキュメント](https://kubernetes.io/docs/concepts/api-extension/custom-resources/)の最初の方をみてください。

# 手順
## CRDを作る
[公式ドキュメント](https://kubernetes.io/docs/tasks/access-kubernetes-api/extend-api-custom-resource-definitions/)を参考にCRDを作ります。

### yamlを作る
```yaml
# crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  # name must match the spec fields below, and be in the form: <plural>.<group>
  name: workflows.workflow.com
spec:
  # group name to use for REST API: /apis/<group>/<version>
  group: workflow.com
  # version name to use for REST API: /apis/<group>/<version>
  version: v1beta
  # either Namespaced or Cluster
  scope: Namespaced
  names:
    # plural name to be used in the URL: /apis/<group>/<version>/<plural>
    plural: workflows
    # singular name to be used as an alias on the CLI and for display
    singular: workflow
    # kind is normally the CamelCased singular type. Your resource manifests use this.
    kind: Workflow
    # shortNames allow shorter string to match your resource on the CLI
    shortNames:
    - wf
```

### applyする
```
$ kubectl apply -f crd.yaml
```

### 確認
```
$ kubectl get crd
NAME                     AGE
workflows.workflow.com   16m

$ kubectl get wf
No resources found.
```

## カスタムコントローラー用のクライアントライブラリを自動生成する
[公式のコード生成ツール](https://github.com/kubernetes/code-generator)を使って、CRDに紐づくクライアントライブラリを作成します。手取り足取りなドキュメントはなくハマりました。

### code-generator他、必要なモジュールをインストールする
```
go get k8s.io/client-go/...
go get k8s.io/apimachinery/...
go get github.com/golang/glog
go get k8s.io/code-generator/...
cd ${カスタムコントローラーを作成するプロジェクトルート}
mkdir -p pkg/client
mkdir -p pkg/apis/workflow/v1beta
```

### 自動生成に必要なファイルを作成する
#### pkg/apis/workflow/register.go
```go
package workflow

const (
	GroupName = "workflow.com"
)
```

#### pkg/apis/workflow/v1beta/doc.go
```go
// +k8s:deepcopy-gen=package

// Package v1beta is the v1beta version of the API.
// +groupName=workflow.com
package v1beta
```

#### pkg/apis/workflow/v1beta/types.go
```go
package v1beta

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Workflow is a specification for a Workflow resource
type Workflow struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Status WorkflowStatus `json:"status"`
	Spec   WorkflowSpec   `json:"spec"`
}

type WorkflowStatus struct {
	Name string `json:"name"`
}

// WorkflowSpec is the spec for a Workflow resource
type WorkflowSpec struct {
	Name string `json:"name"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// WorkflowList is a list of Workflow resources
type WorkflowList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata"`

	Items []Workflow `json:"items"`
}
```

#### pkg/apis/workflow/v1beta/register.go
```go
package v1beta

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"

    "github.com/attsun1031/workflow/pkg/apis/workflow"
)

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: workflow.GroupName, Version: "v1beta"}

// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
    return SchemeGroupVersion.WithKind(kind).GroupKind()
}

// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
    return SchemeGroupVersion.WithResource(resource).GroupResource()
}

var (
    SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
    AddToScheme   = SchemeBuilder.AddToScheme
)

// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
    scheme.AddKnownTypes(SchemeGroupVersion,
        &Workflow{},
        &WorkflowList{},
    )
    metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
    return nil
}
```

### 自動生成を実行する
```
../../../k8s.io/code-generator/generate-groups.sh \
  all \
  github.com/attsun1031/k8s-crd-sample/pkg/client \
  github.com/attsun1031/k8s-crd-sample/pkg/apis \
  workflow:v1beta
```

これで、`pkg/apis/workflow/v1beta/zz_generated.deepcopy.go`というファイルと、`pkg/client`配下にinformerなどが一式作られているはずです。

```
$ ls -R pkg
apis          client

pkg/apis:
workflow

pkg/apis/workflow:
register.go v1beta

pkg/apis/workflow/v1beta:
doc.go                   register.go              types.go                 zz_generated.deepcopy.go

pkg/client:
clientset informers listers

pkg/client/clientset:
versioned

pkg/client/clientset/versioned:
clientset.go doc.go       fake         scheme       typed

pkg/client/clientset/versioned/fake:
clientset_generated.go doc.go                 register.go

pkg/client/clientset/versioned/scheme:
doc.go      register.go

pkg/client/clientset/versioned/typed:
workflow

pkg/client/clientset/versioned/typed/workflow:
v1beta

pkg/client/clientset/versioned/typed/workflow/v1beta:
doc.go                 fake                   generated_expansion.go workflow.go            workflow_client.go

pkg/client/clientset/versioned/typed/workflow/v1beta/fake:
doc.go                  fake_workflow.go        fake_workflow_client.go

pkg/client/informers:
externalversions

pkg/client/informers/externalversions:
factory.go         generic.go         internalinterfaces workflow

pkg/client/informers/externalversions/internalinterfaces:
factory_interfaces.go

pkg/client/informers/externalversions/workflow:
interface.go v1beta

pkg/client/informers/externalversions/workflow/v1beta:
interface.go workflow.go

pkg/client/listers:
workflow

pkg/client/listers/workflow:
v1beta

pkg/client/listers/workflow/v1beta:
expansion_generated.go workflow.go
```

## カスタムコントローラーを作る
公式が提供している[サンプルコントローラープロジェクト](https://github.com/kubernetes/sample-controller)を参考に適当に作ってみます。

client-goと、生成したクライアントライブラリを使ってプログラミングします（基本的には本家のサンプルプロジェクトのコピペでいけます）。本家サンプルをさらにシンプルにしたものgithubにあげています。[controller-main.go](https://github.com/Attsun1031/k8s-crd-sample/blob/master/controller-main.go), [pkg/controller.go](https://github.com/Attsun1031/k8s-crd-sample/blob/master/pkg/controller.go)をご参照ください。

## カスタムコントローラーのDockerイメージを作成する
さきほどのgoプログラムをコンパイルしたうえで、Dockerイメージを作ってpushします。イメージ用のレポジトリは適当にdockerhubとかで用意してください。

```docker
# Dockerfile
FROM alpine:3.7
COPY controller-main /app/controller-main
ENTRYPOINT ["/app/controller-main"]
```

```
# compile go code
$ GOOS=linux GOARCH=amd64 go build controller-main.go

# build image
REPONAME="your-docker-repo-name"
$ docker build . -t ${REPONAME}/sample-controller-main

# push image
$ docker push ${REPONAME}/sample-controller-main:latest
```

## 新しいCRDリソースを作成する
```yaml
# my-new-workflow.yaml
apiVersion: "workflow.com/v1beta"
kind: Workflow
metadata:
  name: my-new-workflow
spec:
  name: my-new-workflow-name
  status:
    name: init
```

```
$ kubectl apply -f my-new-workflow.yaml
$ kubectl describe wf
Name:         my-new-workflow
Namespace:    default
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"workflow.com/v1beta","kind":"Workflow","metadata":{"annotations":{},"name":"my-new-workflow","namespace":"default"},"spec":{"name":"my-n...
API Version:  workflow.com/v1beta
Kind:         Workflow
Metadata:
  Cluster Name:
  Creation Timestamp:  2018-05-06T05:01:10Z
  Resource Version:    71528
  Self Link:           /apis/workflow.com/v1beta/namespaces/default/workflows/my-new-workflow
  UID:                 7e050fa3-50ea-11e8-ac12-025000000001
Spec:
  Name:  my-new-workflow-name
  Status:
    Name:  init
Events:    <none>
```

## カスタムコントローラーをデプロイする
先ほどのDockerイメージを使ったDeployment YAMLを作成して、applyします。

```yaml
# deploy-controller-main.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: controller-main
spec:
  selector:
    matchLabels:
      app: controller-main
  replicas: 1
  template:
    metadata:
      labels:
        app: controller-main
    spec:
      containers:
        - name: controller-main
          image: attsun/sample-controller-main:latest
```

## 完成！
デプロイ後、先ほど作ったWorkfkowを確認すると、status.nameの値が書き換えられています。

```
$ kubectl describe wf
Name:         my-new-workflow
Namespace:    default
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"workflow.com/v1beta","kind":"Workflow","metadata":{"annotations":{},"name":"my-new-workflow","namespace":"default"},"spec":{"name":"my-n...
API Version:  workflow.com/v1beta
Kind:         Workflow
Metadata:
  Cluster Name:
  Creation Timestamp:  2018-05-06T05:01:10Z
  Generation:          0
  Resource Version:    71615
  Self Link:           /apis/workflow.com/v1beta/namespaces/default/workflows/my-new-workflow
  UID:                 7e050fa3-50ea-11e8-ac12-025000000001
Spec:
  Name:  my-new-workflow-name
Status:
  Name:  my-new-workflow-name
Events:
  Type    Reason  Age   From                 Message
  ----    ------  ----  ----                 -------
  Normal  Synced  7s    workflow-controller  Workflow synced successfully
  Normal  Synced  7s    workflow-controller  Workflow synced successfully
```

# まとめ
以上です。カスタムコントローラーの作成については、

* 全体的に情報が分散している
* サンプルコードを読みつつ、ある程度client-goの各モジュールを自力で理解する必要がある
* コードジェネレーションの部分の詳細なガイドがない

という点から、少しハードルが高い感じです。

しかし、カスタムコントローラーの仕組み自体は、kubernetes上でサードパーティアプリケーションを展開する上で非常に簡単かつ無用な複雑性をもちこまないので、広まっていってほしいと思います。

# 参考
##### https://github.com/kubernetes/sample-controller
本家のサンプルコントローラープロジェクト

##### https://blog.openshift.com/kubernetes-deep-dive-code-generation-customresources/
コード自動生成部分について解説している記事

##### https://medium.com/@cloudark/kubernetes-custom-controllers-b6c7d0668fdf
カスタムコントローラーの仕組みについて解説している記事