執筆者 : 森 克彦
1. はじめに
Kubernetesでは複雑な操作が必要なアプリを動かすためのカスタムコントローラを用意しています。ここでは、DPDKアプリのSPPを題材にしてSPPのカスタムコントローラ(以下operator)の開発例を示したいと思います。前提知識としてK8sのオペレーターなど知識が必要ですが、Webで詳しく説明している資料などがありますので、ここでは割愛して実際の開発の流れを示したいと思います。
なお、使用するフレームワークはOperator SDKです。 ここで使用したソフトウェアのバージョンは以下です。
dpdk: 20.02 spp: 20.02 docker: 18.9.7 k8s: 1.17.0 golang: 1.13.3 operator-sdk: 0.13.0
カスタムリソース
カスタムリソースはK8s APIの拡張です。リソースとはK8s APIのエンドポイントで特定のAPIオブジェクトのコレクションを指します。 カスタムリソースは稼働しているK8sクラスターに登録されると、ユーザーはkubectlを使い、ビルトインのリソースである Podと同じようにオブジェクトを作成、アクセスすることが可能です。
カスタムコントローラ
カスタムリソースは単純に構造化データの読み書きの機能を提供します。カスタムリソースを制御するカスタムコントローラと組み合わせることで、カスタムリソースは宣言的APIを提供します。宣言的APIとはリソースのあるべき状態を指定して、K8sオブジェクトの現在の状態を、あるべき状態に同期し続けるように制御することです。 コントローラは構造化データをユーザーが指定したあるべき状態と解釈し、その状態を管理し続けます。カスタムコントローラにより特定アプリケーションの知識をK8s APIの拡張に変換することができます。
2. 事前準備
K8sクラスタ
ここでは、Kubeadmを使ってマスターノード一台、ワーカーノード一台のK8sクラスタを作成します。 マスターノード上にgolangの環境とOperator SDKの環境を構築して、spp-opeartorの開発と動作確認ができるようにします。 ワーカーノード上にsppアプリのコンテナが動作できるようにsppコンテナイメージをビルドします。 構築手順はここを参照ください。 マシンのスペックは以下です。
マスターノード: OS: ubuntu 18.04 CPU: core 2 Memory: 4GB Disk: 30GB NIC: 1 その他の設定: swap領域なし(k8sのバージョン1.8以降はswapを無効にしないと動作しないです) ワーカーノード: OS: ubuntu 18.04 CPU: core 16 Memory: 32GB Disk: 200GB NIC: 1 その他の設定: swap領域なし(k8sのバージョン1.8以降はswapを無効にしないと動作しないです)
K8sクラスタが正しく構築できたことを確認します。
$ kubectl get nodes -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME k8sm Ready master 3m30s v1.17.0 192.168.122.60 none Ubuntu 18.04.3 LTS 4.15.0-72-generic docker://18.9.7 k8sn1 Ready none 40s v1.17.0 192.168.122.93 none Ubuntu 18.04.3 LTS 4.15.0-72-generic docker://18.9.7
GolangとOperator SDK
ここでは、マスターノード上でspp-operator開発できるように以下の手順でGo言語とOperator SDKをインストールします。
Golangのインストール
パッケージをダウンロード $ wget https://dl.google.com/go/go1.13.3.linux-amd64.tar.gz $ sudo tar -C /usr/local/ -xzf go1.13.3.linux-amd64.tar.gz $ sudo rm /usr/bin/go; sudo ln -s /usr/local/go/bin/go /usr/bin/go GOPATHを設定 $ cat <<EOF >>.bashrc export GOPATH=$HOME/go EOF $ source ~/.bashrc golangの確認 $ go version go version go1.13.3 linux/amd64
Operator-SDKのインストール
パッケージをダウンロード $ RELEASE_VERSION=v0.13.0 $ curl -LO https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu $ chmod +x operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu && sudo mkdir -p /usr/local/bin/ && sudo cp operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu /usr/local/bin/operator-sdk && rm operator-sdk-${RELEASE_VERSION}-x86_64-linux-gnu operator-sdkの確認 $ operator-sdk version operator-sdk version: "v0.13.0", commit: "1af9c95bb51420c55a7f7f2b7fabebda24451276", go version: "go1.13.3 linux/amd64
コンテナイメージ
ここでは、ワーカーノード上でsppコンテナイメージを作成します。
必要なリポジトリをダウンロード $ cd ~ $ git clone -b 20.02 http://dpdk.org/git/dpdk $ git clone -b 20.02 http://dpdk.org/git/apps/spp $ git clone https://opendev.org/x/networking-spp $ cd ~/spp sppcコンテナイメージを作成 $ cd ~/spp/tools/sppc $ python build/main.py -t spp --dist-ver 18.04 --dpdk-branch v20.02 --spp-branch v20.02 sppc ctlコンテナイメージを作成 $ cd ~/networking-spp/operator/build_spp_ctl $ docker build . -t sppc/spp-ctl:18.04
3. 新規開発プロジェクト作成
プロジェクト作成
ここでは、マスターノード上で以下の手順でプロジェクトを作成します。 $GOPATH/srcの下にリモートURL(opendev.org/x/networking-spp/)を作成してから、Operator SDKを使ってプロジェクト(operator)の雛形を生成します。
$ mkdir -p $GOPATH/src/opendev.org/x/networking-spp/ $ cd $GOPATH/src/opendev.org/x/networking-spp/ $ operator-sdk new operator --repo=opendev.org/x/networking-spp/operator INFO[0000] Creating new Go operator 'operator'. INFO[0000] Created go.mod INFO[0000] Created tools.go INFO[0000] Created cmd/manager/main.go INFO[0000] Created build/Dockerfile INFO[0000] Created build/bin/entrypoint INFO[0000] Created build/bin/user_setup INFO[0000] Created deploy/service_account.yaml INFO[0000] Created deploy/role.yaml INFO[0000] Created deploy/role_binding.yaml INFO[0000] Created deploy/operator.yaml INFO[0000] Created pkg/apis/apis.go INFO[0000] Created pkg/controller/controller.go INFO[0000] Created version/version.go INFO[0000] Created .gitignore INFO[0000] Validating project INFO[0008] Project validation successful. INFO[0008] Project creation complete.
初期プロジェクトのディレクトリ構造は以下です。
buildにはoperatorのコンテナイメージ作成に必要なファイル、deployにはoperatorをK8sにディプロイするためのyamlファイル、 pkgにはユーザが実装するコードのファイルが置いています。 go.modは依存パッケージのリストファイルです。 cmd/manager/main.goがメインプログラムファイルです。
├── build │ ├── bin │ │ ├── entrypoint │ │ └── user_setup │ └── Dockerfile ├── cmd │ └── manager │ └── main.go ├── deploy │ ├── operator.yaml │ ├── role_binding.yaml │ ├── role.yaml │ └── service_account.yaml ├── go.mod ├── go.sum ├── pkg │ ├── apis │ │ └── apis.go │ └── controller │ └── controller.go ├── tools.go └── version └── version.go
CRD作成
CRD(Custom Resources Definitions)はCR(Custom Resources)の定義仕様です。 ここでは、以下の手順でSppという新しい独自のリソースを定義して、Kubernetes APIを拡張します。 operator-sdkのオプションkindはリソースの名前、api-versionはリソースのAPIアクセスポイントを指定します。 両方ともユーザがK8sでSppリソースを使用する際にyamlで指定する必要なものです。
$ cd $GOPATH/src/opendev.org/x/networking-spp/operator $ operator-sdk add api --api-version=spp.spp/v1 --kind=Spp INFO[0000] Generating api version spp.spp/v1 for kind Spp. INFO[0000] Created pkg/apis/spp/group.go INFO[0000] Created pkg/apis/spp/v1/spp_types.go INFO[0000] Created pkg/apis/addtoscheme_spp_v1.go INFO[0000] Created pkg/apis/spp/v1/register.go INFO[0000] Created pkg/apis/spp/v1/doc.go INFO[0000] Created deploy/crds/spp.spp_v1_spp_cr.yaml INFO[0001] Created deploy/crds/spp.spp_spps_crd.yaml INFO[0001] Running deepcopy code-generation for Custom Resource group versions: [spp:[v1], ] INFO[0012] Code-generation complete. INFO[0012] Running CRD generation for Custom Resource group versions: [spp:[v1], ] INFO[0013] Created deploy/crds/spp.spp_spps_crd.yaml INFO[0013] CRD generation complete. INFO[0013] API generation complete.
CRD作成するとdeploy/crds/spp.spp_spps_crd.yamlにファイルができます。 これを以下の手順でapplyすると、K8sクラスタにSppというリソースを定義できます。
$ cd $GOPATH/src/opendev.org/x/networking-spp/operator $ kubectl apply -f deploy/crds/spp.spp_spps_crd.yaml CRDを確認 $ kubectl get crd NAME CREATED AT spps.spp.spp 2020-01-21T05:36:17Z
また、ユーザがSppリソースを定義するyamlファイルの雛形も作成されています。 この雛形をもとにSppリソースの詳細を定義します。
cat $GOPATH/src/opendev.org/x/networking-spp/operator/deploy/spp.spp_v1_spp_cr.yaml apiVersion: spp.spp/v1 kind: Spp metadata: name: example-spp spec: # Add fields here size: 3
4. API定義
モデル
sppモデル
sppはdpdkを使って仮想化されたネットワークで高速かつ柔軟に機能間連携を実現する新しい技術です。sppは以下のプロセスで構成されています。 ユーザは必要に応じてこれらのプロセスを組み合わせて様々なネットワークモデルを構築できます。この中でctlとprimaryは必須なプロセスでどのモデルを構築する場合でも必要です。また1つのホスト上ではctlとprimaryはそれぞれ1つしか起動できないという制約があります。その他のプロセスはオプションで構築するモデルに応じて複数起動できます。 ctlプロセスは上記で作成したコンテナイメージsppc/spp-ctl:18:04の中にあります。 ctl以外のプロセスは上記で作成したコンテナイメージsppc/spp-ctl:18.04の中にあります。
- ctl: REST APIを受け取るサーバプロセス
- primary: リソースを管理するプロセス
- nfv: nfvプロセス
- vf: vfプロセス
- mirror: mirrorプロセス
- pcap: pcapプロセス
yaml表現
sppのモデルをK8s上で実現する場合は、まずユーザが構築したいモデルをyamlの形で表現する必要があります。このyaml表現で利用できるようなカスタムリソースとそれを操作するコントローラを実装する必要があります。 例えば以下はyaml表現の例です。
apiVersion: spp.spp/v1 kind: Spp metadata: name: primary-vdev spec: ctl: name: ctl image: sppc/spp-ctl:18.04 primary: name: primary image: sppc/spp-ubuntu:18.04 nfvs: - name: nfv:1 image: sppc/spp-ubuntu:18.04 - name: nfv:2 image: sppc/spp-ubuntu:18.04 vfs: - name: vf:1 image: sppc/spp-ubuntu:18.04 - name: vf:2 image: sppc/spp-ubuntu:18.04 - name: vf:3 image: sppc/spp-ubuntu:18.04 mirrors: - name: mirror:1 image: sppc/spp-ubuntu:18.04 pcaps: - name: pcap:1 image: sppc/spp-ubuntu:18.04
上記の例ではapiVersionはリソースのAPIのエンドポイントとバージョンを指定します。kindはリソースの種類です。 metadata.nameはsppリソースの名前です。specの下にctl, primary,nfvs,vfs,mirrors,pcapsの項目を定義します。 これらの項目はそれぞれのプロセスを識別するために用います。さらにそれぞれの下に以下の共通な項目が必要です。
- name: podにつける名前(型:文字列)
- image: コンテナイメージの名前(型:文字列)
Podを識別するnameの項目とコンテナイメージを指定するimageの項目が1つのセットです。k8s上ではこの1セットが1つのPodになります。 ctlとprimaryはこのセットを1つしか指定できませが、それ以外はリストで複数指定できます。 imageはユーザが別のイメージを指定するケースを想定してすべての1セットの中に入れます。
以上のことを踏まえて、ここではctlとprimaryのみで構成されている一番基本的なモデルをもとにspp apiリソースの項目を定義します。 また、以下のようにprimaryはnameとimage以外に必要な項目はあります。項目の詳細説明は割愛します。
- eal: DPDKのオプション
- lcores: 専有CPUの数(型:文字列)
- socket-mem: ソケットメモリ(型:文字列)
- vdevs: vdevs (型: リストのリストの文字列)
- portmask: ポートマスク (型:文字列)
これをyamlファイルにすると以下です。ユーザはこのyamlファイルをapplyするとprimary-vdevという名前のsppリソースを作成できます。
apiVersion: spp.spp/v1 kind: Spp metadata: name: primary-vdev spec: ctl: name: ctl image: sppc/spp-ctl:18.04 primary: name: primary image: sppc/spp-ubuntu:18.04 eal: lcores: "1" socket-mem: "1024" vdevs: [["net_tap1", "iface=foo1"]] portmask: "0x01"
データ構造
ここでは、上記のyamlファイルに基づいてsppのデータ構造(カスタムリソース)を定義します。 CRDを作成したときに以下のファイルが生成されています。
GOPATH/src/opendev.org/x/networking-spp/operator/pkg/apis/spp/v1/spp_types.go
このファイルに以下のコードを追加します。 基本的に変数名 型 で定義します。 json:"XXX"のXXXはyamlファイルの項目名と一致する必要があります。
// VdevOption defines Vdev option type VdevOption []string //EalOption defines EAL options type EalOption struct { Lcores string `json:"lcores"` SocketMem string `json:"socket-mem"` Vdevs []VdevOption `json:"vdevs"` } //SppPrimary defines SPP Primary options type SppPrimary struct { Name string `json:"name"` Image string `json:"image"` Eal EalOption `json:"eal"` PortMask string `json:"portmask"` } // SppCtl defines SPP Ctl options type SppCtl struct { Name string `json:"name"` Image string `json:"image"` }
さらに、上記で定義したCtlとPrimaryを以下のようにSppSpecの中に追加します。
// SppSpec defines the desired state of Spp type SppSpec 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 Ctl SppCtl `json:"ctl"` Primary SppPrimary `json:"primary"` }
最後に、sppリソースの状態を保存する構造体SppStatusを以下のように編集します。 ServiceVipはctlのserviceのClusterIPを保存します。
// SppStatus defines the observed state of Spp type SppStatus 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 ServiceVip string `json:"service-vip"` }
*_types.goファイルを変更した後は、常に次のコマンドを実行して生成されたコードを更新する必要があります。
$ cd $GOPATH/src/opendev.org/x/networking-spp/operator $ operator-sdk generate k8s INFO[0000] Running deepcopy code-generation for Custom Resource group versions: [spp:[v1], ] INFO[0012] Code-generation complete.
5. コントローラ作成
コントローラ作成
データ構造を定義しましたので、次に以下のようにコントローラを作成します。
$ cd $GOPATH/src/opendev.org/x/networking-spp/operator $ operator-sdk add controller --api-version=spp.spp/v1 --kind=Spp INFO[0000] Generating controller version spp.spp/v1 for kind Spp. INFO[0000] Created pkg/controller/spp/spp_controller.go INFO[0000] Created pkg/controller/add_spp.go INFO[0000] Controller generation complete.
要件
sppのプロセスは起動順序の依存関係があります。 まずctlプロセスが起動してからREST API受付可能な状態になったら、次にprimaryプロセスを起動できます。 primaryプロセスを起動する際にオプションでctlプロセスのコンテナのIPアドレスを渡す必要があります。 以上のことを踏まえてコントローラではまず、ctlプロセスのpodを作成します。このpodにラベルをつけます。 次にこのラベルを指定するserviceを作成します。このserviceのClusterIPを取得してctlプロセスにアクセスしてREST API受付可能な状態にあることを確認できたら、 primaryプロセスのpodを作成します。
実装
ここでは、実装するコード例を示します。
共通機能パッケージ
以下の内容のcommonパッケージファイルを作成します。(GOPATH/src/opendev.org/x/operator/pkg/model/common/common.go) この中でctlのport番号を静的変数、labelを作成するMakeLabel関数を定義しています。
package common // define spp ctl port number const ( CTL_PRI_PORT_NUM = 5555 CTL_SEC_PORT_NUM = 6666 CTL_CLI_PORT_NUM = 7777 ) func MakeLabel(key string, value string) map[string]string { return map[string]string{key: value} }
以下の内容のmodelパッケージファイルを作成します。(GOPATH/src/opendev.org/x/operator/pkg/model/model.go) まず、上記で作成したcommonとsppデータ構造/pkg/apis/v1パッケージをそれぞれsppcommon、sppv1という別名してimportします。 その他のパッケージはK8sのパッケージと一般的なGolangパッケージです。 次にSppという構造体を定義します、コントローラの中でこの型の変数を使ってsppの情報を扱う予定です。 Spp.V1の型はsppv1のSpp構造体です。将来別のバージョンのリソースを追加したい場合はここにV2などの型を追加できます。
package model import ( sppcommon "opendev.org/x/networking-spp/operator/pkg/model/common" sppv1 "opendev.org/x/networking-spp/operator/pkg/apis/spp/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "fmt" "strconv" "strings" "time" ) type Spp struct { V1 *sppv1.Spp }
pod機能パッケージ
以下の内容をmodelパッケージファイルに追加します。(GOPATH/src/opendev.org/x/operator/pkg/model/model.go) 上記で定義したSppにメソッドNewCtlを定義します。このメソッドは変数sppの情報をもとにctrlのpodとserviceオブジェクトを作成して返します。 K8sのオブジェクトを作成するときは以下のようにcorev1を使っています。
Spp Ctl */ // NewCtl makes a ctl pod and service using CR from yaml func (spp *Spp) NewCtl() (*corev1.Pod, *corev1.Service) { label := sppcommon.MakeLabel(spp.V1.Name, spp.V1.Spec.Ctl.Name) node := sppcommon.MakeLabel(sppcommon.WORKER_NODE_LABEL, spp.V1.Status.Node) /* make a pod for ctl */ CtlPod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: sppcommon.MakeName(spp.V1.Name, spp.V1.Spec.Ctl.Name, "pod"), Namespace: spp.V1.Namespace, Labels: label, }, Spec: corev1.PodSpec{ NodeSelector: node, Containers: []corev1.Container{ { Name: spp.V1.Spec.Ctl.Name, Image: spp.V1.Spec.Ctl.Image, Command: []string{"/root/start-spp-ctl.sh"}, Ports: []corev1.ContainerPort{ { Name: "primary", ContainerPort: sppcommon.CTL_PRI_PORT_NUM, Protocol: "TCP", }, { Name: "secondary", ContainerPort: sppcommon.CTL_SEC_PORT_NUM, Protocol: "TCP", }, { Name: "cli", ContainerPort: sppcommon.CTL_CLI_PORT_NUM, Protocol: "TCP", }, }, }, }, }, } /* make a service for ctl */ CtlService := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: sppcommon.MakeName(spp.V1.Name, spp.V1.Spec.Ctl.Name, "service"), Namespace: spp.V1.Namespace, }, Spec: corev1.ServiceSpec{ Selector: label, Ports: []corev1.ServicePort{ { Name: "primary", Port: sppcommon.CTL_PRI_PORT_NUM, TargetPort: intstr.FromInt(sppcommon.CTL_PRI_PORT_NUM), Protocol: "TCP", }, { Name: "secondary", Port: sppcommon.CTL_SEC_PORT_NUM, TargetPort: intstr.FromInt(sppcommon.CTL_SEC_PORT_NUM), Protocol: "TCP", }, { Name: "cli", Port: sppcommon.CTL_CLI_PORT_NUM, TargetPort: intstr.FromInt(sppcommon.CTL_CLI_PORT_NUM), Protocol: "TCP", }, }, }, } return CtlPod, CtlService }
コントローラのパッケージ
コントローラを作成すると以下のコントローラの雛形ファイル(GOPATH/src/opendev.org/x/operator/pkg/controller/spp/spp_controller.go)が生成されます。 このファイルを以下のように編集します。まず、上記で作成したパッケージと必要なパッケージをimportします。 次に、メソッドReconcileの中でsppmodel.Sppの型のsppオブジェクトを作成し、r.client.Getを使ってユーザが定義したyaml内容(K8sクラスタのetcdの中)を取得してspp.V1に入れます。
package spp import ( "context" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" sppv1 "opendev.org/x/networking-spp/operator/pkg/apis/spp/v1" sppmodel "opendev.org/x/networking-spp/operator/pkg/model" sppcommon "opendev.org/x/networking-spp/operator/pkg/model/common" "reflect" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" "strconv" "time" ) .... func (r *ReconcileSpp) Reconcile(request reconcile.Request) (reconcile.Result, error) { // Log reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) reqLogger.Info("Reconciling Spp") // Fetch the Spp Costum Resource from the yaml file spp := &sppmodel.Spp{} spp.V1 = &sppv1.Spp{} err := r.client.Get(context.TODO(), request.NamespacedName, spp.V1) if err != nil { if errors.IsNotFound(err) { return reconcile.Result{}, nil } return reconcile.Result{}, err } }
以下のようにservice、podを作成するメソッドとpodを探すメソッドを追加します。いずれも作成したオブジェクトを返します。 serviceとpodを作成する前に同じものがあるかどうかをr.client.Getでチェック、なければ作成します。
func (r *ReconcileSpp) CreateService(spp *sppmodel.Spp, service *corev1.Service) (*corev1.Service, error) { reqLogger := log.WithValues() found_service := &corev1.Service{} // See the as the owner and controller if err := controllerutil.SetControllerReference(spp.V1, service, r.scheme); err != nil { return found_service, err } // Check if the pod already exists err := r.client.Get(context.TODO(), types.NamespacedName{Name: service.Name, Namespace: service.Namespace}, found_service) if err != nil && errors.IsNotFound(err) { reqLogger.Info("Creating a new Service", "Service Name", service.Name, "Service Spec", service.Spec) err = r.client.Create(context.TODO(), service) if err != nil { return found_service, err } // Pod created successfully - don't requeue return found_service, nil } else if err != nil { return found_service, err } return found_service, nil } func (r *ReconcileSpp) FindPod(pod_namespace string, pod_name string) (*corev1.Pod, error) { found_pod := &corev1.Pod{} // Check if the pod already exists err := r.client.Get(context.TODO(), types.NamespacedName{Name: pod_name, Namespace: pod_namespace}, found_pod) if err != nil { return found_pod, err } return found_pod, nil } func (r *ReconcileSpp) CreatePod(spp *sppmodel.Spp, pod *corev1.Pod) (*corev1.Pod, error) { reqLogger := log.WithValues() found_pod := &corev1.Pod{} // See the as the owner and controller if err := controllerutil.SetControllerReference(spp.V1, pod, r.scheme); err != nil { return found_pod, err } // Check if the pod already exists err := r.client.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found_pod) if err != nil && errors.IsNotFound(err) { reqLogger.Info("Creating a new Pod", "Pod Name", pod.Name, "Pod Spec", pod.Spec) err = r.client.Create(context.TODO(), pod) if err != nil { return found_pod, err } // Pod created successfully - don't requeue return found_pod, nil } else if err != nil { return found_pod, err } return found_pod, nil }
上記のメソッドを使ってpodとserviceを作成できるようになったので、以下の内容をReconcileメソッドの最後部分に追加します。 ctlのpodのオブジェクトを作成してそれをr.CreatePodに渡して実際のctlのpodを作成します。serviceも同様です。 次にserviceのClusterIPをspp.V1に保存してr.client.Stauts().Updateを使って更新します。 このr.clientを使ってK8sクラスタのetcdにあるsppリソースを取得、更新します。 これでoperator自身は固有な情報を持たないステートレスになります。通常operatorはDeploymentで冗長化された状態で運用しているため、このようにステートレスが望ましいです。
// Create Ctl Pod and Service, Check ctl status and service-vip and Update Status ctl_pod, ctl_service := spp.NewCtl() found_ctl_pod, err := r.CreatePod(spp, ctl_pod) if err != nil { return reconcile.Result{}, err } found_ctl_service, err := r.CreateService(spp, ctl_service) if err != nil { return reconcile.Result{}, err } spp.V1.Status.ServiceVip = found_ctl_service.Spec.ClusterIP r.client.Status().Update(context.TODO(), spp.V1) }
以下の内容をReconcileメソッドの最後部分に追加します。 ctlのpodの状態がRunningかつREST APIを受付可能なら`primaryのpodを作成します。 spp.CheckRunnig(詳細は割愛)はチェック対象とリトライ回数を渡してREST APIがOKならtrueを返すメソッドです。 また、spp.NewPrimaryはspp.V1.Status.ServiceVipをもとにprimaryのpodを作成して返すNewCtlと似たようなメソッドです。
if found_ctl_pod.Status.Phase == "Running" && spp.CheckRunning("ctl", 6) { // Create a Primary Pod and Update status found_primary_pod, err := r.CreatePod(spp, spp.NewPrimary()) if err != nil { return reconcile.Result{}, err }
動作確認
operatorの起動
以下のようにoperatorを起動します。 spp-operatorを起動する前に必ずsppのcrdが作成されいることを確認ください。 事前にcrdが作成されていないとopeartorの起動は失敗します。
$ cd $GOPATH/src/opendev.org/x/networking-spp/operator $ export OPERATOR_NAME=spp-operator $ operator-sdk up local INFO[0000] Running the operator locally. INFO[0000] Using namespace default. ...
sppリソースの作成
以下の内容をprimary.yamlに保存します。
apiVersion: spp.spp/v1 kind: Spp metadata: name: primary-vdev spec: ctl: name: ctl image: sppc/spp-ctl:18.04 primary: name: primary image: sppc/spp-ubuntu:18.04 eal: lcores: "1" socket-mem: "1024" vdevs: [["net_tap1", "iface=foo1"]] portmask: "0x01"
以下のように作成します。
$ kubectl apply -f primary.yaml
sppリソースの確認
以下のように確認できます。
$ kubectl get spp NAME primary-vdev
また、以下のようにpodやserviceを確認できます。
$ kubectl get all -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod/primary-vdev-ctl-pod 1/1 Running 0 4m39s 10.244.3.71 k8sn3 <none> <none> pod/primary-vdev-primary-pod 1/1 Running 0 4m27s 10.244.3.72 k8sn3 <none> <none> NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR service/primary-vdev-ctl-service ClusterIP 10.108.95.162 <none> 5555/TCP,6666/TCP,7777/TCP 4m39s primary-vdev=ctl
以下のように削除もできます。
$ kubectl delete spp primary-vdev
6. おわりに
いかがでしょうか。ここではsppを題材にしながらも、できるだけsppを深入りしないでoperatorの開発例を示しました。 複雑なアプリをK8s上で扱いたい場合は、ここの手順をご参考いただければ幸いです。