Сегодня будем писать простой оператор для kubernetes с помощью operator-sdk и ansible
Пропустим все шаги по установке, их можно найти на github .
Оператор будет выполнять 2 функции:
- Создавать namespace и применять к ним Limit Ranges и Resource Quota
- Создавать deployment Nginx в namespace
Первые шаги
Для начала нужно создать новый проект с помощью CLI:
1 2 |
operator-sdk new my-first-operator --api-version=prydybailo.io/v1alpha1 --kind=/v1alpha1 --kind=ResourcesAndLimits --type=ansible cd my-first-operator |
Эта команда создаст проект с оператором который подпишется на ресурс ResourcesAndLimits с APIVersion prydybailo.io/v1alpha1 и Kind ResourcesAndLimits
Структура директории выглядит следующим образом:
Директория/Файл | Цель |
build/ | Содержит скрипты с помощью которых operator-sdk будет собран и инициализирован |
deploy/ | Содержит набор Kubernetes манифестов которыми operator будет развернут в кластере |
roles/ | Содержит ansible roles |
watches.yaml | Содержит Group, Version, Kind, и метод запуска Ansible. |
Файл watches вмещает:
- group: Група в Custom Resource на которую подписывается наш оператор.
- version: Версия Custom Resource на которую подписывается наш оператор.
- kind: тип Custom Resource на которую подписывается наш оператор.
- role (default): Путь к нашем Ansible roles.
- playbook: Путь в Ansible playbook. В случае если будем использовать playbook вместо role
- vars: Описывается в виде ключ-значение. Будет передан как
extra_vars
- reconcilePeriod (optional): Интервал согласования как часто будет запускаться роль для данного CR
- manageStatus (optional): При значении true (по умолчанию) оператор будет управлять состоянием CR. Если установлено значение false, статус CR управляется в другом месте, с помощью указанной role/playbook отдельном контроллере.
Пример файла Watches:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
--- - version: v1alpha1 group: foo.example.com kind: Foo role: /opt/ansible/roles/Foo - version: v1alpha1 group: bar.example.com kind: Bar playbook: /opt/ansible/playbook.yml - version: v1alpha1 group: baz.example.com kind: Baz playbook: /opt/ansible/baz.yml reconcilePeriod: 0 manageStatus: false vars: foo: bar |
Подготовка и установка оператора в кластер Kubernetes
Так как наш оператор будет создавать namespaces то ему нужны права на кластер а не только на namespace.
В файле deploy/role:
- Меняем Kind: Role на Kind: ClusterRole
1 2 3 4 5 6 |
apiVersion: rbac.authorization.k8s.io/v1 #kind: Role kind: ClusterRole metadata: creationTimestamp: null name: my-first-operator |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: creationTimestamp: null name: my-first-operator rules: - apiGroups: - "" resources: - pods - services - services/finalizers - endpoints - persistentvolumeclaims - events - configmaps - secrets - namespaces - resourcequotas - limitranges |
RoleBinding
наClusterRoleBinding
Role
неClusterRole
в секцииroleRef
- Указываем namespace в который будет развернут оператор
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#kind: RoleBinding kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: my-first-operator subjects: - kind: ServiceAccount name: my-first-operator namespace: default roleRef: #kind: Role kind: ClusterRole name: my-first-operator apiGroup: rbac.authorization.k8s.io |
WATCH_NAMESPACE=""
1 2 3 4 5 6 |
env: - name: WATCH_NAMESPACE value: "" #valueFrom: # fieldRef: # fieldPath: metadata.namespace |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
--- # tasks file for resourcesandlimits - name: Create namespace k8s: definition: kind: Namespace apiVersion: v1 metadata: name: '{{ meta.name }}' ignore_errors: true - name: Create Resource Quota k8s: definition: kind: ResourceQuota apiVersion: v1 metadata: name: '{{ meta.name }}-resourcequota' namespace: '{{ meta.name }}' spec: hard: limits.cpu: "{{ limits_cpu }}" limits.memory: "{{ limits_memory }}" requests.cpu: "{{ requests_cpu }}" requests.memory: "{{ requests_memory }}" requests.storage: "{{ requests_storage }}" pods: "{{ limit_pods }}" services: "{{ limit_services }}" services.loadbalancers: 0 services.nodeports: 0 replicationcontrollers: 0 - name: Create Limit Ranges k8s: definition: kind: LimitRange apiVersion: v1 metadata: name: '{{ meta.name }}-limitrange' namespace: '{{ meta.name }}' spec: limits: - type: Pod maxLimitRequestRatio: cpu: "{{ max_limit_request_ratio_cpu }}" memory: "{{ max_limit_request_ratio_memmory }}" - type: PersistentVolumeClaim max: storage: "{{ max_storage }}" min: storage: "{{ min_storage }}" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
apiVersion: prydybailo.io/v1alpha1 kind: ResourcesAndLimits metadata: name: developers-team-a spec: limitsCpu: 5 limitsMemory: 5Gi requestsCpu: 5 requestsMemory: 5Gi requestsStorage: 204Gi limitPods: 10 limitServices: 10 maxLimitRequestRatioCpu: 2 maxLimitRequestRatioMemmory: 2 maxStorage: 100Gi minStorage: 20Gi |
Переменные из CR:
- В секции metadata:
- name, namespace — переносятся в ansible как «{{ meta.name }}» , «{{ meta.namespace }}»
- В секции spec:
- somevar — без заглавных букв так и переносятся в ansible как «{{ somevar }}»
- someVar — c заглавными буквами переносятся в ansible как «{{ some_var }}»
Теперь развернем наш CRD:
1 |
kubectl create -f deploy/crds/prydybailo.io_resourcesandlimits_crd.yaml |
Собираем оператор и пушим в registry:
1 2 |
operator-sdk build example/my-first-operator:0.0.1 docker push example/my-first-operator:0.0.1 |
Заменяем сгенерированные поля docker image и imagePullPolicy в deploy/operator.yaml
1 2 |
sed -i 's|{{ REPLACE_IMAGE }}|example/my-first-operator:0.0.1|g' deploy/operator.yaml sed -i 's|{{ pull_policy\|default('\''Always'\'') }}|Always|g' deploy/operator.yaml |
Для MacOS:
1 2 |
sed -i "" 's|{{ REPLACE_IMAGE }}|example/my-first-operator:0.0.1|g' deploy/operator.yaml sed -i "" 's|{{ pull_policy\|default('\''Always'\'') }}|Always|g' deploy/operator.yaml |
Разворачиавем:
1 2 3 4 |
kubectl create -f deploy/service_account.yaml kubectl create -f deploy/role.yaml kubectl create -f deploy/role_binding.yaml kubectl create -f deploy/operator.yaml |
Работа с CR
После удачного запуска оператора развернем наш CR для создания namespace:
1 |
kubectl apply -f deploy/crds/prydybailo.io_v1alpha1_resourcesandlimits_cr.yaml |
Глянем на наш CR:
1 |
kubectl describe resourcesandlimits.prydybailo.io developers-team-a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
Name: developers-team-a Namespace: default Labels: <none> Annotations: kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"prydybailo.io/v1alpha1","kind":"ResourcesAndLimits","metadata":{"annotations":{},"name":"developers-team-a","namespace":"de... API Version: prydybailo.io/v1alpha1 Kind: ResourcesAndLimits Metadata: Creation Timestamp: 2019-12-17T11:53:34Z Generation: 1 Resource Version: 20309 Self Link: /apis/prydybailo.io/v1alpha1/namespaces/default/resourcesandlimits/developers-team-a UID: 0db952fb-2bed-4511-9309-9f9fbe11af66 Spec: Limit Pods: 10 Limit Services: 10 Limits Cpu: 5 Limits Memory: 5Gi Max Limit Request Ratio Cpu: 2 Max Limit Request Ratio Memmory: 2 Max Storage: 100Gi Min Storage: 20Gi Requests Cpu: 5 Requests Memory: 5Gi Requests Storage: 204Gi Status: Conditions: Ansible Result: Changed: 2 Completion: 2019-12-17T11:55:50.36218 Failures: 0 Ok: 4 Skipped: 0 Last Transition Time: 2019-12-17T11:53:34Z Message: Awaiting next reconciliation Reason: Successful Status: True Type: Running Events: <none> |
Наш оператор должен был создать namespace developers-team-a:
1 |
kubectl get namespaces |
Видим что наш namespace появился:
1 2 3 4 5 6 |
NAME STATUS AGE default Active 3h16m developers-team-a Active 5m46s kube-node-lease Active 3h16m kube-public Active 3h16m kube-system Active 3h16m |
Проверяем настройки namespace:
1 |
kubectl describe namespaces developers-team-a |
Видим что наши настройки успешно применились:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
Name: developers-team-a Labels: <none> Annotations: cattle.io/status: {"Conditions":[{"Type":"ResourceQuotaInit","Status":"True","Message":"","LastUpdateTime":"2019-12-17T11:53:43Z"},{"Type":"InitialRolesPopu... lifecycle.cattle.io/create.namespace-auth: true operator-sdk/primary-resource: default/developers-team-a operator-sdk/primary-resource-type: ResourcesAndLimits.prydybailo.io Status: Active Resource Quotas Name: developers-team-a-resourcequota Resource Used Hard -------- --- --- limits.cpu 0 5 limits.memory 0 5Gi pods 0 10 replicationcontrollers 0 0 requests.cpu 0 5 requests.memory 0 5Gi requests.storage 0 204Gi services 0 10 services.loadbalancers 0 0 services.nodeports 0 0 Resource Limits Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio ---- -------- --- --- --------------- ------------- ----------------------- Pod cpu - - - - 2 Pod memory - - - - 2 PersistentVolumeClaim storage 20Gi 100Gi - - - |
Подготовка еще одного CR
Подготовим еще один контроллер для добавления deployment Nginx в какой либо namespace.
Создаем deploy/crds/nginx_cr.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: nginx.prydybailo.io spec: group: prydybailo.io names: kind: Nginx listKind: NginxList plural: nginx singular: nginx scope: Namespaced subresources: status: {} validation: openAPIV3Schema: type: object x-kubernetes-preserve-unknown-fields: true versions: - name: v1alpha1 served: true storage: true |
и deploy/crds/nginx-1.17.6.yaml:
1 2 3 4 5 6 7 8 |
apiVersion: prydybailo.io/v1alpha1 kind: Nginx metadata: name: nginx-1-17-6 namespace: nginx-namespace spec: size: 5 version: 1.17.6 |
Добавляем в файл watches.yml строки:
1 2 3 4 |
- version: v1alpha1 group: prydybailo.io kind: Nginx role: /opt/ansible/roles/nginx |
Копируем директорию resourcesandlimits и переименовываем ее в nginx
1 |
cp -R roles/resourcesandlimits roles/nginx |
Заменяем все содержимое файла roles/nginx/tasks/main.yml на:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
--- - name: start nginx k8s: definition: kind: Deployment apiVersion: apps/v1 metadata: name: '{{ meta.name }}-nginx' namespace: '{{ meta.namespace }}' spec: replicas: "{{size}}" selector: matchLabels: app: nginx version: "nginx-{{version}}" template: metadata: labels: app: nginx version: "nginx-{{version}}" spec: containers: - name: nginx image: "nginx:{{version}}" ports: - containerPort: 80 |
Пересобираем оператор c новой версией docker образа:
1 2 |
operator-sdk build example/my-first-operator:0.0.2 docker push example/my-first-operator:0.0.2 |
Меняем docker image в deploy/operator.yaml:
1 |
sed -i 's|example/my-first-operator:0.0.1|example/my-first-operator:0.0.2|g' deploy/operator.yaml |
Для MacOS:
1 |
sed -i "" 's|example/my-first-operator:0.0.1|example/my-first-operator:0.0.2|g' deploy/operator.yaml |
Добавляем CR:
1 |
kubectl apply -f deploy/crds/nginx_cr.yaml |
Обновляем оператор:
1 |
kubectl apply -f deploy/operator.yaml |
Создаем namespace и развернем наш СR:
1 2 |
kubectl create namespace nginx-namespace kubectl apply -f deploy/crds/nginx-1.17.6.yaml |
Проверяем:
1 |
kubectl -n nginx-namespace get pod |
1 2 3 4 5 6 |
NAME READY STATUS RESTARTS AGE kc-nginx-1-17-6-nginx-859d7dcf99-ddbzm 1/1 Running 0 24s kc-nginx-1-17-6-nginx-859d7dcf99-j884q 1/1 Running 0 24s kc-nginx-1-17-6-nginx-859d7dcf99-mvj5s 1/1 Running 0 24s kc-nginx-1-17-6-nginx-859d7dcf99-wpcfj 1/1 Running 0 24s kc-nginx-1-17-6-nginx-859d7dcf99-x7szg 1/1 Running 0 53s |
Вот и все, с помощью operator-sdk легко можно автоматизировать рутинные вещи