1. Начало работы и подготовка среды
1.1 Введение
-
Operator
Это способ упаковки, запуска и управления приложениями k8s. это покрываетCRD(CustomResourceDeftination) + AdmissionWebhook + Controller, и разверните его на K8S в форме Deployment.- CRDИспользуется для определения декларативного API (yaml), программа всегда будет заставлять наименьшую единицу планирования (POD) стремиться к этому состоянию через это определение;
- AdmissionWebhookДекларативные поля декларации (yaml) и валидации (валидации), используемые для перехвата запроса на отправку мутации (модификации);
-
ControllerГлавный контроллер отслеживает события создания/обновления/удаления ресурса и запускает
Reconcile
функционировать как ответ. Весь процесс настройки называетсяReconcile Loop
(согласованный цикл), по сути, заключается в стремлении POD к состоянию, требуемому определением CRD;
-
Operator
блок-схема -
Kubebuilder
Это основа для разработки оператора, который может генерировать CRD, веб-перехватчик, код и конфигурацию контроллера, а также предоставляет k8s.go-client
.
Основные понятия (цитата):Наггетс.Талант/пост/696287…
1.2 Подготовка окружающей среды
- go version v1.16+.
- docker version 18.03+.
- kubectl version v1.20.3+.
1.2.0 Необходимые знания
- Требуется хорошее знание основ k8s
1.2.1 среда k8s (сервер)
- Подготовьте доступную среду k8s, будь то отдельная машина или кластер.
推荐1.20以上
- Можно использовать на сервере или локально, рекомендуется использовать на сервере
1.2.2 куббилдер (локальный)
- Загрузите kubebuilder соответствующей платформы, если вы используете разработку для Mac, скачайте Mac;
- После завершения загрузки поместите бинарник
kubebuilder
Добавьте к переменным среды, например:# mv kubebuilder_darwin_amd64 /usr/local/bin/kubebuilder # chmod a+x /usr/local/bin/kubebuilder # kubebuilder version // Version: main.version{KubeBuilderVersion:"3.2.0", KubernetesVendor:"1.22.1", GitCommit:"b7a730c84495122a14a0faff95e9e9615fffbfc5", BuildDate:"2021-10-29T18:32:16Z", GoOs:"darwin", GoArch:"amd64"}
1.2.3 кубектл (локальный)
- Для управления удалённым сервером через kubectl обязательно добавьте его в переменную окружения и используйте kubeconfig по умолчанию, в противном случае позже нужно будет модифицировать сгенерированный kubebuilder
Makefile
.
1.2.4 Докер (локальный или серверный)
-
Docker не разбирает, что это такое, в основном используется для упаковки образов при публикации проектов, а также генерируется kubebuilder.
Makefile
используется в;ps: 个人考虑到本地 mac 没有位置安装Docker,所以在服务器安装,但需要注意的是linux和mac使用的controller-gen及kustomize 是不一样;需要在linux使用kubebuilder重新生成一个新的项目用以覆盖现有项目的bin目录。
2. Проект
Справочный адрес:GitHub.com/shadow-от рабов…
2.1 Создание проекта
2.1.1 Создание каталога
- Китайские иероглифы, пробелы, специальные символы, подчеркивания не допускаются, допускаются только дефисы "-"
# mkdir -p /usr/local/k8s-operator # cd /usr/local/k8s-operator
2.1.2 Создать проект
-
инициировать проект, создать домен (домен).
# kubebuilder init --domain shadow.com
-
Создайте API, создайте группу, версию и вид.
# kubebuilder create api --group myapp --version v1 --kind Redis
- После завершения вышеуказанного создания определение следующего CRD выглядит следующим образом:
test.yaml
apiVersion: myapp.shadow.com/v1 kind: Redis ...
- После завершения вышеуказанного создания определение следующего CRD выглядит следующим образом:
-
Структура проекта после создания
2.1.3 Создать CRD
-
При желании установить префикс ресурса (
可选
), config/default/customization.yaml... # 可以修改前缀 namePrefix: shadow-operator- ...
-
В k8s-operator/api/v1/redis_types.go поле атрибута в RedisSepc, где я создал Name, Port, Replicas;
-
install crd
# make install
-
Просмотр crd, доступный через
kubectl describe crd redis.myapp.shadow.com
Проверьте, является ли определение crd только что установленным полем;# kubectl get crd NAME CREATED AT ... redis.myapp.shadow.com 2021-11-16T09:35:42Z
-
Если CRD изменен, его необходимо переустановить.
# make uninstall # make install
2.1.4 Контроллер запуска
-
Наша основная логика
Reconcile
Для реализации в ядре функция будет многократно запускаться событиями, k8s-operator/controllers/redis_controller.go... func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) // TODO(user): your logic here redis := &myappv1.Redis{} if err := r.Get(ctx, req.NamespacedName, redis); err != nil { fmt.Println(err) } else { fmt.Println("得到对象", redis.Spec) } } ...
-
Запустите контроллер и обратите внимание на вывод консоли
# make run
2.1.5 Тестирование Yaml
-
Отредактируйте файл yaml, k8s-operator/config/samples/myapp_v1_redis.yaml
apiVersion: myapp.shadow.com/v1 kind: Redis metadata: name: shadow namespace: default spec: name: shadow port: 2378 replicas: 3
-
apply
# kubectl apply -f config/samples/myapp_v1_redis.yaml
-
Просмотр консольного вывода контроллера
... 得到对象 {Name:shadow Port:2378 Replicas:3} ...
2.2 Простая проверка полей CRD
2.2.1 Демонстрация
-
k8s-operator/api/v1/redis_type.go с добавлением специальных аннотаций для ограничения значения порта. Дополнительные аннотации см. в документе выше.
+kubebuilder:validation:Minimum:=2000
+kubebuilder:validation:Maximum:=2380
type RedisSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // Foo is an example field of Redis. Edit redis_types.go to remove/update //Foo string `json:"foo,omitempty"` Name string `json:"name,omitempty"` // validation: https://book.kubebuilder.io/reference/markers/crd-validation.html //+kubebuilder:validation:Minimum:=2000 //+kubebuilder:validation:Maximum:=2380 Port int `json:"port,omitempty"` Replicas int `json:"replicas,omitempty"` }
-
Переустановите CRD
# make install # make uninstall
-
Проверьте эффект
- config/samples/myapp_v1_redis.yaml
apiVersion: myapp.shadow.com/v1 kind: Redis metadata: name: shadow namespace: default spec: port: 2390 replicas: 3 name: shadow
- После выполнения вы можете увидеть соответствующую ошибку предела
# kubectl apply -f config/samples/myapp_v1_redis.yaml The Redis "shadow" is invalid: spec.port: Invalid value: 2390: spec.port in body should be less than or equal to 2380
- config/samples/myapp_v1_redis.yaml
2.3 Создание вебхука (модификация и проверка)
2.3.1 Необходимые знания
- Что такое вебхук? Вебхук — это отдельный ресурс, который можно разрабатывать самостоятельно.
- мы обычно проходим
MutatingWebhook
иValidatingWebhook
Для перехвата запросов API эти два вебхука также включены по умолчанию в k8s (если они не включены, включите их вручную). Содержание запроса здесьYaml
содержание документа.
2.3.2 Создание вебхука
- Выполните команду создания
# kubebuilder create webhook --group myapp --version v1 --kind Redis --defaulting --programmatic-validation
- После завершения создания сгенерируйте api/v1/redis_webhook.go.
... func (r *Redis) Default() { redislog.Info("default", "name", r.Name) // TODO(user): fill in your defaulting logic. } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. //+kubebuilder:webhook:path=/validate-myapp-shadow-com-v1-redis,mutating=false,failurePolicy=fail,sideEffects=None,groups=myapp.shadow.com,resources=redis,verbs=create;update,versions=v1,name=vredis.kb.io,admissionReviewVersions=v1 var _ webhook.Validator = &Redis{} // ValidateCreate implements webhook.Validator so a webhook will be registered for the type func (r *Redis) ValidateCreate() error { redislog.Info("validate create", "name", r.Name) // 增加:如果是资源名字为 shadow 则不允许创建 if r.Name == "shadow" { return fmt.Errorf("error name.") } // TODO(user): fill in your validation logic upon object creation. return nil } ...
2.3.3 Развертывание в среде k8s
-
Поскольку веб-перехватчик развертывается в сети, мы обычно выполняем проверку сертификата через ЦС, поэтому вот веб-перехватчик, который можно использовать в Интернете.
-
Создайте менеджер сертификатов k8s, который является плагином k8s.
-
Скачайте и примените cert-manager.yaml
# wget https://github.com/jetstack/cert-manager/releases/download/v1.6.1/cert-manager.yaml # kubectl apply -f cert-manager.yaml
-
Проверьте, завершено ли создание
# kubectl get pods -A NAMESPACE NAME READY STATUS RESTARTS AGE cert-manager cert-manager-55658cdf68-9559b 1/1 Running 0 7d18h cert-manager cert-manager-cainjector-967788869-hl472 1/1 Running 0 7d18h cert-manager cert-manager-webhook-7b86bc6578-spdct ...
-
-
Откройте конфигурацию config/default/kustomization.yaml.
... ... bases: - ../crd - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus patchesStrategicMerge: # Protect the /metrics endpoint by putting it behind auth. # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. - manager_auth_proxy_patch.yaml # Mount the controller config file for loading manager configurations # through a ComponentConfig type #- manager_config_patch.yaml # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # 'CERTMANAGER' needs to be enabled to use ca injection - webhookcainjection_patch.yaml # the following config is for teaching kustomize how to do var substitution vars: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR objref: kind: Certificate group: cert-manager.io version: v1 name: serving-cert # this name should match the one in certificate.yaml fieldref: fieldpath: metadata.namespace - name: CERTIFICATE_NAME objref: kind: Certificate group: cert-manager.io version: v1 name: serving-cert # this name should match the one in certificate.yaml - name: SERVICE_NAMESPACE # namespace of the service objref: kind: Service version: v1 name: webhook-service fieldref: fieldpath: metadata.namespace - name: SERVICE_NAME objref: kind: Service version: v1 name: webhook-service
-
развернуть (
该步骤作者是在服务器上执行的,因为本地没有安装docker
), и вам нужно предварительно выбрать зеркальный репозиторий, гавань или реестр докеров.# make install # make docker-build IMG=192.168.6.102:5000/shadow-redis:v1 # make docker-push IMG=192.168.6.102:5000/shadow-redis:v1 # make deploy IMG=192.168.6.102:5000/shadow-redis:v1
- Просмотр развернутого веб-перехватчика
# kubectl get mutatingwebhookconfigurations NAME WEBHOOKS AGE shadow-operator-mutating-webhook-configuration 1 82m ... # kubectl get validatingwebhookconfigurations NAME WEBHOOKS AGE shadow-operator-validating-webhook-configuration 1 47h ...
- Просмотр развернутого веб-перехватчика
2.3.4 Запуск теста
-
Поскольку для проверки TLS развернут cert-manager, вебхук не запускается локально, поэтому отключите вебхук и напрямую используйте для проверки контроллер среды k8s;
- main.go, найти
SetupWebhookWithManager
закомментировать после
//if err = (&myappv1.Redis{}).SetupWebhookWithManager(mgr); err != nil { // setupLog.Error(err, "unable to create webhook", "webhook", "Redis") // os.Exit(1) //}
- main.go, найти
-
запускать
# make run
-
проверять
# kubectl apply -f config/samples/myapp_redis_v1.yaml Error from server (error name.): error when creating "config/samples/myapp_v1_redis.yaml": admission webhook "vredis.kb.io" denied the request: error name.
2.4 Управление ресурсами POD
- Каждый шаг требует перезагрузки контроллера
# make run
- Создайте вспомогательный каталог
# mkdir -p k8s-operator/helper
2.4.1 Добавить/удалить/изменить
-
Основные реализованные функции
- создавать ресурсы;
- удалить ресурсы;
- масштабирование реплики;
- POD автоматически перестраивается после удаления;
- записывать события;
-
helper/redis_helper.go
package helper import ( "context" "fmt" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" v1 "shadow.com/v1/api/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) // 生成 POD name func GetRedisPodNames(redisConfig *v1.Redis) []string { podNames := make([]string, redisConfig.Spec.Replicas) fmt.Printf("%+v", redisConfig) for i := 0; i < redisConfig.Spec.Replicas; i++ { podNames[i] = fmt.Sprintf("%s-%d", redisConfig.Name, i) } fmt.Println("PodNames: ", podNames) return podNames } // 判断 redis pod 是否能获取 func IsExistPod(podName string, redis *v1.Redis, client client.Client) bool { err := client.Get(context.Background(), types.NamespacedName{ Namespace: redis.Namespace, Name: podName, }, &corev1.Pod{}, ) if err != nil { return false } return true } // 是否存在于finalizers,finalizers 是人为删除动作添加的, // 只要finalizers有值则删除无法顺利进行,直到finalizers为空; func IsExistInFinalizers(podName string, redis *v1.Redis) bool { for _, fPodName := range redis.Finalizers { if podName == fPodName { return true } } return false } func CreateRedis(client client.Client, redisConfig *v1.Redis, podName string, schema *runtime.Scheme) (string, error) { if IsExistPod(podName, redisConfig, client) { return "", nil } // 建立 POD 对象 newPod := &corev1.Pod{} newPod.Name = podName newPod.Namespace = redisConfig.Namespace newPod.Spec.Containers = []corev1.Container{ { Name: podName, Image: "redis:5-alpine", ImagePullPolicy: corev1.PullIfNotPresent, Ports: []corev1.ContainerPort{ { ContainerPort: int32(redisConfig.Spec.Port), }, }, }, } // set owner reference,使用ControllerManager为我们管理 POD // 这个就和ReplicateSet是一个道理 err := controllerutil.SetControllerReference(redisConfig, newPod, schema) if err != nil { return "", err } // 创建 POD err = client.Create(context.Background(), newPod) return podName, err }
-
controllers/redis_controller.go
/* Copyright 2021 shadow. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controllers import ( "context" "fmt" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" "shadow.com/v1/helper" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" myappv1 "shadow.com/v1/api/v1" ) // RedisReconciler reconciles a Redis object type RedisReconciler struct { client.Client Scheme *runtime.Scheme EventRecord record.EventRecorder } //+kubebuilder:rbac:groups=myapp.shadow.com,resources=redis,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=myapp.shadow.com,resources=redis/status,verbs=get;update;patch //+kubebuilder:rbac:groups=myapp.shadow.com,resources=redis/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // TODO(user): Modify the Reconcile function to compare the state specified by // the Redis object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) // TODO(user): your logic here redis := &myappv1.Redis{} if err := r.Get(ctx, req.NamespacedName, redis); err != nil { fmt.Println(err) } else { // 如果不为空 则正在删除 if !redis.DeletionTimestamp.IsZero() { return ctrl.Result{}, r.clearRedis(ctx, redis) } fmt.Printf("得到对象 %+v \n", redis.Spec) podNames := helper.GetRedisPodNames(redis) isEdit := false for _, podName := range podNames { podName, err := helper.CreateRedis(r.Client, redis, podName, r.Scheme) if err != nil { return ctrl.Result{}, err } if podName == "" { continue } // 如果存在于 finalizers 证明已经创建了,跳过即可 if controllerutil.ContainsFinalizer(redis, podName) { continue } redis.Finalizers = append(redis.Finalizers, podName) isEdit = true } // 副本收缩 if len(redis.Finalizers) > len(podNames) { r.EventRecord.Event(redis, corev1.EventTypeNormal, "Upgrade", "副本收缩") isEdit = true err := r.rmIfSurplus(ctx, podNames, redis) if err != nil { return ctrl.Result{}, err } } if isEdit { r.EventRecord.Event(redis, corev1.EventTypeNormal, "Updated", "更新 shadow-redis") err = r.Client.Update(ctx, redis) if err != nil { return ctrl.Result{}, err } // 下面说到增加RedisNum 来查看当前存在的POD数量,则需要打开下面的代码 // redis.Status.RedisNum = len(redis.Finalizers) err = r.Status().Update(ctx, redis) return ctrl.Result{}, err } return ctrl.Result{}, nil } return ctrl.Result{}, nil } // 收缩副本 ['redis0','redis1'] ---> podName ['redis0'] func (r *RedisReconciler) rmIfSurplus(ctx context.Context, podNames []string, redis *myappv1.Redis) error { for i := 0; i < len(redis.Finalizers)-len(podNames); i++ { err := r.Client.Delete(ctx, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: redis.Finalizers[len(podNames)+i], Namespace: redis.Namespace, }, }) if err != nil { return err } } redis.Finalizers = podNames return nil } func (r *RedisReconciler) clearRedis(ctx context.Context, redis *myappv1.Redis) error { podList := redis.Finalizers for _, podName := range podList { err := r.Client.Delete(ctx, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: podName, Namespace: redis.Namespace, }, }) if err != nil { fmt.Println("清除Pod异常:", err) } } redis.Finalizers = []string{} return r.Client.Update(ctx, redis) } func (r *RedisReconciler) podDeleteHandler(event event.DeleteEvent, limitInterface workqueue.RateLimitingInterface) { fmt.Println("被删除的对象名称是", event.Object.GetName()) for _, ref := range event.Object.GetOwnerReferences() { // 因为会获取到所有被删除的pod,所以进行一次判断 if ref.Kind == "Redis" && ref.APIVersion == "myapp.shadow.com/v1" { // 重新推送队列,进行 reconcile limitInterface.Add(reconcile.Request{ NamespacedName: types.NamespacedName{ Name: ref.Name, Namespace: event.Object.GetNamespace(), }, }) } } } // SetupWithManager sets up the controller with the Manager. func (r *RedisReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&myappv1.Redis{}). // 监控资源,并对delete动作进行操作 Watches(&source.Kind{Type: &corev1.Pod{}}, handler.Funcs{DeleteFunc: r.podDeleteHandler}). Complete(r) }
-
После завершения модификации протестируйте ее, если вы столкнулись с перехватом webhook, вы можете пройти тест.
kubectl delete
Просто удалите (измените, подтвердите);-
myapp_redis_v1.yaml
apiVersion: myapp.shadow.com/v1 kind: Redis metadata: name: shadow namespace: default spec: port: 2379 replicas: 3 name: shadow
-
Создайте
# kubectl apply -f config/samples/myapp_redis_v1.yaml # kubectl get Redis # kubectl get pods
-
Уменьшите копию, измените myapp_redis_v1.yaml
apiVersion: myapp.shadow.com/v1 kind: Redis metadata: name: shadow namespace: default spec: port: 2379 replicas: 2 name: shadow
# kubectl apply -f config/samples/myapp_redis_v1.yaml
-
Удалить
# kubectl delete Redis shadow
-
Просмотр зарегистрированных событий
# kubectl describe Redis shadow
-
2.4.2 Проверка
-
kubectl get Redis
По умолчанию отображаются только два поля Имя и Возраст.Здесь поле RedisNum расширено для подсчета количества созданных POD.-
api/v1/redis_type.go, добавить статус
type RedisStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file RedisNum int `json:"redis_num"` } //下面两个注释对应的是 kubectl get Redis 看到的状态 //+kubebuilder:printcolumn:JSONPath=".status.redis_num",name=REDIS_NUM,type=integer //+kubebuilder:printcolumn:JSONPath=".metadata.creationTimestamp",name=AGE,type=date
-
controllers/redis_controller.go, откройте комментарии ниже
// redis.Status.RedisNum = len(redis.Finalizers)
-
-
Переустановите CRD и перезапустите контроллер.
# make install # make run
-
Проверять
# kubectl get Redis NAME REDIS_NUM AGE shadow 3 21m
3. Напишите в конце
- Позже, когда фактический боевой проект будет завершен, фактическая боевая глава и исходный код Opeartor будут обновлены.
- 👈 (левый) клик
赞
Пойдем.