Оператор Kubernetes на Go и Kubebuilder для начинающих

Аватарка пользователя Елена Михайлычева

В этой статье шаг за шагом описывается процесс создания простого оператора Kubernetes, который копирует Secret из одного неймспейса в другие.

Обложка поста Оператор Kubernetes на Go и Kubebuilder для начинающих

В этой статье шаг за шагом описывается процесс создания простого оператора Kubernetes, который копирует Secret из одного неймспейса в другие. Также немного рассказывается о том, как проверить, что созданный оператор работает, и как отладить код, если что-то пошло не так.

Данное руководство исключительно практическое и предполагается, что с основными абстракциями Kubernetes вы уже знакомы.

Что понадобится установить

  1. Для создания оператора: kubebuilder.
  2. Для локального запуска Kubernetes: kindkubectl.
  3. Окружение: Ubuntu, Go 1.21.5.

Создание оператора

Оператор будет отслеживать появление объекта типа secretsync. При появлении в кластере такого объекта оператор будет копировать секрет с именем SecretName из неймспейса SourceNamespace в неймспейсы из списка DestinationNamespace. После чего у объекта secretsync обновляется поле статуса LastSyncTime.

1. Создать и инициализировать проект оператора с помощью kubebuilder.

Обратите внимание, что в последней команде указан --kind SecretSync. Это значит, что имя нового CRD (custom resource definition) будет SecretSync.

			mkdir -p secretsync

cd secretsync

kubebuilder init --domain example.com --repo=example.com/operator

kubebuilder create api --group apps --version v1 --kind SecretSync
		

На вопросы в последней команде ответить y:

			INFO Create Resource [y/n]                        
y

INFO Create Controller [y/n]                      
y
		

2. Определить поля Spec и Status нового CRD

Для этого в файле secretsync_types.go отредактировать структуры.

			type SecretSyncSpec struct {
	SourceNamespace       string   `json:"sourceNamespace"`
	DestinationNamespaces []string `json:"destinationNamespaces"`
	SecretName            string   `json:"secretName"`
}

type SecretSyncStatus struct {
	LastSyncTime metav1.Time `json:"lastSyncTime"`
}
		

3. Сгенерировать манифест для нового CRD

			make manifests
		

4. Добавить новый CRD в kubernetes

			make install
		

5. Заполнить манифест для создания экземпляра нового CRD

Для этого отредактировать файл apps_v1_secretsync.yaml (если не указать неймспейс, то будет неймспейс из current-context):

			apiVersion: apps.example.com/v1
kind: SecretSync
metadata:
  labels:
    app.kubernetes.io/name: secretsync
    app.kubernetes.io/instance: secretsync-sample
    app.kubernetes.io/part-of: secretsync
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: secretsync
  name: secretsync-sample
spec:
  sourceNamespace: "default"
  destinationNamespaces: ["production", "development"]
  secretName: "example-secret"
		

6. Добавить объект нового типа secretsync в kubernetes

			kubectl apply -f config/samples/apps_v1_secretsync.yaml
		

7. Добавить оператору права на секреты

Для этого добавить в файле secretsync_controller.go новую строчку после всех директив вида +kubebuilder:rbac

			//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
		

8. Написать логику работы оператора

Для этого отредактировать метод Reconcile в файле secretsync_controller.go.

Тут важно максимально логировать все действия, чтобы в логе пода иметь больше информации.

В начале файла добавить новые строчки с импортом:

			corev1 "k8s.io/api/core/v1"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
		

И добавить логику в контроллер:

			func (r *SecretSyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := log.FromContext(ctx).WithValues("secretsync", req.NamespacedName)
	// Fetch the SecretSync instance
	secretSync := &appsv1.SecretSync{}
	if err := r.Get(ctx, req.NamespacedName, secretSync); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// Fetch the source Secret
	sourceSecret := &corev1.Secret{}
	sourceSecretName := types.NamespacedName{
		Namespace: secretSync.Spec.SourceNamespace,
		Name:      secretSync.Spec.SecretName,
	}

	if err := r.Get(ctx, sourceSecretName, sourceSecret); err != nil {
		log.Error(err, "Unable to get source secret", "SecretName", secretSync.Spec.SecretName, "SourceNamespace", secretSync.Spec.SourceNamespace)
		return ctrl.Result{}, err
	}
	log.Info("Got source Secret in source namespace", "SecretName", secretSync.Spec.SecretName, "SourceNamespace", secretSync.Spec.SourceNamespace)

	// Create or Update the destination Secrets in the target namespaces
	for _, destinationSecretNamespace := range secretSync.Spec.DestinationNamespaces {
		destinationSecret := &corev1.Secret{}
		destinationSecretName := types.NamespacedName{
			Namespace: destinationSecretNamespace,
			Name:      secretSync.Spec.SecretName,
		}
		log.Info("Looking for Secret in destination namespace", "Namespace", destinationSecretNamespace, "SecretName", secretSync.Spec.SecretName)
		if err := r.Get(ctx, destinationSecretName, destinationSecret); err != nil {
			if errors.IsNotFound(err) {
				log.Info("Creating Secret in destination namespace", "Namespace", destinationSecretNamespace)
				destinationSecret = &corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      secretSync.Spec.SecretName,
						Namespace: destinationSecretNamespace,
					},
					Data: sourceSecret.Data, // Copy data from source to destination
				}

				if err := r.Create(ctx, destinationSecret); err != nil {
					return ctrl.Result{}, err
				}
			} else {
				return ctrl.Result{}, err
			}
		} else {
			log.Info("Updating Secret in destination namespace", "Namespace", destinationSecretNamespace)
			destinationSecret.Data = sourceSecret.Data // Update data from source to destination
			if err := r.Update(ctx, destinationSecret); err != nil {
				log.Error(err, "Unable to update secretsync")
				return ctrl.Result{}, err
			}
		}
	}

	secretSync.Status.LastSyncTime = metav1.Now()
	if err := r.Status().Update(ctx, secretSync); err != nil {
		log.Error(err, "Unable to update secretsync status")
		return ctrl.Result{}, err
	}
	log.Info("Status secretsync updated", "LastSyncTime", secretSync.Status.LastSyncTime)

	return ctrl.Result{}, nil
}
		

