repl.info

Hello Custom Controller!

2019年はカスタムコントローラーや!ということで2018年末頃から少しづつ調べている。まずは非常に簡単なものを動かしてみる。

Custom Controller is…

CRDについてのメモにも書いたが、CRD自体はただのデータなので、それだけでは何も起こらない。Kubernetesのdeclarative APIを活用するためにはコントローラーの作成が必要。なお、本記事は以下のページを参考にしています。

作っていく

カスタムコントローラーを作っていくわけだが、いきなり複雑なものを作るのは難しい。そこで、まずはカスタムリソースを読み込み、ログに出力するだけのコントローラーを作る。カスタムコントローラー用のSDKやBuilderがいろいろあるようだがこれも使わず、KubernetesのAPI用コードを出力するだめの code-generator を用いて素朴に作ってみる。code-generatorはいくつかの機能を持っている。

  • deepcopy-gen – 各型についてDeepCopy用のメソッドを生成
  • client-gen – カスタムリソースを扱うためのクライアントセットを生成
  • informer-gen – カスタムリソースの変更に対応するためのイベントベースインターフェースを生成
  • lister-gen – GetとListについて、読み取り用キャッシュレイヤを扱うコードを生成

本格的にカスタムリソースを扱うためにはinformer-genやlister-genが必要になるが、今回はdeepcopy-genとclient-genのみ。deepcopy-genも不要な気がするが、これなしでうまく動かす方法がちと分からなかった。

生成元のコードを用意する

まずは apis/foo/v1alpha/doc.go

// +k8s:deepcopy-gen=package



// Package v1alpha is the v1alpha version of the API.

// +groupName=samplecontroller.k8s.io

package v1alpha

次にapis/foo/v1alpha/register.go

package v1alpha



import (

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"k8s.io/apimachinery/pkg/runtime"

	"k8s.io/apimachinery/pkg/runtime/schema"

)



// SchemeGroupVersion is group version used to register these objects

var SchemeGroupVersion = schema.GroupVersion{Group: "samplecontroller.k8s.io", Version: "v1alpha"}



// 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,

		&Foo{},

		&FooList{},

	)

	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)

	return nil

}

最後にapis/foo/v1alpha/types.go

package v1alpha



import (

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

)



// +genclient

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

// Foo is a specification for a Foo resource

type Foo struct {

	metav1.TypeMeta `json:",inline"`

	metav1.ObjectMeta `json:"metadata,omitempty"`



	Status FooStatus `json:"status"`

	Spec FooSpec `json:"spec"`

}



type FooStatus struct {

	Name string `json:"name"`

}



// FooSpec is the spec for a Workflow resource

type FooSpec struct {

	Name string `json:"name"`

}



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



// FooList is a list of Workflow resources

type FooList struct {

	metav1.TypeMeta `json:",inline"`

	metav1.ListMeta `json:"metadata"`



	Items []Foo `json:"items"`

}

コード生成

これらのファイルを用意したら、code-generatorでコードを生成する。なお、code-generatorのバージョンは kubernetes-1.12.3を使用。

$ bash ~/src/k8s.io/code-generator/generate-groups.sh client,deepcopy github.com/takaishi/hello2019/hello-custom-controller/pkg/client github.com/takaishi/hello2019/hello-custom-controller/pkg/apis foo:v1alpha

Generating deepcopy funcs

Generating clientset for foo:v1alpha at github.com/takaishi/hello2019/hello-custom-controller/pkg/client/clientset

カスタムコントローラーを実装

これで、GoからFooリソースを扱うことが出来る。カスタムコントローラー用のコードを書いていく:

package main



import (

	clientset "github.com/takaishi/hello2019/hello-custom-controller/pkg/client/clientset/versioned"

	"k8s.io/apimachinery/pkg/apis/meta/v1"

	"k8s.io/client-go/rest"



	"fmt"

	"github.com/urfave/cli"

	"log"

	"os"

	"time"

)



func main() {

	log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime)



	app := cli.NewApp()

	app.Flags = []cli.Flag{}



	app.Action = func(c *cli.Context) error {

		return action(c)

	}



	err := app.Run(os.Args)

	if err != nil {

		log.Fatal(err)

	}

}



func action(c *cli.Context) error {

	cfg, err := rest.InClusterConfig()

	if err != nil {

		log.Printf(err.Error())

	}

	client, err := clientset.NewForConfig(cfg)

	if err != nil {

		log.Printf(err.Error())

	}



	for {

		foos, err := client.SamplecontrollerV1alpha().Foos("default").List(v1.ListOptions{})

		if err != nil {

			log.Printf(err.Error())

			continue

		}



		fmt.Printf("%+v\n", foos)



		time.Sleep(10 * time.Second)

	}

	return nil

}

