Kubernetes

По состоянию на 2020 год является самым модным и раскрученным оркестратором для систем виртуализации. Из основных особенностей можно отметить, например, что всё конфигурирование производится посредством применения различных yaml файлов. Что-либо изменить в настройках кластера какими-либо командами не представляется возможным1) - требуется на любой чих написать манифест и применить его на кластер, после чего, возможно, он соизволит что-то сделать.

Разворачивание

В лучших традициях современных практик качаем с гитхаба kebuspray, ставим в систему setuptools и pip3 для python3 и дальше вся магия происходит мимо системного администратора, используя Ansible2)

Итак, мы будем использовать debian buster, но каждый сам выбирает свою судьбу. Берём 4 виртуалки и начинаем корячить. Я решил сделать следующий сетап:

  • master - как мастер
  • slave[1-3] - как нода считалка

А потом пришёл к выводу - гулять, так гулять и растянул мастера на slave[1-2], однако нодой для бутстрапа кластера я выбрал master, в связи с чем ставим на него следующие запчасти:

# apt-get install git python3-pip python3-setuptools

Далее клонируем куда-нибудь современный kubespray, переходим в директорию с ним и устанавливаем недостающие куски

# pip3 install -r requirements.txt

Когда все модули приедут из недоверенных источников можно приступать к предварительной настройке спрея, а именно - нужно заполнить инвентарь хостами3). Есть два различных способа это сделать - через автогенератор, который генерирует по шаблону, что лично мне не нравится, либо сделать руками с любовью и душой. В нашем случае будем использовать второй вариант4).

Берём готовый набор переменных, предлагаемый kubespray, и составляем свой hosts.yaml

# cp -r inventory/sample inventory/k8s
# vim inventory/k8s/hosts.ini

После игр с автогенератором и руками получилось нечто следующее:

hosts.ini
[all]
master ansible_host=192.168.0.2 ip=192.168.0.2
slave1 ansible_host=192.168.0.3 ip=192.168.0.3
slave2 ansible_host=192.168.0.4 ip=192.168.0.4
slave3 ansible_host=192.168.0.5 ip=192.168.0.5
 
[kube-master]
master
slave1
slave2
 
[etcd]
master
slave1
slave3
 
[kube-node]
master
slave1
slave2
slave3
 
[k8s-cluster:children]
kube-master
kube-node

В целом вроде ничего сложного5) Если хочется настроить какие-то специфики кластера - можно обратиться к директории inventory/k8s/group_vars/. Там много интересных крутилочек, причём применять их можно вполне себе на ходу.

Ну и финнальный аккорд6)

# ansible-playbook -i inventory/k8s/hosts.ini --become --become-user=root cluster.yml

Катаем пару раз и если нет фейлов - поздравляю, кластер куба подготовлен к использованию.

Отступление по всяким запчастям, чтоб два раза не вставать

В целом если наш кластер не будет смотреть куда-то вовне - можно и остановаиться на четырёх нодах, но я пошёл дальше и решил осилить Ingress. О том что же это такое будет описано ниже, но если мы уже хоть немного разбираемся в происходящем и знаем что такое Ingress - советуется ставить его на отдельный сервер/виртуальную машинку, чтобы они никак не пересекались по ресурсам с текущим.

В общих чертах включение ingress не очень сложная задача. Сказал он и потратил 5 часов на то, чтобы разобраться что, к чему, зачем и почему эта дрянь не заводится от слова совсем. Как водится - доже если этот объект крякает - не факт, что это утка. Переходим к рецепту.

Подкручиваем inventory/k8s/global_vars/k8s-cluster/addons.yaml:

addons.yaml
...
ingress_nginx_enabled: true
ingress_nginx_host_network: true
ingress_nginx_nodeselector:
  node-role.kubernetes.io/ingress: ""
ingress_nginx_tolerations:
  - key: "node-role.kubernetes.io/ingress"
    operator: "Exists"
...

Ну и попутно поправил hosts.ini:

hosts.ini
[all]
master ansible_host=192.168.0.2 ip=192.168.0.2
ingress1 ansible_host=192.168.0.3 ip=192.168.0.3
slave1 ansible_host=192.168.0.4 ip=192.168.0.4
slave2 ansible_host=192.168.0.5 ip=192.168.0.5
slave3 ansible_host=192.168.0.6 ip=192.168.0.6
 
