operator SDKのQuickstartの手順をなぞると、memcached-operatorという名前のシンプルなOperatorを作ることができます。このOperatorはbusyboxが稼働するPodをひとつデプロイしてくれます。
しかしそれ以上のことをしようとすると、memcached-controller.goというファイルのソースコードを修正する必要が出てきて、Go言語に馴染みがない者にとってはハードルが一気に上がってしまいます。
ソースコードを修正する必要があると言っても、デプロイのパターンを固定してしまえばほとんどのコードは使い回せるはずです。そこで、ここではデプロイしたいものをDeploymentとService(type=NodePort)に固定して、任意のコンテナイメージで再利用が可能なテンプレートを用意し、ほぼ機械的にOperatorを組み立てる手順を考えてみました。
紹介する手順は次のとおりです。基本的な流れはoperator SDKのQuickstartの手順とおおよそ同じです。
- カスタムリソースの仕様を決めて、キーとなる値を環境変数にセットする
- テンプレートファイルを用意して、operator-sdkの操作を開始する
- CRD、CRのマニフェスト、およびOperatorのロジックの主要部分の雛形を生成する
- controllerのソースコードにテンプレートを組み込む
- controllerのソースコードをカスタマイズする
- Operatorをビルドして、K8sクラスタにデプロイする
- カスタムリソースをK8sクラスタにデプロイする
- カスタムリソースを動的変更する
- アプリケーションの疎通を確認する
テンプレートの作成にはoperator-sdk-samplesのmemcached-operatorを参考にしました。作成したテンプレートはGitHubに置いておきました。
手元の検証環境はoperator-sdk v0.18.1, ubuntu 18.04.1LTS, go 1.14.4, kubernetes v1.18.3 (microk8s)を使用しました。
環境変数の与え方によってはうまくいかないケースもあるかもしれませんが、その場合は少しGo言語に踏み込んでみて下さい。この記事が少しでもOperatorの敷居を下げる一助になれば幸いです。
1. カスタムリソースの仕様を決めて、キーとなる値を環境変数にセットする
まずOperatorを使ってデプロイするアプリのコンテナイメージを決めます。ここではtakeyan/flask:0.0.3とします。
次に、Operatorのカウンターパートとなるカスタムリソースのマニフェストを次のように定義します。定義するといってもoperator-sdkのサンプルの構造はそのままで、キーワードを置き換えただけです。
apiVersion: swallowlab.com/v1alpha1
kind: EchoFlask
metadata:
name: example-echoflask
spec:
# Add fields here
size: 3
上記マニフェストのキーワードと、Operatorを使ってデプロイしたいアプリのコンテナイメージ名を、環境変数にセットしておきます。これ以降の手順を進めていくと、operator-sdkが生成する雛形ファイル群の名前やソースコードに、これらの環境変数の値が整合性を保ちながらセットされていくという算段です。
export IMAGENAME=takeyan/flask:0.0.3
export CLUSTERPORT=5000
export APPNAME=echoflask
export VERSION=v1alpha1
export GROUPNAME=swallowlab
export CRKIND=EchoFlask
GROUPNAMEにはswallowlab.comのswallowlabの部分だけ指定しています。controllerのソースコードの中でこの部分だけ使用している箇所がいくつかあったのでそれに合わせました。
2. テンプレートファイルを用意して、operator-sdkの操作を開始する
Operator開発プロジェクトの親になるディレクトリを作成し、そこに以下のテンプレートファイルを配置しておきます。
IMPORT_BLOCK.template
RECONCILE_BLOCK.template
WATCH_BLOCK.template
テンプレートファイルの配置は、この記事のGitHubリポジトリをgit cloneしてそのまま使っていただくのが簡単です。あるいはgit clone後にテンプレートファイルを任意のディレクトリにコピーして下さい。ディレクトリはGOPATH配下でなくても大丈夫です。
git clone https://github.com/takeyan/tk-operator-sdk.git
cd tk-operator-sdk
テンプレートファイルを配置したら、以下の順にコマンドを実行します。operator-sdk newコマンドは、テンプレートファイルを配置したディレクトリがカレントディレクトリになっている状態で実行して下さい。後の手順では、その前提でテンプレートファイルを相対パスで参照しています。
export GO111MODULE=on
operator-sdk new $APPNAME --repo=$APPNAME
cd $APPNAME
go mod tidy
3. CRD、CRのマニフェスト、およびOperatorのロジックの主要部分の雛形を生成する
引き続きoperator-sdkを使って、OperatorのソースコードやCRD、CRのマニフェストファイルを生成していきます。
まずoperator-sdk add apiコマンドを実行し、このとき生成された${APPNAME}_types.goファイルを編集します。ここでは2箇所に1行ずつ追加するだけなので、手修正で済ませました。
operator-sdk add api --api-version=${GROUPNAME}.com/${VERSION} --kind=$CRKIND
vi pkg/apis/${GROUPNAME}/${VERSION}/${APPNAME}_types.go
修正箇所は次の2つの構造体EchoFlascSpecとEchoFlaskStatusです(EchoFlaskの部分は環境変数CRKINDの値がoperator-sdkによってセットされています)。雛形は空でできているので、Size int32 `json:"size"`
および Nodes []string `json:"nodes"`
をそれぞれ挿入します。
// EchoFlaskSpec defines the desired state of EchoFlask
type EchoFlaskSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
Size int32 `json:"size"` // Deploymentマニフェストのreplicasに反映
}
// EchoFlaskStatus defines the observed state of EchoFlask
type EchoFlaskStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
// Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
Nodes []string `json:"nodes"` // デプロイされたPod名一覧を保持
}
${APPNAME}_types.goを編集したら次のコマンドを順次実行していきます。
operator-sdk generate k8s
operator-sdk generate crds
operator-sdk add controller --api-version=${GROUPNAME}.com/${VERSION} --kind=${CRKIND}
operator-sdkによる雛形ファイル生成作業はここで一段落です。
4. controllerのソースコードにテンプレートを組み込む
いよいよcontrollerのソースコードを編集します。念の為ファイルのバックアップを取っておいてから先に進めます。エディターはこの手順ではviを使いました。
cp pkg/controller/${APPNAME}/${APPNAME}_controller.go pkg/controller/${APPNAME}/${APPNAME}_controller.go.bk`date '+%Y-%m-%d_%H:%M:%S'`
vi pkg/controller/${APPNAME}/${APPNAME}_controller.go
まずimportの下の"context"(4行目)の下にカーソルを置いて、IMPORT_BLOCK.templateファイルの中身をコピーします。viの操作例を以下に示します。4行目にカーソルを置いて、:r ../IMPORT_BLOCK.template
を実行しています。
次にPodをwatchしている部分(以下の59行目~67行目)を置き換えます。
まず59行目~67行目を削除します。// TODO(user): Modify this to be the types you create ...
が目印です。
次に削除した場所にWATCH_BLOCK.templateファイルの中身をコピーします。viでは59行目にカーソルを置いて:r ../WATCH_BLOCK.template
を実行するとコピーできます。
最後にReconcile処理のPodを対象にしている部分以降(以下の121行目以降すべて)を置き換えます。
まず121行目以降をすべて削除します。// Define a new Pod object
が目印です。
次に削除した場所にRECONCILE_BLOCK.templateファイルの中身をコピーします。viでは120行目にカーソルを置いて:r ../RECONCILE_BLOCK.template
を実行するとコピーできます。
編集は以上です。編集内容を保存してエディターを終了します。
5. controllerのソースコードをカスタマイズする
controllerに組み込んだテンプレートに含まれている置換前文字列(__XXXXX__
の部分)を、環境変数の値で置き換えていきます。以下のコマンドを実行したらcontrollerのソースコードは完成です。
sed -i "s|__GROUPNAME__|${GROUPNAME}|g" pkg/controller/${APPNAME}/${APPNAME}_controller.go
sed -i "s|__VERSION__|${VERSION}|g" pkg/controller/${APPNAME}/${APPNAME}_controller.go
sed -i "s|__CRKIND__|${CRKIND}|g" pkg/controller/${APPNAME}/${APPNAME}_controller.go
sed -i "s|__APPNAME__|${APPNAME}|g" pkg/controller/${APPNAME}/${APPNAME}_controller.go
sed -i "s|__CLUSTERPORT__|${CLUSTERPORT}|g" pkg/controller/${APPNAME}/${APPNAME}_controller.go
6. Operatorをビルドして、K8sクラスタにデプロイする
Quickstartの手順に従って、まずCRDをKubernetesクラスタにデプロイしておきます。
kubectl create -f deploy/crds/${GROUPNAME}.com_${APPNAME}s_crd.yaml
次にOperatorのコンテナイメージをビルドします。Operatorのコンテナイメージ名はdockerhubのレジストリに登録する前提で、レジストリのURLは省略して命名してあります。
export OPERATOR_IMAGE=takeyan/${APPNAME}-operator:0.0.1
operator-sdk build $OPERATOR_IMAGE
deploy/operator.yamlを修正します。operator-sdkが生成したマニフェストファイルのコンテナイメージ名のところにREPLACE_IMAGEという文字列が入っているので、これを環境変数OPERATOR_IMAGEの値で置き換えます。
sed -i "s|REPLACE_IMAGE|${OPERATOR_IMAGE}|g" deploy/operator.yaml
Operatorのコンテナイメージをレジストリに登録しておきます。dockerhubにログインし、docker pushで登録します。
docker login
docker push $OPERATOR_IMAGE
deployディレクトリ直下の4つのマニフェストファイルをデプロイします。default以外のネームスペースをターゲットとしたい場合は、この時点からネームスペースを指定しておきます。
kubectl create -f deploy/service_account.yaml [-n namespace名]
kubectl create -f deploy/role.yaml [-n namespace名]
kubectl create -f deploy/role_binding.yaml [-n namespace名]
kubectl create -f deploy/operator.yaml [-n namespace名]
7. カスタムリソースをK8sクラスタにデプロイする
次のコマンドでカスタムリソースをデプロイします。カスタムリソースがデプロイされると、それをきっかけにアプリのDeploymentとServiceもデプロイされます。
kubectl apply -f deploy/crds/${GROUPNAME}.com_${VERSION}_${APPNAME}_cr.yaml [-n namespace名]
アプリのDeployment, Pod, Serviceがデプロイされていることを確認します。example-\$APPNAME-deploymentという名前のDeploymentとそのDeploymentに従属するPodが3個、example-$APPNAME-svcという名前のSerivce(タイプはNodePort)ができていれば成功です。
kubectl get all -o wide [-n namespace名]
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# kubectl get all -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/echoflask-68b874ccd-996jx 1/1 Running 0 42s 10.1.94.91 tk-ub18-microk8s <none> <none>
pod/example-echoflask-deployment-65bb9c49c6-fjd5x 1/1 Running 0 25s 10.1.94.92 tk-ub18-microk8s <none> <none>
pod/example-echoflask-deployment-65bb9c49c6-grlp4 1/1 Running 0 24s 10.1.94.93 tk-ub18-microk8s <none> <none>
pod/example-echoflask-deployment-65bb9c49c6-jtc7v 1/1 Running 0 24s 10.1.94.94 tk-ub18-microk8s <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/echoflask-metrics ClusterIP 10.152.183.120 <none> 8383/TCP,8686/TCP 35s name=echoflask
service/example-echoflask-svc NodePort 10.152.183.226 <none> 5000:30523/TCP 24s app=echoflask,echoflask_cr=example-echoflask
service/kubernetes ClusterIP 10.152.183.1 <none> 443/TCP 13d <none>
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES
SELECTOR
deployment.apps/echoflask 1/1 1 1 42s echoflask takeyan/echoflask-operator:0.0.1 name=echoflask
deployment.apps/example-echoflask-deployment 3/3 3 3 25s echoflask takeyan/flask:0.0.3 app=echoflask,echoflask_cr=example-echoflask
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES
SELECTOR
replicaset.apps/echoflask-68b874ccd 1 1 1 42s echoflask takeyan/echoflask-operator:0.0.1 name=echoflask,pod-template-hash=68b874ccd
replicaset.apps/example-echoflask-deployment-65bb9c49c6 3 3 3 25s echoflask takeyan/flask:0.0.3 app=echoflask,echoflask_cr=example-echoflask,pod-template-hash=65bb9c49c6
カスタムリソースをdescribeしてみます。StatusにPodの一覧が表示されるはずです。
kubectl get EchoFlask [-n namespace名]
kubectl describe EchoFlask example-echoflask [-n namespace名]
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# kubectl get EchoFlask
NAME AGE
example-echoflask 4m15s
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# kubectl describe EchoFlask example-echoflask
Name: example-echoflask
Namespace: default
Labels: <none>
Annotations: API Version: swallowlab.com/v1alpha1
Kind: EchoFlask
Metadata:
Creation Timestamp: 2020-06-21T13:42:45Z
Generation: 1
Managed Fields:
(中略)
Spec:
Size: 3
Status:
Nodes:
example-echoflask-deployment-65bb9c49c6-fjd5x
example-echoflask-deployment-65bb9c49c6-grlp4
example-echoflask-deployment-65bb9c49c6-jtc7v
Events: <none>
8. カスタムリソースを動的に変更する
Operator echoflask-operatorが作成するDeploymentのReplicasの値(=Pod数)は、カスタムリソース定義のsizeの値を反映しています。この値を動的に変更して、それがDeploymentに動的に反映されることを確認します。
カスタムリソースexample-echoflaskを編集します。
kubectl edit EchoFlask example-echoflask [-n namespace名]
変更を保存したら、アプリのDeploymentやPodに反映されたことを確認します。sizeの値のとおりにPodの数が変わっていれば成功です。
kubectl get all -o wide [-n namespace名]
kubectl describe EchoFlask example-echoflask [-n namespace名]
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# kubectl get all -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/echoflask-68b874ccd-996jx 1/1 Running 0 18m 10.1.94.91 tk-ub18-microk8s <none> <none>
pod/example-echoflask-deployment-65bb9c49c6-fjd5x 1/1 Running 0 18m 10.1.94.92 tk-ub18-microk8s <none> <none>
pod/example-echoflask-deployment-65bb9c49c6-fx6ns 1/1 Running 0 115s 10.1.94.95 tk-ub18-microk8s <none> <none>
pod/example-echoflask-deployment-65bb9c49c6-grlp4 1/1 Running 0 18m 10.1.94.93 tk-ub18-microk8s <none> <none>
pod/example-echoflask-deployment-65bb9c49c6-jtc7v 1/1 Running 0 18m 10.1.94.94 tk-ub18-microk8s <none> <none>
pod/example-echoflask-deployment-65bb9c49c6-lzd8m 1/1 Running 0 115s 10.1.94.96 tk-ub18-microk8s <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/echoflask-metrics ClusterIP 10.152.183.120 <none> 8383/TCP,8686/TCP 18m name=echoflask
service/example-echoflask-svc NodePort 10.152.183.226 <none> 5000:30523/TCP 18m app=echoflask,echoflask_cr=example-echoflask
service/kubernetes ClusterIP 10.152.183.1 <none> 443/TCP 13d <none>
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/echoflask 1/1 1 1 18m echoflask takeyan/echoflask-operator:0.0.1 name=echoflask
deployment.apps/example-echoflask-deployment 5/5 5 5 18m echoflask takeyan/flask:0.0.3 app=echoflask,echoflask_cr=example-echoflask
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
replicaset.apps/echoflask-68b874ccd 1 1 1 18m echoflask takeyan/echoflask-operator:0.0.1 name=echoflask,pod-template-hash=68b874ccd
replicaset.apps/example-echoflask-deployment-65bb9c49c6 5 5 5 18m echoflask takeyan/flask:0.0.3 app=echoflask,echoflask_cr=example-echoflask,pod-template-hash=65bb9c49c6
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# kubectl describe EchoFlask example-echoflask
Name: example-echoflask
Namespace: default
Labels: <none>
Annotations: API Version: swallowlab.com/v1alpha1
Kind: EchoFlask
Metadata:
Creation Timestamp: 2020-06-21T13:42:45Z
Generation: 2
Managed Fields:
(中略)
Spec:
Size: 5
Status:
Nodes:
example-echoflask-deployment-65bb9c49c6-fjd5x
example-echoflask-deployment-65bb9c49c6-grlp4
example-echoflask-deployment-65bb9c49c6-jtc7v
example-echoflask-deployment-65bb9c49c6-fx6ns
example-echoflask-deployment-65bb9c49c6-lzd8m
Events: <none>
9. アプリケーションの疎通を確認する
最後にアプリの疎通も確認しておきます。takeyan/flask:0.0.3は、/api/echoをhttp GETするとアプリ実行環境のノード名、Pod名、Pod IPアドレス等を含むJSONフォーマットのメッセージを返すように作ってあります。繰り返し実行すると負荷分散される様子が観察できます。
curl localhost:{NodePortのポート番号}/api/echo?Hello=World!
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# curl localhost:30523/api/echo?Hello=World!
{"api":"echo","nodename":"tk-ub18-microk8s","pod_ip":"10.1.94.93","podname":"example-echoflask-deployment-65bb9c49c6-grlp4","query_string":["Hello=World!"],"timestamp":"2020-06-21T14:03:35.389553"}
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# curl localhost:30523/api/echo?Hello=World!
{"api":"echo","nodename":"tk-ub18-microk8s","pod_ip":"10.1.94.94","podname":"example-echoflask-deployment-65bb9c49c6-jtc7v","query_string":["Hello=World!"],"timestamp":"2020-06-21T14:03:37.354148"}
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# curl localhost:30523/api/echo?Hello=World!
{"api":"echo","nodename":"tk-ub18-microk8s","pod_ip":"10.1.94.92","podname":"example-echoflask-deployment-65bb9c49c6-fjd5x","query_string":["Hello=World!"],"timestamp":"2020-06-21T14:03:38.517778"}
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# curl localhost:30523/api/echo?Hello=World!
{"api":"echo","nodename":"tk-ub18-microk8s","pod_ip":"10.1.94.93","podname":"example-echoflask-deployment-65bb9c49c6-grlp4","query_string":["Hello=World!"],"timestamp":"2020-06-21T14:03:39.713882"}
root@tk-ub18-microk8s:~/echoflask-operator/echoflask# curl localhost:30523/api/echo?Hello=World!
{"api":"echo","nodename":"tk-ub18-microk8s","pod_ip":"10.1.94.93","podname":"example-echoflask-deployment-65bb9c49c6-grlp4","query_string":["Hello=World!"],"timestamp":"2020-06-21T14:03:41.128680"}
以上です。