Kubernetes カスタムコントローラの開発例

執筆者 : 森 克彦


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上で扱いたい場合は、ここの手順をご参考いただければ幸いです。