CRDマニフェストの生成

コントローラでカスタムリソースを扱うためには、そのリソースのCRD(Custom Resource Definition)を定義する必要があります。 下記の例の様にCRDは長くなりがちで、人間が記述するには少々大変です。

そこでKubebuilderではcontroller-genというツールを利用して、Goで記述したstructからCRDを生成する方式を採用しています。

kubebuilder create apiコマンドで生成されたapi/v1/tenant_types.goを見てみると、TenantSpec, TenantStatus, Tenant, TenantListという構造体が定義されており、たくさんの// +kubebuilder:から始まるマーカーコメントが付与されています。 controller-genは、これらの構造体とマーカーを頼りにCRDの生成をおこないます。

Tenantがカスタムリソースの本体となる構造体です。TenantListTenantのリストを表す構造体です。これら2つの構造体は基本的に変更することはありません。 TenantSpecTenantStatusTenant構造体を構成する要素です。この2つの構造体を書き換えてカスタムリソースを定義していきます。

一般的にカスタムリソースのSpecはユーザーが記述するもので、システムのあるべき状態をユーザーからコントローラに伝えるために利用されます。 一方のStatusは、コントローラが処理した結果をユーザーや他のシステムに伝えるために利用されます。

TenantSpec

さっそくTenantSpecを定義していきましょう。

作成するカスタムコントローラにおいて、テナントコントローラが扱うカスタムリソースとして、下記のようなマニフェストを検討しました。

apiVersion: multitenancy.example.com/v1
kind: Tenant
metadata:
  name: sample
spec:
  namespaces:
    - test1
    - test2
  namespacePrefix: sample-
  admin:
    kind: User
    name: test
    namespace: default
    apiGroup: rbac.authorization.k8s.io

上記のマニフェストを取り扱うための構造体を用意しましょう。

tenant_types.go

// TenantSpec defines the desired state of Tenant
type TenantSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // Namespaces are the names of the namespaces that belong to the tenant
    // +kubebuiler:validation:Required
    // +kubebuiler:validation:MinItems=1
    Namespaces []string `json:"namespaces"`
    // NamespacePrefix is the prefix for the name of namespaces
    // +optional
    NamespacePrefix string `json:"namespacePrefix,omitempty"`
    // Admin is the identity with admin for the tenant
    // +kubebuiler:validation:Required
    Admin rbacv1.Subject `json:"admin"`
}

まず下記の3つのフィールドを定義します。

  • Namespaces: テナントに属するnamespaceの一覧を指定
  • NamespacePrefix: namespace名のプリフィックスを指定
  • Admin: テナントの管理ユーザーを指定

各フィールドの上に// +kubebuilderという文字列から始まるマーカーと呼ばれるコメントが記述されています。 これらのマーカーによって、生成されるCRDの内容を制御することができます。

付与できるマーカーはcontroller-gen crd -wコマンドで確認することができます。

Required/Optional

NamespacesAdminフィールドには+kubebuiler:validation:Requiredマーカーが付与されています。 これはこのフィールドが必須項目であることを示しており、ユーザーがマニフェストを記述する際にこの項目を省略することができません。 一方のNamespacePrefixには+optionalが付与されており、この項目が省略可能であることを示しています。

マーカーを指定しなかった場合はデフォルトでRequiredなフィールドになります。 ファイル内に下記のマーカーを配置すると、デフォルトの挙動をOptionalに変更することができます。

// +kubebuilder:validation:Optional

+optionalマーカーを付与しなくても、フィールドの後ろのJSONタグにomitemptyを付与した場合は、自動的にOptionalなフィールドとなります。

type SampleSpec struct {
    Value string `json:"value,omitempty"`
}

Optionalなフィールドは、以下のようにフィールドの型をポインタにすることができます。 これによりマニフェストで値を指定しなかった場合の挙動が異なります。 ポインタ型にした場合はnullが入り、実体にした場合はその型の初期値(intの場合は0)が入ります。

type SampleSpec struct {
    // +optional
    Value1 int  `json:"value1"`
    // +optional
    Value2 *int `json:"value2"`
}

Validation

Namespacesフィールドには// +kubebuiler:validation:MinItems=1というマーカーが付与されています。 これは最低1つ以上のnamespaceを記述しないと、カスタムリソースを作成するときにバリデーションエラーとなることを示しています。

MinItems以外にも下記のようなバリデーションが用意されています。 詳しくはcontroller-gen crd -wコマンドで確認してください。

  • リストの最小要素数、最大要素数
  • 文字列の最小長、最大長
  • 数値の最小値、最大値
  • 正規表現にマッチするかどうか
  • リスト内の要素がユニークかどうか

