Содержание
K8s
Основные понятия
https://kube.academy/courses/kubernetes-core-concepts-part-1/lessons/kubernetes-fundamentals
Принципы Cloud Native:
- Контейнеризация (докер)
- Динамическое управление – Кубернетис
- Ориентация на микросервисы – Кубернетис
В Кубернетисе мы задаём желаемое состояние (desired state), т. е., система сама подстраивается и предпринимает конкретные действия, чтобы этого достичь.
Pod
Pods – один или несколько контейнеров. Все контейнеры в одном поде:
- запускаются на одном хосте
- взаимодействуют друг с другом через localhost
- имеют одинаковый доступ к томам
Иными словами, pod – это аналог сервера или виртуальной машины. Это помогает определиться в каждом случае, запускать ли 2 контейнера в 1 поде или в разных, планирование такое же, как действовать в случае с ВМ.
Sidecars – сопутствующие контейнеры в одном поде с основным. К примеру, есть контейнер веб-сервера, и туда при запуске копируется контент. Чтобы поправить какую-то ошибку в контенте, нужно перезапускать контейнер. Вместо этого в под помещается другой контейнер, который копирует контент со стороннего ресурса (например, с git), и кладёт на общий том (единый в рамках одного пода), а веб-сервер просто отображает содержимое.
apiVersion: v1 kind: Pod (с чем работаем) metadata: name: mypod1 (уникальный идентификатор) labels: app: blog spec: (всё в разделе spec характерно для выбранного типа, в данном случае пода) containters: - name: nginx Image: nginx:1.13.1 - name: www-syncer Image: syncer:1.2
Как запустить эту конструкцию?
# Предпочтительный путь (declarative): kubectl apply –f [file | dir | url] # imperative: kubectl create # создать новый ресурс из файла kubectl replace # обновить существующий ресурс из файла kubectl edit # обновить существующий ресурс, используя редактор kubectl patch # обновить существующий ресурс слиянием code snippet
Binding
Чтобы жёстко привязать под к ноде, используется spec.nodeName
. Если под уже существует, то можно изменить имя ноды в свойствах пода или создать объект kind: Binding
.
apiVersion: v1 kind: Binding metadata: name: nginx target: apiVersion: v1 kind: Node name: node02
, а потом послать запрос POST в API в формате json, что имитирует работу планировщика.
# пример $ curl --header "Content-Type:application/json" --request POST --data '{"apiVersion":"v1", "kind":"Binding" ...}' http://$SERVER/api/v1/namespaces/default/pods/$PODNAME/binding/
Service
Для сетевого доступа и балансировки существует тип объекта Service. Для определения целевых подов используются ярлыки (labels).
apiVersion: v1 kind: Service metadata: name: my-blog labels: app: blog spec: selector: app: blog ports: - protocol: TCP port: 80 (порт сервиса) targetPort: 80 (порт подов сервиса)
Вместо имен или IP-адресов подов используется селектор, работающий как запрос для поиска подходящих подов для доступа.
Deployment
А что, если нужно, к примеру, 20 одинаковых подов, чтобы справиться с нагрузкой? 20 одинаковых yml с одной различающейся строкой «name:»! Т. к. это явный идиотизм, существует более высокоуровневый объект Deployment, который занимается оркестровкой подов, создавая и убивая их по необходимости.
apiVersion: apps/v1 kind: Deployment metadata: name:my-blog spec: replicas: 3 (копий подов) selector: matchLabels: app: blog template: metadata: labels: app: blog spec: containers: - name: nginx Image: nginx:1.7.9 Ports: - containerPort: 80
В данном случае раздел template является фактически описанием пода, за исключением имени, т. к. подов может быть много и имена будут присваиваться автоматически.
Ярлыки (labels) выполняют как организационную, так и функциональную работу (как селекторы сервисов), они индексированы и по ним можно искать. Лучше не использовать составных ярлыков типа app: blog-frontend, потому что нет частичного или regexp-сравнения по ярлыкам. Гораздо лучше app: blog / tier: frontend. Также очень желательно иметь некий стандарт написания ярлыков в рамках кластера.
Документация:
https://docs.kubernetes.io
https://kubernetes.io/docs/reference
https://kubectl.docs.kubernetes.io
kubectl explain pods # встроенная справка по разным разделам, в данном случае о подах kubectl api-resources # список доступных типов ресурсов kubectl apply -f service.yaml # запустить сервис или деплой kubectl get service -l "app=blog" # проверить, что сервис создан kubectl get deployment -l "app=blog" # проверить, что деплой создан
Решение проблем
Ноды делятся на 2 типа – worker nodes и control plane. На рабочих ставится контейнерный движок (например, Докер), kubelet, взаимодействующий с докером, и kubeproxy. Control plane состоит из API, с которым взаимодействует админ. API работает с etcd – хранилищем конфигураций. Scheduler – определяет, где поды запускаются. Controller manager – управляет объектами (подами, сервисами и т. д.).
Kubectl get events [-w] # события кластера. По умолчанию хранит данные за час. [-w] – показ в реальном времени. Полезно при проблемах с cubectl apply. Kubectl describe pod gowebapp-3498323ff-348hf # описание объекта вместе с его событиями Kubectl get pods [–o wide] # список объектов, wide – более подробно. Kubectl logs gowebapp-3498323ff-348hf # смотреть логи. Добавить –c <container_name>, если под содержит больше 1 контейнера. Kubectl exec gowebapp-3498323ff-348hf – ls –l /opt/gowebapp # запустить команду. Добавить –c <container_name>, если под содержит больше 1 контейнера.
Управление развертыванием
Deployment создаёт промежуточную сущность ReplicaSets, которая уже создаёт поды. Соответственно, есть две стандартные стратегии развёртывания:
RollingUpdate - стратегия по умолчанию,когда старые поды постепенно замещаются новыми. К примеру, в deployment.yaml была заменена версия image. После применения обновлённого файла создаётся ещё одна ReplicaSet, там создаётся один под из нового образа (в этот момент общее число подов становится на один больше и трафик между ними балансируется, т. к. у них общие ярлыки). Если под создался успешно, гасится один под из старой реплики, и так далее, пока все поды не заменятся на новые в новой реплике. Старая пустая реплика удалится через некоторое время.
Recreate - сначала гасятся все поды, удаляется старая реплика, а затем создаётся новая и там разворачиваются поды. Это нужно для баз MySQL, т. к. недопустима балансировка между старыми и новыми версиями БД.
Также, имеются дополнительные стратегии, они не встроенные, но их можно выстраивать самостоятельно:
Canary - создаётся ещё один файл deployment, где в labels: указан дополнительный track: canary. В основном деплое указывается track: stable. Для canary указывается, например, одна реплика. После применения получается так, что трафик балансируется между всеми подами, т. е., часть трафика попадает на новую версию. Если всё нормально и нужно полностью перейти на новую версию, новый образ прописывается в stable deployment, а в canary кол-во реплик ставится 0.
Blue/Green - популярно в облачной среде, где трафик по щелчку переключается со старой версии на новую. Так же, как и в Canary, создаётся второй деплой - в blue в labels: указан дополнительный color: blue, а в деплое green, соответственно, color: green. В то же самое время, нужно иметь два сервиса - blue и green, где в selector: указаны соответствующие ярлыки, чтобы иметь возможность тестирования деплоя green. Получается, что одновременно работает и прод, и тест, каждый со своим трафиком.
Когда тесты закончились и нужно ввести тестовый вариант (green) в прод, редактируется сервис, где меняется ярлык c color: blue на color: green. Если никто не жалуется и всё нормально, в деплое blue кол-во реплик меняется на 0.
В следующий раз тестовой средой будет уже blue, т. к. green перешёл в прод.
# Развёртывание можно приостанавливать и снова продолжать kubectl rollout [pause | resume] deployment <name> # Откат изменений (declarative, рекомендуется) - ред. деплой и применение kubectl apply -f deployment.yaml # Откат изменений (imperative) kubectl rollout undo deployment <name>
Сам объект Deployment имеет доп. опции, вот некоторые из них:
- progressDeadlineSeconds - макс. время развёртывания в секундах
- minReadySeconds - мин. время готовности пода в секундах без падения какого-либо из входящих в него контейнеров (станд. значение - 0)
- revisionHistoryLimit - кол-во хранимых реплик для возможности отката изменений (10)
- paused - приостановка развёртывания. Если true, то любые изменения в разделе spec не приведёт к развёртыванию.
Лабораторная (https://kube.academy/courses/kubernetes-core-concepts-part-2/lessons/lab-deployment-management).
- Сделали образ docker новой версии, внеся изменения в код, собрали, поставили тэг и залили в каталог (push)
- Для пробы отредактировали основной деплой на новый образ и через 1 сек. поставили развёртывание на паузу - получилось 3 пода: 1 новый и 2 старых. Describe deployment показал, что есть 2 реплики.
kubectl rollout resume deployment <name>
- продолжили, потом проверили - все поды принадлежат новой реплике.- Теперь канареечный деплой. Создаётся соответствующий файл деплоя, где во все labels и matchLabels добавляется
track: canary
. Образ, конечно, новый, реплика одна. - Канареечный деплой применяется. Теперь стало три реплики приложения, одна из которых канареечная, и трафик ходит между всеми, что можно видеть на странице приложения, обновляя её.
- Теперь, если создать доп. сервис для канареечного деплоя, где в раздел
selector:
добавить ярлыкtrack: canary
и запустить этот сервис, то трафик пойдёт только на канареечный сервис, т. к. доступ там тот же, что и у прода. - Чтобы полностью перейти на новую версию в проде, надо обновить версию образа в прод. деплое, а в канареечном поставить
replicas: 0
. Потом применить все .yaml в папке путёмkubectl apply -f ~/folder
- Удалить канареечные сущности:
kubectl delete service canary
,kubectl delete deployment canary
.
Конфигурация подов и контейнеров
Проверки
Есть 2 встроенных типа проверок состояния контейнера:
- Liveness - запущен ли контейнер. При сбое он будет перезапущен согласно политике.
- Readiness - готов ли контейнер принимать запросы. При сбое под удаляется из сервиса, так что запросы на него приходить не будут.
Способы проверки (probe handlers):
- Exec - запуск команды, даёт код возврата, 0 - успешно.
- TCPSocket - TCP-проверка. Порт открыт и принимает соединения - успешно.
- HTTPGet - HTTP GET, коды 2xx-3xx - успешно.
Примеры применения проверок: Readiness probe - когда веб-процесс в контейнере начинает отвечать меньше заданного времени, чтобы при его инициализации не пускать туда трафик, во избежание тормозов на стороне юзера. Для JVM - Liveness probe для URL или для определённых строк в логах (я занят, не шлите на меня трафик). Контейнер может включить режим обслуживания, если он не прошёл Readiness probe.
О самих проверках нужно позаботиться на стороне контейнера/образа, не в K8s!
Настройки проверок:
- initialDelaySeconds - задержка запуска проверки после старта контейнера
- periodSeconds - частота проверок (10 сек)
- timeoutSeconds - тайм-аут при проверке (1 сек)
- successThreshold - кол-во успешных проверок для признания контейнера исправным (1)
- failureThreshold - кол-во неуспешных проверок для признания контейнера сбойным (3)
Пример
apiVersion: v1 kind: Pod metadata: labels: test: readiness name: exec-readiness spec: securityContext: runAsUser: 1000 containers: - name: readiness image: busybox args: - /bin/sh - -c - touch /tmp/healthy; sleep infinity readinessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 5 periodSeconds: 5
Ресурсы
Указываются для каждого контейнера.
Процессор - это эквивалент 1 AWS vCPU / 1 GCP Core / 1 Azure vCore / 1 Hyperthread on Intel CPU w/Hyperthreading. Если в процентах, то 0.1 или 100m - это 10% (100 миллипроцессоров). Самое минимальное значение - 1m.
Память и хранилище - десятичные значения (K, M, G, T, P, E) и двоичные (Ki, Mi, Gi, Ti, Pi, Ei).
Требования к ресурсам (Resource requests) - нужно для более эффективного и понятного распределения по серверам кластера. Т. е., указание, сколько нужно контейнеру, чтобы быть размещённым на том или ином хосте, иначе либо контейнер не будет размещён на хосте с недостаточными ресурсами, либо будет тормозить. По умолчанию - 0.5 vCPU и 256 Mi памяти.
containers: - name: db image: mysql resources: requests: memory: "512Mi" cpu: "0.5"
Предел ресурсов (Resource limits) - чтобы контейнер не сожрал слишком много ресурсов и не завалил всю систему. При попытке превысить ресурсы CPU контейнер будет ограничиваться в потреблении (throttling), при превышении лимита памяти он будет убит и пересоздан. По умолчанию - 1 vCPU и 512 Mi памяти.
containers: - name: db image: mysql resources: limits: memory: "1Gi" cpu: "1"
Когда ноды кластера не имеют достаточно ресурсов для размещения доп. подов, планировщик не может выполнить требований деплоя и поды будут помечены как Pending, пока не призойдут какие-либо изменения ситуации для достижения желаемого состояния, например
- Будут добавлены новые хосты в кластер
- Будут пересмотрены требования к ресурсам в деплое
- Другие поды и деплои будут удалены или уменьшены в размере
- Кол-во желаемых подов будет уменьшено в текущем деплое
Сеть
Трафик в K8s можно поделить на 4 сегмента: внутри пода, между подами, между сервисом и подами, извне к кластеру.
Т. к. под всегда находится на одном хосте, то его внутренний трафик не идёт «по проводу». Внутри пода для связи контейнеров используется localhost, разделяют общий IP-адрес в рамках кластера - в целом, так же, как и в случае виртуальной машины.
У каждого пода есть свой уникальный маршрутизируемый IP-адрес в рамках внутренней виртуальной сети кластера. Также, есть подобие DNS-имени на базе metadata: name:
. Внешние устройства напрямую к подам получить доступ не могут.
Сервис работает как NLB 4-го уровня. Есть 3 типа сервисов:
- ClusterIP - публикация внутри кластера, т. е., поды за сервисом публикуются для других подов в том же кластере. Внешнего доступа нет. Используется, например, для сервисов БД. Это стандартный тип сервиса, если не указано иначе.
- NodePort - публикация как порт на всех рабочих нодах. Т. е., попасть к сервису можно, обратившись на любую рабочую ноду кластера на указанный порт. Это не отменяет необходимости внешнего балансировщика, который уже будет балансировать сами ноды.
- LoadBalancer - создание и управление внешним балансировщиком, где NodePort используется для вхождения трафика, а ClusterIP для балансировки между подами. Есть разные плагины для разных реализаций внешнего балансировщика.
Ingress Controller - это NLB 7-го уровня. Ему, в отличие от NodePort, не нужно открывать кучу портов на нодах. Есть множество реализаций контроллера Ingress, такие, как Nginx, Contour, Traefik, Amazon ALB, Google Layer-7 Load Balancer. Запускается он как поды в кластере, опубликованные на NodePort, которые пробрасывают внешний трафик на ClusterIP сервисов. Для Ingress-контроллера нужно создать объект kind: Ingress
.
https://kubernetes.io/docs/concepts/services-networking/ingress/
Организация ресурсов
Наивысший уровень изоляции сервисов - это разнесение их по разным кластерам - prod, qa, dev и т. п. Также это имеет смысл при каких-то требованиях безопасности или при географическом распределении датацентров. В этом случае имеются некоторые доп. усилия на управление.
Следующий - это namespace в рамках одного кластера. Каждый объект кластера входит в своё пространство, и в рамках этого пространства должен иметь уникальное имя. Это даёт возможность запускать один и тот же сервис в разных пространствах имён без риска перезаписать соседние настройки. Также это ограничивает внутрикластерные DNS-запросы, которые выглядят как поддомен - <service-name>.<namespace-name>.cluster.local. Если нужно подключиться к сервису в другом пространстве имён, необходимо использовать FQDN (<service-name>.<namespace-name>.cluster.local). Можно разграничивать права - одному можно дать namespace только на чтение, другому - полный доступ, а также namespaces задают квоты ресурсов. Хороший пример использования пространства имён - по одному на проект.
# Вывести пространства имён kubectl get namespaces # Вывести поды другого пространства kubectl get pods -n namespace-name
Ярлыки (labels) - могут быть привязаны почти к любому объекту в k8s, даже к нодам (кроме встроенных). Они необязательны, но выполняют также и функциональную роль - использование в селекторах и т. п. Необходимо вести список ярлыков и делать отчёты по ним, избегать составных ярлыков (лучше app: web
tier: frontend
, чем app: web-frontend
). Также, нужна система именования ярлыков - такая, как если бы всё нужно было бы разместить в одном пространстве имён, пользуясь только ярлыками для организации ресурсов.
# ФИльтровать вывод по ярлыку kubectl get pods -l tier=frontend
kubectl знает, с каким кластером он работает, из файла ~/.kube/config
. Там 3 секции - clusters:
, где перечислены кластеры, contexts:
, где соединяются сведения о кластерах и пользователях, и users:
- пользователи. name:
в clusters:
- это просто локальное отображаемое имя. users:
- там имя тоже просто для отображения локально. Если, положим, есть 3 кластера и во всех них одинаковый пользователь, то будет 3 записи в кластерах, 1 в пользователях и 3 в контекстах.
# добавить namespace в контекст конфига kubectl config set-context my-context --user poweruser --cluster cluster1 --namespace my-namespace # Переключиться на новый контекст kubectl config use-context my-context # см. какой контекст сейчас используется kubectl config current-context # все контексты kubectl config get-contexts # переключиться на другое пространство имён в текущем контексте kubectl config set-context $(kubectl config current-context) --namespace=another
https://kube.academy/courses/kubernetes-core-concepts-part-3/lessons/lab-resource-organization
# jsonpath - фильтр сырого вывода json. Ещё опции вывода - yaml, json, wide kubectl get deployment nginx -o jsonpath='{.status.availableReplicas}' # Перенаправить порт из пода (8080) на локальную машину (8000). Потом можно делать запросы, например, curl-ом - localhost:8000 kubectl port-forward <pod_id> 8000:8080
https://kubernetes.io/docs/reference/kubectl/jsonpath
Квота ресурсов для namespace задаётся объектом ResourceQuota.
apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: dev spec: hard: pods: "10" requests.cpu: "4" requests.memory: 5Gi limits.cpu: "10" limits.memory: 10Gi
Stateful apps
Хранилище для таких приложений может быть разных типов. Работает на уровне пода, подобно хранилищу для виртуалки.
Особый тип хранилища - EmptyDir. Временный каталог, существует только когда под запущен. Нужен для организации общего каталога между контейнерами внутри пода.
Persistent Volume - не зависит от времени жизни пода, кочует вместе с ним с хоста на хост. Есть опция hostPath, указывающая путь на ноде, но без крайней необходимости её лучше не применять.
Persistent Volume Claims (PVC) - запрос на организацию хранилища с заданными параметрами (размер, режим доступа и т. д.)
Хранилища могут быть созданы статически (руками), и динамически (по запросу к API провайдера типа Amazon EBS).
Для PVC создаётся объект kind: PersistentVolumeClaim
- файлик с описанием (некий стандартизованный аналог служебной записки в ИТ-отдел на предоставление места на диске), а в spec:
пода пишется раздел volumes:
, который ссылается на соответствующий объект PVC, а в описании контейнеров пишется подраздел volumeMounts:
, монтирующий нужные тома к нужным контейнерам. Если при применении файла PVC такое хранилище уже есть, то просто идёт привязка хранилища к PVC, если нет - хранилище создаётся (если используется динамическое создание).
Чтобы у каждого пода в деплое было своё хранилище, а не одно общее, используется kind: statefulSet
- объект, как бы являющийся расширенной версией деплоя. Он создаёт более стабильные имена подов ${имя_statefulset}-${порядковый_номер}.${имя_сервиса}.${namespace}
, чтобы подходить для систем со своей кластерной организацией, например, БД, которые не любят случайных часто меняющихся имён. StatefulSet также умеет создавать и PVC, которые привязываются к каждому задекларированному поду, создавая уникальные комбинации PVC-Pod. Порядок развёртывания в StatefulSet последовательный: поды создаются от меньшего номера к большему (1→2→3→4), а удаляются от большего к меньшему (4→3→2→1) - это также связано с кластерами БД (см. MariaDB Galera cluster). Перед развёртыванием все поды должны быть запущены или готовы (Running or Ready).
https://kube.academy/courses/kubernetes-core-concepts-part-4/lessons/lab-stateful-applications
Пример объекта StatefulSet
apiVersion: apps/v1 kind: StatefulSet metadata: name: gowebapp-mysql labels: app: gowebapp-mysql tier: backend spec: serviceName: gowebapp-mysql replicas: 1 selector: matchLabels: app: gowebapp-mysql tier: backend template: metadata: labels: app: gowebapp-mysql tier: backend spec: containers: - name: gowebapp-mysql env: - name: MYSQL_ROOT_PASSWORD value: mypassword image: gowebapp-mysql:v1 ports: - containerPort: 3306 livenessProbe: tcpSocket: port: 3306 initialDelaySeconds: 20 periodSeconds: 5 timeoutSeconds: 1 readinessProbe: exec: command: ["mysql", "-uroot", "-pmypassword", "-e", "use gowebapp; select count(*) from user"] initialDelaySeconds: 25 periodSeconds: 10 timeoutSeconds: 1 volumeMounts: - name: mysql-pv mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: mysql-pv spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 5Gi
Динамическая конфигурация приложения
Когда что-то в конфигурации занесено жёстко и в явном виде, это потенциальный источник проблем. Для решения используется объект ConfigMaps
, где перечислены настройки подов и на которые можно ссылаться
- как на переменные окружения - в разделе
spec.containers
пода либо полностью (configMapRef:
), либо дёргать оттуда ключи по отдельности (configMapKeyRef:
) - как на том - том задаётся как configMap и в контейнере он монтируется по определённому пути. Также можно монтировать configMap как полностью, так и отдельные ключи.
Разница в подходах в том, что в случае переменных окружения надо перезапускать контейнер для изменения настроек, в то время как со смонтированным томом настройки в каталоге (там 2 файла) будут обновлены сразу же.
volumeMounts: - name: config-volume mountPath: '/opt/gowebapp/config' volumes: - name: config-volume configMap: name: gowebapp items: - key: webapp-config-json path: config.json
Секретные данные (kind: Secret
) должны быть закодированы в Base64 и могут так же быть представлены подам, как и configMaps - в виде переменных или томов. Secret необязателен к использованию, если уже есть какой-то альтернативный способ хранения секретных данных.
kubectl create secret generic mysql --from-literal=password=mypassword
Потом в файле StatefulSet для mysql заменить
env: - name: MYSQL_ROOT_PASSWORD value: mypassword # на env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql key: password # в разделе readinessProbe: заменить пароль на переменную readinessProbe: exec: command: ["mysql", "-uroot", "-p${MYSQL_ROOT_PASSWORD}", "-e", "use gowebapp; select count(*) from user"]
Дополнительная нагрузка
Помимо сервисов, работающих, пока они не будут остановлены или не упадут сами, существуют задачи, которые должны работать временно. Это kind: Job
. Они работают до получения результата, основаны на подах/контейнерах и будут пытаться выполнить задачу неограниченное кол-во раз (если не задано иное). Так как после подов и задач остаются логи, результаты их работы и т. п., админ решает, как чистить всё это, не k8s. Задачи бывают непараллельные (один под) и параллельные (несколько).
apiVersion: batch/v1 kind: Job metadata: name: pi spec: template: metadata: name: pi spec: securityContext: runAsUser: 1000 containers: - name: pi image: perl command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] restartPolicy: Never
Посмотреть результаты вышеописанного задания можно командой kubectl logs <pod-name>
.
Параллельные задания с очередью (parallel jobs with a work queue) - запуск нескольких подов, каждый из которых берёт на себя часть очереди на обработку - как многопоточная обработка процессорными ядрами. Для этого типа задания нужно
- оставить
.spec.completions
незаданным - каждый под должен иметь возможность проверить, пуста ли очередь и нужно ли выйти
- когда под завершил работу успешно, новый не создаётся
- когда хотя бы один под завершил работу успешно и остальные были уничтожены, задание также считается завершённым успешно
- когда любой под завершил работу успешно, остальные уже не должны ничего делать и также начинают завершать работу
apiVersion: batch/v1 kind: Job metadata: name: primes-parallel-wq labels: app: primes spec: parallelism: 3 template: metadata: name: primes labels: app: primes spec: securityContext: runAsUser: 1000 containers: - name: primes image: debian:stable-slim command: ["bash"] args: ["-c", "current=0; max=110; echo 1; echo 2; for((i=3;i<=max;)); do for((j=i-1;j>=2;)); do if [ `expr $i % $j` -ne 0 ] ; then current=1; else current=0; break; fi; j=`expr $j - 1`; done; if [ $current -eq 1 ] ; then echo $i; fi; i=`expr $i + 1`; done"] restartPolicy: Never
https://kubernetes.io/docs/tasks/job/fine-parallel-processing-work-queue/
Также существуют CronJobs - аналог cron в линуксе. Для каждого запуска создаётся новый job. Хранится 3 успешных и 1 неуспешный job. Jobs должны быть идемпотентными, т. е., их можно вызывать повторно для получения того же результата.
apiVersion: batch/v1beta1 kind: CronJob metadata: name: hello spec: schedule: "*/1 * * * *" jobTemplate: spec: template: spec: securityContext: runAsUser: 1000 containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster restartPolicy: OnFailure
DaemonSet - объект, следящий за тем, чтобы копия конкретного пода была запущена на всех хостах. Это нужно чаще всего для задач администрирования кластера, агентов логирования и мониторинга.
https://kube.academy/courses/kubernetes-core-concepts-part-5/lessons/lab-additional-workloads
Безопасность
https://kube.academy/courses/kubernetes-core-concepts-part-5/lessons/security
Объект NetworkPolicy задаёт, как группы подов могут соединяться друг с другом и с другими сетевыми конечными точками. Для выбора подов используются ярлыки и задаются правила, какой тип трафика разрешён на эти поды.
Изначально все поды внутри пространства имён могут обмениваться любым трафиком. Когда внутри пространства имён применяется сетевая политика, то поды, которые в ней выбраны, будут отбрасывать любые подключения, которые не указаны в политике. Остальные поды, не охваченные политикой, продолжат принимать любой трафик.
Пример политики:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: test-network-policy namespace:default spec: podSelector: matchLabels: tier: backend policyTypes: - Ingress - Egress
Правила Ingress-Egress, на которые идёт ссылка в политике. Логика: если несколько from:
- то это логика И, если один с несколькими селекторами - ИЛИ. В данном случае «трафик из фронтенда проекта gowebapp». Egress - исходящий трафик, в данном случае вовне кластера на контроллер домена для аутентификации.
ingress: - from: - namespaceSelector: matchLabels: project: gowebapp - from: - podSelector: matchLabels: tier: frontend ports: - protocol: TCP port: 3306 egress: - to: - ipBlock: cidr: 10.0.0.0/24 ports: - protocol: TCP port: 389
SecurityContext - часть spec:
пода, где описывается контекст безопасности, например, runAsUser/runAsGroup, SELinux, пути на ноде, которые могут быть смонтированы и т. д.
apiVersion: v1 kind: Pod metadata: name: myapp spec: securityContext: runAsUser: 1000 containers: - name: myapp image: myapp:v1 securityContext: allowPrivilegeEscalation: false - name: sidecar image: mysidecar:v1 securityContext: runAsUser: 2000
В примере выше runAsUser:
на уровне контейнера переопределяет значение, заданное на уровне пода, применяемое ко всем контейнерам в нем.
Права доступа в кластере. Есть несколько способов аутентификации - клиентские сертификаты (используются для кластерных компонентов), токены (сервисные учётки), внешняя аутентификация (пользователи). Сервисные учётки создаются объектом ServiceAccount, и в кластере появляются файлы с описанием той учётки - принадлежность к пространству имён, URL API-сервера, какой сертификат и учётные данные нужно использовать, что избавляет от указания учётных данных в явном виде в приложении. Большинство клиентских библиотек Python, Java и т. п. смотрят в каталог кластера с сервисными учётными данными по умолчанию.
Ролевая модель доступа (RBAC). Строится на основе групп или ролей - kind: Role
. Роль - это набор разрешений (правил). Есть стандартные роли - cluster-admin, admin, edit, view.
Пример RBAC, привязанный к пространству имён и позволяющий только get secrets.
kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: namespace: default name: my-app-secret-reader rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get"]
Есть также kind: ClusterRole
- то же самое, но область действия - кластер в целом. Здесь позволены операции, перечисленные в verbs, только к подам с именами, перечисленными в resourceNames.
kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: pod-reader rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "watch", "list"] resourceNames: ["nginx", "mariadb"]
Вывести список общекластерных ресурсов и привязанных к пространствам имён
kubectl api-resources --namespaced=false kubectl api-resources --namespaced=true
Роли привязываются к пользователям, группам и сервисным учёткам с помощью kind: RoleBinding
(действие в рамках пространства имён). В рамках кластера kind: ClusterRoleBinding
.
kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: my-app-read-secret namespace: default subjects: - kind: ServiceAccount name: my-app apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: my-app-secret-reader apiGroup: rbac.authorization.k8s.io
Проверить возможность выполнять команды
kubectl auth can-i create deployments [--as user --namespace test] kubectl auth can-i delete nodes [--as user --namespace test]
Taints & tolerations
Политика запуска подов на хостах. Taints применяются к хостам, например, app=web
, а tolerations - к подам. Разместиться на такой ноде с указанным тейнтом может только под, у которого соответствует настройка tolerations.
kubectl taint nodes node1 app=web:NoSchedule # убрать kubectl taint nodes node1 app=web:NoSchedule-
Эффект - как действовать, если под нетолерантен к указанному тейнту на хосте.
- NoSchedule - не размещать
- PreferNoSchedule - стараться не размещать
- NoExecute - не размещать и выгнать уже существующие
spec.tolerations
в определении пода:
spec: tolerations: - key: "app" operator: "Equal" value: "web" effect: "NoSchedule"
Двойные кавычки в данном случае обязательны.
Taints & tolerations применяется на мастер-нодах, чтобы там не размещалась рабочая нагрузка - kubectl describe node <master> |grep Taint
.
Taints & tolerations - это только средство ограничения. Если они настроены, то это не значит, что толерантные поды всегда будут размещаться на соответствующих хостах. Эти поды могут размещаться и на хостах, где нет ограничений. А для именно привязки к нодам нужен другой параметр - NodeAffinity
.
Node selectors & affinity
Иногда нужно, чтобы под запускался на конкретном хосте, например, более производительном. Для этого нужно присвоить хосту ярлык
kubectl label nodes <nodename> <key=value>
и потом в spec.nodeSelector
пода прописать соответствующий селектор.
Но если условия более сложные, например, запускаться на нескольких типах нод (OR) или запускаться на всех нодах, кроме указанных (NOT), тут вступает в действие spec.affinity
в свойствах пода.
spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: power operator: In values: - high - medium
- requiredDuringSchedulingIgnoredDuringExecution - не размещать новые, не трогать уже работающие
- preferredDuringSchedulingIgnoredDuringExecution - стараться не размещать новые, не трогать уже работающие
- requiredDuringSchedulingRequiredDuringExecution - сейчас такой опции нет, есть в планах - не размещать новые, выгонять работающие
https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/
Если настроено nodeAffinity, то это не гарантирует, что на этих хостах не будут размещаться другие поды. Чтобы гарантировать жёсткую привязку хостов и подов, нужно сочетать taints и affinities.
Static pods
В отсутствие мастер-ноды, kubelet на рабочей ноде остаётся наедине с собой, и взять инструкции, что ему запускать, неоткуда. Тем не менее, есть каталог, который он регулярно проверяет, куда можно положить манифесты подов. При изменении манифеста под перезапускается с соответствующими параметрами, при удалении - под удаляется. Кроме подов, ничего больше запустить нельзя, т. к. для этого нужны контроллеры на мастер-ноде.
Путь к каталогу манифестов задаётся параметром –pod-manifest-path=/etc/kubernetes/manifests
как аргумент запуска kubelet.service (путь к каталогу может быть любым). Другой вариант - вписать в аргумент запуска –config=kubeconfig.yaml
и задавать этот параметр уже в файле как staticPodPath: /etc/kubernetes/manifests
.
Kubelet, будучи в кластере, запускает и статические, и распределённые на него кластером поды. С мастер-ноды видно все поды, в т. ч. и статические, но статические не могут быть изменены или удалены - для этого нужно работать с манифестами непосредственно на ноде. У статических подов имя родительского хоста прописано в конце их имени.
Статические поды нужны для работы компонентов мастер-хостов, таких как etcd, api и т. п. Они запускаются как поды, а если поды удаляются, то в соответствии с наличием манифестов в соответствующем каталоге, они запускаются вновь. См. пространство имён kube-system
.
Static PODs | DaemonSets |
---|---|
Создаются kubelet-ом непосредственно на узле | Создаётся сервером API (DaemonSet controller) |
Пример применения: компоненты Control Plane | Пример применения: агенты мониторинга, логирования |
Игнорируются Kube-scheduler-ом |
Multiple schedulers
Помимо основного планировщика (или вместо него) можно установить дополнительные, указав имя при запуске аргументом –scheduler-name=
. Или с помощью kubeadm, который запускает планировщик как под, описание которого будет в каталоге манифестов.
Чтобы запустить доп. планировщик, нужно скопировать исходный kube-scheduler.yaml и изменить его под свои нужды, добавив в spec.command
параметр –scheduler-name=
. Также, нужно настроить планировщика-лидера –leader-elect=true
, если мастер-нод несколько. Также нужно настраивать (устарело) –lock-object-name=<имя планировщика>
leader-elect-resource-name=<имя планировщика>
.
После настройки планировщика (его под должен быть в состоянии Running) его имя нужно указывать в spec.schedulerName
пода, которому требуется использовать этот планировщик. При проблемах в работе планировщика состояние пода, который его использует, будет Pending.
Проверить, что за планировщик использовался для размещения пода, можно с помощью kubectl get events
. Там будет SOURCE - имя планировщика, REASON - Scheduled и в сообщении будет написано об успешном размещении.
Логи самого планировщика (не забыть про namespace)
kubectl logs <scheduler name> -n kube-system
https://kubernetes.io/docs/tasks/extend-kubernetes/configure-multiple-schedulers/
Мониторинг и логирование
Есть ряд решений, как бесплатных (Metrics server, Prometheus, Elastic stack и т. д.), так и коммерческих (Datadog, Dynatrace и т. д.)
Metrics server (ранее Heapster) - ставится один на кластер. Собирает метрики с хостов и подов и хранит их только в памяти, на диск не пишет - т. е., историю не посмотреть. Схема работы такая: у kubelet на каждой ноде есть компонент cAdvisor, который через API передаёт данные на Metrics server.
Установить Metrics server
# в minikube minikube addons enable metrics-server # в microk8s microk8s enable metrics-server # для прочих git clone https://github.com/kubernetes-incubator/metrics-server.git kubectl create -f deploy/1.8+/
Через некоторое время будет доступна информация о производительности
kubectl top node kubectl top pod
Логи в k8s можно смотреть примерно так же, как и в докере:
kubectl logs -f <podName>
Но есть нюанс - если в поде несколько контейнеров, то нужно указывать имя контейнера после имени пода.
kubectl logs -f <podName> <containerName>
Команды и аргументы
# В Докере команда передаётся в момент непосредственного запуска, например docker run ubuntu sleep 5 # Чтобы указать команду в образе, используется формат CMD sleep 5 # shell command # или CMD ["sleep", "5"] # json format
Разница в том, что в случае с форматом json первый аргумент должен быть исполняемым (sleep). Также, в случае с json, в процессах не будет дополнительно висеть shell как среда запуска. Теперь, если построить образ, например, docker build -t sleeper .
, то контейнер будет стартовать и через 5 сек завершать работу.
Изменить время с 5 сек на другое можно, запустив построенный образ: docker run sleeper sleep 10
, но это неправильно, т. к. не использует встроенной в образ команды и повторяет её. Для этого вместо CMD используется
ENTRYPOINT ["sleep"]
В таком случае, образ можно запустить так: docker run sleeper 10
, но в этом случае нужно всегда указывать значение в командной строке. Если просто запустить docker run sleeper
, то будет ошибка, т. к. отсутствует необходимый аргумент. Чтобы задать значение по умолчанию, нужно добавить строку CMD к ENTRYPOINT:
ENTRYPOINT ["sleep"] CMD ["5"]
Тогда можно запускать просто docker run sleeper
, и тогда будет всё по умолчанию, или docker run sleeper 10
, что задаст своё значение. Можно даже при большом желании переопределить и саму команду: docker run –entrypoint ls sleeper / -lh
В k8s это пишется в spec.containers
как
spec: containers: - name: sleeper image: sleeper command: ["ls"] args: ["/", "-lh"]
Т. е., command - это аналог ENTRYPOINT, а args - CMD.
Переменные
В ''spec.containers'' напрямую
spec: containers: - name: webapp-db env: - name: MYSQL_ROOT_PASSWORD value: mypassword
С помощью ConfigMap
ConfigMaps нужны для централизованного управления парами «ключ-значение», это хорошо помогает, когда много манифестов подов и замучаешься править их все по отдельности, лучше ссылаться оттуда на ConfigMap.
Создать ConfigMap можно, как и другие объекты в k8s, 2 способами: императивным (командой)
kubectl create configmap <name> --from-literal=key=value --from-literal=key2=value2 kubectl create configmap <name> --from-file=<path>
и декларативным, нарисовав соотв. файл (вместо раздела spec здесь data)
apiVersion: v1 kind: ConfigMap metadata: name: webapp-config data: APP_COLOR: red APP_MODE: test
и применив его. Посмотреть уже существующие - kubectl get/describe configmaps
.
Как сослаться на ConfigMap в описании контейнера:
# целиком spec: containers: envFrom: - configMapRef: name: webapp-config # или только на значение env: - name: APP_COLOR valueFrom: configMapKeyRef: name: webapp-config key: APP_COLOR
Также, есть возможность использовать ConfigMap как volume:
volumes: - name: webapp-config-vol configMap: name: webapp-config
С помощью Secrets
Secrets нужны для хранения конфиденциальных данных. Это то же самое, что и ConfigMap, только данные там зашифрованы.
kubectl create secret generic <name> --from-literal=key=value --from-literal=key2=value2 kubectl create secret generic <name> --from-file=<path>
Описание объекта Secret для декларативного применения:
apiVersion: v1 kind: Secret metadata: name: webapp-secret data: DB_HOST: mysql DB_USER: root DB_PASSWORD: P@ssw0rd
Вопрос только в том, что для Secret нужно указывать данные не в plain text, а закодированным в base64.
echo -n "mysql" |base64 echo -n "root" |base64 echo -n "P@ssw0rd" |base64
Так что описание должно выглядеть так:
apiVersion: v1 kind: Secret metadata: name: webapp-secret data: DB_HOST: bXlzcWw= DB_USER: cm9vdA== DB_PASSWORD: UEBzc3cwcmQ=
Просмотр
kubectl get/describe secrets kubectl get secret <name> -o yaml
Раскодировать значения
echo -n "bXlzcWw=" |base64 -d echo -n "cm9vdA==" |base64 -d echo -n "UEBzc3cwcmQ=" |base64 -d
Как сослаться на Secret в описании контейнера
# целиком spec: containers: envFrom: - secretRef: name: webapp-secret # или только на значение env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: webapp-secret key: DB_PASSWORD
Также, есть возможность использовать Secret как volume:
volumes: - name: webapp-secret-vol secret: secretName: webapp-secret
В этом случае в контейнере появляется каталог, где секреты лежат как файлы (здесь: DB_HOST, DB_USER, DB_PASSWORD) с содержимым в виде соответствующих ключей.
Особый секрет docker-registry для логина в приватный репозиторий Docker
kubectl create secret docker-registry docker-config-secret --docker-server=gitlab.example.com:5050 --docker-username=ВАШ_ЛОГИН --docker-password=ВАШ_ПАРОЛЬ --dry-run=client -o yaml > docker-config-secret.yaml
Обслуживание кластера
Когда хост становится недоступным, то, если он не вернулся в строй через 5 минут, кластер помечает его как умерший и запускает его поды на других хостах. Это называется pod eviction
и регулируется менеджером контроллеров k8s kuber-controller-manager –pod-eviction-timeout=5m0s
.
Если прошло меньше 5 минут, то поды заново запускаются на хосте. Нужно также помнить о том, что если под не управляется деплоем или репликой, то, в случае падения хоста и прошествии 5 минут этот под уже не запустится, когда хост вернётся, и нужно будет заново его создавать.
Чтобы выгнать нагрузку с хоста, есть команда kubectl drain <nodename>
. Хост помечается как unschedulable, и даже когда хост возвращается в строй, он остаётся в том же статусе. DaemonSets, если они есть, не уйдут с ноды, поэтому при их наличии нужно добавлять параметр kubectl drain <nodename> –ignore-daemonsets
.
Для возвращения его в работу нужно дать команду kubectl uncordon <nodename>
.
Команда kubectl cordon <nodename>
помечает ноду как unschedulable, но не выгоняет оттуда уже работающие поды, а лишь препятствует запуску на ней новых.
Обновление
Версия API-сервера не должна быть старее, чем любая версия прочих компонентов. Controller manager и Sheduler могут отставать по версии от API на одну минорную версию, а kubelet и kube-proxy - на две. Исключение - kubectl, который может быть как новее, так и старше на одну минорную версию, чем API.
Обновление поддерживается в пределах трёх минорных версий, например, если кластер работает на версии 1.10, то он может обновиться на 1.11 и 1.12, но не на 1.13. Рекомендуется обновляться последовательно по минорным версиям. В облаках обновление - это нажатие пары кнопок. В kubeadm - kubectl upgrade plan
, kubectl upgrade apply
. Если кластер собран вручную, то придётся обновлять все компоненты самостоятельно. Kubeadm обновляет все компоненты, кроме kubelet, который нужно обновлять самостоятельно на каждой ноде. Перед обновлением нужно обновить сам kubeadm, например, apt upgrade -y kubeadm=1.12.0-00
.
Сначала нужно обновлять мастер-ноды (например, kubectl upgrade apply v1.12.0
), потом - рабочие. Во время обновления мастера функции управления кластером недоступны, но рабочие ноды всё равно продолжают работать как обычно. Но если на рабочих нодах что-то упадёт, то подняться оно не сможет, т. к. нет управляющих компонентов.
Если после обновления мастер-ноды запустить kubectl get nodes
, то в колонке VERSION покажется прежняя версия, потому что там отображается версия kubelet, которая ещё не была обновлена.
# Обновить kubelet apt upgrade -y kubelet=1.12.0-00 systemctl restart kubelet
После этого kubectl get nodes
уже покажет новую версию на мастер-ноде.
Рабочие ноды можно обновить все сразу, но тогда будет простой. Можно обновлять рабочие ноды последовательно, предварительно освобождая их от нагрузки. Есть третий вариант - добавлять ноды с новой версией k8s в кластер, переезжать на них, а ноды со старой версией выводить из кластера. Примерный алгоритм действий на рабочей ноде
# на мастере kubectl drain <nodename> # на ноде apt upgrade -y kubeadm=1.12.0-00 apt upgrade -y kubelet=1.12.0-00 kubeadm upgrade node config --kubelet-version v1.12.0 systemctl restart kubelet # на мастере kubectl uncordon <nodename>
Резервное копирование
Бэкапить надо конфиги, ETCD-кластер и постоянные тома.
Рекомендуется использовать конфигурационные файлы и декларативный путь их применения. Даже если всё навернулось, с помощью конфигов можно быстро восстановить всё обратно. Есть способ и вытащить все созданные объекты в кластере, даже если кто-то ранее пользовался императивным способом создания объектов:
kubectl get all -A -o yaml > all-deploys.yaml
Тем не менее, это не охватывает все группы ресурсов кластера. Есть сторонние решения для резервного копирования через API типа Velero.
Вместо запросов к API можно делать резервную копию ETCD-кластера. В строке его запуска есть параметр –data-dir=/var/lib/etcd
, указывающий на каталог с конфигурацией. ETCD умеет создавать снапшоты.
При всех командах к etcd нужно указывать данные для аутентификации
etcdctl snapshot save snapshot.db \ --endpoints=https://192.168.1.100:2379 \ # адрес кластера etcd (если etcd не локальный) --cacert=/etc/etcd/ca.crt \ # сертификат УЦ --cert=/etc/etcd/etcd-server.crt \ # сертификат кластера --key=/etc/etcd/etcd-server.key # ключ
# сделать снапшот etcdctl snapshot save snapshot.db # состояние снапшота etcdctl snapshot status snapshot.db # чтобы восстановить, нужно сначала остановить сервер API service kube-apiserver stop # а потом восстановить снапшот, указав НОВЫЙ каталог с конфигурацией ETCD etcdctl snapshot restore snapshot.db --data-dir=/var/lib/etcd-from-backup
Будет инициализирована новая конфигурация кластера и регистрация его членов как новых членов нового кластера, что препятствует их регистрации в старом кластере. Затем нужно прописать этот каталог в конфигурацию etcd.service
. Далее
# перечитать конфиг сервисов systemctl daemon-reload # перезапустить etcd service etcd restart # запустить API service kube-apiserver start
Резервное копирование конфигурации ресурсов и ETCD имеют свои достоинства и недостатки, но в случае облаков, когда нет прямого доступа к etcd-кластеру, запросы к API для резервного копирования будут лучшим выбором.
Безопасность
Первая линия защиты - доступ к API.
- Кто может получать доступ (аутентификация)? Способы регулирования - имя и пароль/токен, сертификаты, внешний сервис типа LDAP и сервисные учётки для машин.
- Что может делать получивший доступ (авторизация)? Способы регулирования - RBAC, ABAC, Node authorization, Webhook mode.
В k8s нельзя завести учётки пользователей, но можно создать сервисные учётки - kubectl create serviceaccount <name>
. Аутентификацией пользователей занимается сервер API перед обработкой запросов от них.
Статические файлы-списки с паролями или токенами
Самая простая форма. Это CSV, где перечислены учётные данные пользователей (пароль, имя, ID, группа).
password,user1,u0001,group1 password,user2,u0002,group2 password,user3,u0003,group1
Путь к этому файлу нужно прописать в аргументе запуска kube-apiserver –basic-auth-file=user-details.csv
. Если использовать kubeadm, то тогда можно в описание статического пода spec.containers.command
kube-apiserver.yaml прописать тот же аргумент. При сохранении изменений под автоматически перезапустится. Пример команды, использующей логин и пароль: curl -v -k https://master-node-ip:6443/api/v1/pods -u «user1:password»
.
Файл с токенами подобен парольному, просто вместо паролей там токены и аргумент другой - –token-auth-file=user-details.csv
. Пример команды, использующей логин и токен: curl -v -k https://master-node-ip:6443/api/v1/pods –header «Authorization: Bearer weoih02939wfJfi393ruUr»
.
Статические файлы-списки не рекомендуются к использованию, т. к. учётные данные хранятся в открытом виде.
Сертификаты
Каждый компонент кластера подключается к другому по защищённому асимметричным шифрованием соединению, т. е., у каждого есть пара ключей (crt, key). Но, например, у API, который является и сервером, и клиентом (для ETCD и Kubelet) одновременно, может быть несколько пар ключей.
Над всем этим должен быть по меньшей мере один УЦ. Можно и второй для пары ETCD - API (как клиента ETCD). УЦ должен подписывать нижестоящие сертификаты.
Как создавать сертификаты
# CA key openssl genrsa -out ca.key 2048 # CA csr openssl req -new -key ca.key -subj "/CN=KUBERNETES-CA" -out ca.csr # CA crt (self-signed) openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt # admin key openssl genrsa -out admin.key 2048 # admin csr (в subj нужно писать реальное имя аутентификации, и, например, добавить группу) openssl req -new -key admin.key -subj "/CN=kube-admin/O=system:masters" -out admin.csr # admin crt (ca signed) openssl x509 -req -in admin.csr -CA ca.crt -CAkey ca.key -out admin.crt # для системных компонентов к имени нужно добавить "SYSTEM:", например openssl req -new -key kube-scheduler.key -subj "/CN=system:kube-scheduler" -out kube-scheduler.csr openssl req -new -key kube-controller-manager.key -subj "/CN=system:kube-controller-manager" -out kube-controller-manager.csr # наконец, kube-proxy openssl req -new -key kube-proxy.key -subj "/CN=kube-proxy" -out kube-scheduler.csr # далее по аналогии # Использовать сертификат в запросе можно так: curl https://kube-apiserver:6443/api/v1/pods \ --key admin.key --cert admin.crt --cacert ca.crt
Другой способ - в файле конфигурации
- kube-config.yaml
apiVersion: v1 clusters: - cluster: certificate-authority: ca.crt server: https://kube-apiserver:6443 name: kubernetes kind: Config users: - name: kubernetes-admin user: client-certificate: admin.crt client-key: admin.key
Всем компонентам для проверки подлинности требуется указывать сертификат УЦ - он указывается в каждом клиенте и сервере.
Если ETCD-серверов несколько, то, помимо пары ключей etcdserver (–key-file=
, –cert-file=
) генерируется ещё одна - etcdpeer (–peer-key-file=
, –peer-cert-file=
) для соединения между членами ETCD-кластера.
Для API-сервера нужно добавлять все альтернативные имена, например,
kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local
У каждого кублета на нодах тоже должен быть свой сертификат, названный по имени ноды. Так как ноды являются частью системы, то имена в сертификатах должны быть типа system:node:node01, system:node:node02, system:node:node03 и т. д. Также, группа должна быть system:nodes. Прописываются сертификаты в конфигурациях кублетов, естественно, с упоминанием сертификата УЦ.
Как проверить сертификаты
# Если k8s установлен вручную и работает как сервисы, то поиск строчек запуска (для поиска местоположения сертификатов) нужно делать в сервисах. cat /etc/systemd/system/kube-apiserver.service # и смотреть в логи нужно соответственно journalctl -u etcd.service -l # Если же он установлен с помощью kubeadm, то k8s крутится на статических подах и нужно смотреть строчки запуска там. cat /etc/kubernetes/manifests/kube-apiserver.yaml # логи kubectl logs etcd-master # если нет возможности посмотреть логи через kubectl, тогда через докер docker ps -a docker logs <container name>
Создаётся табличка с заголовками типа Component, Type, Cert path, CN, ALT, Org, Issuer, Expiration. Дальше просматриваются свойства сертификатов, и табличка заполняется.
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout
Certificate API
В Кубере есть инструмент подписи сертификатов, находящийся в Controller manager. По сути, УЦ в Кубере - это просто пара ключей, и они должны храниться в максимально безопасном месте, обычно на мастер-ноде, которая в этом случае является сервером УЦ. Для облегчения работы с сертификатами есть соответствующий API.
Теперь, вместо того, чтобы залезать на мастер-ноду и руками подписывать сертификат, создаётся объект CertificateSigningRequest, который позволяет удобно работать с запросами.
- Пользователь создаёт ключ и запрос, и отправляет запрос админу.
- Админ создаёт CertificateSigningRequest, вбивая в
spec.request
содержимое файла запроса, закодированное в base64 (например,cat user.csr |base64
). - Теперь запрос можно увидеть, набрав команду
kubectl get csr
, а одобрить -kubectl certificate approve <name>
.
Вывести содержимое полученного объекта - kubectl get csr user -o yaml
. Из раздела status.certificate
можно скопировать значение и, раскодировав его из base64, получить подписанный сертификат. echo "YfbuE..=03" |base64 -d > user.crt
KubeConfig
Так как на любую команду, если использовать сертификаты для аутентификации, придётся добавлять параметры, указывающие на эти сертификаты, например curl https://kube-apiserver:6443/api/v1/pods –key admin.key –cert admin.crt –cacert ca.crt
, есть более удобный способ прописать эти учётные данные в конфигурационный файл ~/.kube/config
.
Этот файл имеет 3 секции - Clusters (перечислены кластеры), Contexts, Users (перечислены учётки). Контекст - это объединение кластеров и пользователей: кто к каким кластерам подключается. В контексте сертификатов сертификат УЦ прописывается в кластерном разделе, а пользовательская пара ключей - в пользовательском.
Контекст по умолчанию прописывается в том же файле current-context: userName@clusterName
.
Вывести текущий конфиг: kubectl config view
. Можно указать путь к конфигу с помощью параметра –kubeconfig=/path/customconf
.
Изменить контекст в конфиге: kubectl config use-context userName@clusterName
.
Помимо самого кластера, в конфиге можно указать и пространство имён для подключения параметром namespace: <namespaceName>
в contexts.name.context
.
Можно вместо прописывания пути к файлу (certificate-authority: /etc/kubernetes/pki/ca.crt
) прописать закодированное в base64 содержимое этого файла (certificate-authority-data: LKE0I3rH…*h3+=
)
Сервисные учётки
Нужны для работы приложений с Кубером, например, Prometheus (получение метрик) или Jenkins (создание объектов).
kubectl create serviceaccount <name> kubectl get serviceaccount
Сервисная учётка автоматически создаётся с токеном, использующимся для аутентификации. Токен хранится как связанный с этой учёткой Secret.
Стандартно в каждом пространстве имён существует сервисная учётка default, которая автоматически предоставляется подам через смонтированный том, давая доступ к API. Если сервисная учётка нужна другая, её можно указать с описании пода в разделе spec - serviceAccount: name-sa
.
Если под создан отдельно, то для изменения сервисной учётки в нём нужно удалить его и создать заново. В деплое это делается автоматически при изменении описания пода в нём.
Если явно не указана сервисная учётка в описании пода, то автоматически монтируется default. Чтобы отключить это, в spec нужно прописать automountServiceAccountToken: false
.
Защита образов
В описании контейнера есть имя образа, например, image: nginx
. По факту, это image: docker.io/nginx/nginx
, т. е., если не указано, то пользователь/учётка подставляется исходя из имени образа/репозитория, а реестр образов по умолчанию это Docker Hub (docker.io). Реестр образов кубера - Google Registry (gcr.io).
Если репозиторий приватный, то в Докере нужно сначала зайти в него, а потом уже запускать контейнеры из его образов, например,
# логин и пароль указываются в интерактивном режиме docker login private-repo.io docker run private-repo.io/apps/app
В Кубере, чтобы указать учётные данные для образа из приватного репозитория, указанного в image:
, нужно сослаться на Secret типа docker-registry.
kubectl create secret docker-registry <secret name> \ --docker-server= --docker-username= --docker-password= --docker-email=
spec: containers: - name: app image: private-repo.io/apps/app imagePullSecrets: - name: <secret name>
securityContext
В Докере при запуске контейнера можно указывать разные опции безопасности, например
docker run --user=1000 ubuntu sleep 5000 docker run --cap-add NET_ADMIN ubuntu
В Кубере, так как контейнеры запускаются внутри подов, опции могут указываться на уровне подов, что повлияет на все контейнеры внутри, и на уровне контейнера. Опция в контейнере перекрывает опцию пода, если она задана и там, и там.
apiVersion: v1 kind: Pod metadata: name: app spec: securityContext: runAsUser: 1000 containers: - name: app image: ubuntu command: ["sleep", "5000"] securityContext: runAsUser: 1000 capabilities: add: ["NET_ADMIN"]
В данном случае, capabilities на уровне пода не поддерживаются.
Сетевые политики
Не все сетевые решения для k8s поддерживают сетевые политики, например, Kube-router, Calico, Romana, Weave-net - поддерживают, а Flannel - нет.
- Ingress - входящий трафик
- Egress - исходящий трафик
Всем подам по умолчанию разрешён любой трафик. К примеру, если приложение состоит из веб-части, api и БД, то веб-часть может слать трафик напрямую к БД, что нежелательно. Поэтому можно нарисовать сетевую политику, которая разрешает определённый трафик с определённых подов. В данном случае, нужно сделать политику для пода БД, которая разрешает Ingress-трафик на порт 3306 c пода API.
Привязка политики к поду делается так же, как и реплик с сервисами - через ярлыки и селекторы.
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: db-policy spec: # применяется к подам с ярлыком role: db podSelector: matchLabels: role: db policyTypes: - Ingress - Egress ingress: # разрешить трафик с подов с ярлыком name: api-pod - from: - podSelector: matchLabels: name: api-pod # только для подов из пространства имён с ярлыком name: prod # не забыть прописать сначала этот ярлык для пространства имён namespaceSelector: matchLabels: name: prod # разрешить подключаться внешнему серверу - ipBlock: cidr: 192.168.1.10/32 ports: - protocol: TCP port: 3306 egress: # разрешить исходящий трафик на сервер по порту 80 - to: - ipBlock: cidr: 192.168.1.20/32 ports: - protocol: TCP port: 80
Если убрать podSelector и оставить namespaceSelector, то все поды в определённом пространстве имён смогут подключаться к поду БД.
Логика в spec.ingress.from
: все объекты в списке (начинающиеся на -) - ИЛИ, пункты внутри списка - И. То есть, в примере выше - podSelector И namespaceSelector. Если написать - namespaceSelector
, то будет podSelector ИЛИ namespaceSelector, что совершенно изменит логику.
Далее нужно применить политику.
MicroK8S
https://microk8s.io/
https://microk8s.io/docs
# install snap install microk8s --classic # install add-ons microk8s.enable dashboard dns ingress registry # status, info microk8s status microk8s kubectl get all microk8s kubectl get nodes microk8s kubectl get services microk8s.kubectl cluster-info microk8s.kubectl get po --all-namespaces # добавить юзера в группу sudo usermod -a -G microk8s <user> sudo chown -f -R <user> ~/.kube newgrp microk8s # затем перезайти в систему # Прописать алиас, чтобы не набирать бесконечное "microk8s kubectl" echo "alias kubectl='microk8s kubectl'" >> .bash_aliases echo "alias k='microk8s kubectl'" >> .bash_aliases # Для того, чтобы автокомплит работал с алиасом source <(kubectl completion bash) >> ~/.bashrc source <(kubectl completion bash | sed s/kubectl/k/g) >> ~/.bashrc
https://kubernetes.io/docs/reference/kubectl/cheatsheet/#bash
Alias & auto-completion: https://kubernetes.io/docs/tasks/tools/included/optional-kubectl-configs-bash-linux/
Получить доступ к панели мониторинга
Геморрой начался с самого начала. Приборная панель ставится и включается без проблем, но зайти на неё можно только с localhost «по соображениям безопасности».
microk8s.kubectl -n kube-system edit service kubernetes-dashboard # Заменить type: clusterIP # на type: NodePort # Узнать порт microk8s.kubectl -n kube-system get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE dashboard-metrics-scraper ClusterIP 10.152.183.107 <none> 8000/TCP 46m kube-dns ClusterIP 10.152.183.10 <none> 53/UDP,53/TCP,9153/TCP 46m kubernetes-dashboard NodePort 10.152.183.79 <none> 443:31315/TCP 46m metrics-server ClusterIP 10.152.183.235 <none> 443/TCP 46m
Теперь можно заходить по https://, адресу ноды и, в данном случае, порту 31315.
Узнать токен входа:
token=$(microk8s kubectl -n kube-system get secret | grep default-token | cut -d " " -f1) microk8s kubectl -n kube-system describe secret $token
https://www.thegeekdiary.com/how-to-access-kubernetes-dashboard-externally/
https://microk8s.io/docs/addon-dashboard
Кластер
# На будущей мастер-ноде выполнить microk8s.add-node # Полученную ссылку запустить на будущей подчинённой ноде
Развёртывание приложения
# Название, образ kubectl create deployment kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1 # Публикация - прокси даёт доступ с локального хоста и работает, пока его не остановят (надо запускать в отдельном окне) kubectl proxy # проверка curl http://localhost:8001/version
Поды, узлы
Под - из чего состоит приложение: контейнеры и ресурсы (хранилище, сеть, конфиги). Все контейнеры в поде имеют один и тот же IP-адрес (адреса?) и пространство портов, выполняющиеся в общем контексте на одном и том же узле.
Узел - рабочая машина, виртуальная или физическая. Каждый узел управляется мастером (ведущим узлом). Узел может содержать несколько подов, которые мастер Kubernetes автоматически размещает на разные узлы кластера. Ведущий узел при автоматическом планировании (распределении подов по узлам) учитывает доступные ресурсы на каждом узле.
В каждом узле Kubernetes как минимум работает:
- Kubelet — процесс, отвечающий за взаимодействие между мастером Kubernetes и узлом; он управляет подами и запущенными контейнерами на рабочей машине.
- Среда выполнения контейнера (например, Docker или rkt), отвечающая за получение (загрузку) образа контейнера из реестра, распаковку контейнера и запуск приложения.
Наиболее распространенные операции:
- kubectl get — вывод списка ресурсов
- kubectl describe — вывод подробной информации о ресурсе
- kubectl logs — вывод логов контейнера в поде
- kubectl exec — выполнение команды в контейнере пода
Перечисленные выше команды можно использовать, чтобы узнать, когда и где приложения были развернуты, их текущее состояние и конфигурацию.
Создание сервиса для открытия доступа к приложению
Под - расходный материал, они создаются и уничтожаются, а механизм ReplicaSet следит, чтобы поддерживалось заданное их количество.
Сервисы - логическое объединение подов.
Публикация сервиса
root@vmls-k8s1:~# kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.152.183.1 <none> 443/TCP 2d1h root@vmls-k8s1:~# kubectl expose deployment/kubernetes-bootcamp --type="NodePort" --port 8080 service/kubernetes-bootcamp exposed root@vmls-k8s1:~# kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.152.183.1 <none> 443/TCP 2d1h kubernetes-bootcamp NodePort 10.152.183.216 <none> 8080:31689/TCP 52s root@vmls-k8s1:~# kubectl describe services/kubernetes-bootcamp Name: kubernetes-bootcamp Namespace: default Labels: app=kubernetes-bootcamp Annotations: <none> Selector: app=kubernetes-bootcamp Type: NodePort IP: 10.152.183.216 Port: <unset> 8080/TCP TargetPort: 8080/TCP NodePort: <unset> 31689/TCP Endpoints: 10.1.17.2:8080 Session Affinity: None External Traffic Policy: Cluster Events: <none>
Ярлыки (labels)
# Узнать имена ярлыков root@vmls-k8s1:~# kubectl describe deployment Name: kubernetes-bootcamp Namespace: default CreationTimestamp: Mon, 17 Aug 2020 12:04:21 +0000 Labels: app=kubernetes-bootcamp Annotations: deployment.kubernetes.io/revision: 1 Selector: app=kubernetes-bootcamp Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable StrategyType: RollingUpdate MinReadySeconds: 0 RollingUpdateStrategy: 25% max unavailable, 25% max surge Pod Template: Labels: app=kubernetes-bootcamp Containers: kubernetes-bootcamp: Image: gcr.io/google-samples/kubernetes-bootcamp:v1 Port: <none> Host Port: <none> Environment: <none> Mounts: <none> Volumes: <none> Conditions: Type Status Reason ---- ------ ------ Available True MinimumReplicasAvailable Progressing True NewReplicaSetAvailable OldReplicaSets: <none> NewReplicaSet: kubernetes-bootcamp-6f6656d949 (1/1 replicas created) Events: <none> # Запрос подов по имени ярлыка root@vmls-k8s1:~# kubectl get pods -l app=kubernetes-bootcamp NAME READY STATUS RESTARTS AGE kubernetes-bootcamp-6f6656d949-btv4z 1/1 Running 0 46h # Запрос сервисов по имени ярлыка root@vmls-k8s1:~# kubectl get services -l app=kubernetes-bootcamp NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes-bootcamp NodePort 10.152.183.216 <none> 8080:31689/TCP 79m # Задать новый ярлык root@vmls-k8s1:~# kubectl label pod kubernetes-bootcamp-6f6656d949-btv4z app=v1 --overwrite pod/kubernetes-bootcamp-6f6656d949-btv4z labeled # Запрос подов, где виден новый ярлык root@vmls-k8s1:~# kubectl describe pods kubernetes-bootcamp-6f6656d949-btv4z Name: kubernetes-bootcamp-6f6656d949-btv4z Namespace: default Priority: 0 Node: 10.1.0.223/10.1.0.223 Start Time: Mon, 17 Aug 2020 12:04:21 +0000 Labels: app=v1 pod-template-hash=6f6656d949 Annotations: <none> Status: Running IP: 10.1.17.2 IPs: IP: 10.1.17.2 Containers: kubernetes-bootcamp: Container ID: containerd://215ec311ea17ae5b923d93be3391c4b190cf29349b4173c7d0d1790e340c4f25 Image: gcr.io/google-samples/kubernetes-bootcamp:v1 Image ID: gcr.io/google-samples/kubernetes-bootcamp@sha256:0d6b8ee63bb57c5f5b6156f446b3bc3b3c143d233037f3a2f00e279c8fcc64af Port: <none> Host Port: <none> State: Running Started: Mon, 17 Aug 2020 12:04:49 +0000 Ready: True Restart Count: 0 Environment: <none> Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-gzgxh (ro) Conditions: Type Status Initialized True Ready True ContainersReady True PodScheduled True Volumes: default-token-gzgxh: Type: Secret (a volume populated by a Secret) SecretName: default-token-gzgxh Optional: false QoS Class: BestEffort Node-Selectors: <none> Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s node.kubernetes.io/unreachable:NoExecute for 300s Events: <none> # Запрос подов с новым ярлыком root@vmls-k8s1:~# kubectl get pods -l app=v1 NAME READY STATUS RESTARTS AGE kubernetes-bootcamp-6f6656d949-btv4z 1/1 Running 0 46h
Удаление сервиса
root@vmls-k8s1:~# kubectl delete service -l app=kubernetes-bootcamp service "kubernetes-bootcamp" deleted # Убедиться, что сервиса нет root@vmls-k8s1:~# kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.152.183.1 <none> 443/TCP 2d3h # и что он недоступен root@vmls-k8s1:~# curl localhost:$NODE_PORT curl: (7) Failed to connect to localhost port 31689: Connection refused # в то время под живой и как изнутри него всё работает root@vmls-k8s1:~# kubectl exec kubernetes-bootcamp-6f6656d949-btv4z -- curl localhost:8080 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 84 0 84 0 0 15587 0 --:--:-- --:--:-- --:--:-- 16800 Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-6f6656d949-btv4z | v=1
https://kubernetes.io/ru/docs/tutorials/kubernetes-basics/expose/
Масштабирование
В случае масштабирования развёртывания создаются новые поды, которые распределяются по узлам с доступными ресурсами. У сервисов есть встроенный балансировщик нагрузки, который распределяет сетевой трафик всех подов в открытом извне развертывании.
# Под пока 1 root@vmls-k8s1:~# kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE kubernetes-bootcamp 1/1 1 1 2d # Посмотреть ReplicaSet root@vmls-k8s1:~# kubectl get rs NAME DESIRED CURRENT READY AGE kubernetes-bootcamp-6f6656d949 1 1 1 2d # Размножить root@vmls-k8s1:~# kubectl scale deployments/kubernetes-bootcamp --replicas=4 deployment.apps/kubernetes-bootcamp scaled # Теперь подов 4 root@vmls-k8s1:~# kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE kubernetes-bootcamp 4/4 4 4 2d root@vmls-k8s1:~# kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES kubernetes-bootcamp-6f6656d949-2k99k 1/1 Running 0 82s 10.1.17.4 10.1.0.223 <none> <none> kubernetes-bootcamp-6f6656d949-52l9m 1/1 Running 0 82s 10.1.35.3 10.1.0.222 <none> <none> kubernetes-bootcamp-6f6656d949-btv4z 1/1 Running 0 2d 10.1.17.2 10.1.0.223 <none> <none> kubernetes-bootcamp-6f6656d949-lhnkd 1/1 Running 0 83s 10.1.17.3 10.1.0.223 <none> <none> # В логах есть отражение этого факта root@vmls-k8s1:~# kubectl describe deployments/kubernetes-bootcamp Name: kubernetes-bootcamp Namespace: default CreationTimestamp: Mon, 17 Aug 2020 12:04:21 +0000 Labels: app=kubernetes-bootcamp Annotations: deployment.kubernetes.io/revision: 1 Selector: app=kubernetes-bootcamp Replicas: 4 desired | 4 updated | 4 total | 4 available | 0 unavailable StrategyType: RollingUpdate MinReadySeconds: 0 RollingUpdateStrategy: 25% max unavailable, 25% max surge Pod Template: Labels: app=kubernetes-bootcamp Containers: kubernetes-bootcamp: Image: gcr.io/google-samples/kubernetes-bootcamp:v1 Port: <none> Host Port: <none> Environment: <none> Mounts: <none> Volumes: <none> Conditions: Type Status Reason ---- ------ ------ Progressing True NewReplicaSetAvailable Available True MinimumReplicasAvailable OldReplicaSets: <none> NewReplicaSet: kubernetes-bootcamp-6f6656d949 (4/4 replicas created) Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ScalingReplicaSet 3m7s deployment-controller Scaled up replica set kubernetes-bootcamp-6f6656d949 to 4
Балансировка
# Смотрим свойства сервиса - порт ноды root@vmls-k8s1:~# kubectl describe services/kubernetes-bootcamp Name: kubernetes-bootcamp Namespace: default Labels: app=kubernetes-bootcamp Annotations: <none> Selector: app=kubernetes-bootcamp Type: NodePort IP: 10.152.183.74 Port: <unset> 8080/TCP TargetPort: 8080/TCP NodePort: <unset> 31386/TCP Endpoints: 10.1.17.3:8080,10.1.17.4:8080,10.1.35.2:8080 + 1 more... Session Affinity: None External Traffic Policy: Cluster Events: <none> # Теперь при каждом запросе они идут в разные поды root@vmls-k8s1:~# curl localhost:31386 Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-6f6656d949-rztf4 | v=1 root@vmls-k8s1:~# curl localhost:31386 Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-6f6656d949-lhnkd | v=1 root@vmls-k8s1:~# curl localhost:31386 Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-6f6656d949-52l9m | v=1
Свёртывание масштабирования
root@vmls-k8s1:~# kubectl scale deployments/kubernetes-bootcamp --replicas=2 deployment.apps/kubernetes-bootcamp scaled root@vmls-k8s1:~# kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE kubernetes-bootcamp 2/2 2 2 2d1h root@vmls-k8s1:~# kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES kubernetes-bootcamp-6f6656d949-glxgv 1/1 Terminating 0 67s 10.1.35.4 10.1.0.222 <none> <none> kubernetes-bootcamp-6f6656d949-lhnkd 1/1 Running 0 20m 10.1.17.3 10.1.0.223 <none> <none> kubernetes-bootcamp-6f6656d949-qmct2 1/1 Running 0 67s 10.1.60.8 vmls-k8s1 <none> <none> kubernetes-bootcamp-6f6656d949-rztf4 1/1 Terminating 0 122m 10.1.35.2 10.1.0.222 <none> <none>
https://kubernetes.io/ru/docs/tutorials/kubernetes-basics/scale/scale-intro/
Плавающие обновления (rolling updates)
Плавающие обновления позволяют обновить развёртывания без простоев, шаг за шагом заменяя старые поды на новые. Новые поды будут запущены на узлах, имеющих достаточно ресурсов.
В предыдущем модуле мы промасштабировали приложение до нескольких экземпляров. Это необходимо сделать, чтобы иметь возможность обновлять приложение, не влияя на его доступность. По умолчанию, максимальное количество подов, которое может быть недоступно во время обновления, и максимальное количество новых подов, которое можно создать, равны 1. Эти две опции могут быть определены в абсолютном (числа) или относительном соотношении (проценты). В Kubernetes обновления версионируются, поэтому любое обновление развёртывания можно откатить до предыдущей (стабильной) версии.
Балансировщик будет работать только для доступных подов.
# Список развёртываний root@vmls-k8s1:~# kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE kubernetes-bootcamp 4/4 4 4 2d1h # Список подов root@vmls-k8s1:~# kubectl get pods NAME READY STATUS RESTARTS AGE kubernetes-bootcamp-6f6656d949-lhnkd 1/1 Running 0 35m kubernetes-bootcamp-6f6656d949-qhlcg 1/1 Running 0 17s kubernetes-bootcamp-6f6656d949-qmct2 1/1 Running 0 16m kubernetes-bootcamp-6f6656d949-rvflg 1/1 Running 0 17s # Описание подов (о версии см. строку Image) root@vmls-k8s1:~# kubectl describe pods Name: kubernetes-bootcamp-6f6656d949-lhnkd Namespace: default Priority: 0 Node: 10.1.0.223/10.1.0.223 Start Time: Wed, 19 Aug 2020 12:45:51 +0000 Labels: app=kubernetes-bootcamp pod-template-hash=6f6656d949 Annotations: <none> Status: Running IP: 10.1.17.3 IPs: IP: 10.1.17.3 Controlled By: ReplicaSet/kubernetes-bootcamp-6f6656d949 Containers: kubernetes-bootcamp: Container ID: containerd://3feba85a18a3e9306cd5d5d9dcb29be8fa7c173b4bb1c907449634e2437c23fc Image: gcr.io/google-samples/kubernetes-bootcamp:v1 # дальнейшая простыня обрезана # Обновить до v2 root@vmls-k8s1:~# kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=jocatalin/kubernetes-bootcamp:v2 deployment.apps/kubernetes-bootcamp image updated # Старые поды убиваются, создаются новые свежей версии root@vmls-k8s1:~# kubectl get pods NAME READY STATUS RESTARTS AGE kubernetes-bootcamp-6f6656d949-lhnkd 1/1 Terminating 0 39m kubernetes-bootcamp-6f6656d949-qhlcg 1/1 Terminating 0 4m2s kubernetes-bootcamp-6f6656d949-qmct2 1/1 Terminating 0 20m kubernetes-bootcamp-6f6656d949-rvflg 1/1 Terminating 0 4m2s kubernetes-bootcamp-86656bc875-5hmdv 1/1 Running 0 28s kubernetes-bootcamp-86656bc875-5pj79 1/1 Running 0 17s kubernetes-bootcamp-86656bc875-c8mpt 1/1 Running 0 17s kubernetes-bootcamp-86656bc875-s75z4 1/1 Running 0 27s
Проверка
# Узнаём порт для подключения root@vmls-k8s1:~# kubectl describe services/kubernetes-bootcamp Name: kubernetes-bootcamp Namespace: default Labels: app=kubernetes-bootcamp Annotations: <none> Selector: app=kubernetes-bootcamp Type: NodePort IP: 10.152.183.74 Port: <unset> 8080/TCP TargetPort: 8080/TCP NodePort: <unset> 31386/TCP Endpoints: 10.1.17.6:8080,10.1.17.7:8080,10.1.35.6:8080 + 1 more... Session Affinity: None External Traffic Policy: Cluster Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedToUpdateEndpointSlices 4m46s endpoint-slice-controller Error updating Endpoint Slices for Service default/kubernetes-bootcamp: Error updating kubernetes-bootcamp-xpgkx EndpointSlice for Service default/kubernetes-bootcamp: Operation cannot be fulfilled on endpointslices.discovery.k8s.io "kubernetes-bootcamp-xpgkx": the object has been modified; please apply your changes to the latest version and try again # Проверка запросом, видно, что версия 2 root@vmls-k8s1:~# curl localhost:31386 Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-86656bc875-s75z4 | v=2 # Ещё один способ проверить root@vmls-k8s1:~# kubectl rollout status deployments/kubernetes-bootcamp deployment "kubernetes-bootcamp" successfully rolled out # Проверка версии Image подов root@vmls-k8s1:~# kubectl describe pods Name: kubernetes-bootcamp-86656bc875-5hmdv Namespace: default Priority: 0 Node: 10.1.0.222/10.1.0.222 Start Time: Wed, 19 Aug 2020 13:24:47 +0000 Labels: app=kubernetes-bootcamp pod-template-hash=86656bc875 Annotations: <none> Status: Running IP: 10.1.35.6 IPs: IP: 10.1.35.6 Controlled By: ReplicaSet/kubernetes-bootcamp-86656bc875 Containers: kubernetes-bootcamp: Container ID: containerd://d4c020babb066f3eb28f002cf96370f3b5bfdc5f3edc34dca76bb2a300e9f326 Image: jocatalin/kubernetes-bootcamp:v2 # дальнейшая простыня обрезана
Откат обновления
# Проба обновиться до версии 10 root@vmls-k8s1:~# kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=gcr.io/google-samples/kubernetes-bootcamp:v10 deployment.apps/kubernetes-bootcamp image updated # Проверяем - обновление не прошло root@vmls-k8s1:~# kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE kubernetes-bootcamp 3/4 2 3 2d1h root@vmls-k8s1:~# kubectl get pods NAME READY STATUS RESTARTS AGE kubernetes-bootcamp-64468f5bc5-p8zb6 0/1 ErrImagePull 0 56s kubernetes-bootcamp-64468f5bc5-s2btt 0/1 ErrImagePull 0 56s kubernetes-bootcamp-86656bc875-5hmdv 1/1 Running 0 13m kubernetes-bootcamp-86656bc875-c8mpt 1/1 Running 0 13m kubernetes-bootcamp-86656bc875-s75z4 1/1 Running 0 13m # Если запустить root@vmls-k8s1:~# kubectl describe pods # , то будет ясно, что такой версии нет # Откатываем назад, к предыдущей рабочей версии root@vmls-k8s1:~# kubectl rollout undo deployments/kubernetes-bootcamp deployment.apps/kubernetes-bootcamp rolled back # Всё снова в порядке root@vmls-k8s1:~# kubectl get pods NAME READY STATUS RESTARTS AGE kubernetes-bootcamp-86656bc875-5hmdv 1/1 Running 0 14m kubernetes-bootcamp-86656bc875-786cb 1/1 Running 0 28s kubernetes-bootcamp-86656bc875-c8mpt 1/1 Running 0 14m kubernetes-bootcamp-86656bc875-s75z4 1/1 Running 0 14m # В строке Image будет видно, что опять развёрнута v2 root@vmls-k8s1:~# kubectl describe pods
https://kubernetes.io/ru/docs/tutorials/kubernetes-basics/update/update-intro/
LinuxFoundationX: Introduction to Kubernetes
Kubernetes - Оркестратор контейнеров, автоматизация развёртывания и отказоустойчивости.
Master node - управляющий узел (или несколько узлов в HA mode)
Worker nodes - рабочие
etcd - распределённое хранилище ключей-значений, там хранится конфигурация кластера. Stacked - на мастере, external - на выделенном хосте.
Компоненты мастера
- API server - центральный компонент, работает с etcd, перехватывает запросы от пользователей, опреаторов и внешних агентов, проверяет и обрабатывает их. Является посредником между всеми остальными компонентами. Гибко настраивается, поддерживает добавление другого сервера API, для которого первичный API становится прокси-сервером.
- Scheduler - распределяет объекты, такие, как поды, по узлам. Решения зависят от состояния кластера и требований нового объекта к системе. Планировщик через API запрашивает кол-во доступных ресурсов на рабочих нодах, а также требования объекта к системе в его конфиге. Требования могут включать в себя пользовательские и операторские директивы, например, чтобы объект был размещён на ноде с ярлыком disk==ssd. Гибко настраивается. Поддерживаются сторонние планировщики, тогда в конфигурации объекта должно быть указано имя этого планировщика, в противном случае используется стандартный. Планировщик - крайне важен и довольно сложен в кластерной конфигурации.
- Controller managers - запускает процессы (controllers), регулирующие состояние кластера. Сравнивают желаемое состояние кластера (desired state) с текущим состоянием, получаемым из etcd через API. kube-controller-manager работает, когда ноды становятся недоступны, приводит число подов к желаемому. cloud-controller-manager при недоступности нод управляет хранилищами, LB и маршрутизацией.
- etcd - хранилище данных состояния кластера. Данные добавляются, но никогда не заменяются. Устаревшие данные архивируются для экономии места. Есть etcd CLI для резервного копирования, снапшотов и восстановления данных. В пром. использовании крайне важно настроить HA для etcd.
Компоненты рабочего узла
- Container Runtime - среда запуска контейнеров (Docker, CRI-O, containerd, rkt, rktlet)
- kubelet - агент, управляемый с мастер-ноды, получает определения запускаемых подов, взаимодействует с Containter runtime с помощью Container Runtime Interface (CRI), мониторит состояние контейнеров. CRI состоит из ImageService, работающего с образами, и RuntimeService, работающего с подами и контейнерами.
- kube-proxy - сетевой агент, отвечающий за динамическое обновление, обслуживание и все сетевые правила.
- addons - сторонние добавления, не реализованные непосредственно в K8s, например, DNS, Dashboard, Monitoring, Logging.
Сеть
Из-за особенностей архитектуры микросервисов, настройка сетевого взаимодействия крайне важна и довольно сложна. Есть 4 слоя сетевых взаимодействий:
- Между контейнерами внутри пода (Container-to-container)
- Между подами на одной ноде или между нодами в кластере (Pod-to-Pod)
- Между подом и сервисом в одном пространстве имени (namespace) или между пространств имён в кластере (Pod-to-Service)
- Между внешними клиентами и сервисом (External-to-Service)
Все эти вещи должны быть определены до внедрения кластера K8s.
Container-to-container
При запуске каждого контейнера для него создается изолированное сетевое пространство (network namespace), которое называется сетевым пространством имён. Сетевое пространство имен совместно используется контейнерами вместе с операционной системой хоста. Когда запущен под, network namespace создаётся внутри него, и все контейнеры внутри делят это пространство и могут обращаться друг к другу через localhost.
Pod-to-Pod
В K8s поды распределяются между нодами случайным образом. Независимо от расположения подразумевается, что все поды могут общаться между собой в рамках кластера без использования NAT - это принципиальное требование к строению сети в K8s. В K8s поды рассматриваются как VM в сети, где каждая имеет свой IP-адрес. Эта модель называется IP-per-Pod и гарантирует то, что поды могут общаться друг с другом. Контейнеры внутри пода делят сетевое пространство имён и должны координировать назначенные порты равно как приложения внутри ОС. Тем не менее, контейнеры, равно как и вся сетевая модель K8s, использует Container Network Interface (CNI), поддерживающую плагины, некоторые из которых поддерживают сетевые политики. Container runtime разгружает систему, передавая назначение IP-адресов CNI, который соединяется с плагинами, такими как Bridge или MACvlan для назначения адреса. Как только адрес получен, CNI возвращает его обратно Container runtime.
Pod-to-External World
Для нормальной работы с контейнеризованными приложениями их надо опубликовать во внешний мир. В K8s это делается через сервисы (services), комплексные сущности, включающие в себя сетевые правила на кластерных нодах. Через публикацию сервисов посредством kube-proxy, приложения становятся доступны из внешнего мира по виртуальному IP.
Установка
4 основных конфигурации:
- Всё в одном - мастер и рабочая нода совмещены. Подходит для тестовых, учебных и т. п. целей. Пример - Minikube.
- 1-нодовый etcd, 1-мастер, много рабочих нод
- 1-нодовый etcd, мультимастер, много рабочих нод
- Мульти-etcd, мультимастер, много рабочих нод, рекомендован для продуктива.
Установка на локальную машину
- Minikube - single-node local Kubernetes cluster (рекомендуется)
- Docker Desktop - single-node local Kubernetes cluster for Windows and Mac
- CDK on LXD - multi-node local cluster with LXD containers.
Локально на VM
VMs могут быть созданы Vagrant, VMware vSphere, KVM или чем-то другим. Есть разные программы автоматизации установки, такие как Ansible или kubeadm.
Локально на голое железо
Поверх ОС, таких как RHEL, CoreOS, CentOS, Fedora, Ubuntu и т. д. Большинство программ автоматизации для VM подходят и здесь.
Устанавливать можно также на хостинге, на Turnkey Cloud Solutions, на Turnkey On-Premise Solutions.
Программы для установки
- kubeadm - рекомендованный способ создания single- or multi-node Kubernetes cluster. Не поддерживает provisioning of hosts.
- kubespray - AWS, GCE, Azure, OpenStack, or bare metal.
- kops
- kube-aws
- Можно ставить from scratch, если ничего не подходит.
Требования для minikube
- kubectl - управление кластером K8s. Ставится отдельно.
- Гипервизор 2-го типа, типа Hyper-V. Minikube поддерживает опцию –vm-driver=none, позволяющую ставить K8s не в гипервизор, а на локальную машину, но тогда там должен быть установлен Docker (а гипервизор - не должен), и должен быть определена bridge network for Docker. Иначе при перезапуске сети связь с кластером может потеряться.
- В BIOS нужно включить VT-x/AMD-v.
- При первом запуске Minikube нужен выход в интернет для скачивания необходимых пакетов, а позже - для скачивания образов.
# Поставить в Hyper-V, движок CRI-O minikube.exe start --container-runtime=cri-o --driver=hyperv # состояние minikube status # зайти внутрь виртуалки minikube ssh
https://kubernetes.io/docs/tasks/tools/install-minikube/
# Т. к. докер не установлен, при попытке вывести список контейнеров будет ошибка: $ sudo docker container ls Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? # В CRI-O другая команда: $ sudo runc list
Доступ к системе
- kubectl - CLI tool
- dashboard - web-based user interface
- curl - access the cluster via APIs
API делится на 3 группы:
- Core Group (/api/v1) - содержит объекты: поды, сервисы, ноды, пространства имён, карты конфигурации, секреты и т. д.
- Named Group (/apis/$NAME/$VERSION). Объекты содержат уровни: alpha level, beta level, stable level
- System-wide (/healthz, /logs, /metrics, /ui и т. д.)
Доступ к API может быть непосредственным или через CLI/Web UI.
kubectl обычно ставится до minicube, но можно ставить и после. После установки kubectl получает свою конфигурацию от minikube автоматически (в иных случаях может понадобиться настройка). Рекомендуется версию kubectl иметь ту же, то и версию minikube.
Конфиг лежит в ~/.kube.
# Вывести на экран: kubectl config view # Информация о кластере: kubectl cluster-info # Включить и открыть веб-панель minikube dashboard # kubectl proxy публикует веб-панель наружу, блокируя терминал до своего завершения.
Если просто запустить kubectl proxy, то публикуется порт 8001, через который можно обратиться к API, например,
curl http://localhost:8001/ (выведет список API endpoints), можно обратиться к конкретной ветке:
http://localhost:8001/api/v1
http://localhost:8001/apis/apps/v1
http://localhost:8001/healthz
http://localhost:8001/metrics
Чтобы обратиться к API без запуска kubectl proxy, нужна аутентификация через Bearer Token или предоставив набор ключей и сертификатов. Bearer Token генерируется на мастер-ноде и возвращается клиенту, который, используя его, может подключиться к ресурсам K8s без предоставления доп. информации.
# Get the token: $TOKEN=$(kubectl describe secret -n kube-system $(kubectl get secrets -n kube-system | grep default | cut -f1 -d ' ') | grep -E '^token' | cut -f2 -d':' | tr -d '\t' | tr -d " ") # Get the API server endpoint: $APISERVER=$(kubectl config view | grep https | cut -f 2- -d ":" | tr -d " ") # Confirm that the APISERVER stored the same IP as the Kubernetes master IP by issuing the following 2 commands and comparing their outputs: $ echo $APISERVER https://192.168.99.100:8443 $ kubectl cluster-info Kubernetes master is running at https://192.168.99.100:8443 ... # Access the API server using the curl command, as shown below: $ curl $APISERVER --header "Authorization: Bearer $TOKEN" --insecure { "paths": [ "/api", "/api/v1", "/apis", "/apis/apps", ...... ...... "/logs", "/metrics", "/openapi/v2", "/version" ] }
Помимо токена, можно извлечь клиентский сертификат, ключ и данные об удостоверяющем центре из файла .kube/config, закодировать и пустить через curl:
curl $APISERVER --cert encoded-cert --key encoded-key --cacert encoded-ca
Объекты K8s
Объектная модель K8s представляет различные постоянные сущности в кластере. Эти сущности описывают:
- Какие приложения запущены и на какой ноде
- Потребление ресурсов
- Политики приложений: перезапуск и обновление, отказоустойчивость и т. д.
Для каждого объекта определяется секция spec, где описывается желаемое состояние (desired state). Кластер отслеживает статус объекта (status) и сравнивает текущее состояние (actual state) с желаемым. Описание идёт в формате YAML, которое kubectl конвертирует в JSON и посылает API.
Пример:
# required - API endpoint, к которому идёт подключение, должен соответствовать существующему типу apiVersion: apps/v1 # required - тип объекта (варианты: Pod, Replicaset, Namespace, Service и т. д.) kind: Deployment # required - осн. информация: имя, ярлыки, пространство имён и т. д. metadata: name: nginx-deployment labels: app: nginx # required - desired state of the Deployment object spec: replicas: 3 selector: matchLabels: app: nginx # Pods template, удаление вышестоящих значений metadata и labels template: metadata: labels: app: nginx # desired state of the Pod spec: containers: - name: nginx image: nginx:1.15.11 ports: - containerPort: 80
После создания объекта Deployment, K8s определяет его статус, т. е., actual state.
Pods
Под - это наименьшая логическая единица, содержащая один или несколько контейнеров, которые:
- Помещаются на одну ноду в рамках пода
- Делят общее сетевое пространство
- Имеют доступ к одним и тем же внешним хранилищам (volumes)
Поды не умеют сами восстанавливаться (self-heal), поэтому нуждаются во внешних управляющих их состоянием контроллерах, таких как Deployments, ReplicaSets, ReplicationControllers и т. д. Выше был пример вложенной в Deployment спецификации пода, описанной в шаблоне (Template). Вот пример конфигурации пода:
apiVersion: v1 kind: Pod metadata: name: nginx-pod labels: app: nginx spec: containers: - name: nginx image: nginx:1.15.11 ports: - containerPort: 80
Labels
Ярлыки нужны для организации и выбора объектов, разные объекты могут иметь одинаковые ярлыки. Имеют вид ключ:значение. Ключей два: app и env. Например,
app=frontend env=qa app=backend env=dev
Селекторы ярлыков - используются контроллерами. Есть 2 типа:
- Equality-Based Selectors - позволяет фильтровать объекты по ключам и значениям ярлыков. = или == равно, != не равно. Например, env==dev или env=dev выбирает объекты, где env равен dev.
- Set-Based Selectors - позволяет фильтровать объекты по наборам значений. In, notin для значений, exist/does not exist для ключей. Например, env in (dev,qa) выбираются объекты, где env равен либо dev, либо qa. !app выбирает объекты без ключа app.
ReplicationControllers
ReplicationControllers регулируют требуемое кол-во запущенных подов в кластере, уничтожая лишние или запуская недостающие. Обычно поды не запускаются независимо, так как они не могут сами перезапуститься в случае ошибок. Поэтому рекомендуется использовать какой-либо тип контроллера репликации для создания подов и управления ими.
Стандартный контроллер - это Deployment, настраивающий ReplicaSet для управления жизненным циклом подов.
ReplicaSets
ReplicaSet - контроллер репликации нового поколения. Поддерживает оба селектора ярлыков (equality- and set-based selectors), в то время как ReplicationControllers поддерживают только equality-based. В настоящий момент, это единственное различие между ними.
Масштабирование подов может быть реализовано вручную или автоматически (autoscaler).
ReplicaSets сами могут быть использованы независимо как Pod controllers, но они предоставляют ограниченный функционал. Рекомендуемый способ оркестровки подов - это Deployments, которые управляют созданием, удалением и обновлением подов. Deployment автоматически создаёт ReplicaSet, которые, в свою очередь, создают поды, поэтому нет нужды управлять ReplicaSets и подами раздельно.
Deployments
Объекты Deployment занимаются обновлением (declarative updates) подов и ReplicaSets. DeploymentController - один из компонентов мастер-ноды, который обеспечивает соответствие текущего состояния желаемому. Позволяет бесшовные обновления (или их откат), и непосредственно управляет ReplicaSets для масштабирования.
Например, новый Deployment создаёт ReplicaSet A, который создаёт 3 пода, где каждый шаблон пода настроен запускать один контейнер nginx:1.7.9. В этом случае, ReplicaSet A связан с nginx:1.7.9, представляя состояние Deployment-а. В частности, это состояние записано как Revision 1.
Если в Deployment поменять шаблон подов, к примеру, обновить там версию на nginx:1.9.1, то Deployment создаст ReplicaSet B для новых контейнеров, созданных из образа 1.9.1, и это представление будет новой записью состояния - Revision 2. Незаметный переход между двумя ReplicaSets (ReplicaSet A v. 1.7.9 → ReplicaSet B v. 1.9.1 или Revision 1 → Revision 2) называется Deployment rolling update.
Обновление запускается, когда обновляется шаблон подов в развёртывании. Масштабирование или присвоение ярлыков на запускает обновления, так как не меняет номер ревизии.
Когда обновление завершено Deployment показывает обе ReplicaSets (A и B), где A содержит 0 подов, а B - 3. Таким образом Deployment сохраняет своё предыдущее состояние, как ревизии. Как только ReplicaSet B и его 3 пода версии 1.9.1 готовы, Deployment начинает активно ими управлять. Тем не менее, Deployment хранит своё предыдущее состояние, обеспечивая возможность отката на предыдущую ревизию.
Namespaces
Если пользователей или команд, использующих кластер, много, то можно разграничить кластер на виртульные кластеры, используя пространства имён. Имена ресурсов и объектов внутри одного пространства имён уникальны, но не в рамках всех пространств имён.
Перечислить все пространства имён:
$ kubectl get namespaces NAME STATUS AGE default Active 24h kube-node-lease Active 24h kube-public Active 24h kube-system Active 24h kubernetes-dashboard Active 16h
Обычно Kubernetes создаёт 4 пространства:
- kube-system - содержит объекты, созданные K8s, в основном, агентами управления мастер-ноды
- kube-public - особое пространство, открытое для чтения всем, используется для предоставления открытой информации о кластере
- kube-node-lease - самое новое пространство, содержит информацию об использовании нод и пульс
- default - объекты и ресурсы, созданные администраторами и разработчиками. По умолчанию, подключение идёт к этому пространству
Хорошей практикой считается создавать больше пространств имён, если требуется разграничение по пользователям и командам. Чтобы разделить кластер на пространства имён, используются ресурсные квоты (Resource Quotas).
Пример обновления Deployment и его отката
# создать $ kubectl create deployment mynginx --image=nginx:1.15-alpine deployment.apps/mynginx created # вывести информацию о deployment, replicaset, pods под ярлыком app=mynginx $ kubectl get deploy,rs,po -l app=mynginx NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/mynginx 1/1 1 1 23s NAME DESIRED CURRENT READY AGE replicaset.apps/mynginx-76bcb97766 1 1 1 23s NAME READY STATUS RESTARTS AGE pod/mynginx-76bcb97766-rbnq8 1/1 Running 0 23s # Масштабировать до 3 экз. $ kubectl scale deploy mynginx --replicas=3 deployment.apps/mynginx scaled # инфо ещё раз - теперь будет 3 пода $ kubectl get deploy,rs,po -l app=mynginx NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/mynginx 3/3 3 3 11m NAME DESIRED CURRENT READY AGE replicaset.apps/mynginx-76bcb97766 3 3 3 11m NAME READY STATUS RESTARTS AGE pod/mynginx-76bcb97766-q9r49 1/1 Running 0 58s pod/mynginx-76bcb97766-rbnq8 1/1 Running 0 11m pod/mynginx-76bcb97766-s672f 1/1 Running 0 58s # Обратите внимание на номер replicaset (в данном случае, 76bcb97766). Теперь посмотрим на образ: $ kubectl describe deployment |grep "Image" Image: nginx:1.15-alpine # Номера ревизий деплоя nginx $ kubectl rollout history deploy mynginx deployment.apps/mynginx REVISION CHANGE-CAUSE 1 <none> # Детали 1-й ревизии. Видно номер replicaset и образ, с которыми ревизия связана. $ kubectl rollout history deploy mynginx --revision=1 deployment.apps/mynginx with revision #1 Pod Template: Labels: app=mynginx pod-template-hash=76bcb97766 Containers: nginx: Image: nginx:1.15-alpine Port: <none> Host Port: <none> Environment: <none> Mounts: <none> Volumes: <none> # Обновить образ деплоя $ kubectl set image deployment mynginx nginx=nginx:1.16-alpine deployment.apps/mynginx image updated # Теперь ревизий две, появилась ещё одна ReplicaSet, все три пода перешли под неё. # Откатить обновление обратно $ kubectl rollout undo deployment mynginx --to-revision=1 deployment.apps/mynginx rolled back # Ревизия 1 стала ревизией 3 $ kubectl rollout history deploy mynginx deployment.apps/mynginx REVISION CHANGE-CAUSE 2 <none> 3 <none>
Стандартная настройка позволяет последовательно обновиться 10 раз и откатиться на любую версию.
Аутентификация, авторизация, контроль входа
Каждый запрос к API проходит эти три стадии.
- Аутентификация - вход для пользователя
- Авторизация - пускает API-запросы от вошедшего пользователя
- Контроль входа (Admission Control) - модули, изменяющие или отклоняющие запросы на основании доп. проверок, например, квота.
Аутентификация
В K8s нет объекта user, и не хранится никаких логинов (usernames) и прочих связанных с этим данных. Тем не менее, K8s может использовать логины для входа. Есть 2 типа пользователей:
- Обычные пользователи (Normal Users) - управляются вне кластера, например, User/Client Certificates, a file listing usernames/passwords, Google accounts, etc.
- Служебные учётки (Service Accounts) - через них внутренние процессы кластера подключаются к API для выполнения разных операций. Большинство служебных учёток создаются автоматически самим API, но они также могут быть созданы и вручную. Служебные учётки связаны с выделенным им пространством имён и получают соответствующие права подключения к API как Секреты (as Secrets).
Поддерживается и анонимный доступ, в дополнение к вышеперечисленным. Также поддерживается и смена роли пользователя (User impersonation), что полезно при решении проблем с политикой авторизации (authorization policies).
Для аутентификации используются следующие модули:
- Клиентские сертификаты - указывается файл, содержащий один или несколько УЦ с опцией –client-ca-file=SOMEFILE для сервера API. УЦ, упомянутые в файле, должны удостоверять клиента, преставленного серверу API.
- Статический токен-файл - файл с заранее заданными bearer tokens, опция –token-auth-file=SOMEFILE для сервера API. В настоящее время эти токены не имеют срока действия и не могут быть изменены без перезапуска сервера API.
- Bootstrap-токены - этот способ пока на тестировании. Используется по большей части для bootstrapping нового K8s-кластера.
- Статический парольный файл - то же, что и статический токен-файл. Это файл с базовой информацией об аутентификации, опция –basic-auth-file=SOMEFILE. Также не имеют срока действия и не могут быть изменены без перезапуска сервера API.
- Токены служебных учёток - это автоматически включённый механизм, использующий подписанные bearer tokens для проверки запросов. Эти токены прикрепляются к подам, используя ServiceAccount Admission Controller, позволяющий процессам общаться с API.
- Токены OpenID - служат для подключения к OAuth2-провайдерам типа Azure Active Directory, Salesforce, Google и т. д. для снижения нагрузки путём выноса аутентификации на сторонние сервисы.
- Токены Webhook - выноспроверки bearer tokens на сторонний сервис.
- Аутентифицирующий прокси - если нужна какая-то доп. логика аутентификации.
Можно включить сразу несколько способов, и первый сработавший способ пускает внутрь (short-circuits the evaluation). Для успешной аутентификации нужно 2 компонента: the service account tokens authenticator and one of the user authenticators.
Авторизация
После успешной аутентификации, пользователи могут посылать запросы к API, и эти запросы авторизуются различными модулями K8s. Атрибуты запросов к API, рассматриваемые K8s - user, group, extra, Resource, Namespace и т. д. Атрибуты запросов фильтруются политиками, и в соответствии с этим запрос выполняется или нет.
Так же как и на этапе аутентификации, авторизация имеет несколько модулей, и для одного кластера может быть настроено сразу несколько. В таком случае, запрос будет проходить проверку поочерёдно в каждом модуле. Если какой-либо модуль одобрил или отклонил запрос, это и становится решением.
- Node Authorizer - спецавторизация для API-запросов от кублетов (kubelets).
- Attribute-Based Access Control (ABAC) Authorizer - предоставляет доступ запросам на основании политик и атрибутов. Например пользователю student доступны поды из пространства имён lfs158 только на чтение.
{ "apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": { "user": "student", "namespace": "lfs158", "resource": "pods", "readonly": true } }
Чтобы включить ABAC authorizer, нужно запустить API-сервер с ключом --authorization-mode=ABAC. Также, необходимо определить политику авторизации ключом --authorization-policy-file=PolicyFile.json.
- Webhook Authorizer - авторизация через сторонние сервисы. Чтобы запустить Webhook authorizer, нужно запустить API-сервер с ключом --authorization-webhook-config-file=SOME_FILENAME, где SOME_FILENAME - это конфигурация удалённого сервиса авторизации.
- Role-Based Access Control (RBAC) Authorizer - в основном, регулируется доступ к ресурсам на основе пользовательских ролей. В K8s субъектам, таким как users, service accounts и т. д., могут быть присвоены разные роли. Путём создания ролей, можно ограничивать доступ к разным операциям (отражены глаголами), таким как create, get, update, patch и т. д.
В RBAC существует два вида ролей:
- Role - предоставление доступа к ресурсам того или иного пространства имён
- ClusterRole - то же, что и Role, но в рамках всего кластера
Пример Role:
kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: namespace: lfs158 name: pod-reader rules: - apiGroups: [""] # "" indicates the core API group resources: ["pods"] verbs: ["get", "watch", "list"]
Здесь создаётся роль pod-reader, имеющая доступ на чтение подов в пространстве имён lfs158. После создания роли к ней можно привязать пользователей с помощью RoleBinding.
У RoleBindings также есть два типа:
- RoleBinding - привязывает пользователей к тому же пространству имён, как Role. Можно сослаться и на ClusterRole, в случае, если в рамках ClusterRole определены ресурсы пространства имён.
- ClusterRoleBinding - доступ на уровне кластера и всех пространств имён
Пример RoleBinding:
kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: pod-read-access namespace: lfs158 subjects: - kind: User name: student apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io
Выдава доступа пользователю student на чтение подов в пространстве имён lfs158.
Для включения RBAC authorizer нужно запустить API-сервер с ключом --authorization-mode=RBAC. RBAC authorizer конфигурирует политики автоматически.
Контроль входа
Admission control - настройка политик гранулярного доступа (granular access control policies), включающая разрешение привилегированных контейнеров, проверка квот ресурсов и т. д. Эти политики проводятся в жизнь контроллерами входа, такими как ResourceQuota, DefaultStorageClass, AlwaysPullImages и т. д., и действуют только после успешной аутентификации и авторизации API-запросов.
Для использования контроля входа, нужно запустить API-сервер в ключом --enable-admission-plugins, где контроллеры разделены запятыми:
--enable-admission-plugins=NamespaceLifecycle,ResourceQuota,PodSecurityPolicy,DefaultStorageClass
Некоторые контроллеры входа включены в K8s изначально.
Пример настройки RBAC
# создаётся namespace, к которому будет дан доступ $ kubectl create namespace lfs158 # создаётся каталог для сертификатов $ mkdir rbac $ cd rbac # генерация ключа и сертификата ~/rbac$ openssl genrsa -out student.key 2048 ~/rbac$ openssl req -new -key student.key -out student.csr -subj "/CN=student/O=learner" # Закодировать сертификат в base64 и добавить поле request в файл конфигурации .yaml ~/rbac$ cat student.csr | base64 | tr -d '\n' # создать файл конфигурации ~/rbac$ nano signing-request.yaml
apiVersion: certificates.k8s.io/v1beta1 kind: CertificateSigningRequest metadata: name: student-csr spec: groups: - system:authenticated request: <assign base64 value from the cat command> usages: - digital signature - key encipherment - client auth
# Создать объект запроса на подпись сертификата (certificate signing request object) ~/rbac$ kubectl create -f signing-request.yaml # Вывести список запросов, его состояние будет "ожидание" (pending) ~/rbac$ kubectl get csr # Подтвердить запрос ~/rbac$ kubectl certificate approve student-csr # Извлечь подтверждённый сертификат, декодировать его из base64 и сохранить в файл ~/rbac$ kubectl get csr student-csr -o jsonpath='{.status.certificate}' | base64 --decode > student.crt # Настроить права для student присвоением ключа и сертификата: ~/rbac$ kubectl config set-credentials student --client-certificate=student.crt --client-key=student.key # Создать новую запись context в файле клиентской конфигурации kubectl для student, связанную с пространством имён lfs158 кластера minikube: ~/rbac$ kubectl config set-context student-context --cluster=minikube --namespace=lfs158 --user=student # теперь, если вывести информацию, то там появится контекст, связанный со student: ~/rbac$ kubectl config view # Создать новый деплой в пространстве имён lfs158 ~/rbac$ kubectl -n lfs158 create deployment nginx --image=nginx:alpine # если сейчас попробовать вывести список подов из контекста student-context, то будет ошибка, # т. к. в нём не настроены разрешения для student ~/rbac$ kubectl --context=student-context get pods # Создать конф. файл для ролевого объекта pod-reader, который позволяет get, watch, list в lfs158 для подов. ~/rbac$ vim role.yaml
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: pod-reader namespace: lfs158 rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "watch", "list"]
# Создать ролевой объект ~/rbac$ kubectl create -f role.yaml # Вывести его из изначального контекста minikube, но из пространства имён lfs158: ~/rbac$ kubectl -n lfs158 get roles # Создать конф. файл YAML для объекта rolebinding, присваивающий разрешения роли pod-reader для student. ~/rbac$ nano rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: pod-read-access namespace: lfs158 subjects: - kind: User name: student apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io
# Создать объект rolebinding ~/rbac$ kubectl create -f rolebinding.yaml # Вывести его из изначального контекста minikube, но из пространства имён lfs158: ~/rbac$ kubectl -n lfs158 get rolebindings # Теперь разрешения присвоены, и список подов успешно выводится ~/rbac$ kubectl --context=student-context get pods
Сервисы
Хотя архитектура микросервисов нацелена на разделение компонентов приложения, эти микросервисы нуждаются в агентах, логически связывающих, группирующих их в одно целое, и балансировать трафик среди них.
Сервисы используются для группировки подов и предоставления доступа к контейнеризованному приложению извне. Для доступа используется kube-proxy daemon, работающий на каждой рабочей ноде. Обнаружение сервиса (Service discovery) и типы сервиса (service types) определяют уровень доступа (access scope) к сервису.
Чтобы получить доступ к приложению, клиент должен подключаться к подам. Так как поды эфемерны, IP-адреса у них не могут быть постоянными, т. к. поды появляются и исчезают в произвольный момент. Чтобы решить эту проблему, K8s предоставляет более высокий уровень абстракции - сервисы, которые логически группируют поды и определяет политику доступа к ним.
Группировка осуществляется через ярлыки и селекторы. Например, есть поды с ярлыками app:frontend и app:db, и используя селекторы select app==frontend и select app==db, поды группируются в логические наборы. Логические наборы, имеющие имя, и есть сервисы, в данном случае, frontend-svc и db-svc.
Сервисы могут представлять одиночные поды, ReplicaSets, Deployments, DaemonSets и StatefulSets.
Пример сервиса:
kind: Service apiVersion: v1 metadata: name: frontend-svc spec: selector: app: frontend ports: - protocol: TCP port: 80 targetPort: 5000
В данном случае создаётся сервис frontend-svc, в который входят все поды с ярлыком app:frontend. Стандартно каждый сервис получает IP-адрес, доступный только внутри кластера (ClusterIP). Здесь это 172.17.0.4 (frontend-svc) и 172.17.0.5 (db-svc).
Теперь клиент подключается к сервису через ClusterIP, который перенаправляет трафик на один из своих подов, балансируя трафик между ними.
Можно также указать порт пода, который получает трафик. В примере, frontend-svc получает трафик на порт 80 и затем пробрасывает его на targetPort 5000 пода. Если targetPort не указан, то трафик будет проброшен на тот же номер порта, на котором был принят.
Логический набор IP-адресов подов вместе с targetPort называется конечной точкой сервиса (Service endpoint). В примере, у frontend-svc 3 конечных точки: 10.0.1.3:5000, 10.0.1.4:5000 и 10.0.1.5:5000. Они создаются и управляются автоматически сервисом, а не администратором кластера.
kube-proxy
На всех рабочих нодах есть служба kube-proxy, которая смотрит на API-сервер мастер-ноды за добавлением или удалением сервисов и конечных точек. В примере, для каждого нового сервиса на каждой ноде kube-proxy настраивает правила iptables для захвата трафика с ClusterIP и перенаправления его на одну из конечных точек. Таким образом, любая нода может получать внешний трафик и затем маршрутизировать его внутри кластера на основе правил iptables. Когда сервис удаляется, kube-proxy удаляет связанные с ним правила iptables.
Service Discovery
Так как сервисы - это первичный режим связи в K8s, нужен способ их обнаружения. Их два:
- Переменные окружения (Environment Variables) - так как поды стартуют на любой ноде, служба kubelet добавляет набор переменных окружения в под для всех активных сервисов. Например, если есть активный сервис под названием redis-master, опубликованный на порту 6379 и ClusterIP 172.17.0.6, то в свежесозданном поде будут следующие переменные окружения:
REDIS_MASTER_SERVICE_HOST=172.17.0.6 REDIS_MASTER_SERVICE_PORT=6379 REDIS_MASTER_PORT=tcp://172.17.0.6:6379 REDIS_MASTER_PORT_6379_TCP=tcp://172.17.0.6:6379 REDIS_MASTER_PORT_6379_TCP_PROTO=tcp REDIS_MASTER_PORT_6379_TCP_PORT=6379 REDIS_MASTER_PORT_6379_TCP_ADDR=172.17.0.6
В этом случае, надо быть осторожным во время настройки (ordering) сервисов, потому что поды не имеют переменных тех сервисов, которые появились после создания подов.
- DNS - в K8s есть дополнение DNS, создающее запись DNS для каждого сервиса в формате my-svc.my-namespace.svc.cluster.local. Сервисы могут найти другие сервисы в рамках пространства имён просто по имени. Если добавить сервис redis-master в пространстве имён my-ns, все поды в одном пространстве имён находят сервис по имени, redis-master. Поды из других пространств имён находят этот сервис, добавляя к имени суффикс - redis-master.my-ns.
Это общепринятая и рекомендуемая практика. В примере выше видно, что настроен внутренний DNS, где frontend-svc привязан к 172.17.0.4, а db-svc - к 172.17.0.5.
ServiceType
Во время создания сервиса, выбирается область доступа (access scope). Варианты:
- Доступен только в рамках кластера
- Доступен в рамках кластера и извне
- Сопоставляется с объектом (entity), который находится внутри или снаружи кластера
Область доступа определяется ServiceType, который может быть настроен во время создания сервиса.
Стандартный ServiceType - это ClusterIP, виртуальный адрес сервиса, который используется для связи с сервисом и доступен только в рамках кластера.
NodePort
NodePort ServiceType - это динамически выбираемый порт (стандартно из диапазона 30000-32767), который присваивается сервису на всех рабочих нодах. Например, если NodePort равен 32233 для сервиса frontend-svc, значит, если подключиться к любой рабочей ноде на порт 32233, то она перенаправит трафик на ClusterIP 172.17.0.4. Если нужно указать порт вручную, его можно присвоить из стандартного диапазона.
NodePort используется для публикации сервиса во внешний мир. Клиент подключается к любой рабочей ноде на указанный порт, и дальше его запросы пробрасываются через ClusterIP сервиса к приложениям. Чтобы получить доступ извне к нескольким приложениям, можно настроить обратный прокси - ingress, и задать правила для доступа к сервисам кластера.
LoadBalancer
С сервис-типом балансировщика:
- NodePort and ClusterIP создаются автоматически, и внешний балансировщик будет распределять трафик между ними
- Сервис занимает статический порт на каждой рабочей ноде
- Опубликованный сервис использует внешний балансировщик облачного провадера
LoadBalancer ServiceType будет работать, только если нижележащая инфраструктура поддерживает автоматическое создание балансировщиков и K8s, например, Google Cloud Platform и AWS. Если эта функция на настроена, IP-адрес балансировщика не задан - сервис будет работать как NodePort type.
ExternalIP
Сервис может быть привязан ко внешнему IP, если он может маршрутизировать на одну или несколько рабочих нод. Трафик, допущенный до кластера с внешнего адреса на сервис-порт, пробрасывается до одной из конечных точек. Этот тип требует использования внешних облачных провайдеров, таких как Google Cloud Platform или AWS.
Помните, что внешние адреса (ExternalIPs) не управляются K8s. Администратор кластера должен настроить маршрутизацию, привязывающую внешний адрес к одной из нод.
ExternalName
Внешнее имя - особый тип, у которого нет селекторов (Selectors) и не задаются никакие конечные точки(endpoints). Когда к кластеру идёт обращение на ExternalName, оно возвращает запись CNAME внешнего сервиса.
Основное назначение ServiceType - сделать внешние сервисы типа my-database.example.com доступными для приложение внутри кластера. Если внешний сервис принадлежит к тому же пространству имён, для всех приложений и сервисов этого пространства имён он будет доступен просто по имени my-database.
Развёртывание приложения
Используя панель управления (dashboard).
# Запустить dashboard - стандартно оно запускается в пространстве имён default minikube dashboard # Если не открывается в браузере, то можно посмотреть ссылку в консоли, она будет чем-то вроде # Opening http://127.0.0.1:55304/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/
В интерфейсе нажать кнопку + (create new resource), потом Create from form.
- application name webserver
- Container image nginx:alpine
- Number of Pods 3
- Service none (он будет создан позже).
Show advanced options - там можно задать Labels, Namespace, Environment Variables и т. д. Стандартно, ярлык app совпадает с именем приложения (в данном случае будет k8s-app: webserver).
После нажатия кнопки Deploy запустится развёртывание. Как и ожидалось, деплой webserver создал ReplicaSet (webserver-5d58b6b749), который, в свою очередь, создал три пода (webserver-5d58b6b749-xxxxx).
Если не работает простое название образа nginx:alpine, надо добавить полный URL docker.io/library/nginx:alpine или k8s.gcr.io/nginx:alpine.
После того, как деплой «webserver» создан, на панели слева можно посмотреть детали про Deployments, ReplicaSets и Pods в пространстве имён «default». То же можно сделать и из командной строки.
kubectl get deployments
kubectl get replicasets
kubectl get pods
# Подробности про отдельный под
kubectl describe pod webserver-5d58b6b749-ffrhq
В подробностях про под есть информация о ярлыках, в данном случае это k8s-app=webserver.
# С ключом -L в вывод добавляются соответствующие колонки, здесь - k8s-app и label2 kubectl get pods -L k8s-app,label2 # В данном случае в колонке k8s-app будет значение webserver, а label2 пуста. # Ключ -l - использование селектора. Вывести все поды с ключом k8s-app и значением webserver kubectl get pods -l k8s-app=webserver
Установить приложение через CLI
# сначала удалим старый экземпляр, созданный через панель kubectl delete deployments webserver # после удаления деплоя удалятся и наборы реплик, и поды. Команды ничего не покажут kubectl get replicasets kubectl get pods
Теперь надо сделать файл, например, webserver.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: webserver labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80
# создать деплой из файла (можно использовать URL) kubectl create -f webserver.yaml # теперь снова есть и наборы реплик, и поды.
Публикация приложения
Ранее были рассмотрены ServiceTypes, они задают способ доступа к сервису. NodePort ServiceType - статический порт на всех рабочих нодах, при подключении на который запрос проксируется на ClusterIP сервиса.
Конфигурация, к примеру, webserver-svc.yaml
apiVersion: v1 kind: Service metadata: name: web-service labels: run: web-service spec: type: NodePort ports: - port: 80 protocol: TCP selector: app: nginx
# применить kubectl create -f webserver-svc.yaml # или опубликовать уже существующий деплой: kubectl expose deployment webserver --name=web-service --type=NodePort # список сервисов kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 8h web-service NodePort 10.107.77.9 <none> 80:30168/TCP 2m8s
web-service теперь создан, и его ClusterIP is 10.107.77.9. В секции PORT(S) видно, что статический порт на нодах - 30168. Если подключиться на этот порт к ноде, запрос будет переадресован на ClusterIP, порт 80.
Нет нужды сначала создавать Deployment, а затем сервис, их можно создавать в любом порядке. Сервис найдёт и подключит поды с помощью селектора. Для получения детальной информации о сервисе, например, web-service, нужно набрать команду
kubectl describe service web-service
В данном случае, web-service использует app=nginx как селектор для логической группировки трёх подов, которые показаны как конечные точки (endpoints). Когда запрос достигает сервиса, он обслуживается одним из подов, перечисленных в секции endpoints.
Доступ к приложению
Наше приложение работает на виртуальной машине minikube. Чтобы узнать её IP, нужно выполнить команду
minikube ip
Затем нужно открыть браузер и вбить туда этот адрес и тот порт, который отображался при выводе списка сервисов. Например, http://192.168.224.6:30401. Есть способ открыть сервис в браузере, выполнив команду
minikube service web-service
В данном случае, отобразится стартовая страница nginx внутри одного из подов-конечных точек, куда сервис перенаправил запрос, действуя как балансировщик.
Проверки состояния и готовности
Иногда приложения могут не отвечать на запросы или запускаться с задержкой. Добавление проверок на состояние и готовность (Readiness and Liveness Probes) позволяют сервису kubelet отслеживать состояние приложения, запущенного внутри контейнера пода и принудительно перезапускать этот контейнер, если приложение не отвечает. Если настраиваются обе проверки, то рекомендуется дать достаточно времени, чтобы Readiness Probe не прошла проверку, и только потом проверять Liveness Probe. Если и Readiness, и Liveness Probes не проходят проверку, значит есть шанс того, что контейнер никогда не достигнет состояния готовности.
Liveness Probe
Когда приложение в контейнере внутри пода не отвечает на запросы, это может произойти, например, при зависании или нехватке памяти. В этом случае, рекомендуется перезапустить контейнер, чтобы приложение снова было доступно. Чтобы не перезапускать его вручную, нужно настроить Liveness Probe, которая проверяет «здоровье» приложения, и в случае чего перезапускает контейнер.
Liveness Probes устанавливаются путём настройки:
- Liveness command
- Liveness HTTP request
- TCP Liveness Probe
Liveness Command
Здесь проверяется наличие файла /tmp/healthy
apiVersion: v1 kind: Pod metadata: labels: test: liveness name: liveness-exec spec: containers: - name: liveness image: k8s.gcr.io/busybox args: - /bin/sh - -c - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600 livenessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 5 periodSeconds: 5
Проверка наличия этого файла происходит каждые 5 сек (periodSeconds). Параметр initialDelaySeconds говорит kubelet ждать 5 сек перед первой проверкой. Командная строчка сначала создаёт файл, а через полминуты стирает его, что вызывает непрохождение проверки, и под перезапускается.
Liveness HTTP request
kubelet шлёт запрос HTTP GET на конечную точку /healthz приложения, на порт 8080. Если не отвечает - перезапуск контейнера.
livenessProbe: httpGet: path: /healthz port: 8080 httpHeaders: - name: X-Custom-Header value: Awesome initialDelaySeconds: 3 periodSeconds: 3
TCP Liveness probe
kubelet пытается открыть TCP Socket контейнера с приложением. Не открывается - перезапуск контейнера.
livenessProbe: tcpSocket: port: 8080 initialDelaySeconds: 15 periodSeconds: 20
Readiness Probes
Иногда приложения должны соответствовать определённым требованиям, прежде чем начать работать. Например, убедиться, что зависимый сервис готов к работе, или определить необходимость загрузки большого набора данных. В этом случае используются проверки готовности. Пока они не пройдены, приложение (под с контейнерами) не получает рабочий трафик.
readinessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 5 periodSeconds: 5
Readiness Probes настраиваются так же, как и Liveness Probes, см. документацию.
Управление томами (Volumes)
Контейнеры могут быть и производителями, и потребителями данных. Сами контейнеры - временная сущность, но некоторые данные, с которыми они работают, обязаны храниться постоянно. Для этого K8s обязан предоставлять хранилище, и он использует тома (Volumes) нескольких типов, и несколько других форм хранилищ для работы с данными контейнера.Рассмотрим объекты PersistentVolume и PersistentVolumeClaim, которые подключают постоянно действующие тома (persistent storage Volumes) к подам.
При перезапуске контейнера, все данные внутри него удаляются. Чnобы не терять информацию, в K8s используются тома (Volumes) - внешний каталог на устройстве хранения данных. Это хранилище данных, содержимое и режим доступа к нему определяются типом тома (Volume Type).
Том подключается к поду и может использоваться всеми контейнерами внутри него. Том живёт столько же, сколько под, но переживает контейнеры внутри него, что позволяет сохранить данные в случае перезапуска контейнеров.
Некоторые типы томов
- emptyDir - Пустой том, создающийся во время запуска пода на рабочей ноде. Жёстко зависим от жизни пода - если под перестаёт существовать, содержимое emptyDir безвозвратно удаляется.
- hostPath - Каталог на хосте. Если под удаляется, данные остаются.
- gcePersistentDisk - монтирует диск Google Compute Engine (GCE) в под.
- awsElasticBlockStore - монтирует AWS EBS Volume в под.
- azureDisk - монтирует Microsoft Azure Data Disk.
- azureFile - монтирует Microsoft Azure File Volume.
- cephfs - монтирует существующий CephFS volume. Если под удаляется, том отмонтируется, и данные остаются.
- nfs - монтирует NFS share.
- iscsi - монтирует iSCSI share.
- secret - передаёт поду секретные данные, например, пароли.
- configMap - предоставляет поду данные конфигурации, консольные команды и аргументы.
- persistentVolumeClaim - подключает PersistentVolume к поду.
PersistentVolumes
В типичной ситуации, хранилище управляется сисадминами, а пользователь получает инструкции по его использованию и хранилищем не управляет. Тут примерно то же самое, но управление становится трудоёмким, учитывая количество типов томов. В K8s есть подсистема PersistentVolume (PV), предоставляющая различные API для пользователей и админов, чтобы пользоваться и управлять постоянным хранилищем (persistent storage). Для управления томом, существует PersistentVolume API resource type, а для его использования - PersistentVolumeClaim API resource type.
Persistent Volume - это сетевое хранилище внутри кластера, предоставляемое администратором.
PersistentVolumes могут динамически предоставляться на основе StorageClass resource. StorageClass содержит предустановленных «снабженцев» (provisioners) и параметры для создания PersistentVolume. Используя PersistentVolumeClaims, пользователь посылает запрос на динамическое создание PV, которое связано с ресурсом StorageClass.
Некоторые типы томов, поддерживающих управление хранилищем с использованием PersistentVolumes:
- GCEPersistentDisk
- AWSElasticBlockStore
- AzureFile
- AzureDisk
- CephFS
- NFS
- iSCSI
PersistentVolumeClaims
PersistentVolumeClaim (PVC) - это запрос на предоставление хранилища от пользователя. Запросы на PersistentVolume базируются на типе, режиме доступа и размере.
Есть 3 режима доступа:
- ReadWriteOnce (чтение-запись одной нодой)
- ReadOnlyMany (только чтение многими нодами)
- ReadWriteMany (чтение-запись многими нодами)
Как только подходящий PersistentVolume найден, он привязывается к PersistentVolumeClaim. После успешной привязки, ресурс PersistentVolumeClaim может использоваться подом.
После завершения работы пользователем, прикреплённые PersistentVolumes могут быть освобождены. Нижестоящие PersistentVolumes, следовательно, могут быть снова задействованы либо для проверки или сбора данных админом, либо для удаления тома вместе с данными, либо подготовлены для будущего использования - будет удалены только данные.
Container Storage Interface (CSI)
Разные оркестраторы контейнеров - Kubernetes, Mesos, Docker или Cloud Foundry имеют собственные методы управления внешними хранилищами при использовании Volumes. Для производителей хранилищ, трудно поддерживать разные плагины томов для разных оркестраторов. Производители и участники сообществ различных оркестраторов работают над стандартизацией интерфейса Volume; плагин тома написан по стандарту CSI, разработанном для разных оркестраторов.
К версии K8s v1.13 CSI стабилизировался, что позволяет устанавливать новые CSI-совместимые тома очень просто. С CSI можно расширять набор поддерживаемых storage providers без необходимости интегрировать их в код самого K8s.
Using a Shared hostPath Volume Type
В этом примере показано, как два контейнера в поде делят один и тот же том, и как данные на томе переживают под.
Сначала нужно создать каталог на хосте, в данном, случае, внутри minikube VM:
minikube ssh mkdir pod-volume cd pod-volume pwd # Путь /home/docker/pod-volume будет использован в конфигурации hostPath volume type # выйти из VM exit
Файл конфигурации:
apiVersion: v1 kind: Pod metadata: name: share-pod labels: app: share-pod spec: volumes: - name: host-volume hostpath: path: /home/docker/pod-volume containers: - image: nginx name: nginx ports: - containerPort: 80 volumeMounts: - mountPath: /usr/share/nginx/html name: host-volume - image: debian name: debian volumeMounts: - mountPath: /host-volume name: host-volume command: ["/bin/sh", "-c", "echo Testing hostPath Volume Type > /host-vol/index.html; sleep 3600"]
Два контейнера (nginx и debian) подключаются к заданному здесь же тому hostPath, и с контейнера debian создаётся содержимое index.html, которое отобразит nginx вместо своей стандартной страницы.
# запустить конфигурацию kubectl create -f share-pod.yaml # поглядеть, запустилось ли kubectl get pods # опубликовать на NodePort 80 kubectl expose pod share-pod --type=NodePort --port=80 # посмотреть инфу kubectl get services,endpoints # открыть в браузере, страница nginx должна быть изменена minikube service share-pod # грохнуть share-pod kubectl delete pod share-pod # Под удалился, kubectl get pods # но сервис живой, хоть и без IP-адреса, т. к. под удалён kubectl get services,endpoints
Рисуем новую конфигурацию - один контейнер nginx, который будет читать данные с того же тома:
apiVersion: v1 kind: Pod metadata: name: check-pod labels: app: share-pod spec: volumes: - name: check-volume hostPath: path: /home/docker/pod-volume containers: - image: nginx name: nginx ports: - containerPort: 80 volumeMounts: - mountPath: /usr/share/nginx/html name: check-volume
# Запуск kubectl create -f check-pod # поглядеть, запустилось ли kubectl get pods # посмотреть инфу kubectl get services,endpoints # Сервис share-pod всё так же отображается и снова получил IP-адрес, т. к. в сервисе появился новый под. # Запустить сервис (он уже был ранее опубликован наNodePort) в браузере minikube service share-pod
Результат - nginx прочитал данные с тома и снова показывает изменённую на первом этапе страницу приветствия.
ConfigMaps и Secrets
Во время развёртывания приложения бывает нужно передать ему параметры конфигурации, разрешения, пароли, токены и т. д. К примеру, необходимо развернуть 10 приложений для заказчиков, и для каждого заказчика в интерфейсе должно отображаться название компании. Вместо того, чтобы задавать 10 разных докер-образов, можно просто использовать шаблонный образ и передать названия компаний заказчиков как параметры запуска (runtime parameters). В таких случаях используется ConfigMap API resource. Если нужно передать секретную информацию, используется Secret API resource.
ConfigMaps
ConfigMaps позволяет отделить конфигурацию от образа контейнера. Конфигурация передаётся как пары «ключ-значение», воспринимаемые подами или другими компонентами или контроллерами в форме переменных окружения, наборов команд и аргументов или томов. Можно создавать ConfigMaps из буквальных значений, из конфигурационных файлов, из одного или нескольких файлов или каталогов.
Создание ConfigMap из буквальных значений (Literal Values) и отображение подробностей
ConfigMap может быть создан командой kubectl create, а подробности можно посмотреть командой kubectl get.
# Создать kubectl create configmap my-config --from-literal=key1=value1 --from-literal=key2=value2 # Подробности kubectl get configmaps my-config -o yaml apiVersion: v1 data: key1: value1 key2: value2 kind: ConfigMap metadata: creationTimestamp: 2019-05-31T07:21:55Z name: my-config namespace: default resourceVersion: "241345" selfLink: /api/v1/namespaces/default/configmaps/my-config uid: d35f0a3d-45d1-11e7-9e62-080027a46057
Опция -o yaml - вывод в YAML-формате. Как можно видеть, объект имеет тип ConfigMap, и внутри него есть пары «ключ-значение». Имя и прочие детали являются частью поля metadata.
Создание ConfigMap из файла конфигурации
Сначала нужно создать файл конфигурации, где указаны тип, метаданные, поля данных, нацеленные на конечную точку v1 на API-сервере:
apiVersion: v1 kind: ConfigMap metadata: name: customer1 data: TEXT1: Customer1_Company TEXT2: Welcomes You COMPANY: Customer1 Company Technology Pct. Ltd.
Если имя файла конфигурации customer1-configmap.yaml создать ConfigMap можно следующей командой:
kubectl create -f customer1-configmap.yaml
Создание ConfigMap из файла
Создаётся файл permission-reset.properties следующего содержания:
permission=read-only allowed="true" resetCount=3
Затем создаётся ConfigMap:
kubectl create configmap permission-config --from-file=<path/to/>permission-reset.properties
Использование ConfigMaps внутри подов
Как переменные окружения
Внутри контейнера можно запрашивать данные «ключ-значение» всей ConfigMap или значения отдельных ключей как переменные окружения.
В следующем примере все переменные окружения контейнера myapp-full-container получают значения ключей full-config-map:
... containers: - name: myapp-full-container image: myapp envFrom: - configMapRef: name: full-config-map ...
А здесь все переменные окружения контейнера myapp-specific-container получают значения от отдельных пар «ключ-значение» разных ConfigMaps:
... containers: - name: myapp-specific-container image: myapp env: - name: SPECIFIC_ENV_VAR1 valueFrom: configMapKeyRef: name: config-map-1 key: SPECIFIC_DATA - name: SPECIFIC_ENV_VAR2 valueFrom: configMapKeyRef: name: config-map-2 key: SPECIFIC_INFO ...
Таким образом, переменной SPECIFIC_ENV_VAR1 присвоено значение ключа SPECIFIC_DATA из config-map-1, а переменной SPECIFIC_ENV_VAR2 значение ключа SPECIFIC_INFO из config-map-2.
Как тома
Мы можем смонтировать ConfigMap (например, vol-config-map) как том внутри пода. Для каждого ключа ConfigMap там создаётся одноимённый файл, и содержимое файла - это значение.
... containers: - name: myapp-vol-container image: myapp volumeMounts: - name: config-volume mountPath: /etc/config volumes: - name: config-volume configMap: name: vol-config-map ...
Secrets
Положим, имеется приложение Wordpress, где он подключается к базе MySQL, используя пароль. Во время создания деплоя для Wordpress, можно захардкодить пароль прямо в YAML, но это плохая практика, т. к. пароль будет доступен любому, кто получит доступ к конфиг. файлу.
Объект Secret позволяет закодировать секретную информацию перед тем, как использовать. Кодировать можно пароли, токены или пары «ключ-значение», подобно ConfigMaps; к тому же, можно контролировать, как эта информация будет использоваться, сокращая риск случайного раскрытия. В деплоях (Deployments) или других ресурсах, объект Секрет будет указываться без раскрытия содержимого.
Важно помнить, что данные Секрета хранятся в виде обычного текста (plain text) внутри etcd, поэтому админы должны ограничивать доступ к API-серверу и etcd. Недавно появилась созможность шифрования этих данных в etcd, эту возможность нужно включить на уровне API.
Создание секрета из непосредственного ввода (from Literal) и показ подробностей
# создание kubectl create secret generic my-password --from-literal=password=mysqlpassword # детали kubectl get secret my-password NAME TYPE DATA AGE my-password Opaque 1 8m # ещё детали kubectl describe secret my-password Name: my-password Namespace: default Labels: <none> Annotations: <none> Type Opaque Data ==== password: 13 bytes
При выводе деталей нигде не показывается содержимое секрета. Тип показан как Opaque («непрозрачный»).
Создание секрета вручную
Секрет можно создать вручную из конфига YAML. Следующий пример называется mypass.yaml. Там есть 2 типа карт (maps) для приватной информации внутри секрета: data и stringData.
С инфокартами (data maps), каждое значение приватной информации должно быть закодировано base64. Если нужно использовать конфиг-файл для секрета, надо закодировать пароль base64:
echo mysqlpassword | base64 bXlzcWxwYXNzd29yZAo=
И затем уже использовать полученные значение в конфиге:
apiVersion: v1 kind: Secret metadata: name: my-password type: Opaque data: password: bXlzcWxwYXNzd29yZAo=
Помните, что кодирование base64 - это не шифрование, и любой может легко декодировать данные:
echo "bXlzcWxwYXNzd29yZAo=" | base64 --decode mysqlpassword
Поэтому убедитесь, что вы не поместили конфиг секрета в исходный код.
Со картами данных строк (stringData maps), нет надобности кодировать значение каждого поля приватной информации. Это значение будет закодировано, когда будет создан секрет my-password:
apiVersion: v1 kind: Secret metadata: name: my-password type: Opaque stringData: password: mysqlpassword
Создать секрет, используя mypass.yaml:
kubectl create -f mypass.yaml
Создание секрета из файла и отображение подробностей
Для этого используется команда kubectl create secret.
# Сначала закодировать и записать в файл echo mysqlpassword | base64 echo -n 'bXlzcWxwYXNzd29yZAo=' > password.txt # Теперь создать секрет из файла kubectl create secret generic my-file-password --from-file=password.txt # После создания можно посмотреть подробности с помощью команд get и describe. # Серкртная информация не отображается, тип - Opaque. kubectl get secret my-file-password kubectl describe secret my-file-password
Ипользование секретов внутри подов
Секреты используются контейнерами как смонтированные тома или переменные окружения и берутся как целиком, так и специфические «ключи-значения».
Использование сектретов как переменных окружения
Ссылка только на ключ password секрета my-password и привязка значения к переменной WORDPRESS_DB_PASSWORD:
.... spec: containers: - image: wordpress:4.7.3-apache name: wordpress env: - name: WORDPRESS_DB_PASSWORD valueFrom: secretKeyRef: name: my-password key: password ....
Использование секретов как файлов
Можно смонтировать секрет как том. В примере создаётся файл для каждого ключа секрета my-password, где файлы имеют те же названия, что и ключи. Внутри файлов - значения.
.... spec: containers: - image: wordpress:4.7.3-apache name: wordpress volumeMounts: - name: secret-volume mountPath: "/etc/secret-data" readOnly: true volumes: - name: secret-volume secret: secretName: my-password ....
Пример использования ConfigMaps
web-config.yaml - пространство имён default, две строки с данными - STRING и PATH.
apiVersion: v1 kind: ConfigMap metadata: name: web-config namespace: default data: STRING: Welcome to example of ConfigMaps! PATH: /usr/share/nginx/html/index.html
# Применить конфиг kubectl create -f web-config.yaml # Проверить, создалось ли kubectl get configmaps # или kubectl get cm # можно так: kubectl describe cm web-config
app-config.yaml
apiVersion: v1 kind: Pod metadata: name: app-config spec: containers: - image: nginx name: nginx command: ["/bin/sh", "-c", "echo $(DATA_STRING) > $(DATA_PATH); sleep 3600"] env: - name: DATA_STRING valueFrom: configMapKeyRef: name: web-config key: STRING optional: true - name: DATA_PATH valueFrom: configMapKeyRef: name: web-config key: PATH optional: true restartPolicy: Never
# Создать под на основе конфига kubectl create -f app-config.yaml # т.к. сервис не создавался и не публиковался, лезем внутрь контейнера kubectl exec app-config -- /bin/sh -c 'cat /usr/share/nginx/html/index.html' # Welcome to example of ConfigMaps!
Ingress
В прошлых главах мы получали доступ к приложению через публикацию сервиса, для чего использовались ServiceTypes - чаще всего, NodePort и LoadBalancer. Для LoadBalancer ServiceType необходимо иметь поддержку нижележащей инфраструктуры, но даже если такая поддержка есть, то использовать её для всех сервисов накладно и по ресурсам, и по деньгам. Управление NodePort ServiceType порой сложно, т. к. нужно держать настройки прокси в актуальном состоянии и отслеживать номера присвоенных портов. Ingress API - ещё один уровень абстракции перед сервисами, предоставляющий универсальный метод управления доступом к приложениям извне.
Сервисы определяют правила маршрутизации для конкретных сервисов по отдельности и существуют, пока существует сервис. Так как сервисов много, то много и правил. Но если разделить правила маршрутизации и приложение и централизовать управление этими правилами, то можно обновлять приложение, не беспокоясь о доступе к нему извне. Этим и занимается Ingress - «набор правил, разрешающий входящие подключения к сервисам кластера».
Ingress - балансировщик HTTP/HTTPS 7-го уровня со следующими возможностями:
- TLS (Transport Layer Security)
- Name-based virtual hosting
- Fanout routing
- Loadbalancing
- Custom rules
При использовании Ingress, пользователь не подключается непосредственно к сервису, а к Ingress, который переправляет его запрос к желаемому сервису. Пример конфига:
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: virtual-host-ingress namespace: default spec: rules: - host: blue.example.com http: paths: - backend: serviceName: webserver-blue-svc servicePort: 80 - host: green.example.com http: paths: - backend: serviceName: webserver-green-svc servicePort: 80
В этом примере, запросы к blue.example.com и green.example.com идут к Ingress и оттуда направляются соответственно на webserver-blue-svc и webserver-green-svc. Это пример правила Name-Based Virtual Hosting, т. е., запросы к поддоменам.
Fanout Ingress rules - когда запросы к example.com/blue и example.com/green направляются соответственно на webserver-blue-svc и webserver-green-svc, т. е., меняется URL.
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: fan-out-ingress namespace: default spec: rules: - host: example.com http: paths: - path: /blue backend: serviceName: webserver-blue-svc servicePort: 80 - path: /green backend: serviceName: webserver-green-svc servicePort: 80
Ingress Controller
Ingress Controller - приложение, следящее за изменениями ресурсов Ingress на API-сервере мастер-ноды и обновляет балансировщик в соответствии с ними. K8s поддерживает несколько контроллеров Ingress, и если нужно, можно сделать свой собственный. Самые популярные - GCE L7 Load Balancer Controller и Nginx Ingress Controller. Прочие - Istio, Kong, Traefik и т. д.
# Запустить Ingress Controller в Minikube minikube addons enable ingress
Развёртывание Ingress resource
После Ingress Controller нужно создать Ingress resource. Например, если был создан файл конфигурации virtual-host-ingress.yaml с правилом Name-Based Virtual Hosting, то нужно выполнить команду:
kubectl create -f virtual-host-ingress.yaml
Как только Ingress resource создан, можно получить доступ к сервисам webserver-blue-svc или webserver-green-svc через адреса blue.example.com и green.example.com. Т. к. текущая установка - это Minikube, надо отредактировать файл hosts, добавив туда строку
<ip-address> blue.example.com green.example.com
Углублённые темы
Annotations
Аннотации можно прицепить к любому объекту, они имеют формат «ключ-значение», например, в деплое
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: webserver annotations: description: Deployment based PoC dates 2nd May'2019 developer: Vasya ....
В отличие от ярлыков, аннотации не используются для распознавания и выбора объектов. Они используются для доп. информации, такой, как
- Store build/release IDs, PR numbers, git branch
- Phone/pager numbers of people responsible, or directory entries specifying where such information can be found
- Pointers to logging, monitoring, analytics, audit repositories, debugging tools
и т. п.
Будут видны в деталях деплоя
kubectl describe deployment webserver
Jobs и CronJobs
Job (задача) создаёт поды для конкретной задачи и несёт ответственность за сбои пода, обеспечивает выполнение задачи. Когда задача выполнена, поды уничтожаются автоматически. Настройки конфигурации Job-а включают в себя:
- parallelism - кол-во подов, запускаемых параллельно
- completions - число ожидаемых выполнений
- activeDeadlineSeconds - время, отведённое на задачу
- backoffLimit - число попыток выполнения, после которых задача помечается как неуспешная
- ttlSecondsAfterFinished - отсрочка чистки после завершения задач
Начиная с версии K8s 1.4, появилась возможность выполнять задачи по расписанию (CronJobs), в этом случае каждый запланированный раз будет создаваться новая задача. Настройка конфигурации задачи по расписанию включает:
- startingDeadlineSeconds - интервал для запуска пропущенных задач
- concurrencyPolicy - разрешение или запрет на одновременное выполнение задач или замещения старых задач новыми
Quota Management
Когда кластером пользуется множество людей, возникает вопрос распределения ресурсов. ResourceQuota API resource ограничивает потребляемые ресурсы в рамках пространства имён.
Типы квот:
- Compute Resource Quota - ограничение общего кол-ва ресурсов
- Storage Resource Quota - ограничение ресурсов хранения (PersistentVolumeClaims, requests.storage и т. п.)
- Object Count Quota - ограничение кол-ва объектов определённого типа (pods, ConfigMaps, PersistentVolumeClaims, ReplicationControllers, Services, Secrets и т. п.)
Autoscaling
Несмотря на то, что масштабировать объекты вручную достаточно просто, но когда в кластере их сотни и тысячи, нужна автоматизация - динамическое масштабирование, добавляющее или удаляющее объекты из кластера, основываясь на потреблении ресурсов, их доступности и требованиях приложений.
Автомасштабирование может быть задействовано в кластере через контроллеры, которые периодически регулируют кол-во запущенных объектов, сверяясь с разными метриками. Ниже приведены некоторые типы контроллеров автомасштабирования. Они могут использоваться как отдельно, так и совместно с другими для более тонкой настройки:
- Horizontal Pod Autoscaler (HPA) - регулирует число реплик в ReplicaSet, Deployment или Replication Controller на основе загрузки процессора.
- Vertical Pod Autoscaler (VPA) - регулирует требования контейнера к ресурсам (CPU и память) в поде и динамически регулируют их по ходу работы (in runtime - в контейнерном движке?) на основе истории использования ресурсов, текущей доступности и событий реального времени.
- Cluster Autoscaler - автоматически меняет размер кластера, если есть неэффективно используемые ресурсы или какие-то ноды недостаточно нагружены.
DaemonSets
Если нужно собирать данные мониторинга со всех нод или запускать на них сервис хранилища (storage daemon), на всех нодах должен постоянно работать особый под, и это делается объектом DaemonSet. DaemonSet управляет агентами kube-proxy, запускающимися на каждой ноде кластера.
Когда нода добавляется в кластер, под с DaemonSet запускается на ней с помощью стандартного кластерного планировщика. Когда нода выключается или выходит из состава кластера или удаляется DaemonSet, соответствующие поды собираются как мусор.
Новая функция ресурса DaemonSet - запуск подов только на указанных нодах путём настройки nodeSelectors и node affinity rules. Подобно Deployment resources, DaemonSets поддерживают rolling updates и rollbacks.
Конфиг DaemonSet идентичен ReplicaSet, за исключением kind.
apiVersion: apps/v1 kind: DaemonSet metadata: name: monitoring spec: selector: matchLabels: app: monitoring-agent template: metadata: labels: app: monitoring-agent spec: containers: - name: monitoring-agent image: monitoring-agent
StatefulSets
Контроллер StatefulSet используется для stateful-приложений, требующих уникальных данных типа имени, сетевых идентификаторов, строгой очерёдности и т. п., например, кластер MySQL, кластер etcd.
StatefulSet controller предоставляет уникальные данные (identity) и гарантирует очерёдность деплоя и масштабирования подов. Подобно Deployments, StatefulSets используют ReplicaSets как промежуточное звено для управления подами и поддерживают rolling updates и rollbacks.
Kubernetes Federation
Даёт возможность управления несколькими кластерами из одной панели управления. Общая синхронизация ресурсов и обнаружение, что позволяет развёртывать географически распределённые приложения, обеспечивать доступ к ним через глобальный DNS и строить высокодоступную инфраструктуру.
Хотя это пока альфа-версия, федерация полезна при создании гибридной системы, когда одна часть вертится внутри собственной сети, а вторая часть в облаке, что даёт возможность не привязываться к поставщику услуг. Настраивается вес каждого кластера для распределения нагрузки.
Custom Resources
В Kubernetes, ресурс - это конечная точка API, хранящая набор объектов API. Например, ресурс Pod содержит все Pod-объекты.
Хотя в большинстве случаев существующие ресурсы Kubernetes достаточны для выполнения наших требований, можно создавать новые, используя настраиваемые ресурсы (custom resources). Custom resources по природе динамические и могут появляться и исчезать в существующем кластере в любое время.
Для создания своего ресурса необходимо создать и установить custom controller, который может интерпретировать структуру ресурса и выполнять необходимые действия. Custom controllers могут быть установлены и управляться в уже работающем кластере. Есть 2 способа добавления настраиваемого ресурса:
- Custom Resource Definitions (CRDs) - самый простой, не требующий большого знания программирования.
- API Aggregation - чтобы получить более гибкое управление, можно написать API-агрегаторы. Это подчинённые API-сервера, находящиеся за первичным, который является для них прокси-сервером для запросов.
Helm
Чтобы развернуть приложение, нужно написать кучу манифестов - Deployments, Services, Volume Claims, Ingress и т. д., что иногда бывает трудно. Есть способ объединить их все в одном формате, называемом схемой (Chart). Эти схемы распространяются через репозитории подобно пакетам rpm и deb. Helm - это пакетный менеджер для K8s, который ставит/обновляет/удаляет схемы в кластере, по типу apt или yum в линуксе. k8s здесь рассматривается как операционная система.
Клиент подключается к серверу для управления чартами.
Установить Helm: https://helm.sh/docs/intro/install/
- Чарт — формат пакета в Helm, это каталог определённой стуктуры с текстовыми файлами (набор шаблонов, файл с дефолтными значениями переменных и метаданные).
- Пакетом в Helm называется чарт упакованный в .tgz архив, у которого есть версия.
- Релиз - экземпляр чарта, который работает в кластере Kubernetes
- Приложение для Kubernetes — это набор манифестов.
- Шаблон (шаблонизированный манифест) — это манифест, часть значений в котором заменена на переменные.
Структура чарта
<chart_name>/ .helmignore # список файлов, игнорируемых при создании чарта Chart.yaml # метаинформация: зависимости, разработчики, описание, версия и т. д. values.yaml # значения переменных по умолчанию charts/ # каталог для чартов, от которых зависит этот чарт crds/ # собственные определения ресурсов templates/ # шаблоны .yaml (манифесты k8s), куда подставляются переменные из values.yaml _helpers.tpl # Вспомогательные шаблоны NOTES.txt # Текст, выводимый после применения чарта (инстукция по подключению и т. п.)
Для шаблонизации в Helm используются go templates, в котором, подобно jinja2, есть свои правила использования. Все инструкции шаблона заключаются в символы {{
и }}
. Текст вне этих символов будет простым текстом. Простой текст копируется из шаблона в вывод без какого-либо изменения.
В {{ }}
могут быть записаны выражение/функция/обращение к переменной.
Часто можно встретить такие конструкции:
{{ .Values.<имя переменной> }}
— значения берутся из файла с переменными values.yaml.
{{ .Chart.<имя переменной> }}
— значения переменных ищутся в файле Chart.yaml.
{{ .Release.Name }}
— имя, которое передаётся первым параметром команде helm install.
https://helm.sh/docs/chart_template_guide/builtin_objects/
Пример Chart.yaml
apiVersion: v2 # версия api чарта name: nginx # имя чарта type: application version: 0.1.0 # версия чарта, должна быть по SemVer2 appVersion: "latest" # версия приложения description: A Helm chart dependencies: - name: backend version: 0.1.0 - name: frontend version: 0.1.0
Пример values.yaml
image: repository: nginx tag: 1.21.6 imagePullPolicy: IfNotPresent replicas: 2 service: port: 80 resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "128Mi" cpu: "200m"
Пример templates/deploy.yaml
--- apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }} labels: app: {{ .Chart.Name }} spec: replicas: {{ .Values.replicas }} selector: matchLabels: app: {{ .Chart.Name }} template: metadata: labels: app: {{ .Chart.Name }} spec: containers: — name: nginx image: "{{ .Values.image.repository }}:{{.Values.image.tag }}" imagePullPolicy: {{ .Values.imagePullPolicy }} ports: — containerPort: {{.Values.service.port }} resources: {{ toYaml .Values.resources | indent 10 }}
# Проверить чарт helm lint ./<chart_name> # Создать пакет (Chart.Name-Chart.Version.tgz) helm package ./<chart_name> # Запустить в Кубере helm install <name> ./<chart_name> # Переопределить версию образа при запуске helm install --set image.tag=1.21.5 <name> ./<chart_name> # посмотреть информацию о шаблонах и значениях переменных, которые отправил Helm helm get all <name> # Список версий чартов. helm search repo bitnami/rabbitmq -l # Chart version - версия чарта, это не версия приложения, там меняются параметры, добавляются функции и т. п. # App version - версия приложения. # Установить определённую версию чарта helm install myrabbitmq bitnami/rabbitmq --version 6.18.3 # Показать readme чарта helm show readme bitnami/rabbitmq --version 6.18.3 |less # readme уже установленного чарта (релиза) helm status myrabbitmq # Обновление. Реплики также задаются через переменную Helm, а не через кластер напрямую. helm гupgrade myrabbitmq bitnami/rabbitmq --version 6.18.3 --set replicas=3 --set rabbitmq.password=pass --set rabbitmq.erlangCookie=cookie # Обновление на новую версию helm гupgrade myrabbitmq bitnami/rabbitmq --version 6.27.0 --set replicas=3 --set rabbitmq.password=pass --set rabbitmq.erlangCookie=cookie # История релизов helm history myrabbitmq # Откатиться на пред. версию helm rollback myrabbitmq # Деинсталляция helm uninstall myrabbitmq # Показать манифест с подставленными переменными helm template ./<chart_name> # Конкретный шаблон helm template ./<chart_name> -s templates/jenkins-deploy.yaml
У имени деплоя должно быть уникальное имя в рамках пространства имён, поэтому в metadata.name нужно указывать изменяющееся имя, например
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}
Можно заменить эту конструкцию на функцию
metadata: name: {{ printf "%s-%s" .Release.Name .Chart.Name }} # Так же можно поступить и с образом (только разделитель будет ":") image: {{ printf "%s:%s" .Values.image.repository .Values.image.tag }}
Значение по умолчанию
env: - name: USERNAME value: {{ default "test" .Values.username | quote }}
Quote нужен для того, чтобы нормально обрабатывались значения с пробелами
В это время в values.yaml: username: ""
Условие if/else: если пароль предоставлен –set password=
, использовать его, иначе значение по умолчанию.
Дефис перед открывающими фигурными скобками удаляет всю пустоту в начале строки (типа trimStart). Можно делать это и в конце -}}
env: - name: PASSWORD {{- if .Values.password }} value: {{ .Values.password }} {{- else }} value: testPassword {{- end }}
В values.yaml: password: ""
Использовать дочерние значения
ports: {{- range .Values.containerPorts }} - name: {{ .name }} containerPort: {{ .port }} {{- end }}
В values.yaml:
containerPorts: - name: http port: 8080
Макрос шаблона позволяет избавиться от повторяющихся инструкций в разных шаблонах. Макросы содержатся в _helpers.tpl. Вместо повторяющихся в каждом шаблоне
metadata: name: {{ printf "%s-%s" .Release.Name .Chart.Name }}
пишем
metadata: name: {{ template "nginx.fullname" . }}
Вариант с кодированием base64: если значения даны, то кодировать их, если нет - сгенерировать и кодировать.
apiVersion: v1 kind: Secret metadata: name: {{ include "minio.fullname" . }} labels: {{- include ""minio.labels" . |nindent 4 }} data: {{- if empty .Values.accessKey }} access-key: {{ randAlphaNum 10 | b64enc | quote }} {{- else }} access-key: {{ .Values.accessKey | b64enc | quote }} {{- end }} {{- if empty .Values.secretKey }} access-key: {{ randAlphaNum 10 | b64enc | quote }} {{- else }} access-key: {{ .Values.secretKey | b64enc | quote }} {{- end }}
Security Contexts and Pod Security Policies
Время от времени нужно задавать специфические разрешения и настраивать доступ к подам и контейнерам. Security Contexts позволяют задать Discretionary Access Control for object access permissions, privileged running, capabilities, security labels, etc. Но их эффект ограничен отдельными подами и контейнерами, у которых заданы соответствующие настройки в разделе spec.
Если нужно задать настройки безопасности на многих подах или контейнерах в рамках всего кластера, надо настраивать Pod Security Policies. Они позволяют задавать тонкие настройки безопасности to control the usage of the host namespace, host networking and ports, file system groups, usage of volume types, enforce Container user and group ID, root privilege escalation, etc.
Network Policies
Изначально в Kubernetes все поды свободно общаются без ограничений, но иногда необходимо ограничить доступ. Сетевые политики - это наборы правил, определяющие, как поды могут общаться с другими подами и ресурсами в кластере и вне его. Поды, находящиеся вне этой политики, продолжают общение без ограничений. Network Policies очень похожи на типичный межсетевой экран.
Network Policy API resource specifies podSelectors, Ingress and/or Egress policyTypes, and rules based on source and destination ipBlocks and ports. Very simplistic default allow or default deny policies can be defined as well. As a good practice, it is recommended to define a default deny policy to block all traffic to and from the Namespace, and then define sets of rules for specific traffic to be allowed in and out of the Namespace.
Let's keep in mind that not all the networking solutions available for Kubernetes support Network Policies. Review the Pod-to-Pod Communication section from the Kubernetes Architecture chapter if needed. By default, Network Policies are namespaced API resources, but certain network plugins provide additional features so that Network Policies can be applied cluster-wide.
Monitoring and Logging
Сбор данных по использованию подов, нод, сервисов и т. д. нужен, чтобы понимать потребление ресурсов и на этой основе принимать решения по масштабированию приложения. Вот 2 популярных решения по мониторингу:
- Metrics Server - агрегатор данных использования ресурсов со всего кластера
- Prometheus - собирает данные от разных компонентов и объектов.
Другой важный компонент - логи. Можно собирать логи разных кластерных компонентов, объектов, нод и т. д., но Kubernetes не предоставляет логирование по всему кластеру, так что для этого нужны сторонние решения. Популярное решение сбора логов - это Elasticsearch, который использует fluentd как агент на нодах.
What's Next on Your Kubernetes Journey?
Now that you have a better understanding of Kubernetes, you can continue your journey by:
- Participating in activities and discussions organized by the Kubernetes community
- Attending events organized by the Cloud Native Computing Foundation and The Linux Foundation
- Expanding your Kubernetes knowledge and skills by enrolling in the self-paced LFS258 - Kubernetes Fundamentals, LFD259 - Kubernetes for Developers, or the instructor-led LFS458 - Kubernetes Administration and LFD459 - Kubernetes for App Developers, paid courses offered by The Linux Foundation
- Preparing for the Certified Kubernetes Administrator or the Certified Kubernetes Application Developer exams, offered by the Cloud Native Computing Foundation
And many other options.