9. Собрать образ контроллера

Можно образ положить в локальный докер (но потом нужно будет руками, скопировать в kubernetes) или в докер хаб (нужно быть залогиненным в hub.docker.com в командной строке через docker login, после билда сделать make docker-push IMG=secretsync:1.0.0).

В данном случае используем локальный докер (поэтому обязательно указать номер версии, а не latest).

			make docker-build IMG=secretsync:1.0.0
		

10. Проверить, что образ есть в локальном докере

			elena@elena-ABC:~/secretsync$ docker image ls
REPOSITORY                        TAG       IMAGE ID       CREATED              SIZE
secretsync                        1.0.0     e0073fbcd2cb   About a minute ago   54.7MB
		

11. Загрузить образ оператора в kind, чтобы его можно было использовать при установке оператора

			kind load docker-image secretsync:1.0.0
		

12. Развернуть оператор в kubernetes

			make deploy IMG=secretsync:1.0.0
		

Проверка работы оператора

1. Создать неймспейсы, в которые будем копировать секрет

			kubectl create namespace development

kubectl create namespace production
		

2. Создать секрет

			kubectl create secret generic example-secret \
    --from-literal=username=admin \
    --from-literal=password='S!B\*d$zDsb='
		

3. Проверить, что новый CRD создан

список crd доступен в любом неймспейсе, а для экземпляров при создании можем задать неймспейс.

			elena@elena-ABC:~/secretsync$ kubectl get crd
NAME                              CREATED AT
secretsyncs.apps.example.com      2024-01-24T08:57:08Z
		

4. Получить объекты с типом нового CRD

			elena@elena-ABC:~/secretsync$ kubectl get secretsync
NAME                AGE
secretsync-sample   3m
		

5. Проверить, что в созданном объекте есть все поля Spec

Поля статуса появятся, когда опертор их заполнит и обновит.

			elena@elena-ABC:~/secretsync$ kubectl describe secretsync secretsync-sample
Annotations:  <none>
API Version:  apps.example.com/v1
Kind:         SecretSync
Name:         secretsync-sample
Namespace:    default
Labels:       app.kubernetes.io/created-by=secretsync
              app.kubernetes.io/instance=secretsync-sample
              app.kubernetes.io/managed-by=kustomize
              app.kubernetes.io/name=secretsync
              app.kubernetes.io/part-of=secretsync
Metadata:
  Creation Timestamp:  2024-01-25T08:15:30Z
  Generation:          1
  Resource Version:    505086
  UID:                 62eeaafc-eed9-4f5e-ad06-4c847c0b0e09
Spec:
  Destination Namespaces:
    production
    development
  Secret Name:       example-secret
  Source Namespace:  default
Events:              <none>
		

6. Посмотреть под с контроллером оператора

			elena@elena-ABC:~/secretsync$ kubectl get pod -A
NAMESPACE              NAME                                               READY   STATUS    RESTARTS       AGE
secretsync-system      secretsync-controller-manager-655c4b8bf4-jknt4     2/2     Running   0              7m32s
		

7. Проверить логи оператора

			kubectl logs secretsync-controller-manager-655c4b8bf4-jknt4 -n=secretsync-system
		

8. Проверить, что секрет скопирован в неймспейсы

			elena@elena-ABC:~/secretsync$ kubectl get secret -A
NAMESPACE     NAME             TYPE     DATA   AGE
default       example-secret   Opaque   2      138m
development   example-secret   Opaque   2      3m53s
production    example-secret   Opaque   2      3m53s
		

9. Если в логах есть ошибки, то можно их исправить и проверить работу оператора без деплоя перед тем, как собирать образ

			make run
		

10. Откатить деплой оператора и CRD

			make undeploy
		

Отладка

1. Убедиться, что в папке ~/.kube есть файл config c конфигом kubernetes

2. В VS Code при начале отладки по F5 можно добавить launch.json конфиг

			{
    "version": "0.2.0",
    "configurations": [      
        {
            "name": "Launch Package",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "program": "${workspaceFolder}/cmd"
        }
    ]
}
		

3. Поставить брейкпойнт, например, в Reconcile методе и начать отладку по F5 (Run/Start Debugging)

Заключение

Эта статья появилась во многом по мотивам этого отличного материала, в котором мне немного не хватило подробностей и части про проверку работы оператора и отладку.

Если хочется посмотреть, каким может быть оператор Kubernetes, то вот пример оператора для Postgres.

Golang
Kubernetes
213