なぜブルーグリーンデプロイメントなのか?

詳細に入る前に、ブルーグリーンデプロイメントがなぜ素晴らしいのかを簡単に振り返りましょう:

  • ダウンタイムゼロのデプロイメント
  • 問題が発生した場合の簡単なロールバック
  • 本番環境に近い環境でのテストが可能
  • 運用チームのリスクとストレスの軽減

これをKubernetes Operatorsの力で実現することを想像してみてください。ワクワクしますね!

舞台設定:カスタムコントローラー

私たちのミッションは、ブルーグリーンデプロイメントを管理するカスタムコントローラーを作成することです。このコントローラーは、カスタムリソースの変更を監視し、デプロイメントプロセスを調整します。

まずは、カスタムリソースを定義しましょう:

apiVersion: mycompany.com/v1
kind: BlueGreenDeployment
metadata:
  name: my-awesome-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-awesome-app
  template:
    metadata:
      labels:
        app: my-awesome-app
    spec:
      containers:
      - name: my-awesome-app
        image: myregistry.com/my-awesome-app:v1
        ports:
        - containerPort: 8080

ここでは特に難しいことはありません。標準的なKubernetesデプロイメントですが、私たちのカスタムリソースタイプです!

本質:コントローラーロジック

次に、コントローラーロジックに進みましょう。Go言語を使用します。なぜなら、Goは素晴らしいからです(すみません、つい言ってしまいました)。


package controller

import (
	"context"
	"fmt"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
	"sigs.k8s.io/controller-runtime/pkg/source"

	mycompanyv1 "github.com/mycompany/api/v1"
)

type BlueGreenReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
	log := r.Log.WithValues("bluegreen", req.NamespacedName)

	// BlueGreenDeploymentインスタンスを取得
	blueGreen := &mycompanyv1.BlueGreenDeployment{}
	err := r.Get(ctx, req.NamespacedName, blueGreen)
	if err != nil {
		if errors.IsNotFound(err) {
			// オブジェクトが見つからない場合、返す。作成されたオブジェクトは自動的にガベージコレクトされる。
			return reconcile.Result{}, nil
		}
		// オブジェクトの読み取りエラー - リクエストを再キュー。
		return reconcile.Result{}, err
	}

	// デプロイメントが既に存在するか確認し、存在しない場合は新しいものを作成
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: blueGreen.Name + "-blue", Namespace: blueGreen.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// 新しいデプロイメントを定義
		dep := r.deploymentForBlueGreen(blueGreen, "-blue")
		log.Info("新しいデプロイメントを作成", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		err = r.Create(ctx, dep)
		if err != nil {
			log.Error(err, "新しいデプロイメントの作成に失敗", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return reconcile.Result{}, err
		}
		// デプロイメントが正常に作成された - 返して再キュー
		return reconcile.Result{Requeue: true}, nil
	} else if err != nil {
		log.Error(err, "デプロイメントの取得に失敗")
		return reconcile.Result{}, err
	}

	// デプロイメントのサイズが仕様と一致することを確認
	size := blueGreen.Spec.Size
	if *found.Spec.Replicas != size {
		found.Spec.Replicas = &size
		err = r.Update(ctx, found)
		if err != nil {
			log.Error(err, "デプロイメントの更新に失敗", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
			return reconcile.Result{}, err
		}
		// 仕様が更新された - 返して再キュー
		return reconcile.Result{Requeue: true}, nil
	}

	// BlueGreenDeploymentのステータスをポッド名で更新
	// このデプロイメントのポッドをリスト
	podList := &corev1.PodList{}
	listOpts := []client.ListOption{
		client.InNamespace(blueGreen.Namespace),
		client.MatchingLabels(labelsForBlueGreen(blueGreen.Name)),
	}
	if err = r.List(ctx, podList, listOpts...); err != nil {
		log.Error(err, "ポッドのリストに失敗", "BlueGreenDeployment.Namespace", blueGreen.Namespace, "BlueGreenDeployment.Name", blueGreen.Name)
		return reconcile.Result{}, err
	}
	podNames := getPodNames(podList.Items)

	// 必要に応じてステータスを更新
	if !reflect.DeepEqual(podNames, blueGreen.Status.Nodes) {
		blueGreen.Status.Nodes = podNames
		err := r.Status().Update(ctx, blueGreen)
		if err != nil {
			log.Error(err, "BlueGreenDeploymentステータスの更新に失敗")
			return reconcile.Result{}, err
		}
	}

	return reconcile.Result{}, nil
}

// deploymentForBlueGreenはブルーグリーンデプロイメントオブジェクトを返す
func (r *BlueGreenReconciler) deploymentForBlueGreen(m *mycompanyv1.BlueGreenDeployment, suffix string) *appsv1.Deployment {
	ls := labelsForBlueGreen(m.Name)
	replicas := m.Spec.Size

	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      m.Name + suffix,
			Namespace: m.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: ls,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: ls,
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{{
						Image: m.Spec.Image,
						Name:  "bluegreen",
						Ports: []corev1.ContainerPort{{
							ContainerPort: 8080,
							Name:          "bluegreen",
						}},
					}},
				},
			},
		},
	}
	// BlueGreenDeploymentインスタンスをオーナーおよびコントローラーとして設定
	controllerutil.SetControllerReference(m, dep, r.Scheme)
	return dep
}

