リソースの削除
ここではKubernetesにおけるリソースの削除処理について解説します。
実はコントローラーにおいて削除処理は難しい問題です。 例えばMarkdownViewリソースが削除されたら、そのMarkdownViewに紐付く形で作成されたConfigMap, Deployment, Serviceリソースも一緒に削除しなければなりません。 しかし、もしMarkdownViewが削除されたというイベントを取りこぼしてしまうと、そのリソースに関する情報は消えてしまい、関連するどのリソースを削除すべきか判断できなくなってしまうからです。
そこでKubernetesでは、ownerReferenceによるガベージコレクションと、Finalizerというリソース削除の仕組みを提供しています。
ownerReferenceによるガベージコレクション
1つめのリソース削除の仕組みはownerReferenceによるガベージコレクションです。(参考) これは親リソースが削除されると、そのリソースの子リソースもガベージコレクションにより自動的に削除されるという仕組みです。
Kubernetesではリソースの親子関係を表すために.metadata.ownerReferences
フィールドを利用します。
controller-runtimeが提供しているcontrollerutil.SetControllerReference 関数を利用することで、指定したリソースにownerReferenceを設定することができます。
先ほど作成した、reconcileConfigMap
関数でcontrollerutil.SetControllerReference
を利用してみましょう。
func (r *MarkdownViewReconciler) reconcileConfigMap(ctx context.Context, mdView viewv1.MarkdownView) error {
logger := log.FromContext(ctx)
cm := &corev1.ConfigMap{}
cm.SetNamespace(mdView.Namespace)
cm.SetName("markdowns-" + mdView.Name)
op, err := ctrl.CreateOrUpdate(ctx, r.Client, cm, func() error {
if cm.Data == nil {
cm.Data = make(map[string]string)
}
for name, content := range mdView.Spec.Markdowns {
cm.Data[name] = content
}
return ctrl.SetControllerReference(&mdView, cm, r.Scheme)
})
if err != nil {
logger.Error(err, "unable to create or update ConfigMap")
return err
}
if op != controllerutil.OperationResultNone {
logger.Info("reconcile ConfigMap successfully", "op", op)
}
return nil
}
この関数を利用すると、ConfigMapリソースに以下のような.metadata.ownerReferences
が付与され、このリソースに親リソースの情報が設定されます。
apiVersion: v1
kind: ConfigMap
metadata:
creationTimestamp: "2021-07-25T09:35:43Z"
name: markdowns-markdownview-sample
namespace: default
ownerReferences:
- apiVersion: view.zoetrope.github.io/v1
blockOwnerDeletion: true
controller: true
kind: MarkdownView
name: markdownview-sample
uid: 8e8701a6-fa67-4ab8-8e0c-29c21ae6e1ec
resourceVersion: "17582"
uid: 8803226f-7d8f-4632-b3eb-e47dc36eabf3
data:
・・省略・・
この状態で親のMarkdownViewリソースを削除すると、子のConfigMapリソースも自動的に削除されます。
なお、異なるnamespaceのリソースをownerにしたり、cluster-scopedリソースのownerにnamespace-scopedリソースを指定することはできません。
また、SetControllerReference
と似た関数でcontrollerutil.SetOwnerReferenceもあります。
SetControllerReference
は、1つのリソースに1つのオーナーのみしか指定できず、controller
フィールドとblockOwnerDeletion
フィールドにtrueが指定されているため子リソースが削除されるまで親リソースの削除がブロックされます。
一方のSetOwnerReference
は1つのリソースに複数のオーナーを指定でき、子リソースの削除はブロックされません。
controllerutil.SetControllerReference
は、Server-Side Applyで利用するApplyConfiguration型には対応していません。
そこで、以下のような補助関数を用意しましょう。
func controllerReference(mdView viewv1.MarkdownView, scheme *runtime.Scheme) (*metav1apply.OwnerReferenceApplyConfiguration, error) {
gvk, err := apiutil.GVKForObject(&mdView, scheme)
if err != nil {
return nil, err
}
ref := metav1apply.OwnerReference().
WithAPIVersion(gvk.GroupVersion().String()).
WithKind(gvk.Kind).
WithName(mdView.Name).
WithUID(mdView.GetUID()).
WithBlockOwnerDeletion(true).
WithController(true)
return ref, nil
}
Server-Side Applyでガベージコレクションを利用する際は、この補助関数を利用してApplyConfiguration型を作成するときに ownerReferenceを設定します。
svcName := "viewer-" + mdView.Name
owner, err := controllerReference(mdView, r.Scheme)
if err != nil {
return err
}
svc := corev1apply.Service(svcName, mdView.Namespace).
WithLabels(map[string]string{
"app.kubernetes.io/name": "mdbook",
"app.kubernetes.io/instance": mdView.Name,
"app.kubernetes.io/created-by": "markdown-view-controller",
}).
WithOwnerReferences(owner).
WithSpec(corev1apply.ServiceSpec().
WithSelector(map[string]string{
"app.kubernetes.io/name": "mdbook",
"app.kubernetes.io/instance": mdView.Name,
"app.kubernetes.io/created-by": "markdown-view-controller",
}).
WithType(corev1.ServiceTypeClusterIP).
WithPorts(corev1apply.ServicePort().
WithProtocol(corev1.ProtocolTCP).
WithPort(80).
WithTargetPort(intstr.FromInt(3000)),
),
)
Finalizer
Finalizerの仕組み
ownerReferenceとガベージコレクションにより、親リソースと一緒に子リソースを削除できると説明しました。 しかし、この仕組だけでは削除できないケースもあります。 例えば、親リソースと異なるnamespaceやスコープの子リソースを削除したい場合や、Kubernetesで管理していない外部のリソースを削除したい場合 などは、ガベージコレクション機能は利用できません。
例えばTopoLVMでは、LogicalVolumeというカスタムリソースを作成すると、ノード上にLVM(Logical Volume Manager)のLV(Logical Volume)を作成します。 Kubernetes上のLogicalVolumeカスタムリソースが削除されたら、それに合わせてノード上のLVも削除しなければなりません。
そのようなリソースの削除には、Finalizerという仕組みを利用できます。
Finalizerの仕組みを利用するためには、まず親リソースのfinalizers
フィールドにFinalizerの名前を指定します。
なお、この名前はMarkdownViewコントローラーが管理しているFinalizerであると識別できるように、他のコントローラーと衝突しない名前にしておきましょう。
apiVersion: view.zoetrope.github.io/v1
kind: MarkdownView
metadata:
finalizers:
- markdownview.view.zoetrope.github.io/finalizer
# 以下省略
finalizers
フィールドが付与されているリソースは、リソースを削除しようとしても削除されません。
代わりに、以下のようにdeletionTimestamp
が付与されるだけです。
apiVersion: view.zoetrope.github.io/v1
kind: MarkdownView
metadata:
finalizers:
- markdownview.view.zoetrope.github.io/finalizer
deletionTimestamp: "2021-07-24T15:23:54Z"
# 以下省略
カスタムコントローラーはdeletionTimestamp
が付与されていることを発見すると、そのリソースに関連するリソースを削除し、その後にfinalizers
フィールドを削除します。
finalizers
フィールドが空になると、Kubernetesがこのリソースを完全に削除します。
このような仕組みにより、コントローラーが削除イベントを取りこぼしたとしても、対象のリソースが削除されるまでは何度もReconcileが呼び出されるため、子のリソースの情報が失われて削除できなくなるという問題を回避できます。 一方で、カスタムリソースよりも先にコントローラーを削除してしまった場合は、いつまでたってもカスタムリソースが削除されないという問題が発生することになるので注意しましょう。
Finalizerの実装方法
それではFinalizerを実装してみましょう。 controller-runtimeでは、Finalizerを扱うためのユーティリティ関数としてcontrollerutil.ContainsFinalizer、controllerutil.AddFinalizer、controllerutil.RemoveFinalizerなどを提供しているのでこれを利用しましょう。
以下のように、Finalizersフィールドを利用して、独自のリソース削除処理を実装できます。
finalizerName := "markdwonview.view.zoetrope.github.io/finalizer"
if !mdView.ObjectMeta.DeletionTimestamp.IsZero() {
// deletionTimestampがゼロではないということはリソースの削除が開始されたということ
// finalizersに上記で指定した名前が存在した場合は削除処理を実施する
if controllerutil.ContainsFinalizer(&mdView, finalizerName) {
// ここで外部リソースを削除する
deleteExternalResources()
// finalizersフィールドをクリアしてリソースを削除できるようにする
controllerutil.RemoveFinalizer(&mdView, finalizerName)
err = r.Update(ctx, &mdView)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// deletionTimestampが付与されていなければ、finalizersフィールドを追加します。
if !controllerutil.ContainsFinalizer(&mdView, finalizerName) {
controllerutil.AddFinalizer(&mdView, finalizerName)
err = r.Update(ctx, &mdView)
if err != nil {
return ctrl.Result{}, err
}
}