LoginSignup
1
0

More than 1 year has passed since last update.

[Controller編] Server-Side Applyを試す

Last updated at Posted at 2022-07-29

はじめに

[kubectl編] Server-Side Applyを試すにてServer-Side Apply(以降SSA)の概念をkubectlコマンドを通して理解しました。
今回は自前でのController実装を通じてをSSAを実行します。

SSAを行う関数

SSAを行える関数はclient-goのApply()controller-runtimeのPatch()がそれぞれ存在します。
使いわけは好みや用途次第かと思いますが、個人的にはSSAがGAになった1.22で導入されたclient-goのApply()の方がシンプルで使い易いかなあと思います。
シンプルである理由は後述します。

Controllerを介さないSSAのコード

まず、ControllerなしでSSAしてみます。
以下のコードはclient-goのApply()を使ったSSAの単純なコードです。
リソースの定義をapplyconfigurationsを使用して作成して、FieldManagerを指定して、Apply関数を実行するだけです。
なお、applyconfigurationsは全てのフィールドがポインタかつomitemptyとなっているので、未指定のフィールドがnilになります。
つまり、余分なフィールド更新が走らないということです。
詳細は以下の記事が非常に参考になります。

package main

import (
	"context"
	"flag"
	"fmt"
	"os"

	"github.com/go-logr/logr"
	"go.uber.org/zap/zapcore"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	appsv1apply "k8s.io/client-go/applyconfigurations/apps/v1"
	corev1apply "k8s.io/client-go/applyconfigurations/core/v1"
	metav1apply "k8s.io/client-go/applyconfigurations/meta/v1"
	"k8s.io/client-go/kubernetes"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

var (
	log        logr.Logger
	kclient    client.Client
	kclientset *kubernetes.Clientset
)

func init() {
	opts := zap.Options{
		Development: true,
		TimeEncoder: zapcore.ISO8601TimeEncoder,
	}
	opts.BindFlags(flag.CommandLine)
	flag.Parse()
	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
	log = ctrl.Log.WithName("test-ssa")

	_, kclientset = getClient()
}

func getClient() (client.Client, *kubernetes.Clientset) {
	scheme := runtime.NewScheme()
	_ = clientgoscheme.AddToScheme(scheme)

	kubeconfig := ctrl.GetConfigOrDie()

	kclient, err := client.New(kubeconfig, client.Options{Scheme: scheme})
	if err != nil {
		log.Error(err, "unable to create client")
		os.Exit(1)
	}

	kclientset, err := kubernetes.NewForConfig(kubeconfig)
	if err != nil {
		log.Error(err, "unable to create clientset")
		os.Exit(1)
	}

	return kclient, kclientset
}

func main() {
	var (
		ctx              = context.Background()
		fieldMgr         = "my-field-manager"
		deploymentClient = kclientset.AppsV1().Deployments("default")
	)

	deploymentApplyConfig := appsv1apply.Deployment("codecreate-nginx", "default").
		WithSpec(appsv1apply.DeploymentSpec().
			WithReplicas(3).
			WithSelector(metav1apply.LabelSelector().
				WithMatchLabels(map[string]string{"apps": "codecreate-nginx"})).
			WithTemplate(corev1apply.PodTemplateSpec().
				WithLabels(map[string]string{"apps": "codecreate-nginx"}).
				WithSpec(corev1apply.PodSpec().
					WithContainers(corev1apply.Container().
						WithName("my-nginx").
						WithImage("nginx:latest")))))

	applied, err := deploymentClient.Apply(ctx, deploymentApplyConfig, metav1.ApplyOptions{
		FieldManager: fieldMgr,
		Force:        true,
	})
	if err != nil {
		log.Error(err, "unable to apply")
		os.Exit(1)
	}

	log.Info(fmt.Sprintf("Applied: %s", applied.GetName()))

	return
}

次にcontroller-runtimeのPatch()を使ったSSAを見てみます。
なお、main()以外は同じであるため、省略します。
リソース定義を作成するまではいいのですが、Patch()は適用対象のpatchを、applyconfigurationsそのままで使用できず、runtime.Objectとして指定する必要があります。
そのため、その適用対象リソース定義をpatchとするため、unstructured.Unstructuredを使って、interfaceに変換します。
この一手間が面倒なので、client-goのApply()の方が使い易いと記載としています。

func main() {
	var (
		ctx      = context.Background()
		fieldMgr = "my-field-manager"
	)

	deploymentApplyConfig := appsv1apply.Deployment("codecreate-nginx", "default").
		WithSpec(appsv1apply.DeploymentSpec().
			WithReplicas(3).
			WithSelector(metav1apply.LabelSelector().
				WithMatchLabels(map[string]string{"apps": "codecreate-nginx"})).
			WithTemplate(corev1apply.PodTemplateSpec().
				WithLabels(map[string]string{"apps": "codecreate-nginx"}).
				WithSpec(corev1apply.PodSpec().
					WithContainers(corev1apply.Container().
						WithName("my-nginx").
						WithImage("nginx:latest")))))

	obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(deploymentApplyConfig)
	if err != nil {
		log.Error(err, "unable to convert unstructured")
		os.Exit(1)
	}

	patch := &unstructured.Unstructured{
		Object: obj,
	}

	b := true
	kclient.Patch(ctx, patch, client.Apply, &client.PatchOptions{
		FieldManager: fieldMgr,
		Force:        &b,
	})
	log.Info(fmt.Sprintf("Applied: %s", patch.GetName()))

	return
}

unstructured.Unstructuredを使用するくだりは、中国語ですが、以下の記事が参考になるので、見てみるのもいいかと思います。

Controller実装

CRで指定した値を使用し、deploymentリソースのspecフィールドを書き換えるSSAを行うControllerを実装します。

実際のコードは以下のリポジトリに置いています。

SSAがGAされた時のKubernetes blog記事を確認すると、It is strongly recommended that all Custom Resource Definitions (CRDs) have a schema.とあることから、CRDにSSA対象のリソース定義を埋め込むべきと読み取れます。

そこで、今回は、DeploymentSpecApplyConfigurationをCRの.Specフィールドに埋め込み、deploymentリソースの.specや.spec.template.specを書き換えるSSAを実装してみました。

なお、applyconfigurationsをCRに埋め込んだ場合、build時に以下のDeepCopy関数が生成されます。

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DeploymentSpecApplyConfiguration) DeepCopyInto(out *DeploymentSpecApplyConfiguration) {
	*out = *in
	if in.Replicas != nil {
		in, out := &in.Replicas, &out.Replicas
		*out = new(int32)
		**out = **in
	}
	if in.Selector != nil {
		in, out := &in.Selector, &out.Selector
		*out = new(metav1.LabelSelectorApplyConfiguration)
		(*in).DeepCopyInto(*out)
	}
	if in.Template != nil {
		in, out := &in.Template, &out.Template
		*out = new(corev1.PodTemplateSpecApplyConfiguration)
		(*in).DeepCopyInto(*out)
	}
	if in.Strategy != nil {
		in, out := &in.Strategy, &out.Strategy
		*out = new(appsv1.DeploymentStrategyApplyConfiguration)
		(*in).DeepCopyInto(*out)
	}
	if in.MinReadySeconds != nil {
		in, out := &in.MinReadySeconds, &out.MinReadySeconds
		*out = new(int32)
		**out = **in
	}
	if in.RevisionHistoryLimit != nil {
		in, out := &in.RevisionHistoryLimit, &out.RevisionHistoryLimit
		*out = new(int32)
		**out = **in
	}
	if in.Paused != nil {
		in, out := &in.Paused, &out.Paused
		*out = new(bool)
		**out = **in
	}
	if in.ProgressDeadlineSeconds != nil {
		in, out := &in.ProgressDeadlineSeconds, &out.ProgressDeadlineSeconds
		*out = new(int32)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentSpecApplyConfiguration.
func (in *DeploymentSpecApplyConfiguration) DeepCopy() *DeploymentSpecApplyConfiguration {
	if in == nil {
		return nil
	}
	out := new(DeploymentSpecApplyConfiguration)
	in.DeepCopyInto(out)
	return out
}

しかし、ApplyConfigurationはDeepCopy関数を持っていないため、ビルド時に以下のエラー発生します。

$ make docker-build 
:
api/v1/zz_generated.deepcopy.go:42:9: (*in).DeepCopyInto undefined (type *"k8s.io/client-go/applyconfigurations/meta/v1".LabelSelectorApplyConfiguration has no field or method DeepCopyInto)
api/v1/zz_generated.deepcopy.go:47:9: (*in).DeepCopyInto undefined (type *"k8s.io/client-go/applyconfigurations/core/v1".PodTemplateSpecApplyConfiguration has no field or method DeepCopyInto)
api/v1/zz_generated.deepcopy.go:52:9: (*in).DeepCopyInto undefined (type *"k8s.io/client-go/applyconfigurations/apps/v1".DeploymentStrategyApplyConfiguration has no field or method DeepCopyInto)

この問題については、以下の記事を参考に自前でDeepCopy関数を作成の上、解決しました。

今回のControllerでは、以下のようにCRのspecフィールドにDeploymentSpecApplyConfigurationを持たせ、CRでdeploymentの.spec配下の指定ができるようにしました。

// SSAPracticeSpec defines the desired state of SSAPractice
type SSAPracticeSpec struct {
	DepSpec *DeploymentSpecApplyConfiguration `json:"depSpec"`
}

また、deployment固有の設定では、relicasとstrategyのみ指定できるようにしています。

deploymentApplyConfig := appsv1apply.Deployment("ssapractice-nginx", "ssa-practice-controller-system").
	WithSpec(appsv1apply.DeploymentSpec().
		WithSelector(metav1apply.LabelSelector().
			WithMatchLabels(labels)))

if ssapractice.Spec.DepSpec.Replicas != nil {
	replicas := *ssapractice.Spec.DepSpec.Replicas
	deploymentApplyConfig.Spec.WithReplicas(replicas)
}

if ssapractice.Spec.DepSpec.Strategy != nil {
	types := *ssapractice.Spec.DepSpec.Strategy.Type
	rollingUpdate := ssapractice.Spec.DepSpec.Strategy.RollingUpdate
	deploymentApplyConfig.Spec.WithStrategy(appsv1apply.DeploymentStrategy().
		WithType(types).
		WithRollingUpdate(rollingUpdate))
}

deploymentにより作成されるPodの設定は、全て受け付けるようにしています。
なお、nameとimageフィールドのいずれか1つは必須としています。

errMsg := "The name or image field is required in the '.Spec.DepSpec.Template.Spec.Containers[]'."
podTemplate = ssapractice.Spec.DepSpec.Template
for _, v := range podTemplate.Spec.Containers {
	if v.Name == nil && v.Image == nil {
		return ctrl.Result{}, fmt.Errorf("Error: %s", errMsg)
	}
}

podTemplate.WithLabels(labels)
for i, v := range podTemplate.Spec.Containers {
	if v.Image == nil {
		var (
			image  string  = "nginx"
			pimage *string = &image
		)
		podTemplate.Spec.Containers[i].Image = pimage
	}
	if v.Name == nil {
		var (
			s             = strings.Split(*v.Image, ":")
			pname *string = &s[0]
		)
		podTemplate.Spec.Containers[i].Name = pname
	}
}
deploymentApplyConfig.Spec.WithTemplate(podTemplate)

owner, err := createOwnerReferences(ssapractice, r.Scheme, log)
if err != nil {
	log.Error(err, "Unable create OwnerReference")
	return ctrl.Result{}, err
}
deploymentApplyConfig.WithOwnerReferences(owner)

上記で、リソース定義を作成した後は、前述のApply関数によりSSAを行います。

CRを親とするownerReferencesも設定しているので、CRが削除されるとGCによりdeploymet、replicaset,pod全て削除されるようになっています。

動作確認

ログのみ貼っていますが、以下手順で正常に動作していることが確認できます。

# CRデプロイ前の状況確認
$ kubectl -n ssa-practice-controller-system get deployment,pod
NAME                                                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ssa-practice-controller-controller-manager   1/1     1            1           3m

NAME                                                              READY   STATUS    RESTARTS   AGE
pod/ssa-practice-controller-controller-manager-6bbb65b89f-64j7r   2/2     Running   0          3m

# .spec.replicas, strategy, containers[].name, imageを指定
$ cat config/samples/ssapractice_v1_ssapractice.yaml 
apiVersion: ssapractice.jnytnai0613.github.io/v1
kind: SSAPractice
metadata:
  name: ssapractice-sample
  namespace: ssa-practice-controller-system
spec:
  depSpec:
    replicas: 5
    strategy:
      type: RollingUpdate
      rollingUpdate:
        maxSurge: 30%
        maxUnavailable: 30%
    template:
      spec:
        containers:
          - name: nginx
            image: nginx:latest

# CRのapply
$ kubectl apply -f config/samples/ssapractice_v1_ssapractice.yaml 
ssapractice.ssapractice.jnytnai0613.github.io/ssapractice-sample created

# deploymentとpodが作成される
$ kubectl -n ssa-practice-controller-system get deployment,pod
NAME                                                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ssa-practice-controller-controller-manager   1/1     1            1           3m55s
deployment.apps/ssapractice-nginx                            5/5     5            5           12s

NAME                                                              READY   STATUS    RESTARTS   AGE
pod/ssa-practice-controller-controller-manager-6bbb65b89f-64j7r   2/2     Running   0          3m55s
pod/ssapractice-nginx-6b855c568f-7ffx6                            1/1     Running   0          12s
pod/ssapractice-nginx-6b855c568f-bmxxj                            1/1     Running   0          12s
pod/ssapractice-nginx-6b855c568f-mmzrh                            1/1     Running   0          12s
pod/ssapractice-nginx-6b855c568f-qlm2c                            1/1     Running   0          12s
pod/ssapractice-nginx-6b855c568f-xk48m                            1/1     Running   0          12s

# deploymentのyamlより、CR指定の全てのフィールドが反映されていることが確認できる。
$ kubectl -n ssa-practice-controller-system get deployment ssapractice-nginx -oyaml --show-managed-fields=true
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
  creationTimestamp: "2022-07-29T12:38:07Z"
  generation: 1
  managedFields:
  - apiVersion: apps/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:ownerReferences:
          k:{"uid":"5db9ff25-f526-439d-8b06-74544a93a1a4"}: {}
      f:spec:
        f:replicas: {}
        f:selector: {}
        f:strategy:
          f:rollingUpdate:
            f:maxSurge: {}
            f:maxUnavailable: {}
          f:type: {}
        f:template:
          f:metadata:
            f:labels:
              f:apps: {}
          f:spec:
            f:containers:
              k:{"name":"nginx"}:
                .: {}
                f:image: {}
                f:name: {}
    manager: ssapractice-fieldmanager
    operation: Apply
    time: "2022-07-29T12:38:07Z"
  - apiVersion: apps/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .: {}
          f:deployment.kubernetes.io/revision: {}
      f:status:
        f:availableReplicas: {}
        f:conditions:
          .: {}
          k:{"type":"Available"}:
            .: {}
            f:lastTransitionTime: {}
            f:lastUpdateTime: {}
            f:message: {}
            f:reason: {}
            f:status: {}
            f:type: {}
          k:{"type":"Progressing"}:
            .: {}
            f:lastTransitionTime: {}
            f:lastUpdateTime: {}
            f:message: {}
            f:reason: {}
            f:status: {}
            f:type: {}
        f:observedGeneration: {}
        f:readyReplicas: {}
        f:replicas: {}
        f:updatedReplicas: {}
    manager: kube-controller-manager
    operation: Update
    subresource: status
    time: "2022-07-29T12:38:18Z"
  name: ssapractice-nginx
  namespace: ssa-practice-controller-system
  ownerReferences:
  - apiVersion: ssapractice.jnytnai0613.github.io/v1
    blockOwnerDeletion: true
    controller: true
    kind: SSAPractice
    name: ssapractice-sample
    uid: 5db9ff25-f526-439d-8b06-74544a93a1a4
  resourceVersion: "91943"
  uid: e8def3e2-2186-41b4-8d73-8ce27e2f96b6
spec:
  progressDeadlineSeconds: 600
  replicas: 5
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      apps: ssapractice-nginx
  strategy:
    rollingUpdate:
      maxSurge: 30%
      maxUnavailable: 30%
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        apps: ssapractice-nginx
    spec:
      containers:
      - image: nginx:latest
        imagePullPolicy: Always
        name: nginx
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
:

おわりに

kubectlでの手動のSSA、Controllerを通したSSAと、2回にわたり、SSAについて記載しました。この記事が皆様のk8sライフに少しでも役に立てると幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0