// labelsForBlueGreenは、指定されたブルーグリーンCR名に属するリソースを選択するためのラベルを返す
func labelsForBlueGreen(name string) map[string]string {
	return map[string]string{"app": "bluegreen", "bluegreen_cr": name}
}

// getPodNamesは渡されたポッドの配列のポッド名を返す
func getPodNames(pods []corev1.Pod) []string {
	var podNames []string
	for _, pod := range pods {
		podNames = append(podNames, pod.Name)
	}
	return podNames
}

ふう!これはかなりのコード量ですが、分解してみましょう:

  1. BlueGreenReconciler構造体を定義し、Reconcileメソッドを実装します。
  2. Reconcileメソッドでカスタムリソースを取得し、デプロイメントが存在するか確認します。
  3. デプロイメントが存在しない場合、deploymentForBlueGreenを使用して新しいものを作成します。
  4. デプロイメントのサイズが仕様と一致することを確認し、必要に応じて更新します。
  5. 最後に、カスタムリソースのステータスをポッド名で更新します。

秘密のソース:ブルーグリーンマジック

ここでブルーグリーンデプロイメントの魔法が起こります。ブルーとグリーンの両方のデプロイメントを作成し、それらを切り替えるロジックを追加する必要があります。コントローラーを強化しましょう:


func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
	// ... (前のコード)

	// ブルーデプロイメントを作成または更新
	blueDeployment := r.deploymentForBlueGreen(blueGreen, "-blue")
	if err := r.createOrUpdateDeployment(ctx, blueDeployment); err != nil {
		return reconcile.Result{}, err
	}

	// グリーンデプロイメントを作成または更新
	greenDeployment := r.deploymentForBlueGreen(blueGreen, "-green")
	if err := r.createOrUpdateDeployment(ctx, greenDeployment); err != nil {
		return reconcile.Result{}, err
	}

	// 切り替える時期かどうかを確認
	if shouldSwitch(blueGreen) {
		if err := r.switchTraffic(ctx, blueGreen); err != nil {
			return reconcile.Result{}, err
		}
	}

	// ... (残りのコード)
}