[kube-master]
master
slave1
slave2
 
[etcd]
master
slave1
slave3
 
[kube-node]
master
slave1
slave2
slave3
ingress1
 
[kube-ingress]
ingress1
 
[k8s-cluster:children]
kube-master
kube-node

Но и это ещё не всё. Мы сделали отдельную группу серверов в инвентаре, теперь нужно описать специфики для неё. Создаём файлец inventory/k8s/group_vars/kube-ingress.yml:

kube-ingress.yml
node_labels:
  node-role.kubernetes.io/ingress: ""
node_taints:
  - "node-role.kubernetes.io/ingress=:NoSchedule"

Суть происходящего здесь в том, что мы вешаем тэг ingress на сервера в этой группе, а так же через теинты запрещаем шедулить на этот хост контейнеры.

Как приятное дополнение можно влезть обеими ногами в inventory/k8s/group_vars/k8s-cluster/k8s-cluster.yml. Там много интересных крутилочек - мне понравился результат от поворачивания следующих:

k8s-cluster.yml
kube_service_addresses: 10.0.0.0/16
kube_pods_subnet: 10.10.0.0/16
kube_proxy_mode: iptables
cluster_name: k8s.ow1.in
kubeconfig_localhost: true
kubectl_localhost: true

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

Ещё в дополнение к абстракциям, которые абстрагированы достаточно глубоко предлагается использовать helm - это очередной composer, который исправляет фатальные недостатки в уже существующих. В целом я постараюсь про него рассказать, так что можно включить там же в addons.yaml

Терминология и архитектура

Перед тем как продолжать - неплохо бы вообще разобраться с чем мы имеем дело, зачем и почему. Обратимся к моим художествам:

 k8s cluster

Поверхностно проанализировав изображение выше можно заключить, что центральной частью, вокруг которой всё крутится, является API и это на самом деле так. Все взаимодействия производятся через него. Немного раскроем термины:

  • API - непосредственно программный интерфейс (в данном конкретном случае HTTP API)
  • etcd - демон, который служит хранилищем для большинства данных (в том числе конфигурационных) в кластере
  • controller manager - как можно догадаться из названия - главный робот в системе, который следит за её состоянием и меняет в соответствии с запрошенным
  • scheduler - запчасть, которая занимается выбором нод, на которых запустятся pod'ы, ну и сервисы в конечном итоге
  • kubelet - клиентский сервис, который стоит на конечных нодах и общается через api с управляющей частью кластера7)
  • docker - любая, поддерживаемая k8s, система виртуализации

В списке выше я упомянул один из уже логических терминов k8s. Давай посмотрим на второе художество:

k8s logical

Как по мне эта небольшая схемка выглядит как набор костылей и подпорок, но для меня весь k8s так выглядит, поэтому в данном вопросе можно опустить моё мнение и перейти к непосредственно рассмотрению запчастей:

  • container - очевидно самая базовая сущность, которая и выполняет всю работу8)
  • pod - самая минимальная структура, которую можно создать посредством k8s. В общем случае pod это набор контейнеров, достаточный для запуска сервиса (типа docker-compose), но никто не запрещает извратиться и запустить по поду на контейнер (только надо учитывать, что в каждом поде как минимум два контейнера - один управлящий от куба, второй запущенный)
  • ReplicaSet - абстракция, которая позволяет задать количество одновременно запущенных подов, повлиять на шедулер в плане их размазывания по кластеру и много другое относящееся к отказоустойчивости. Если сервер с одним подом завалится - replicaset даст команду поднять новый на другой ноде, чтобы количество подов было равно фактору репликации. Всё это автоматический и радостно похрюкивая
  • Deployment - суть деплоймента, пожалуй, в том, что он является некой общей сущностью, собирающей в себе все нижестоящие, этакое максимально общее описание сервиса внутрик кластера кубера
  • label - каждому поду можно назначить лейбл, чтобы последствии с ним работать и приклеивать к этому лейблу, например, эндпоинты и сервисы
  • Service - структура данных, которая описывает тот или иной сервис, позволяет, например, задать порт для сервиса и с помощью поиска по тому же label направить весь трафик на любую ноду из выбранных. Физически не занимается доставкой трафика, служит исключительно логической структурой для предоставления данных другим запчастям куба
  • EndPoint - является некоторым расширением Service, поскольку уже позволяет задать ip-адрес и порт, на котором будет слушать fe, отправляющий все запросы непосредственно в группу серверов
  • Ingress - ещё один способ направить траффик внутрь кластера, в отличии от EndPoint позволяет дополнительно настроить всевозможные крутилки для балансировки и прочие специфики для frontend части. По сути с помощью Ingress можно заменить непосредственно EndPoint и описать правила прохождения запросов вплоть до location в nginx, после чего воткнуть его в существующий Service

