なになれ

IT系のことを記録していきます

Kubernetes Operatorを試した

試したものはPrometheusのOperatorです。

github.com

Kubernetes Operatorとは

OperatorはKubernetesに特化したアプリケーションです。Custom Controllerとして実装されています。
Operatorはアプリケーションのデプロイとスケーリングのノウハウをカプセル化しています。

何がうれしいのか

PrometheusやDBといったステートフルなアプリケーションをデプロイやスケーリングさせようとするといくつかの手順を踏まえて行う必要があります。
こういった単純な運用では扱うことが困難なアプリケーションの運用手順をCustom Controllerに持たせることで楽に運用できる仕組みです。

試してみる

Docker for MacKubernetes環境で試しました。

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の画面が表示できます。

f:id:hi1280:20191203230135p:plain:w600

まとめ

Operatorというフレームワークのもとで、多くのアプリケーションが公開されています。

operatorhub.io

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の理解がスムーズになります。
こちらの資料がとても参考になりました。

speakerdeck.com

Custom ControllerとCustom Resourceを作る

ControllerとResourceは一体です。
Custom Resourceというユーザ独自のリソースを作るにあたり、Custom ControllerというCustom Resource用のControllerを用意する必要があります。

今回はKubebuilderというCustom Resource、Custom Controllerを作成するためのSDKを利用します。

github.com

なお、Custom Resource、Custom Controllerは素の状態でも作成することが可能ですが、ControllerとResourceの仕組みに則って作成する必要があり、なかなか複雑な作業になります。

Custom Controllerの例としてsample controllerというものが公式で公開されています。これと同内容のものを作成します。
deploymentのリソースがセルフヒーリングするサンプルになっています。

github.com

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 Resources - Kubernetes

個人の方がCustom Resourceについてまとめているドキュメントを参考にしながらCustom Resourceを理解していきました。
Custom ResourceはKubernetes環境をより便利にできる機能です。
Kubebuilderを使うとより楽にCustom Resourceを準備することができます。
ただこれを使って何をするのかは分かっていません。Kubernetesを使っている人たちはどのようにCustom Resourceを使っているのかを知っていく必要があると思います。

Kubernetesでカナリアリリースを試す

Kubernetesではコンテナイメージを実行するのにDeploymentが使われます。
ただこのDeploymentではカナリアリリースを実現することはできません。
そこでArgo Rolloutsを使って、カナリアリリースを試します。

argoproj.github.io

以下に試した内容を紹介します。
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リソースでコンテナイメージを実行します。
strategycanaryを指定することでカナリアリリースになります。
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で簡単にカナリアリリースを実現することができます。