Custom Resource Definition

V tomto článku sa pozrieme na to, ako zadefinovať objekt na strane Kubernetesu, a to pomocou CRD - Custom Resource Definition.

Je to podobné ako s databázou. Kym si nezadefinujeme tabuľku a jej štruktúru, nemôžeme do nej INSERT-ovať alebo SELECT-ovať dáta. Namiesto tabuliek Kubernetes API umožňuje definovať typy objektov, a to práve pomocou Custom Resource Definition - CRD.

Vráťme sa v rýchlosti k minulému článku. V Go som si zadefinoval môj vlastný typ Starfighter. Ten som vedel serializovať a deserializovať na štandardný Kubernetes YAML manifest. Čo by sa ale stalo, ak by som tento YAML aplikoval cez kubectl apply?

$ kubectl apply -f - << EOF
apiVersion: starwars.okontajneroch.sk/v1
kind: Starfighter
metadata:
   name: x-wing-1
   namespace: default
spec:
   faction: rebellion
   type: x-wing
   pilot: Luke Skywalker
EOF
error: resource mapping not found for name: "x-wing-1" namespace: "default" from "./xwing.yaml": no matches for kind "Starfighter" in version "starwars.okontajneroch.sk/v1"
ensure CRDs are installed first

Kubernetes totiž ešte nema znalosť o tomto objekte. Pre svoj Starfighter potrebujem vytvoriť a aplikovať jeho CRD - Custom Resource Definition. V ňom popíšem štrukturu dát, aké má mať vlastnosti a aké akcie nad objektom môžem vykonávať. CRD nieje nič iné ako ďalší resource, podobne ako Pod alebo Deployment. CRD pre môj Starfighter bude vyzerať následovne:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: starfighters.starwars.okontajneroch.sk
spec:
  group: starwars.okontajneroch.sk
  scope: Namespaced
  names:    
    plural: starfighters
    singular: starfighter
    kind: Starfighter
    shortNames:
       - sf
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              faction:
                type: string
              pilot:
                type: string
              type:
                type: string
          status:
            type: object
            properties:
              phases:
                items:
                  type: string
                type: array

Trojica parametrov apiVersion, kind a metadata su chronický známa. To čo treba zmieniť je, že parameter name by mal dodržiavať konvenciu <množnéčíslo>.<skupina> , a to bez verzie. V mojom prípade ide o starfighters.starwars.okontajneroch.sk. Opäť ide o inú formu zápisu GVR/GVK.

V časti spec sa nachádza niekoľko parametrov. Parameter group je skupina, ktorej typ objektu bude patriť. Parameter scope môže mať hodnoty Namespaced alebo Cluster. To určuje, či objekt bude musieť byť súčasťou nejakého menného priestoru, alebo bude definovaný pre celý cluster. Od tohto sa odvýja aj samotný endpoint objektov.

Potom nasleduje names, kde definujem rôzne formy mena môjho typu. Formy singular a plural používa Kubernetes API na registráciu endpointov. No častejšie sa s tymito menami stretávame pri kubectl. Napríklad kubectl get starfighters, alebo kubectl describe starfighter.

Parameter kind už asi nepotrebuje nejake extra vysvetlovanie. Meno by malo byť jednodné číslo, a malo by začínať veľkým písmenom.

Posledným parametrom je shotNames. Ide o zoznam tzv. skratených mien. Skrátene mená su známe opäť pri kubectl. Ide o pomôcku aby sme nemuseli vypisovať dlhé mená. Napríklad namiesto kubectl get HorizontalPodAutoscalers mi stačí použiť kubectl get hpa.

Časť versions: je najkomplexnejšia. CRD môže definovať viacero verzii. Každá verzia ma svoju schému. Schéma definuje samotnú štrukturu objektu a je v CRD definovaná pomocou Open API špecifikácie. Kubernetes podporuje Open API verzie 2 a 3. Odporúča sa ale používať Open API v3. Preto aj ja používam openAPIV3Schema. V tejto časti sa definuje samotná štruktúra objektu, čiže to, čo bude tvoriť spec môjho Starfighter objektu.

      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              faction:
                type: string
              pilot:
                type: string
              type:
                type: string

Tu je vidieť, že spec bude mať 3 parametre: faction, pilot a type. Všetky parametre sú typu ľubovolný string, bez nejakej dodatočnej validácie. Pre definovanie pokročilejších štruktúr doporučujem zabdrnuť do dokumentácie k Open API.

Ďalšia časť je status z jediným parametrom phases. Tento parameter je pole reťazcov.

          status:
            type: object
            properties:
              phases:
                items:
                  type: string
                type: array

To ako presne vytvoriť komplexnejšie štruktuy je mimo Kubernetesu a bolo by to na samotný článok. Ako som už vyžšie spomínal, CRD sa spolieha na OpenAPI V3 špecifickáciu. Preto doporučujem zabdrnuť do dokumentácie k Open API. Napríklad [schema object](1. Schema Object)

Takto vytvorené CRD použijem pomocou kubectl apply:

$ kubectl apply -f ./starfighter_crd.yaml

Po tomto kroku cluster už bude poznať objekt Starfighter. Overiť si to môžem pomocou kubectl api-resources:

$ kubectl api-resources --api-group='starwars.okontajneroch.sk'
NAME          SHORTNAMES  APIVERSION                    NAMESPACED  KIND
starfighters  sf          starwars.okontajneroch.sj/v1  true        Starfighter
$ kubectl apply -f - << EOF
apiVersion: starwars.okontajneroch.sk/v1
kind: Starfighter
metadata:
   name: x-wing-1
   namespace: default
spec:   
   faction: rebellion
   type: x-wing
   pilot: Luke Skywalker
EOF

Generovanie CRD z Go typov

Existuje ale aj možnosť - vlastne 2 možnosti, ako vygenerovať CRD z môjho Go kódu. A to sú nástroje controller-gen, ktorý patri pod projekt Kubebuilder, a code-generator, ktorý je priamo nástrojom Kubernetesu. Ja pôjdem cestou controller-gen, kedže je jednoduchší. Nainštalujem si ho pomocou go install:

$ go install sigs.k8s.io/controller-tools/cmd/controller-gen   

V ďalšiom kroku označím pomocou markera +kubebuilder:object:generate=true ten typ, pre ktorý chcem generovať CRD. V súbore ./api/v1/types.go pridám marker pre Starfighter:

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:object:generate=true
type Starfighter struct {
	...
}

Generátor potrebuje ešte vedieť, do akej skupiny (group) budú objekty patriť. To nastavím pre celú knižnicu a to tak, že opäť pridam špecialný komentár do api/v1/doc.go:

// +k8s:deepcopy-gen=package
// +groupName=starwars.okontajneroch.sk
package v1

Pre takto upravený kód teraz môžem spustiť controller-gen, ktorý sa postará o vygenerovanie potrebných CRD:

$ controller-gen crd paths=./api/... output:crd:dir=./k8s

Ak všetko prebehlo správne, tak v adresári ./k8s najdem vygenerovaný súbor starwars.okontajneroch.sk_starfighters.yaml, ktorého obsahom bude práve CRD pre môj typ Starfighter:

$ cat ./k8s/`starwars.okontajneroch.sk_starfighters.yaml
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.13.0
  name: starfighters.starwars.okontajneroch.sk
spec:
  group: starwars.okontajneroch.sk
  names:
    kind: Starfighter
    listKind: StarfighterList
    plural: starfighters
    singular: starfighter
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
      ...

Ide o trochu ukecanejšiu verziu CRD, ktorý sme si pred tým ručne napisali. Ak sa však pozornejšie pozrieme na CRD, tak napríklad momentálne mi chyba shortNames. Ako povedať controller-gen aby vygeneroval CRD s krátkym menom sf? Stačí opäť môj typ obohatiť o špecialný komentár:

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:object:generate=true
// +kubebuilder:resource:shortName={"sf"}
type Starfighter struct {
	...

Takýchto markerov, ktoré vedia ovplyvniť generovanie CRD je viacero. Celý zoznam sa dá pozrieť na https://book.kubebuilder.io/reference/markers/crd.

Generovanie Go typov z CRD

Predstavme si teraz inú situáciu: Mam k dispozícii už hotové CRD, napríklad CRD operátora, ktorý bol napísaný v inom jazyku. A ja by som chcel robiť nejakú automatizáciu v Go. Potrebujem teda z CRD vygenerovať Go typy.

Treba hneď na začiatok povedať, že toto nieje štandardný prípad. Takýmto prístupom chcem zasahovať do objektov, ktoré patria úplne inému programu. Tu ja osobne nepoznám nejake spoľahlivé a jednoduché riešenie. Preto ak môžem, tak sa takémuto prípadu snažim vyvarovať.

Ale rozoberiem aspoň tie spôsoby, ktoré su mne známe.

Asi najlepšie je nájsť Go knižnicu s typmi a jednoducho si ju pridať do svojho projektu ako závislosť.

Druhou dobrou možnosťou je napísať si typy ručne. Toto je však vhodné len pre objekty s jednoduchou štrukturou. Pri komplikovanejších objektoch to môže byť problém.

Inou možnosťou je, si nechať typy vygenerovať cez generátor crd-codegen od RedHatu. Tu však musím upozorniť, že tento projekt je experimentálny a v dobe písania tohto článku, 3 roky neudržiavaný.

No a posledná z možnosti je pozerať na CRD ako Open API. Čiže prekonvertovať CRD YAML na OpenAPI, a následne aplikovať OpenAPI generátor (napríklad oapi-codegen), ktorý vygeneruje Go typy.

Uvedené možnosti dobre ilustruju fakt, že ide o neštandardný prípad. To je ale môj osobný prístup. Samozrejme ak niekto pozná lepší, spoľahlivejší a jednoduchší spôsob ako vygenerovať typy z CRD, budem rád ak mi napiše.

Záver

Pre mnohých zrejme CRD nieje nič nové. Stretol sa s nim asi každý, čo sa o trošku viac šuchol o Kubernetes. Tento článok bol taka povinná jazda. Teraz už mám môj fitkívny objekt Starfighter k dispozícii ako typ v Go, tak aj na strane klastra. Ďalší krok je teda logicky samotná komunikácia s Kubernetes API, čiže sa pozriem na Client-Go knižnicu.

Tak ako aj v minulom článku, kompletný kód a projekt je dostupny na GitHube github.com/okontajneroch

Ak sa ti článok páčil a chceš ma podporiť, tak budem rád za lajk, zdielanie alebo followovanie na LinkedIn. Môj LinkedIn je tiež otvorený každému, kto má akukoľvek otázku k tejto téme.

<