Но хватит скучной теории, кому будет интересно - всегда можно пожевать документацию. Время разламывать кластер.

Мой первый pod

Ну чтож, поскольку самый маленький из доступных это pod - начнём с его запуска. Как я писал раньше - в кубере нельзя сделать ничего руками, только применением yaml файлов. Какого-то профита в этом я не вижу, но кто я такой, чтобы спорить с модниками. Итак, создаём свой первый yaml:

pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
  - image: nginx:1.14
    name: nginx
    ports:
    - containerPort: 80

В нём написано следующее:

  • Общие поля для объявления ресурсов:
    • apiVersion - попытка отслеживания API в кубере
    • kind - тип ресурса, который мы создаём
    • metadata - блок метаинформации
      • name - имя объявляемого ресурса
  • Специфичные для ресурса Pod поля:
    • spec - спецификация ресурса
      • containers - описание запускаемых контейнеров в данном поде
        • image - образ, из которого следует запустить контейнер
        • name - имя контейнера, которое пропишется во внутренний dns сервиса, чтобы разные контейнеры одного пода могли взаимодействовать между собой
        • ports - в терминологии docker это Expose, т.е. порт, который должен быть открыт за пределами контейнера

Собственно подготовив yaml используем, пожалуй, самую частую команду для заливки конфигурации:

# kubectl apply -f pod.yaml

Если всё успешно - нам ответят, что pod/nginx-pod created и можно наблюдать за изменением его статуса через следующий набор команд:

# kubectl get pods
# kubectl describe pod nginx-pod
# kubectl logs nginx-pod

Если мы сделаем describe на наш запущенный pod - то увидим, что ему был присвоем IP-адрес. Теперь если стукнуться в этот адрес каким-нибудь curl на 80 порт - мы получим стандартную заглушку nginx. Поздравляю - теперь ты полноправный администратор кубернетес - можешь хвастаться своим школьным друзьям после уроков как по щелчку пальцев весь мир ложится к твоим ногам.

Однако, как это не грустно, один под с одним контейнером это не верх искусства и бородатые дядьки придумали как увеличить нагрузочную вместимость сервисов. Один из примеров, пожалуй, горизонтальное масштабирование9) В связи с этим предлагается запустить, например, второй pod. Чтобы это сделать - достаточно в ранее созданном pod.yaml поменять name в блоке metadata и мы получим отдельный под с совершенно идентичной начинкой.

Меняем имя, делаем apply нового манифеста и видим, что у нас появился новый pod:

root@master:~/deploy/deploy# kubectl get pod
NAME               READY   STATUS    RESTARTS   AGE
nginx-pod          1/1     Running   0          17m
nginx-pod-second   1/1     Running   0          32s

Сделав describe на этот pod увидим, что у него другой ip-адрес и, скорее всего, он даже запущен на другой ноде.

Продолжаем наше погружение в бездны, а именно - теперь настало время поговорить о том, как можно обновлять метаданные pod'ов и сами поды непосредственно. Всё невероятно просто - правим yaml, не меняя name в metadata и дальше куб сделает всё сам. Например сменим версию nginx для nginx-pod-second с 1.14 на 1.12. Для этого в соответствующем pod.yaml правим image в блоке containers. Перед тем, как вгрузить обновления убедимся в том из какого образа запущен контейнер сейчас:

root@master:~/deploy/deploy# kubectl describe pod nginx-pod-second
...
Containers:
  nginx:
    Image:          nginx:1.14
...

