[Controller編] Server-Side Applyを試す
はじめに
[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ライフに少しでも役に立てると幸いです。
Discussion