Kubernetes Operatorを試した
試したものはPrometheusのOperatorです。
Kubernetes Operatorとは
OperatorはKubernetesに特化したアプリケーションです。Custom Controllerとして実装されています。
Operatorはアプリケーションのデプロイとスケーリングのノウハウをカプセル化しています。
何がうれしいのか
PrometheusやDBといったステートフルなアプリケーションをデプロイやスケーリングさせようとするといくつかの手順を踏まえて行う必要があります。
こういった単純な運用では扱うことが困難なアプリケーションの運用手順をCustom Controllerに持たせることで楽に運用できる仕組みです。
試してみる
Docker for MacのKubernetes環境で試しました。
Prometheus Operator
Prometheus OperatorのDeploymentと権限として必要なClusterRoleBinding,ClusterRole,ServiceAccountです。
このDeploymentがPrometheusによる監視が希望通りに動作しているかをチェックする存在です。
prometheus-operator.yaml
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/name: prometheus-operator app.kubernetes.io/version: v0.34.0 name: prometheus-operator roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: prometheus-operator subjects: - kind: ServiceAccount name: prometheus-operator namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/name: prometheus-operator app.kubernetes.io/version: v0.34.0 name: prometheus-operator rules: - apiGroups: - apiextensions.k8s.io resources: - customresourcedefinitions verbs: - '*' - apiGroups: - monitoring.coreos.com resources: - alertmanagers - prometheuses - prometheuses/finalizers - alertmanagers/finalizers - servicemonitors - podmonitors - prometheusrules verbs: - '*' - apiGroups: - apps resources: - statefulsets verbs: - '*' - apiGroups: - "" resources: - configmaps - secrets verbs: - '*' - apiGroups: - "" resources: - pods verbs: - list - delete - apiGroups: - "" resources: - services - services/finalizers - endpoints verbs: - get - create - update - delete - apiGroups: - "" resources: - nodes verbs: - list - watch - apiGroups: - "" resources: - namespaces verbs: - get - list - watch --- apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/name: prometheus-operator app.kubernetes.io/version: v0.34.0 name: prometheus-operator namespace: default spec: replicas: 1 selector: matchLabels: app.kubernetes.io/component: controller app.kubernetes.io/name: prometheus-operator template: metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/name: prometheus-operator app.kubernetes.io/version: v0.34.0 spec: containers: - args: - --kubelet-service=kube-system/kubelet - --logtostderr=true - --config-reloader-image=quay.io/coreos/configmap-reload:v0.0.1 - --prometheus-config-reloader=quay.io/coreos/prometheus-config-reloader:v0.34.0 image: quay.io/coreos/prometheus-operator:v0.34.0 name: prometheus-operator ports: - containerPort: 8080 name: http resources: limits: cpu: 200m memory: 200Mi requests: cpu: 100m memory: 100Mi securityContext: allowPrivilegeEscalation: false nodeSelector: beta.kubernetes.io/os: linux securityContext: runAsNonRoot: true runAsUser: 65534 serviceAccountName: prometheus-operator --- apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/name: prometheus-operator app.kubernetes.io/version: v0.34.0 name: prometheus-operator namespace: default --- apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/name: prometheus-operator app.kubernetes.io/version: v0.34.0 name: prometheus-operator namespace: default spec: clusterIP: None ports: - name: http port: 8080 targetPort: http selector: app.kubernetes.io/component: controller app.kubernetes.io/name: prometheus-operator
$ kubectl apply -f prometheus-operator.yaml
監視対象アプリ
監視する対象のアプリをデプロイします。
example-app.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: example-app spec: replicas: 3 selector: matchLabels: app: example-app template: metadata: labels: app: example-app spec: containers: - name: example-app image: fabxc/instrumented_app ports: - name: web containerPort: 8080 --- kind: Service apiVersion: v1 metadata: name: example-app labels: app: example-app spec: selector: app: example-app ports: - name: web port: 8080
$ kubectl apply -f example-app.yaml
ServiceMonitor
ServiceMonitorリソースで監視対象を決めることで、Prometheusの監視対象を設定できます。
ServiceMonitorはその名の通り、Serviceリソースの監視を行うことができます。
そのほかにPod単位で監視可能なPodMonitorがあります。
service-monitor.yaml
apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: example-app labels: team: frontend spec: selector: matchLabels: app: example-app endpoints: - port: web
$ kubectl apply -f service-monitor.yaml
Prometheus本体のデプロイ
Prometheusの設定です。
PrometheusリソースでServiceMonitorと関連付けています。
権限として必要なClusterRoleBinding,ClusterRole,ServiceAccountを設定しています。
prometheus.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: prometheus --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: name: prometheus rules: - apiGroups: [""] resources: - nodes - services - endpoints - pods verbs: ["get", "list", "watch"] - apiGroups: [""] resources: - configmaps verbs: ["get"] - nonResourceURLs: ["/metrics"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: name: prometheus roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: prometheus subjects: - kind: ServiceAccount name: prometheus namespace: default --- apiVersion: monitoring.coreos.com/v1 kind: Prometheus metadata: name: prometheus spec: serviceAccountName: prometheus serviceMonitorSelector: matchLabels: team: frontend resources: requests: memory: 400Mi enableAdminAPI: false --- apiVersion: v1 kind: Service metadata: name: prometheus spec: type: NodePort ports: - name: web nodePort: 30900 port: 9090 protocol: TCP targetPort: web selector: prometheus: prometheus
$ kubectl apply -f prometheus.yaml
Prometheusの動作確認
ブラウザでPrometheusのServiceのポートを指定してローカルで表示するとPrometheusの画面が表示できます。
まとめ
Operatorというフレームワークのもとで、多くのアプリケーションが公開されています。
Helmでも似たようにKubernetes上で動作するアプリケーションがパッケージ管理されています。
ただ利用する場合には設定値を決める必要があるので手軽に利用したいというケースには不向きです。
Prometheusなどのアプリケーションを手軽にセットアップしたい場合にはOperatorを使うと良いと思います。
KubernetesのCustom Resourceを試した
KubernetesのCustom ResourceはKubernetesをユーザが拡張するためのものです。
Kubernetesの標準にあるDeploymentのようなリソースをユーザが独自に作成し、リソースの定義に応じて、Kubernetes上のリソースをコントロールすることが可能です。
ControllerとResourceについて
Custom Resourceを学ぶにあたり、Kubernetesの仕組みを理解する必要があります。
ControllerとResourceというものがKubernetesでは機能していて、これがCustom Resourceのベースになっています。
まずはこれらをざっと理解しておくとCustom Resourceの理解がスムーズになります。
こちらの資料がとても参考になりました。
Custom ControllerとCustom Resourceを作る
ControllerとResourceは一体です。
Custom Resourceというユーザ独自のリソースを作るにあたり、Custom ControllerというCustom Resource用のControllerを用意する必要があります。
今回はKubebuilderというCustom Resource、Custom Controllerを作成するためのSDKを利用します。
なお、Custom Resource、Custom Controllerは素の状態でも作成することが可能ですが、ControllerとResourceの仕組みに則って作成する必要があり、なかなか複雑な作業になります。
Custom Controllerの例としてsample controllerというものが公式で公開されています。これと同内容のものを作成します。
deploymentのリソースがセルフヒーリングするサンプルになっています。
Kubebuilderをインストールする
Kubebuilderを下記のドキュメントの通りにインストールします。
Quick Start - The Kubebuilder Book
プロジェクトを作成する
$ cd go/src/tutorial.kubebuilder.io $ kubebuilder init --domain my.domain
APIを作成する
$ kubebuilder create api --group webapp --version v1 --kind MyKind Create Resource [y/n] y Create Controller [y/n] y
Custom Resourceを修正する
api/v1/mykind_types.goを修正します。
このファイルの定義内容がcustom resourceのyamlの定義に反映されます。
mykind_types.go
package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // MyKindSpec defines the desired state of MyKind type MyKindSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // DeploymentName is the name of the Deployment resource that the // controller should create. // This field must be specified. // +kubebuilder:validation:MaxLength=64 DeploymentName string `json:"deploymentName"` // Replicas is the number of replicas that should be specified on the // Deployment resource that the controller creates. // If not specified, one replica will be created. // +optional // +kubebuilder:validation:Minimum=0 Replicas *int32 `json:"replicas,omitempty"` } // MyKindStatus defines the observed state of MyKind type MyKindStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file // ReadyReplicas is the number of 'ready' replicas observed on the // Deployment resource created for this MyKind resource. // +optional // +kubebuilder:validation:Minimum=0 ReadyReplicas int32 `json:"readyReplicas,omitempty"` } // +kubebuilder:object:root=true // MyKind is the Schema for the mykinds API type MyKind struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MyKindSpec `json:"spec,omitempty"` Status MyKindStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // MyKindList contains a list of MyKind type MyKindList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []MyKind `json:"items"` } func init() { SchemeBuilder.Register(&MyKind{}, &MyKindList{}) }
goのstructを使ってcustom resourceのデータ構造を定義します。
kubernetesでは、項目名はcamelcaseがルールなので、structのjson tagを利用して項目名を変換します。
kubebuilderのmetadataを使って、validationなどの設定が可能です。
詳細は以下にあります。
Markers for Config/Code Generation - The Kubebuilder Book
Custom Controllerを修正する
controllers/mykind_controller.goを修正します。
mykind_controller.go(前半部分)
package controllers import ( "context" apps "k8s.io/api/apps/v1" core "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" "github.com/go-logr/logr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" webappv1 "tutorial.kubebuilder.io/sample-controller/api/v1" ) // MyKindReconciler reconciles a MyKind object type MyKindReconciler struct { client.Client Log logr.Logger Recorder record.EventRecorder } // +kubebuilder:rbac:groups=webapp.my.domain,resources=mykinds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=webapp.my.domain,resources=mykinds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch func (r *MyKindReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.Log.WithValues("mykind", req.NamespacedName) // your logic here log.Info("fetching MyKind resource") myKind := webappv1.MyKind{} if err := r.Client.Get(ctx, req.NamespacedName, &myKind); err != nil { log.Error(err, "failed to get MyKind resource") // Ignore NotFound errors as they will be retried automatically if the // resource is created in future. return ctrl.Result{}, client.IgnoreNotFound(err) } if err := r.cleanupOwnedResources(ctx, log, &myKind); err != nil { log.Error(err, "failed to clean up old Deployment resources for this MyKind") return ctrl.Result{}, err } log = log.WithValues("deployment_name", myKind.Spec.DeploymentName) log.Info("checking if an existing Deployment exists for this resource") deployment := apps.Deployment{} err := r.Client.Get(ctx, client.ObjectKey{Namespace: myKind.Namespace, Name: myKind.Spec.DeploymentName}, &deployment) if apierrors.IsNotFound(err) { log.Info("could not find existing Deployment for MyKind, creating one...") deployment = *buildDeployment(myKind) if err := r.Client.Create(ctx, &deployment); err != nil { log.Error(err, "failed to create Deployment resource") return ctrl.Result{}, err } r.Recorder.Eventf(&myKind, core.EventTypeNormal, "Created", "Created deployment %q", deployment.Name) log.Info("created Deployment resource for MyKind") return ctrl.Result{}, nil } if err != nil { log.Error(err, "failed to get Deployment for MyKind resource") return ctrl.Result{}, err } log.Info("existing Deployment resource already exists for MyKind, checking replica count") expectedReplicas := int32(1) if myKind.Spec.Replicas != nil { expectedReplicas = *myKind.Spec.Replicas } if *deployment.Spec.Replicas != expectedReplicas { log.Info("updating replica count", "old_count", *deployment.Spec.Replicas, "new_count", expectedReplicas) deployment.Spec.Replicas = &expectedReplicas if err := r.Client.Update(ctx, &deployment); err != nil { log.Error(err, "failed to Deployment update replica count") return ctrl.Result{}, err } r.Recorder.Eventf(&myKind, core.EventTypeNormal, "Scaled", "Scaled deployment %q to %d replicas", deployment.Name, expectedReplicas) return ctrl.Result{}, nil } log.Info("replica count up to date", "replica_count", *deployment.Spec.Replicas) log.Info("updating MyKind resource status") myKind.Status.ReadyReplicas = deployment.Status.ReadyReplicas if r.Client.Status().Update(ctx, &myKind); err != nil { log.Error(err, "failed to update MyKind status") return ctrl.Result{}, err } log.Info("resource status synced") return ctrl.Result{}, nil }
Reconcile関数はCustom Resourceがapplyされて変更された時に実行される関数になります。
ここにLogicを書いていきます。
Client
でResourceを操作することが可能です。
GetでResourceを取得します。
CreateでResourceを作成します。
UpdateでResourceを更新します。
mykind_controller.go(後半部分)
// cleanupOwnedResources will Delete any existing Deployment resources that // were created for the given MyKind that no longer match the // myKind.spec.deploymentName field. func (r *MyKindReconciler) cleanupOwnedResources(ctx context.Context, log logr.Logger, myKind *webappv1.MyKind) error { log.Info("finding existing Deployments for MyKind resource") // List all deployment resources owned by this MyKind var deployments apps.DeploymentList if err := r.List(ctx, &deployments, client.InNamespace(myKind.Namespace), client.MatchingField(deploymentOwnerKey, myKind.Name)); err != nil { return err } deleted := 0 for _, depl := range deployments.Items { if depl.Name == myKind.Spec.DeploymentName { // If this deployment's name matches the one on the MyKind resource // then do not delete it. continue } if err := r.Client.Delete(ctx, &depl); err != nil { log.Error(err, "failed to delete Deployment resource") return err } r.Recorder.Eventf(myKind, core.EventTypeNormal, "Deleted", "Deleted deployment %q", depl.Name) deleted++ } log.Info("finished cleaning up old Deployment resources", "number_deleted", deleted) return nil } func buildDeployment(myKind webappv1.MyKind) *apps.Deployment { deployment := apps.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: myKind.Spec.DeploymentName, Namespace: myKind.Namespace, OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&myKind, webappv1.GroupVersion.WithKind("MyKind"))}, }, Spec: apps.DeploymentSpec{ Replicas: myKind.Spec.Replicas, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "deployment-name": myKind.Spec.DeploymentName, }, }, Template: core.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "deployment-name": myKind.Spec.DeploymentName, }, }, Spec: core.PodSpec{ Containers: []core.Container{ { Name: "nginx", Image: "nginx:latest", }, }, }, }, }, } return &deployment } var ( deploymentOwnerKey = ".metadata.controller" ) func (r *MyKindReconciler) SetupWithManager(mgr ctrl.Manager) error { if err := mgr.GetFieldIndexer().IndexField(&apps.Deployment{}, deploymentOwnerKey, func(rawObj runtime.Object) []string { // grab the Deployment object, extract the owner... depl := rawObj.(*apps.Deployment) owner := metav1.GetControllerOf(depl) if owner == nil { return nil } // ...make sure it's a MyKind... if owner.APIVersion != webappv1.GroupVersion.String() || owner.Kind != "MyKind" { return nil } // ...and if so, return it return []string{owner.Name} }); err != nil { return err } return ctrl.NewControllerManagedBy(mgr). For(&webappv1.MyKind{}). Owns(&apps.Deployment{}). Complete(r) }
SetupWithManager関数でReconcile関数が呼ばれるように設定します。
cleanupOwnedResources関数で既存のCustom ResourceがDeploymentを作成していた場合にそれを削除します。
buildDeployment関数でDeploymentリソースを作成します。
起動処理を修正する
main.goを修正します。
main.go
package main import ( "flag" "os" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" webappv1 "tutorial.kubebuilder.io/sample-controller/api/v1" "tutorial.kubebuilder.io/sample-controller/controllers" // +kubebuilder:scaffold:imports ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { _ = clientgoscheme.AddToScheme(scheme) _ = webappv1.AddToScheme(scheme) // +kubebuilder:scaffold:scheme } func main() { var metricsAddr string var enableLeaderElection bool flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") flag.Parse() ctrl.SetLogger(zap.Logger(true)) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, LeaderElection: enableLeaderElection, Port: 9443, }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } if err = (&controllers.MyKindReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("MyKind"), Recorder: mgr.GetEventRecorderFor("mykind-controller"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MyKind") os.Exit(1) } // +kubebuilder:scaffold:builder setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } }
Recorderをcontrollerに渡すようにして、Custom Resourceのイベントを記録するようにします。
Custom Resourceをkubernetes環境にインストールする
$ make install
Custom Controllerを動かす
$ make run
結果を確認する
Custom Resourceを以下のように定義します。
webapp_v1_mykind.yaml
apiVersion: webapp.my.domain/v1 kind: MyKind metadata: name: mykind-sample spec: # Add fields here foo: bar deploymentName: mykind replicas: 1
custom resourceを作成します。
$ kubectl apply config/samples/webapp_v1_mykind.yaml
しばらくするとdeploymentが作られます。
$ kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE mykind 1/1 1 1 33s
deploymentを削除してもしばらくするとまた同じdeploymentが復活します。
$ kubectl delete deployments mykind
まとめ
公式のドキュメントは内容が難しいと思います。
個人の方がCustom Resourceについてまとめているドキュメントを参考にしながらCustom Resourceを理解していきました。
Custom ResourceはKubernetes環境をより便利にできる機能です。
Kubebuilderを使うとより楽にCustom Resourceを準備することができます。
ただこれを使って何をするのかは分かっていません。Kubernetesを使っている人たちはどのようにCustom Resourceを使っているのかを知っていく必要があると思います。
Kubernetesでカナリアリリースを試す
Kubernetesではコンテナイメージを実行するのにDeploymentが使われます。
ただこのDeploymentではカナリアリリースを実現することはできません。
そこでArgo Rolloutsを使って、カナリアリリースを試します。
以下に試した内容を紹介します。
Argo Rolloutsのバージョンはv0.9.1時点の内容になります。
Argo Rolloutsとは
Kubernetesにおけるデプロイ戦略にブルーグリーンデプロイメントとカナリアリリースを追加します。
今回はこの2つのうち、カナリアリリースを試します。
カナリアリリースを試す
Argo Rolloutsをインストールする
kubectl create namespace argo-rollouts kubectl apply -n argo-rollouts -f https://raw.githubusercontent.com/argoproj/argo-rollouts/stable/manifests/install.yaml
Argo RolloutsのKubectl Pluginをインストールする
brew install argoproj/tap/kubectl-argo-rollouts
検証用のイメージを準備する
カナリアリリースによって、指定した割合で新バージョンになっているか確認するためのイメージを用意します。
nginxを使って、表示する内容を変更したイメージを用意します。
Dockerfile
FROM nginx COPY app /usr/share/nginx/html
version:0.0.1
app/index.html
<html><body>1</body></html>
version:0.0.2
app/index.html
<html><body>2</body></html>
カナリアリリースを実施する
Argo Rolloutsをインストールすることで使用可能になったRolloutリソースでコンテナイメージを実行します。
strategy
でcanary
を指定することでカナリアリリースになります。
setWeight
で新バージョンをデプロイした時の割合を決めます。ここでは20%にしています。
nginx.yaml
apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: sample spec: replicas: 5 selector: matchLabels: app: sample template: metadata: labels: app: sample spec: containers: - name: nginx image: hi1280/nginx:0.0.1 ports: - containerPort: 80 strategy: canary: steps: - setWeight: 20 - pause: {}
version:0.0.1のnginxイメージをrolloutリソースでデプロイします。
kubectl apply -f nginx.yaml
最初のデプロイ時点での状態です。
kubectl pluginのgetコマンドで状態を可視化できます。
$ kubectl argo rollouts get rollout sample --watch Name: sample Namespace: default Status: ✔ Healthy Strategy: Canary Step: 2/2 SetWeight: 100 ActualWeight: 100 Images: hi1280/nginx:0.0.1 (stable) Replicas: Desired: 5 Current: 5 Updated: 5 Ready: 5 Available: 5 NAME KIND STATUS AGE INFO ⟳ sample Rollout ✔ Healthy 11s └──# revision:1 └──⧉ sample-747794b696 ReplicaSet ✔ Healthy 11s stable ├──□ sample-747794b696-rbczl Pod ✔ Running 11s ready:1/1 ├──□ sample-747794b696-sgtj7 Pod ✔ Running 11s ready:1/1 ├──□ sample-747794b696-xklcb Pod ✔ Running 11s ready:1/1 ├──□ sample-747794b696-xvf54 Pod ✔ Running 11s ready:1/1 └──□ sample-747794b696-zxrpn Pod ✔ Running 11s ready:1/1
version:0.0.2のnginxイメージをデプロイします。
set imageコマンドでイメージをデプロイできます。
kubectl argo rollouts set image sample nginx=hi1280/nginx:0.0.2
デプロイした後の状態です。20%の割合でデプロイされていることが分かります。
$ kubectl argo rollouts get rollout sample --watch Name: sample Namespace: default Status: ॥ Paused Strategy: Canary Step: 1/2 SetWeight: 20 ActualWeight: 20 Images: hi1280/nginx:0.0.1 (stable) hi1280/nginx:0.0.2 (canary) Replicas: Desired: 5 Current: 5 Updated: 1 Ready: 5 Available: 5 NAME KIND STATUS AGE INFO ⟳ sample Rollout ॥ Paused 9m6s ├──# revision:2 │ └──⧉ sample-5669f74b97 ReplicaSet ✔ Healthy 17s canary │ └──□ sample-5669f74b97-rzl8z Pod ✔ Running 17s ready:1/1 └──# revision:1 └──⧉ sample-747794b696 ReplicaSet ✔ Healthy 9m6s stable ├──□ sample-747794b696-rbczl Pod ✔ Running 9m6s ready:1/1 ├──□ sample-747794b696-xklcb Pod ✔ Running 9m6s ready:1/1 ├──□ sample-747794b696-xvf54 Pod ✔ Running 9m6s ready:1/1 └──□ sample-747794b696-zxrpn Pod ✔ Running 9m6s ready:1/1
promoteコマンドを実行することで、100%の割合でデプロイします。
kubectl argo rollouts promote sample
100%でデプロイした後の状態です。
$ kubectl argo rollouts get rollout rollouts-demo --watch Name: sample Namespace: default Status: ✔ Healthy Strategy: Canary Step: 2/2 SetWeight: 100 ActualWeight: 100 Images: hi1280/nginx:0.0.2 (stable) Replicas: Desired: 5 Current: 5 Updated: 5 Ready: 5 Available: 5 NAME KIND STATUS AGE INFO ⟳ sample Rollout ✔ Healthy 11m ├──# revision:2 │ └──⧉ sample-5669f74b97 ReplicaSet ✔ Healthy 2m47s stable │ ├──□ sample-5669f74b97-rzl8z Pod ✔ Running 2m47s ready:1/1 │ ├──□ sample-5669f74b97-wtvbq Pod ✔ Running 37s ready:1/1 │ ├──□ sample-5669f74b97-zvvjl Pod ✔ Running 37s ready:1/1 │ ├──□ sample-5669f74b97-8l8z9 Pod ✔ Running 35s ready:1/1 │ └──□ sample-5669f74b97-jjhkj Pod ✔ Running 35s ready:1/1 └──# revision:1 └──⧉ sample-747794b696 ReplicaSet • ScaledDown 11m
少ない割合で新バージョンをデプロイして、その後、新バージョンに変更することができました。
指定した時間だけ新バージョンをデプロイする
カナリアリリースの設定によって、指定した時間だけ新バージョンを試して、自動的に旧バージョンに戻すといったことが可能です。
steps
に記述します。
ここでは、30秒間は20%の割合で新バージョンを試して、その後は0%の割合にすることで旧バージョンに戻しています。
nginx.yaml
apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: sample spec: replicas: 5 selector: matchLabels: app: sample template: metadata: labels: app: sample spec: containers: - name: nginx image: hi1280/nginx:0.0.1 ports: - containerPort: 80 strategy: canary: steps: - setWeight: 20 - pause: duration: 30 - setWeight: 0 - pause: {}
duration
の単位は秒数になります。
Rolloutリソースでローリングアップデートを実施する
ローリングアップデートはDeploymentリソースでも実施できますが、リソースが異なるので取り回しが面倒です。
ローリングアップデートも同じリソースでできると便利です。
ローリングアップデートはcanary:{}
で実現できます。
nginx.yaml
apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: sample spec: replicas: 5 selector: matchLabels: app: sample template: metadata: labels: app: sample spec: containers: - name: nginx image: hi1280/nginx:0.0.1 ports: - containerPort: 80 strategy: canary: {}
まとめ
カナリアリリースは既に一般的なデプロイ戦略だと思いますが、Kubernetesではそれができないのが残念なところでした。
Argo Rolloutsを使うとKubernetesで簡単にカナリアリリースを実現することができます。