Теперь производим kubectl apply нового yaml и смотрим что произойдёт. Если мы попросим kubectl get pods - то увидим, что количество рестартов у pod'а стало 1, однако время жизни самого pod'а не изменилось. Теперь сделав describe на наш pod мы обнаружим, что базовый образ контейнера сменился:

root@master:~/deploy/deploy# kubectl describe pod nginx-pod-second
...
Containers:
  nginx:
    Image:          nginx:1.12
...

Делая вывод из всего произошедшего - администратору требуется исключительно вносить правки в конфигурацию кластера, всей остальной грязной работой занимаются совместно service controller, scheduler, kubelet и прочие запчасти, которые прячут от системного администратора всё происходящее под капотом.

Так же из доступных администратору команд есть ещё одна - kubectl delete. Как я понимаю идеологию всего происходящего - есть два основопологающих факта: манифест первичен и если тебе надо что-то перезапустить - редеплой. Максимум state-less система. Однако есть альтернативно одарённые личности, которые любят забивать дрелью шурупы в бетонные стены и используют один инструмент для всего, не особо напрягая то немногое, что было выдано природой на спавне. Моё личное мнение - экосистема кубернетес в частности и докеро-контейнеров в целом хорошо подходит для state-less задач. Всякие молотилки/считалки, всевозможные вебморды для API, различные варианты тестирования (в целом тесткейсы отлично укладываются в практику отработал и умер, отправив выхлоп куда-нибудь). Напротив же state-full вещи, типа баз данных и прочих систем хранения информации (частично даже кэши) не предназначенны для запуска в рамках данной идеологии, однако господа разработчики в пылу своего фанатичного делирия любезно предоставили нам возможность использования persistent volumes.

Но мы отвлеклись. Итак, чтобы лишний мусор не забивал наш кластер10), в широких кругах принято прибирать за собой. Сделать это достаточно просто:

# kubectl delete pod --all

Если тебе не надо было удалять всё, а только что-то одно - советую в следующий раз либо немного подумать над тем, что ты копипастишь в консоль, либо пойти дальше и почитать что значит команда.

На этом, пожалуй, с подами можно и закончить, рассказывать тут особо больше и не о чем.

ReplicaSet или просто скажи за чем смотреть

Продолжая движение вверх по костылям мы приходим к следующей сущности, которая завётся ReplicaSet. Суть данного изобретения сводится к тому, что мы указываем шаблон, из которого должны разворачваться поды и количество одновременно запущенных подов. На этом полномочия ReplicaSet пожалуй и всё, но тем не менее попробуем создать свою и пострелять в поды ради интереса.

Наш yaml несколько видоизменится

replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-replicaset
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-pods
  template:
    metadata:
      labels:
        app: nginx-pods
    spec:
      containers:
      - image: nginx:1.14
        name: nginx
        ports:
        - containerPort: 80

Как водится в кругах хипсторов - использовать одинаковую нотацию моветон, поэтому первым делом спешу обратить внимание на apiVersion, он изменился совсем не в рамках первоначального шаблона. И такое повсеместно, так что быть максимум внимательным. Дальше в целом общие поля примерно очевидны. Переходим к специфичным:

  • replicas - требуемое количество одновременно запущенных реплик
  • selector - блок, указывающий на какие поды распрострянется этот манифест
    • matchLabels - тип фильтра
      • app - ключ лейбла и значение
  • template - шаблон, из которого будут подниматься поды
    • metadata - мета для каждого пода
      • labels - блок с ключ-значение парами лейблов
    • spec - блок описания подов, тут всё как и в предыдущем разделе

Из интересного - у ресурсов, создаваемых автоматически, будет префикс, указанный в name и постфикс, сгенерированный рандомно.

Собсвенно результат применения нашего yaml следующий:

root@master:~/deploy/deploy# kubectl get rs
NAME               DESIRED   CURRENT   READY   AGE
nginx-replicaset   3         3         0       5s
root@master:~/deploy/deploy# kubectl get pod
NAME                     READY   STATUS    RESTARTS   AGE
nginx-replicaset-7lt4d   1/1     Running   0          8s
nginx-replicaset-q5cw7   1/1     Running   0          8s
nginx-replicaset-xfphs   1/1     Running   0          8s