func (r *BlueGreenReconciler) createOrUpdateDeployment(ctx context.Context, dep *appsv1.Deployment) error {
	// デプロイメントが既に存在するか確認
	found := &appsv1.Deployment{}
	err := r.Get(ctx, types.NamespacedName{Name: dep.Name, Namespace: dep.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		// デプロイメントを作成
		err = r.Create(ctx, dep)
		if err != nil {
			return err
		}
	} else if err != nil {
		return err
	} else {
		// デプロイメントを更新
		found.Spec = dep.Spec
		err = r.Update(ctx, found)
		if err != nil {
			return err
		}
	}
	return nil
}

func shouldSwitch(bg *mycompanyv1.BlueGreenDeployment) bool {
	// 切り替える時期を判断するロジックを実装
	// これはタイマー、手動トリガー、または他の基準に基づく可能性があります
	return false
}

func (r *BlueGreenReconciler) switchTraffic(ctx context.Context, bg *mycompanyv1.BlueGreenDeployment) error {
	// ブルーとグリーンの間でトラフィックを切り替えるロジックを実装
	// これはサービスまたはイングレスリソースの更新を含む可能性があります
	return nil
}

この強化版は、ブルーとグリーンの両方のデプロイメントを作成し、切り替えのタイミングと方法を決定するためのプレースホルダ関数を含んでいます。

すべてをまとめる

コントローラーロジックができたので、オペレーターをセットアップする必要があります。ここに基本的なmain.goファイルがあります:


package main

import (
	"flag"
	"os"

	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/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"

	mycompanyv1 "github.com/mycompany/api/v1"
	"github.com/mycompany/controllers"
)

var (
	scheme   = runtime.NewScheme()
	setupLog = ctrl.Log.WithName("setup")
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
	utilruntime.Must(mycompanyv1.AddToScheme(scheme))
}

func main() {
	var metricsAddr string
	var enableLeaderElection bool
	flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "メトリックエンドポイントがバインドするアドレス。")
	flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
		"コントローラーマネージャーのリーダー選出を有効にします。これを有効にすると、アクティブなコントローラーマネージャーが1つだけになります。")
	flag.Parse()

	ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme:             scheme,
		MetricsBindAddress: metricsAddr,
		LeaderElection:     enableLeaderElection,
		Port:               9443,
	})
	if err != nil {
		setupLog.Error(err, "マネージャーの開始に失敗しました")
		os.Exit(1)
	}

	if err = (&controllers.BlueGreenReconciler{
		Client: mgr.GetClient(),
		Log:    ctrl.Log.WithName("controllers").WithName("BlueGreen"),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "コントローラーの作成に失敗しました", "controller", "BlueGreen")
		os.Exit(1)
	}

	setupLog.Info("マネージャーを開始します")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "マネージャーの実行に問題があります")
		os.Exit(1)
	}
}

デプロイメントとテスト

オペレーターの準備が整ったので、デプロイしてテストする時が来ました。以下は簡単なチェックリストです:

  1. オペレーターのイメージをビルドし、コンテナレジストリにプッシュします。
  2. オペレーターのための必要なRBACロールとバインディングを作成します。
  3. オペレーターをKubernetesクラスターにデプロイします。
  4. BlueGreenDeploymentカスタムリソースを作成し、魔法を見守りましょう!

BlueGreenDeploymentを作成する例を示します:


apiVersion: mycompany.com/v1
kind: BlueGreenDeployment
metadata:
  name: my-cool-app
spec:
  replicas: 3
  image: mycoolapp:v1

落とし穴と注意点

これを本番環境で実装する前に、次の点に注意してください:

  • リソース管理:2つのデプロイメントを同時に実行すると、リソース使用量が2倍になる可能性があります。計画を立てましょう!
  • データベースのマイグレーション:後方互換性のないデータベーススキーマには注意が必要です。
  • スティッキーセッション:アプリがスティッキーセッションに依存している場合、切り替え時に注意が必要です。
  • テスト:本番環境以外でオペレーターを徹底的にテストしてください。後で自分に感謝することになります。

まとめ

以上で、ブルーグリーンデプロイメントを扱うカスタムKubernetesオペレーターが完成しました。カスタムリソースからコントローラーロジック、デプロイメントのヒントまで、多くのことをカバーしました。

これは始まりに過ぎません。このオペレーターを拡張して、より複雑なシナリオに対応したり、モニタリングやアラートを追加したり、CI/CDパイプラインと統合したりすることができます。

"大いなる力には大いなる責任が伴う" - ベンおじさん(そしてすべてのDevOpsエンジニア)

自信を持ってデプロイしてください!問題が発生した場合は、ロールバックがあるので安心です。

考えるための材料

これを自分のプロジェクトに実装する際、次のことを考慮してください:

  • このオペレーターをどのように拡張してカナリアデプロイメントを扱うことができるでしょうか?
  • デプロイメントプロセス中に収集するのに役立つメトリックは何でしょうか?
  • これをPrometheusやGrafanaのような外部ツールとどのように統合することができるでしょうか?

コーディングを楽しんでください。そして、あなたのデプロイメントが常にグリーン(またはブルー、お好みに応じて)でありますように!