はじめに
本稿は、KubernetesのOperator初学者のために、Operatorのコントローラの部分だけを簡単に書いてみて、Operatorが何をしているかを明らかにします。
ここではPythonとkubernetesモジュール(kubernetes Python SDK)に加えて、Python用のOperator Framework(Kopf: Kubernetes Operators Framework)を用いて、Operatorとして動作する簡単なプログラムを書きます。これは「CRDのない超簡単Kubernetes Operator」の第3弾であり、第2弾の続編です。まだ読んでいない方は第1弾のシェルスクリプト編と第2弾のPython編から読むことをお勧めします。
シェルスクリプト編では、この一連の記事の目的なども説明しています。
このPython Operator Framework編では、Python編で作成したOperatorを、Operator Framework(Kopf)を使用して再度作成します。Operator Frameworkが、Operatorの作成者に対してどのような利便性を提供するのか、その導入部となることを意図しています。
目標
本稿と、関連する一連の記事では以下を目標とします。取り消し線の項目は、本稿ではなく別記事で説明します。
Kubernetes Operatorをざっくり理解するPython SDKを使用してOperatorのロジックを作成する- Python Operator SDK (kopf)を使用してOperatorを作成する
繰り返しになりますがここではOperatorのコントローラが実現するロジックやコーディングを中心に説明し、CRDについては触れません。また作成したロジックをOperatorとして動作させるための周辺作業については必要最小限の説明とさせてください。
必要なもの
前提とする知識
Kubernetesの一般的なオブジェクト(Deployment, Pod)と内容とその関係について理解している必要があります。
また、プログラムを読んでわかる程度のPythonの知識を必要とします。
少なくともPython編の内容に目を通しておくことをお勧めします。コードの半分以上を再利用することになりますが、重複した内容は本稿では説明しません。
用意する環境
シェルスクリプト編、Python編と同様に、見てわかることを目標にしているため、必要な環境は特にありません。
もちろん、実際に動かした方が理解が進むはずなので、何らかのKubernetesクラスターを用意していただくのが良いです。本稿の例を作成する際にOpenShift Localを使用しました。動かすのであれば、Pythonの実行環境も必要なります。
PythonでKubernetes APIにアクセスするために、Kubernetes Python clientを使用します。これはGitHubでも提供されていますが、kubernetesパッケージとしてPyPIで提供されているので、pipコマンドでインストールできます。
$ pip install -y kubernetes
APIのドキュメントは、同じくGitHubで提供されています。
それに加えて、Kopfも必要となります。これもPyPIで提供されているため、pipコマンドでインストールできます。
$ pip install -y kopf
ドキュメントはRead the Docsで公開されています。
https://kopf.readthedocs.io/en/stable/
今回のお題
ここは、Python編と全く同じです。
この記事の基本方針はCRDを用意しないことであるため、既存のKubernetesリソースであるDeploymentを利用する単純なDeploymentコントローラを作成します。Kubernetesには組み込みでDeploymentコントローラが動作しており、Deploymentの記述に従い、Podが作成されます。ここではその動作と並行で別のPodを起動するOperatorを作成します。
- 指定したnamespace(ここではsimple-deploymentとする)に存在するDeploymentリソースに対して、同じ名前のPodを作成する。
- コンテナは何でも構わない(今回は、適当に無限ループするシェルスクリプトとする)。
- Deploymentリソースが削除されたら、Podも削除される。
標準的なDeploymentコントローラのサブセットであり、これをsimple-deployment Operatorと名付けます。
そのため、1つのDeploymentに対して、標準的なDeploymentが起動するPodと、simple-deployment operatorが起動するPodの2種類のPodが起動することになります。
simple-deployment operator
control loop
Python編のコントロールループを振り返ってみましょう。こんな感じでした。
def control_loop() -> None:
while True:
deployment_list = get_deployment_list('simple-deployment')
for item in deployment_list:
deployment_name = item.metadata.name
namespace = item.metadata.namespace
# Reconciliation
pod_name = deployment_name
if (find_pod(pod_name, namespace) == None):
start_pod(make_pod(pod_name, deployment_name), namespace)
sleep(10)
Kopfを使用すると、これを次のように書けます。
import kopf
@kopf.daemon('deployment')
def monitor_deployment(stopped, logger, body, **kwargs):
deployment_name = body.metadata.name
pod_name = body.metadata.name
namespace = body.metadata.namespace
while not stopped:
if (find_pod(pod_name, namespace) == None):
start_pod(make_pod(pod_name, deployment_name), namespace)
stopped.wait(10)
Deploymentと同じ名前を持つPodをfind_pod
で探し、存在しなければmake_pod
で新しく作成しstart_pod
で起動するのは、Python編のコードと同じです。それぞれの関数もほぼPython編と同じです。
違いは、@kopf.daemon
でデコレートされた関数monitor_deployment
の定義が中心となります。この後は、Python編との違いとして次の4点を説明します。
- Daemonデコレータ
- Daemonの関数とその動き
- Daemonの停止方法
- ownerReferenceの設定
Daemonデコレータ
Kopfでは、一つのリソースを監視し、それぞれのリソースに対して長時間処理を継続するものをDaemonと呼びます。
@kopf.daemon('deployment')
@kopf.daemon('deployment')
デコレータは、deployment
に対するDaemonを作成します。Daemonの実態はデコレート対象の関数です。ここではmonitor_deployment
が相当します。Daemonはリソースそれぞれに対して生成されます。つまりこの例ではクラスターにDeploymentが100個あると、DaemonはそれぞれのDeployment毎に100個作られます。
Python編のコードでは無限ループの中にDeploymentを探すコードが入っていましたが、ここでは監視対象のリソースはFrameworkが探してきて、それぞれのリソースに対してDaemonを起動する点が異なります。
実はこのままだと、全namespaceのDeploymentが対象となります。お題である指定したnamespace、つまりsimple-deployment
namespaceに限定するにはどうすれば良いのでしょうか。これは、デコレータのfield filterを使用します。
@kopf.daemon('deployment', field='metadata.namespace', value='simple-deployment')
これで、このDaemonの処理対象はmetadata.namespace
の値がsimple-deployment
であるDeploymentリソースに限定されます。
デコレート対象の関数は、通常のPython関数を使用したSync Daemonと、async関数を使用したAsync Daemonの2種類がありますが、本稿では、Syncの方のみを扱います。
Daemonの関数とその動き
kopf.deamon
がデコレートする関数のシグニチャーを見てみましょう。
def monitor_deployment(stopped, logger, body, **kwargs):
kwargs
とlogger
は今回使用しないので説明を省略します。
body
パラメータ
body
は処理対象のリソースを表します。このコードではDeploymentのnameとnamespaceの値を次のように取得しています。
deployment_name = body.metadata.name
namespace = body.metadata.namespace
stopped
パラメータ
Deamonの動作はstopped
パラメータで制御され、この値がFalseの間だけwaitしながら動作するようコードを作成します。つまり、Deamonの骨格は次のようになります。
while not stopped:
stopped.wait(10)
Python編のコントローラは無限ループを持っていましたが、Operator Frameworkではこのコードがその無限ループに相当します。この中でPodの存在確認と、存在しない場合の起動を行います。
time.sleep
関数ではなく、stopped
のwait
メソッドを使用する理由は、wait
メソッドの場合は、stopped
の値が変更したことを検知すると指定した時間待つことなく、メソッドから抜けるからです。パラメータの10は10秒を表します。
Deamonの停止方法
Daemonの動作を停止するには、関数からリターンします。通常は、監視対象のリソースが削除されると、Frameworkがそれを検知ししてstopped
の値を反転させるため、上記のwhileループから抜けてDaemonが終了します。
なお、Daemonが監視しているリソースを削除すると、削除が完了するまで約60秒ほどかかります。これはDaemonが完全に停止するまでは監視対象のリソースに対して削除防止のために以下のfinalizer
が設定されるためです。
finalizers:
- kopf.zalando.org/KopfFinalizerMarker
これを制御するためには、@kopf.daemon
デコレータに、cancellation_timeout
を設定します。
@kopf.daemon('deployment', field='metadata.namespace', value='simple-deployment', cancellation_timeout=10)
ownerReferenceの設定
Python編のコードでは、リソースのガベージコレクションのために、make_pod
関数で作成するPodリソースのownerReference
に、Deploymentのuidをはじめとする情報を設定したのを覚えているでしょうか。Kopfを使用すると、その設定はadopt
関数を呼び出すだけでframeworkが設定を行ってくれます。
def make_pod(name:str, deployment_name:str) -> V1Pod:
"""
Create a pod controled by the deployment
"""
pod = make_prototype_pod()
...途中省略...
# set the deployment as its owner for garbage collection of the deployment
kopf.adopt(pod)
return pod
これにより、Daemonの管理対象のリソース(ここではDeployment)の情報がPodのownerReferenceに設定されます。
Operator Frameworkの利点
Python編の最後に書いたように、Operator Frameworkを使用するとOperatorが行う処理を簡単に記述するためのサポートが得られます。ガベージコレクションのためのownerReference
の設定もその一つです。
KopfはDaemonだけでなくEventを持ち、そのHandlerを設定できます。詳細は省略しますが、例えばリソースの作成(create)、削除(delete)や変更(update)を検知し、定義されているHandlerを呼び出します。詳しくは、マニュアルを参照してください。これを使えば、リソースの一部が変更された際に行うアクションを定義できます。
コード
最後に、コード全体を示します。
import kopf
import logging
from kubernetes import client, config, utils
from kubernetes.client.rest import ApiException
from kubernetes.client import V1Pod, V1ObjectMeta, V1PodSpec, V1Deployment
config.load_kube_config("./kubeconfig")
@kopf.daemon('deployment', field='metadata.namespace', value='simple-deployment',
cancellation_timeout=5)
def monitor_deployment(stopped, logger, body, **kwargs):
logger.info(f"A handler is called with body: {body}")
logger.info(f"Deploymnet name = {body.metadata.name}, {body.metadata.namespace}")
deployment_name = body.metadata.name
pod_name = body.metadata.name
namespace = body.metadata.namespace
while not stopped:
if (find_pod(pod_name, namespace) == None):
logging.info(f"pod {pod_name} is not found, create a new one")
start_pod(make_pod(pod_name, deployment_name), namespace)
stopped.wait(10)
logging.info(f"Deamon for {deployment_name} is terminated.")
def find_pod(name:str, namespace:str) -> V1Pod:
"""
get specified pod
"""
api = client.CoreV1Api()
try:
response = api.read_namespaced_pod(name, namespace)
except ApiException as e:
logging.error(f"Exception when calling CoreV1Api->read_namespaced_pod: {e}")
return None
return response
def make_prototype_pod() -> V1Pod:
"""
Create a generic V1Pod data
"""
metadata = V1ObjectMeta()
spec = V1PodSpec(containers=[])
# pod construction
pod = V1Pod(metadata=metadata, spec=spec)
return pod
def make_pod(name:str, deployment_name:str) -> V1Pod:
"""
Create a pod controled by the deployment
"""
pod = make_prototype_pod()
pod.metadata.name = name
pod.spec.containers = [{
"command": [
"bash", "-c", f"while true ; do echo {name} ; sleep 1 ; done"
],
"image": "ubi8",
"name": "echo",
}]
pod.metadata.labels = { "app": name }
# set the deployment as its owner for garbage collection of the deployment
kopf.adopt(pod)
#logging.info(f"create pod is {pod}")
return pod
def start_pod(pod:V1Pod, namespace:str) -> None:
"""
instanceate the pod
"""
api = client.CoreV1Api()
try:
response = api.create_namespaced_pod(namespace, pod)
except ApiException as e:
logging.error(f"Exception when calling CoreV1Api->create_namespaced_pod: {e}")
Operatorの実行
Kopfで作成したコードは、Pythonインタープリタで直接実行するのではなく、kopf run
コマンドで起動します。例えば、コードのファイルがsimple-deployment.py
ファイルであった場合、このOperatorを起動するには、次のコマンドを実行します。kopf
コマンドは、pip install kopf
によりインストールされているはずです。
$ kopf run simple-deployment.py
Kubernetes上でこのコードをOperatorとして起動するには、これを行うコンテナや、必要な権限を持たせたserviceaccountなどが必要となりますが、前述したようにそこは省略します。
まとめ
Python Operator Frameworkを使用して、Deploymentリソースを参照するsimple-deployment
Operatorを作成しました。
このsimple-deployment
は簡単なOperatorであるため、Pythonを使用してロジックを書き下しても(Python編)、Frameworkを使用しても(Python Operator Framework編)でも大きな違いは感じられないかもしれません。恐らくOperatorの処理が複雑であれば複雑であるほど、こうしたFrameworkの利点は大きくなるはずです。実際、下記の記事で紹介した、MySQL Operator for Kubernetesは、MySQLのInnoDBコンテナのクラスターを構成し、その障害時の回復処理まで実装している複雑なOperatorですが、Kopfを使用して開発されています。