А дальше очень важный и интересный момент, который стоит очень ясно понимать, если ты, мой маленький друг, будешь использовать k8s когда-нибудь в своих проектах. Если ты попробуешь изменить что-нибудь в манифесте ReplicaSet и загрузишь новый манифест поверх старого в кластер - кубер даже ухом не поведёт редеплоить твои контейнеры - т.е. если ты вот сейчас возьмёшь на yaml, поменяешь там версию nginx, например и сделаешь kubectl apply - ничего не произойдёт, поды будут ехать на той версии, на которой ехали, но в случае если откажет одна из нод, на которых присутствует под этой replicaset - можно получить ощутимый выстрел в ногу в связи с тем, что новый под, поднятый на замену упавшему, будет развёрнут уже по новой спецификации и ты получишь неконсистентный кластер.

Чтобы этого избежать люди придумали Deployment, следующий уровень абстракции, который позволяет более лучше следить за происходящим на кластере и реагировать на изменения в манифестах.

В общем и целом про ReplicaSet наверное тоже нечего говорить. Очевидно если поменять фактор репликации - это так же сразу отразится на кластере. Не применяются только изменения в шаблоне.

Удаление replicaset удалит следом всё, что она насоздавала.

Deployment или вот ваш кофе, Сэр

Всё так, ты не ослышался, этот тип ресурса является самым предпочтительным из всех при деплое сервисов. Он будет сам поднимать инстансы, будет сам следить за консистентностью кластера, описанного в факторе репликации, сам постепенно деплоить и выводить из ротации поды, а так же подтирать тебе жопу, сопли и приносить кофе по утрам. В общем это то, ради чего всё затевалось в основном - максимальная автоматизация при минимальной прозрачности происходящего.

Встречаем, манифест Deployment:

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-pod
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: nginx-pod
    spec:
      containers:
      - image: nginx:1.14
        name: nginx
        ports:
        - containerPort: 80

Что мы видим тут - незначительные изменения в полях общей части, а так же пара новых блоков в дополнение к блокам ReplicaSet в специфичной, а именно

  • strategy - блок, описывающий стратегию деплоя/редеплоя
    • rollingUpdate - так называемые катящиеся релизы - суть - непрерывная разработка ПО, в следствии чего постоянная выкатка новых релизов
      • maxSurge - максимальное поличество подов на обновлении
      • maxUnavailable - максимальное количество недоступных подов, если при обновлении вывалился один под из-за ошибок в ПО - кубер не пойдёт дальше обновлять поды в Deployment
    • type - ну и собственно тип обновления

Собственно применяем наш манифест и наблюдаем, как запускается наш первый деплоймент. Как мы можем наблюдать deployment создал следом за собой replicaset, которая в свою очередь насоздавала pod'ов:

root@master:~/deploy/deploy# kubectl get deployment
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3/3     3            3           13s
root@master:~/deploy/deploy# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-5bff7844cb   3         3         3       16s
root@master:~/deploy/deploy# kubectl get pod
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-5bff7844cb-lsrsp   1/1     Running   0          19s
nginx-deployment-5bff7844cb-vr9q6   1/1     Running   0          19s
nginx-deployment-5bff7844cb-zc4hc   1/1     Running   0          19s

А теперь главное отличие Deployment от ReplicaSet - это как раз тот самый RollingUpdate11) Меняем версию nginx в манифесте, например и наблюдаем что происходит на кластере:

root@master:~/deploy/deploy# kubectl get pod
NAME                                READY   STATUS              RESTARTS   AGE
nginx-deployment-5bff7844cb-lsrsp   0/1     Terminating         0          2m53s
nginx-deployment-5bff7844cb-vr9q6   1/1     Running             0          2m53s
nginx-deployment-5bff7844cb-zc4hc   1/1     Running             0          2m53s
nginx-deployment-74878b6d74-bvsmt   0/1     ContainerCreating   0          7s
nginx-deployment-74878b6d74-fjjrs   0/1     ContainerCreating   0          9s

старые поды по одному начали выводиться из ротации, а новые начали создаваться, при этом deployment создал новый ReplicaSet и по мере запуска подов в новом ReplicaSet он уменьшает фактор репликации в старом:

