Инструменты пользователя

Инструменты сайта


learning:k8s

Содержание

K8s

Основные понятия

https://kube.academy/courses/kubernetes-core-concepts-part-1/lessons/kubernetes-fundamentals

Принципы Cloud Native:

  1. Контейнеризация (докер)
  2. Динамическое управление – Кубернетис
  3. Ориентация на микросервисы – Кубернетис

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

Pod

Pods – один или несколько контейнеров. Все контейнеры в одном поде:

  1. запускаются на одном хосте
  2. взаимодействуют друг с другом через localhost
  3. имеют одинаковый доступ к томам

Иными словами, 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" # проверить, что деплой создан

Решение проблем

https://kube.academy/courses/kubernetes-core-concepts-part-1/lessons/kubernetes-architecture-troubleshooting

Ноды делятся на 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 встроенных типа проверок состояния контейнера:

  1. Liveness - запущен ли контейнер. При сбое он будет перезапущен согласно политике.
  2. 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 типа сервисов:

  1. ClusterIP - публикация внутри кластера, т. е., поды за сервисом публикуются для других подов в том же кластере. Внешнего доступа нет. Используется, например, для сервисов БД. Это стандартный тип сервиса, если не указано иначе.
  2. NodePort - публикация как порт на всех рабочих нодах. Т. е., попасть к сервису можно, обратившись на любую рабочую ноду кластера на указанный порт. Это не отменяет необходимости внешнего балансировщика, который уже будет балансировать сами ноды.
  3. 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, где перечислены настройки подов и на которые можно ссылаться

  1. как на переменные окружения - в разделе spec.containers пода либо полностью (configMapRef:), либо дёргать оттуда ключи по отдельности (configMapKeyRef:)
  2. как на том - том задаётся как 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 необязателен к использованию, если уже есть какой-то альтернативный способ хранения секретных данных.

https://kube.academy/courses/kubernetes-core-concepts-part-4/lessons/lab-dynamic-application-configuration

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.

  1. Кто может получать доступ (аутентификация)? Способы регулирования - имя и пароль/токен, сертификаты, внешний сервис типа LDAP и сервисные учётки для машин.
  2. Что может делать получивший доступ (авторизация)? Способы регулирования - 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, который позволяет удобно работать с запросами.

  1. Пользователь создаёт ключ и запрос, и отправляет запрос админу.
  2. Админ создаёт CertificateSigningRequest, вбивая в spec.request содержимое файла запроса, закодированное в base64 (например, cat user.csr |base64).
  3. Теперь запрос можно увидеть, набрав команду 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 слоя сетевых взаимодействий:

  1. Между контейнерами внутри пода (Container-to-container)
  2. Между подами на одной ноде или между нодами в кластере (Pod-to-Pod)
  3. Между подом и сервисом в одном пространстве имени (namespace) или между пространств имён в кластере (Pod-to-Service)
  4. Между внешними клиентами и сервисом (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 основных конфигурации:

  1. Всё в одном - мастер и рабочая нода совмещены. Подходит для тестовых, учебных и т. п. целей. Пример - Minikube.
  2. 1-нодовый etcd, 1-мастер, много рабочих нод
  3. 1-нодовый etcd, мультимастер, много рабочих нод
  4. Мульти-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

  1. kubectl - управление кластером K8s. Ставится отдельно.
  2. Гипервизор 2-го типа, типа Hyper-V. Minikube поддерживает опцию –vm-driver=none, позволяющую ставить K8s не в гипервизор, а на локальную машину, но тогда там должен быть установлен Docker (а гипервизор - не должен), и должен быть определена bridge network for Docker. Иначе при перезапуске сети связь с кластером может потеряться.
  3. В BIOS нужно включить VT-x/AMD-v.
  4. При первом запуске 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 группы:

  1. Core Group (/api/v1) - содержит объекты: поды, сервисы, ноды, пространства имён, карты конфигурации, секреты и т. д.
  2. Named Group (/apis/$NAME/$VERSION). Объекты содержат уровни: alpha level, beta level, stable level
  3. 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 пространства:

  1. kube-system - содержит объекты, созданные K8s, в основном, агентами управления мастер-ноды
  2. kube-public - особое пространство, открытое для чтения всем, используется для предоставления открытой информации о кластере
  3. kube-node-lease - самое новое пространство, содержит информацию об использовании нод и пульс
  4. 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 существует два вида ролей:

  1. Role - предоставление доступа к ресурсам того или иного пространства имён
  2. 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 также есть два типа:

  1. RoleBinding - привязывает пользователей к тому же пространству имён, как Role. Можно сослаться и на ClusterRole, в случае, если в рамках ClusterRole определены ресурсы пространства имён.
  2. 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 может использоваться подом.

Использование 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.

learning/k8s.txt · Последнее изменение: 19.12.2022 13:02 — viacheslav

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki