ズンドコキヨシ with Kubernetes Ansible Operator - Operator SDKのAnsible Operatorを使ってみた
Sun, Dec 20, 2020
Kubernetes operator-sdk zundoko ansibleTable of Contents
久しぶりにズンドコしたくなったので、Operator SDKのAnsible operatorを使って、KubernetesクラスタでズンドコキヨシするZundoko Ansible Operatorを作った話し。
Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから
— てくも (@kumiromilk) 2016年3月9日
「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ!」って出力した後終了って関数作ったら満点で単位貰ってた
書いたものはGitHubに置いた。
なお、これはKubernetes Advent Calendar 2020 その2の20日目の記事です。
Kubernetes Operatorとは
KubernetesのOperatorというのはCoreOS社(現Red Hat)によって提唱された概念(実装パターン)で、KubernetesのAPIで登録されるKubernetesオブジェクトの内容に従って何らかの処理をするController (e.g. Deployment Controller)の一種。 Controllerが汎用的なのに対して、特定のアプリケーションに特化しているものがOperatorと呼ばれる。
Operatorを実装しようとすると、アプリケーションごとの細かな設定をKubernetesオブジェクトで表現するためにKubernetesのAPIを拡張したくなるんだけど、そのための機能がCustom Resource Definition (CRD)。
CRDとは
KubernetesのAPIを簡単に拡張できる仕組みで、Kubernetesオブジェクト(aka. カスタムリソース)を定義するKubernetesオブジェクト。
定義したいカスタムリソースの名前や型やバリデーションなんかをYAMLで書いてKubernetesに登録すると、そのカスタムリソースをKubernetesのREST APIとかkubectlで作成したり取得したりできるようになる。
Kubernetes Advent Calendar 2020 その2の二日目の記事を見ると分かりやすいかも。
Operatorの仕組み
Operatorは、CRDで定義されたリソース(または組み込みのKubernetesオブジェクト)の作成、更新、削除を監視して、カスタムリソースの内容に応じた何らかの処理をするサービス。
普通、カスタムリソースにはOperatorが管理するアプリケーションの状態を表現させて、Operatorはカスタムリソースの内容とアプリケーションの状態が同じになるように、Deploymentでアプリを起動したりアプリの設定をいじったりする。
Operatorの処理は、カスタムリソースの作成、更新、削除のたびに呼び出される一つの関数の中に記述することになっている。 同じ関数が何度も繰り返し呼ばれ、呼ばれる度にカスタムリソースの内容とアプリケーションの状態をそろえるような処理をすることから、Operatorの処理はReconciliationループと呼ばれる。
Operator作成ツール
Operatorを作るツールとして以下がある。
-
オペレータのプロジェクトテンプレート生成、ビルド、デプロイをするCLIツール。Goのほか、AnsibleやHelmでもOperatorを書けるのが面白い。Operator Frameworkの一部として提供されていて、Lifecycle Managerというオペレータのライフサイクルを管理するものがあったり、OperatorHub.ioというコミュニティサイトがあったり、エコシステムが充実している。
-
オペレータのプロジェクトテンプレート生成、ビルド、デプロイをするCLIツール。開発言語はGo。
-
Metacontroller自体がオペレータを管理するオペレータ。オペレータの定義をKubernetesに登録すると、Reconciliationループを回してその中でWebフックを実行してくるので、それを受けて処理を実行するオペレータを任意の言語で書ける。
-
Kudoには標準的な処理を実装したオペレータが含まれていて、YAMLでワークフローを記述するだけで簡単にオペレータが実装できる。
以前はKubebuilder (v1)でズンドコしたんだけど、今回はOperator SDKのAnsible operatorを使う。
Ansible operator
Ansible operatorを使えばAnsible Playbookを書くだけでオペレータを実装できる。
Ansible operatorは、Operator SDKのマニュアルによると以下のようなアーキテクチャになっている。
オペレータの監視対象のカスタムリソースに変化があるとControllerが検知し、カスタムリソースの内容をAnsible変数に詰めてAnsible Runnerを起動してAnsible Playbookを実行する。
このAnsible Playbookではカスタムリソース(などのKubernetesオブジェクト)をいじるタスクを書くことができるんだけど、Kubernetesへのアクセスは全てProxyを介して行われる。 Proxyは、カスタムリソースにOperator SDKのアノテーションやオーナーリファレンスを挿入したりしてくれる。
Zundoko Ansible Operator
Ansible operatorで実装するのはズンドコきよしを実行するZundoko Ansible Operator。 Zundoko Ansible Operatorは、以前Kubebuilderで作ったやつと同様、以下のカスタムリソースを扱う。
Hikawa: 作るとズンドコきよしを開始する。
例:
apiVersion: zundokokiyoshi.kaitoy.github.com/v1beta2 kind: Hikawa metadata: name: hikawa spec: intervalMillis: 500
後述の事情で一部のプロパティの型が変わったので、バージョンがv1beta2になっている。 あと、今回は
intervalMillis
を考慮した処理にはできなかったので、intervalMillis
は無意味なプロパティになり下がった。Zundoko: 「ズン」か「ドコ」を表す。Hikawaがオーナー。
例:
apiVersion: zundokokiyoshi.kaitoy.github.com/v1beta1 kind: Zundoko metadata: name: hikawa-zundoko-0001 spec: say: Doko
Kiyoshi: 「キ・ヨ・シ!」を表す。Hikawaがオーナー。
例:
apiVersion: zundokokiyoshi.kaitoy.github.com/v1beta1 kind: Kiyoshi metadata: name: hikawa-kiyoshi spec: say: Kiyoshi !
Zundoko Ansible OperatorはHikawaが作成されるとReconciliationループを開始し、ランダムに「ズン」か「ドコ」をセットしたZundokoを作成する。 「ズン」を4つ作ったあとに「ドコ」を作ったら、Kiyoshiを作成してReconciliationループを止める。
Ansible operatorを使ったオペレータの書き方
Ansible operatorを使ったオペレータを書くには、まずOperator SDKでプロジェクトテンプレートを生成する。
Operator SDKインストール
Operator SDKは、GitHubのリリースページからバイナリをひとつダウンロードしてPATHの通った場所に置くだけでインストールできる。 今回はv1.2.0をインストールした。
[root ~]# curl -Lo /usr/local/bin/operator-sdk https://github.com/operator-framework/operator-sdk/releases/download/v1.2.0/operator-sdk-v1.2.0-x86_64-linux-gnu
Zundoko Ansible Operatorプロジェクトテンプレート生成
コマンドひとつでZundoko Ansible Operatorプロジェクトテンプレートを生成できる。
[root ~]# mkdir zundoko-ansible-operator
[root ~]# cd zundoko-ansible-operator
[root zundoko-ansible-operator]# operator-sdk init --plugins=ansible --domain=kaitoy.github.com
Ansible operatorを使うために--plugins=ansible
を指定している。
--domain
で指定しているドメインは、このプロジェクトのCRDで定義するカスタムリソースのapiGroup
のサフィックスとして使われる。
CRDのひな型生成
HikawaとZundokoとKiyoshiのCRDのひな型を生成する。
[root zundoko-ansible-operator]# operator-sdk create api --group zundokokiyoshi --version v1beta2 --kind Hikawa --generate-role
[root zundoko-ansible-operator]# operator-sdk create api --group zundokokiyoshi --version v1beta1 --kind Zundoko --generate-role
[root zundoko-ansible-operator]# operator-sdk create api --group zundokokiyoshi --version v1beta1 --kind Kiyoshi --generate-role
ここで--group
で指定したやつと前節で--domain
に指定したやつをjoinしたものがカスタムリソースのapiGroup
になる。
ここまでで以下のようなファイルが生成された。
Dockerfile
: オペレータのコンテナイメージをビルドするDockerfile。Makefile
: コンテナイメージのビルド、CRDのレンダリング、オペレータのデプロイ等の操作を定義したMakefile。config/crd/bases/*.yaml
: 上記operator-sdk create api
コマンドで生成したCRDのひな型。config/manager/manager.yaml
: オペレータのDeployment。config/rbac/*.yaml
: オペレータがカスタムリソース等にアクセスするためのRole系のマニフェスト。roles/{hikawa,zundoko,kiyoshi}/
: カスタムリソース毎のAnsible Roleのひな型。ここにReconciliationループの処理を書く。watches.yaml
: オペレータに監視させるカスタムリソース(などのKubernetesオブジェクト)と、対応させるAnsible Roleの定義。molecule/
: Ansible MoleculeによるE2Eテストのひな型。今回は触ってない。
(プロジェクト構造は公式マニュアルにも載っている。)
今回いじるのは、config/crd/bases/*.yaml
とwatches.yaml
とroles/hikawa/
。
CRDの具体化
生成された時点でのHikawaのCRDのひな型は以下のようになっている。
config/crd/bases/zundokokiyoshi.kaitoy.github.com_hikawas.yaml
:
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: hikawas.zundokokiyoshi.kaitoy.github.com
spec:
group: zundokokiyoshi.kaitoy.github.com
names:
kind: Hikawa
listKind: HikawaList
plural: hikawas
singular: hikawa
scope: Namespaced
versions:
- name: v1beta2
schema:
openAPIV3Schema:
description: Hikawa is the Schema for the hikawas API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: Spec defines the desired state of Hikawa
type: object
x-kubernetes-preserve-unknown-fields: true
status:
description: Status defines the observed state of Hikawa
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
served: true
storage: true
subresources:
status: {}
openAPIV3Schema
以下にOpenAPI Specificationでカスタムリソースのプロパティを定義するんだけど、ひな型では当然なにも定義されてなくて、代わりにx-kubernetes-preserve-unknown-fields: trueというのが書いてある。
これはつまりどんなプロパティでも入れられるということで、そのままでも使えなくはないけど、バリデーションなどを利かせるためにはちゃんと中身を書いたほうがいい。
Hikawaは以下のように変更した。
diff --git a/config/crd/bases/zundokokiyoshi.kaitoy.github.com_hikawas.yaml b/config/crd/bases/zundokokiyoshi.kaitoy.github.com_hikawas.yaml
index 4e5a3cb..91eb651 100644
--- a/config/crd/bases/zundokokiyoshi.kaitoy.github.com_hikawas.yaml
+++ b/config/crd/bases/zundokokiyoshi.kaitoy.github.com_hikawas.yaml
@@ -32,11 +32,29 @@ spec:
spec:
description: Spec defines the desired state of Hikawa
type: object
- x-kubernetes-preserve-unknown-fields: true
+ properties:
+ intervalMillis:
+ type: integer
+ format: int64
+ numZundokos:
+ type: string
+ pattern: '^\d+$'
+ sayKiyoshi:
+ type: boolean
+ required:
+ - intervalMillis
status:
description: Status defines the observed state of Hikawa
+ properties:
+ kiyoshied:
+ type: boolean
+ numZundokosSaid:
+ type: string
+ pattern: '^\d+$'
+ required:
+ - numZundokosSaid
+ - kiyoshied
type: object
- x-kubernetes-preserve-unknown-fields: true
type: object
served: true
storage: true
spec.numZundokos
が期待するZundokoの数で、status.numZundokosSaid
が実際のZundokoの数。
Zundoko Ansible Operatorはこれらの差異を見てZundokoを生成する。
spec.sayKiyoshi
とstatus.kiyoshied
も同じ関係。
ここで、spec.numZundokos
もstatus.numZundokosSaid
も数値なのに型がstringになっているのは、AnsibleのPlaybookで変数を埋め込むときに使うことになるJinja2テンプレートが文字列しか返せないから。
以前書いたHikawaでは普通にintegerにしてたから、Ansible operatorの制約により型を変える羽目になった形。
これがHikawaのバージョンがv1beta2に変わった理由。
ZundokoとKiyoshiも適当にプロパティを定義した。
余談だけど、昔Kubebuilder v1でZundoko Operator作った時とはCRDのバージョンが変わっていて、v1beta1からv1に上がってた。 大きく変わったところは、カスタムリソースの複数のバージョンのが書けるようになったところ。
監視するカスタムリソースとAnsible Roleの設定
オペレータに監視させるカスタムリソースはwatches.yaml
に定義する。
自動で生成されたものは以下。
watches.yaml
:
---
# Use the 'create api' subcommand to add watches to this file.
- version: v1beta2
group: zundokokiyoshi.kaitoy.github.com
kind: Hikawa
role: hikawa
- version: v1beta1
group: zundokokiyoshi.kaitoy.github.com
kind: Zundoko
role: zundoko
- version: v1beta1
group: zundokokiyoshi.kaitoy.github.com
kind: Kiyoshi
role: kiyoshi
# +kubebuilder:scaffold:watch
これは、Hikawaに変化があったらhikawa
というAnsible Roleを実行し、Zundokoに(以下略)という意味。
実行されたRoleではカスタムリソースなどのKubernetesオブジェクトを作ったりするんだけど、そのとき、Roleの処理で作られたKubernetesオブジェクトには、そのRoleを起動したKubernetesへのオーナーリファレンスがProxyによって挿入される。
Zundoko Ansible Operatorの場合は、Roleの処理でZundokoとKiyoshiを作るんだけど、それらのオーナーをHikawaにしたいので、RoleはHikawaから起動しないといけない。 ので以下のように変更した。
watches.yaml
:
diff --git a/watches.yaml b/watches.yaml
index 9dadc5a..947c6f3 100644
--- a/watches.yaml
+++ b/watches.yaml
@@ -4,12 +4,6 @@
group: zundokokiyoshi.kaitoy.github.com
kind: Hikawa
role: hikawa
-- version: v1beta1
- group: zundokokiyoshi.kaitoy.github.com
- kind: Zundoko
- role: zundoko
-- version: v1beta1
- group: zundokokiyoshi.kaitoy.github.com
- kind: Kiyoshi
- role: kiyoshi
+ watchDependentResources: false
+ manageStatus: false
# +kubebuilder:scaffold:watch
watchDependentResources
は、オーナーになってるリソース(i.e. オーナーリファレンスが挿入されたリソース)に変更があったときに、オーナーのリソース(i.e. オーナーリファレンスが指しているリソース)に対応するAnsible Roleを実行するかのフラグだけど、これはオフにした。
ZundokoやKiyoshiは生成された後変わることはなく、監視する必要はないので。
ZundokoやKiyoshiが生成されるタイミングでHikawaのAnsible Roleを呼んでくれたらちょっとうれしかったんだけど、watchDependentResources
をオンにしてもそれはしてくれなかった。
manageStatus
は、カスタムリソースのstatus
のプロパティをAnsible operatorに自動でいれてもらうかのフラグ。
Hikawaのstatus
は自分で制御したかったのでオフにした。
Reconciliationループの実装
ReconciliationループはAnsible Roleのhikawa
のタスクとして書く。
タスクの記述には、Ansibleビルトインのモジュールの他、Kubernetesオブジェクトをapplyできるcommunity.kubernetes.k8sモジュール、Kubernetesオブジェクトをgetできるcommunity.kubernetes.k8s_infoモジュール、Kubernetesオブジェクトのstatus
を更新できるoperator_sdk.util.k8s_statusモジュールなどがデフォルトで使える。
(オペレータのコンテナイメージのビルドの時に入れれば好きなAnsibleモジュールを使える。)
また、Ansible Roleを起動したKubernetesオブジェクトのmetadataやプロパティがAnsible変数として渡されるので、タスクからそれを参照できる。
hikawa
のタスクは以下のように書いた。
roles/hikawa/tasks/main.yml
:
#
# Hikawaに属するZundokoの一覧を取得する。
#
- name: Get a list of all Zundokos
community.kubernetes.k8s_info:
api_version: zundokokiyoshi.kaitoy.github.com/v1beta1
kind: Zundoko
namespace: '{{ ansible_operator_meta.namespace }}'
label_selectors:
- hikawa.zundokokiyoshi.kaitoy.github.com = {{ ansible_operator_meta.name }}
register: zundoko_list
#
# 実際のZundokoの数をもとにHikawaのstatusを更新する。
#
- name: Update Hikawa status
when: _zundokokiyoshi_kaitoy_github_com_hikawa.status.kiyoshied is not defined or not _zundokokiyoshi_kaitoy_github_com_hikawa.status.kiyoshied
operator_sdk.util.k8s_status:
api_version: zundokokiyoshi.kaitoy.github.com/v1beta2
kind: Hikawa
name: '{{ ansible_operator_meta.name }}'
namespace: '{{ ansible_operator_meta.namespace }}'
status:
numZundokosSaid: '{{ zundoko_list.resources | length }}'
kiyoshied: false
#
# Hikawaのspec.numZundokosとstatus.numZundokosSaidの差異が無くなるようにZundokoを生成する。
#
- name: Create a Zundoko
when: (zundoko_list.resources | length ) < (num_zundokos | int)
block:
- name: Do create a Zundoko
community.kubernetes.k8s:
state: present
definition:
apiVersion: zundokokiyoshi.kaitoy.github.com/v1beta1
kind: Zundoko
metadata:
name: '{{ ansible_operator_meta.name }}-zundoko-{{ num_zundokos.zfill(4) }}'
namespace: '{{ ansible_operator_meta.namespace }}'
labels:
hikawa.zundokokiyoshi.kaitoy.github.com: '{{ ansible_operator_meta.name }}'
spec:
say: '{{ ["Zun", "Doko"] | random }}'
- name: Go to next loop
fail:
msg: Fail Intentionally in order to go to next loop
#
# Zundokoの一覧をみて、Kiyoshiを生成すべきか判定する。
# 生成すべきでなければ、Hikawaのspec.numZundokosをインクリメントして、Zundokoの生成を促す。
# 生成すべきであれば、Hikawaのspec.sayKiyoshiをtrueにして、Kiyoshiの生成を促す。
#
- name: Judge
when: (zundoko_list.resources | length | string) == num_zundokos and not say_kiyoshi
vars:
zundokos: '{{ zundoko_list.resources | sort(attribute="metadata.name") | map(attribute="spec") | map(attribute="say") | join(" ") }}'
block:
- name: Increment numZundokos
when: not zundokos.endswith('Zun Zun Zun Zun Doko')
community.kubernetes.k8s:
state: present
definition:
apiVersion: zundokokiyoshi.kaitoy.github.com/v1beta2
kind: Hikawa
metadata:
name: '{{ ansible_operator_meta.name }}'
namespace: '{{ ansible_operator_meta.namespace }}'
spec:
numZundokos: '{{ (num_zundokos | int) + 1 }}'
- name: Set sayKiyoshi to True
when: zundokos.endswith('Zun Zun Zun Zun Doko')
community.kubernetes.k8s:
state: present
definition:
apiVersion: zundokokiyoshi.kaitoy.github.com/v1beta2
kind: Hikawa
metadata:
name: '{{ ansible_operator_meta.name }}'
namespace: '{{ ansible_operator_meta.namespace }}'
spec:
sayKiyoshi: true
#
# Hikawaのspec.sayKiyoshiがtrueになっていたらKiyoshiを生成する。
#
- name: Kiyoshi !
when: say_kiyoshi and not _zundokokiyoshi_kaitoy_github_com_hikawa.status.kiyoshied
block:
- name: Create a Kiyoshi
community.kubernetes.k8s:
state: present
definition:
apiVersion: zundokokiyoshi.kaitoy.github.com/v1beta1
kind: Kiyoshi
metadata:
name: '{{ ansible_operator_meta.name }}-kiyoshi'
namespace: '{{ ansible_operator_meta.namespace }}'
spec:
say: 'Kiyoshi !'
- name: Update kiyoshied
operator_sdk.util.k8s_status:
api_version: zundokokiyoshi.kaitoy.github.com/v1beta2
kind: Hikawa
name: '{{ ansible_operator_meta.name }}'
namespace: '{{ ansible_operator_meta.namespace }}'
status:
kiyoshied: true
Hikawaがユーザによって登録されると、上記Roleが実行される。
RoleのなかでHikawaのspec
が更新されるので、それを契機にまた上記Roleが実行される、というのが、「キ・ヨ・シ!」に到達するまで続くという寸法。
「Get a list of all Zundokos」タスクで、Roleを起動したHikawaに属するZundokoだけを取得するためにラベルセレクタを使っているのは、オーナーリファレンスで絞り込む方法が分からなかったから。 KubebuilderでGoで書いたときは普通にオーナーリファレンスで絞れたんだけど…
「Create a Zundoko」タスクで、Zundokoを作ったあとにfail
させてるのは、Roleをエラーにして再実行を促すため。
HikawaのwatchDependentResources
をtrue
にしておけば、Zundokoの作成を契機にRoleが実行されてくれると思ったんだけど、されなかったので、苦肉の策でfail
させてる。
Goで書いたときはどうだったっけな…
Zundoko Ansible Operatorのコンテナイメージのビルド
オペレータのDockerfileはOperator SDKが生成したものをそのまま使えた。
Dockerfile
:
FROM quay.io/operator-framework/ansible-operator:v1.2.0
COPY requirements.yml ${HOME}/requirements.yml
RUN ansible-galaxy collection install -r ${HOME}/requirements.yml \
&& chmod -R ug+rwx ${HOME}/.ansible
COPY watches.yaml ${HOME}/watches.yaml
COPY roles/ ${HOME}/roles/
COPY playbooks/ ${HOME}/playbooks/
普通にDockerでビルドすればZundoko Ansible Operatorのコンテナイメージができる。
[root zundoko-ansible-operator]# docker build -t kaitoy/zundoko-ansible-operator:0.0.1 .
Zundoko Ansible Operatorのデプロイ
作ったオペレータのデプロイは、プロジェクトのルートディレクトリでコマンド一つ実行するだけでできる。
[root zundoko-ansible-operator]# export IMG=zundoko-ansible-operator:0.0.1
[root zundoko-ansible-operator]# make deploy
これでZundoko Ansible Operatorが動き出した。
[root zundoko-ansible-operator]# kubectl get po --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-6b5cbb9f46-gzhgj 1/1 Running 2 3d21h
kube-system coredns-6b5cbb9f46-v8p9q 1/1 Running 3 12d
kube-system weave-net-28st9 2/2 Running 6 3d21h
zundoko-ansible-operator-system zundoko-ansible-operator-controller-manager-7bcbf96f56-ccvcs 2/2 Running 0 26s
上記make deploy
の中では、kustomizeをダウンロードして、kustomizeでCRDとかDeploymentをレンダリングして、kubectlでそれらをapply、ということをしてくれてる。
因みに、Operator SDKはオペレータのPodSecurityPolicyは作ってくれないので、PodSecurityPolicyが有効な環境では自前で作っておいてやる必要がある。
ズンドコきよし実行
Hikawaを登録するとズンドコしはじめる。
[root zundoko-ansible-operator]# cat <<EOF | kubectl create -f -
apiVersion: zundokokiyoshi.kaitoy.github.com/v1beta2
kind: Hikawa
metadata:
name: hikawa
spec:
intervalMillis: 500
EOF
末尾にデモ動画を張っておくけど、Zundoko Ansible Operatorの動作はすごく遅くて、30秒に一回くらいしかZundokoを生成できない。 Ansible Roleの実行自体速くはないんだけど、多分Ansible Runnerの起動に時間がかかってるのが一番のボトルネック。
Ansible operatorはズンドコきよしには向かないという結果だった。