root@master:~/deploy/deploy# kubectl get rs
NAME                          DESIRED   CURRENT   READY   AGE
nginx-deployment-5bff7844cb   0         0         0       6m17s
nginx-deployment-74878b6d74   3         3         3       3m33s

Через некоторое время имеем следующую картину:

root@master:~/deploy/deploy# kubectl get pod
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-74878b6d74-bvsmt   1/1     Running   0          36s
nginx-deployment-74878b6d74-fh6zx   1/1     Running   0          24s
nginx-deployment-74878b6d74-fjjrs   1/1     Running   0          38s

т.е. полностью обновлённый кластер наших приложений с минимальным даунтаймом.

Иногда одного того, что наш контейнер запустился не достаточно, чтобы точно установить, что ПО внутри контейнера функционирует нормально. Возможно оно долго стартует, возможно нужно дождаться миграций, возможно что-то ещё. По факту в спецификации шаблона мы можем, например, указать так называемые readinessProbe, которая в случае успешного прохождения укажет, что нода готова к работа и livenessProbe, которая в свою очередь будет периодически исполняться кубом и в случае неуспешного прохождения под будет выкинут из кластера.

Возьмём для примера всё тот же nginx

deployemnt-with-probe.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-pod
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: nginx-pod
    spec:
      containers:
      - image: nginx:1.14
        name: nginx
        ports:
        - containerPort: 80
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /
            port: 80
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /
            port: 80
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
          initialDelaySeconds: 10

Собственно из интересного - деплой нового конфига теперь происходил чуть медленнее, а всё потому, что у нас в livenessProbe есть поле initialDelaySeconds - перед тем как начать проверять под на то, что она жива - куб обождёт 10 секунд после запуска, сделает проверку и, в случае успеха, перейдёт к следующему поду.

Ещё в рамках deployment есть такое понятие как управление ресурсами, но имея рутовый доступ разработчики обычно слабо заботятся об ограничениях своих поделок. Чаще даже наоборот - сетуют на админов, что мало ресурсов, накиньте побольше, почему я должен быть на одном сервере с другими и прочее, прочее. В любом случае это тебе на закуску, мой благодарный читатель.

ConfigMap или вот же, положила!

Этот функционал идёт несколько вразрез с основной идеологией контейнеризации, которая, на сколько я её себе представляю, заключается в максимальной упаковке всех нужных запчастей в один мешочек из которого впоследствии без каких-либо телодвижений можно будет развернуть полноценный сервис. Как оказалось реальная жизнь суровее, чем розовые фантазии фанатиков и сбоку прикрутили это.

Суть ConfigMap в том, что мы создаём ресурс, в котором описываем некоторую конфигурацию, а впоследствии подмонтируем эту конфигурацию по нужному пути в контейнере до запуска.

Запустим простенький проект состоящий из нескольких запчастей и используем всё то, что узнали раньше.

Для начала создадим длва ConfigMap - для nginx и для php:

nginx-configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
data:
  default.conf: |
    server {
      listen 80 default_server;
 
      root /var/www/html;
 
      server_name _;
      index index.php index.html;
 
      rewrite ^$ /index.php;
      rewrite ^/$ /index.php;
 
      location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param REQUEST_METHOD $request_method;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass 127.0.0.1:9000;
      }
    }
php-configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: first-app
data:
  index.php: |
    <?php
    phpinfo();
    ?>

В nginx мы настраиваем конфиг для домена test.k8s.ow1.in, а в php просто создаём файлик с минимальным кодом php, который выводит информацию о сервере. Ничего сложного. Применяем на кластер и переходим дальше

1)
ну хорошо, я утрирую, есть команда delete
2)
О нём у меня тоже есть что сказать
3)
да-да, тут будут проскальзывать термины ansible, поскольку он и используется для деплоя
4)
и немного первый
5)
опечатался в блоке etcd и он отъехал на хост не с мастером, но тем не менее это никак не повлияет на работу
6)
АРПЕДЖИО, так сказать, для тех кто в курсе
7)
как я понимаю всё это безобразие работает на событиях, но это не точно
8)
семь менеджеров на одного инженера, ага
9)
в общем случае увеличение количества одинаковых инстансов, давайте зальём проблему деньгами
10)
да и любой другой
11)
или любая другая доступная стратегия обновления ПО