コントローラー用のマニフェスト。defaultアカウントがオブジェクトを取得できるようにRBACの設定も行っている:

---

apiVersion: rbac.authorization.k8s.io/v1

kind: Role

metadata:

  name: foo-reader

  namespace: default

rules:

  - apiGroups: ["samplecontroller.k8s.io"]

    verbs: ["get", "list"]

    resources: ["foos"]

---

apiVersion: rbac.authorization.k8s.io/v1

kind: RoleBinding

metadata:

  name: foo-reader-rolebinding

  namespace: default

roleRef:

  apiGroup: rbac.authorization.k8s.io

  kind: Role

  name: foo-reader

subjects:

  - kind: ServiceAccount

    name: default

    namespace: default

---

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: rtakaishi/sample-controller-main:latest

          imagePullPolicy: IfNotPresent

デプロイして動作確認する

カスタムコントローラーのビルドからKubernetesへのデプロイまで行うためのMakefile:

date := $(shell date +'%s')



default:

	env GOOS=linux GOARCH=amd64 go build -o controller-main main.go

	docker build . -t rtakaishi/sample-controller-main

	kubectl apply -f ./deploy-controller-main.yaml

	kubectl patch deploy controller-main -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(date)\"}}}}}"

デプロイする:

$ eval (minikube docker-env)

$ make

カスタムコントローラーのログを見ると、Fooリソースを取得してログに出力していることが確認できた:

$ stern controller-main

+ controller-main-66f5cb49df-4bhq6 › controller-main

controller-main-66f5cb49df-4bhq6 controller-main &{TypeMeta:{Kind: APIVersion:} ListMeta:{SelfLink:/apis/samplecontroller.k8s.io/v1alpha/namespaces/default/foos ResourceVersion:98881 Continue:} Items:[{TypeMeta:{Kind:Foo APIVersion:samplecontroller.k8s.io/v1alpha} ObjectMeta:{Name:foo-001 GenerateName: Namespace:default SelfLink:/apis/samplecontroller.k8s.io/v1alpha/namespaces/default/foos/foo-001 UID:77506f4f-0e40-11e9-95ac-263ada282756 ResourceVersion:3649 Generation:1 CreationTimestamp:2019-01-02 03:42:45 +0000 UTC DeletionTimestamp:<nil> DeletionGracePeriodSeconds:<nil> Labels:map[] Annotations:map[kubectl.kubernetes.io/last-applied-configuration:{"apiVersion":"samplecontroller.k8s.io/v1alpha","kind":"Foo","metadata":{"annotations":{},"name":"foo-001","namespace":"default"},"spec":{"deploymentName":"deploy-foo-001","replicas":1}}

controller-main-66f5cb49df-4bhq6 controller-main ] OwnerReferences:[] Initializers:nil Finalizers:[] ClusterName:} Status:{Name:} Spec:{Name:}} {TypeMeta:{Kind:Foo APIVersion:samplecontroller.k8s.io/v1alpha} ObjectMeta:{Name:foo-002 GenerateName: Namespace:default SelfLink:/apis/samplecontroller.k8s.io/v1alpha/namespaces/default/foos/foo-002 UID:775194f8-0e40-11e9-95ac-263ada282756 ResourceVersion:93582 Generation:1 CreationTimestamp:2019-01-02 03:42:45 +0000 UTC DeletionTimestamp:<nil> DeletionGracePeriodSeconds:<nil> Labels:map[] Annotations:map[kubectl.kubernetes.io/last-applied-configuration:{"apiVersion":"samplecontroller.k8s.io/v1alpha","kind":"Foo","metadata":{"annotations":{},"name":"foo-002","namespace":"default"},"spec":{"deploymentName":"deploy-foo-002","hoge":"huga","replicas":1}}

controller-main-66f5cb49df-4bhq6 controller-main ] OwnerReferences:[] Initializers:nil Finalizers:[] ClusterName:} Status:{Name:} Spec:{Name:}}]}

非常にシンプル、というかとくに何もしていないが、カスタムコントローラーができたといっていいだろう。理屈ではログ出力の部分をロジックに置き換えればいいはず。実際にはinformerを用いてイベントベースで処理を行うようなコードになるのだと思う。次はログ出力だけではなく別リソースを扱うようにしていきたい。

ソースコードなどはtakaishi/hello2019にあります。