TenantStatus

次にテナントリソースの状態を表現するためにTenantStatusConditionsフィールドを追加します。 このようなCondition型は様々なリソースで利用されている頻出パターンなので覚えておくとよいでしょう。

tenant_types.go

// TenantStatus defines the observed state of Tenant
type TenantStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // Conditions is an array of conditions.
    // +optional
    Conditions []TenantCondition `json:"conditions,omitempty"`
}

type TenantCondition struct {
    // Type is the type fo the condition
    Type TenantConditionType `json:"type"`
    // Status is the status of the condition
    Status corev1.ConditionStatus `json:"status"`
    // Reason is a one-word CamelCase reason for the condition's last transition.
    // +optional
    Reason string `json:"reason,omitempty"`
    // Message is a human-readable message indicating details about last transition.
    // +optional
    Message string `json:"message,omitempty"`
    // Message is a human-readable message indicating details about last transition.
    LastTransitionTime metav1.Time `json:"lastTransitionTime"`
}

// TenantConditionType is the type of Tenant condition.
// +kubebuilder:validation:Enum=Ready
type TenantConditionType string

// Valid values for TenantConditionType
const (
    ConditionReady TenantConditionType = "Ready"
)

汎用的な Condition 型が Go 1.19 で追加されるので、こちらを使っていくのも良いです。

TenantConditionTypeには// +kubebuilder:validation:Enum=Readyというマーカーが付与されています。 これによりTenantConditionTypeは列挙型となり、マーカーで列挙した値(ここでは"Ready")以外の値を指定できなくなります。

このStatusフィールドにより、ユーザーや他のシステム(モニタリングシステムなど)がテナントリソースの状態を確認することができるようになります。

テナントの作成に成功した場合には下記のようなStatusになります。

apiVersion: multitenancy.example.com/v1
kind: Tenant
metadata:
  name: sample
spec: # 省略
status:
  conditions:
  - type: Ready
    status: True
    lastTransitionTime: "2020-07-18T09:01:02Z"

テナントの作成に失敗すると下記のようなStatusになります。

apiVersion: multitenancy.example.com/v1
kind: Tenant
metadata:
  name: sample
spec: # 省略
status:
  conditions:
  - type: Ready
    status: False
    reason: Failed
    message: "failed to create 'test1' namespace"
    lastTransitionTime: "2020-07-18T10:15:34"

Tenant

続いてTenant構造体のマーカーを見てみましょう。

tenant_types.go

// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="ADMIN",type="string",JSONPath=".spec.admin.name"
// +kubebuilder:printcolumn:name="PREFIX",type="string",JSONPath=".spec.namespacePrefix"
// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"

// Tenant is the Schema for the tenants API
type Tenant struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   TenantSpec   `json:"spec,omitempty"`
    Status TenantStatus `json:"status,omitempty"`
}
  • +kubebuilder:object:root=true: Tenant構造体がカスタムリソースのrootオブジェクトであることを表すマーカーです。
  • +kubebuilder:resource:scope=Cluster: Tenantがcluster-scopeのカスタムリソースであることを表すマーカーです。namespaced-scopeの場合は"scope=Namespaced"とするか、このマーカーを省略します。

上記に加えて+kubebuilder:subresource+kubebuilder:printcolumnを付与します。

subresource

+kubebuilder:subresource:statusというマーカーを追加すると、statusフィールドがサブリソースとして扱われるようになります。

サブリソースを有効にするとstatusが独自のエンドポイントを持つようになります。 これによりTenantリソース全体を取得・更新しなくても、statusのみを取得したり更新することが可能になります。 ただし、あくまでもメインのリソースに属するリソースなので、個別に作成や削除することはできません。

ユーザーがspecフィールドを記述し、コントローラがstatusフィールドを記述するという役割分担を明確にすることができるので、基本的にはstatusはサブリソースにしておくのがよいでしょう。

なお、CRDでは任意のフィールドをサブリソースにすることはできず、statusscaleの2つのフィールドのみに対応しています。

printcolumn

+kubebuilder:printcolumnマーカーを付与すると、kubectlでカスタムリソースを取得したときに表示する項目を指定することができます。

表示対象のフィールドはJSONPathで指定することが可能です。 これによりJSONPath=".status.conditions[?(@.type=='Ready')].status"と記述すると、status.conditionsの中のtypeが"Ready"な要素のstatusの値を表示することができます。

kubectlでTenantリソースを取得すると、下記のようにPREFIXやREADYの値が表示されていることが確認できます。

$ kubectl get tenant
NAME            ADMIN     PREFIX           READY
tenant-sample   default   tenant-sample-   True

results matching ""

    No results matching ""