Как устроен Kubernetes

Написав статью о создании локального кластера, пришла мысль о том, что стоит описать как пользоваться k8s, как он работает, из чего состоит

Kubernetes (далее k8s) предназначен для контейнезированных приложений, так же предназначен и для:

  • Автоматизации развертывания
  • Автоматизации масштабирования
  • Автоматизации управления

Но крутить k8s все же придется. Без основ знаний Docker-контейнеризации разбираться с k8s, смысла его изучать нет. Лучше перед тем как приступить к работе с k8s, хорошо-бы знать основы докера. Почему надо знать Docker? k8s - аркестрирует докер контейнеры, и сам докер является основной, на котором строится k8s

Основным объектом k8s является - Сluster (кластер), то есть когда говорят именно о k8s, всегда подразумевается кластер

Сам кластер состоит из Nodes (нод), вы всегда создаете Cluster состоящий из Nodes

Существует два типа нод

  • Worker Node - это сервер на котором запускаются и работают ваши контейнеры
  • Master Node - это сервер, который управляет Worker Nodes

Количество master и worker nodes может быть больше чем один. Зачастую кластер поднимают с одним мастером и множеством воркер нод, но для отказоустойчивости, k8s может реплицировать master node в большом количестве

Все основные команды, которые инициализируют создание ваших рабочих контейнеров, вы посылаете на master node

Master Node - требуется для контроля всего кластера, внутри этой ноды работает 3 главный процесса

  • kube-apiserver - необходим для управления worker nodes
  • kube-controller-manager - наблюдает за общим состоянием кластера
  • kube-scheduler - отвечает за назначение подов узлам

Worker Node - на нем работает 2 главных процесса

  • kublet - общается с kube-apiserver master node
  • kube-proxy - сетевой “интерфейс” каждого worker node

Kubernetes cluster

Для работы кластера, требуется всегда минимум 1 единица каждого типа ноды, то есть, чтобы запустить кластер, нужен один Master Node, и один Worker Node

Вот пример схемы обычного кластера k8s, где имеются множество Worker Nodes, которые можно впоследствии работы добавлять в кластер, тем самым увеличивая мощность своего кластера, и как минимум одна Master Node

Так же появился новый объект “Docker Image Registry” - репозиторий, где вы храните свои контейнеры, это может быть свой личный репозиторий или например Docker Hub. Мастер нода собирает контейнеры и разливает их по воркерам

Главные объекты Kubernetes

Чтобы понимать работу k8s, нужно знать из чего он состоит

Container - Docker контейнер не состоит в кластере и не является объектом k8s, но контейнер создается. Так же необходимо учитывать принцип работы и создание контейнеров, хоть он и не является объектом. Вот такой интересный факт

Pod - самый простой объект в k8s, Pod состоит из одного или множества контейнеров. То есть, мы не запускаем контейнеры в кластере, а запускаем именно Pod

Deployments - Deployments состоит из одинаковых Pod, получается такая “матрешка”, одно состоит из другого

Service - дает доступ нам доступ к Pods, которые работают в Deployments

Nodes - ноды, это уже серверы, которые является объектами нашего кластера

Cluster - и сам кластер, совокупность наших серверов

Есть еще множество объектов k8s, которые думаю можно опустить, потому как без них k8s может существовать, но сейчас мы выделили самые главные

Рассмотрим объекты на схеме ниже

У нас есть k8s кластер, в котором есть Master, и например 2 Worker сервера

Самый простой и самый маленький объект, который мы можем запустить в кластере - это Pod. На схеме указан, как Web1 и MyApp, которые состоят их контейнеров, указан на схеме WebServer и TG Bot

Запуская Pod, мы указываем какой Image/Container будет использован в нем. В каждом Pod запускается неограниченное количество контейнеров, но хорошей практикой считается, что на один Pod запускается один контейнер. Плохой пример на схеме - это Pod MyApp в нем будет и Web-сервер, и например Telegram бот.

Почему, запускать в одном Pod более одного контейнера - плохо?

Представим, что у нас не тестовая, а уже боевая среда, и нам нужно сделать Autoscaling нашего приложения, например нагрузка поднялась.

Пришло много пользователей на ваш Web-сервер, который находится в Pod MyApp. Нам необходимо поднять еще один контейнер, для распределения нагрузки

Когда у нас несколько контейнеров в одном Pod, то при автомасштабировании (Autoscaling), поднимется еще один Pod, внутри которого будет еще 2 таких же одинаковых контейнера

Это приведет к лишнему потреблению ресурсов кластера, получается мы получаем один лишний контейнер TG Bot, который нам в случае нагрузки, не нужен

Автомасштабирование само по себе не работает, его необходимо определять заранее, и подготовить кластер для этого.

Чтобы автомасштабирование работало, для этого нам необходим Deployments, который по сути объединяет Pod’ы.

То есть Deployments создает копии Pod. Обычно отдельно Pod в одиночку не запускают, только для теста.

В основном запускают на кластере именно Deployments, указывают в нем, сколько копий Pod минимум будет работать, какие контейнеры/образы будут использоваться.

Так же Deployments, служит для обновления версий ваших контейнеров.

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

Services предоставляет доступ к вашему приложению, если говорить с точки зрения объектов кластера, то Service подключается к нужному нам Deployments - конечным итогом может быть сайт, порт, API-сервер и т.д.

Ну и еще раз пройдемся по объектам, учитывая все выше сказанное

Pod - объект в котором находится контейнер

Deployments - шаблон одинаковых Pods, который занимается Autoscaling, обновлением без даун-тайма, держит минимальное количество рабочих Pods

Service - предоставляет доступ к Deployments через, Cluster IP, NodePort, LoadBalancer, ExternalName (DNS)

Nodes - сервера на которых крутятся контейнеры

Cluster - логическое объединение всех Nodes

Pods

Разберем команды для работы с Pods

kubectl get pods - покажет какие pods сейчас запущены в кластере

kubectl run web-test --image=nginx:latest --port=80 - запустим один Pod в нашем кластере.

web-test - это имя нашего pod

–image=nginx:latest - Docker image container, который я взял с Docker Hub

–port - порт на котором будет работать наш pod

kubectl get pods - запрашиваем повторно поды кластера и получаем вывод о статусе пода

NAME       READY   STATUS    RESTARTS      AGE
web-test   1/1     Running   0             3m11s

Чтобы увидеть более подробную информацию, где запущен Pod, выполним

kubectl describe pods web-test

Подобный вывод даст нам инфу, о том, где запущен Pod, когда он был запущен, внутренний IP кластера, и т.д.

Name:             web-test
Namespace:        default
Priority:         0
Service Account:  default
Node:             k8s-node-02/192.168.99.56
Start Time:       Mon, 29 May 2023 10:54:22 +0000
Labels:           run=web-test
Annotations:      <none>
Status:           Running
IP:               10.244.2.16
IPs:
  IP:  10.244.2.16
Containers:
  web-test:
    Container ID:   containerd://a9daa9c65ba0974c43e0a02c004b1dfa3db3ffc52e7ec981b40f0499164cfc9f
    Image:          nginx:latest
    Image ID:       docker.io/library/nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Mon, 29 May 2023 10:57:26 +0000
    Last State:     Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Mon, 29 May 2023 10:55:51 +0000
      Finished:     Mon, 29 May 2023 10:56:54 +0000
    Ready:          True
    Restart Count:  3
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-hzrhd (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  kube-api-access-hzrhd:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
QoS Class:                   BestEffort
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type     Reason          Age                    From               Message
  ----     ------          ----                   ----               -------
  Normal   Scheduled       4m40s                  default-scheduler  Successfully assigned default/web-test to k8s-node-02
  Normal   Pulled          4m38s                  kubelet            Successfully pulled image "nginx:latest" in 1.225488709s (1.225540089s including waiting)
  Normal   Pulled          4m35s                  kubelet            Successfully pulled image "nginx:latest" in 1.203974653s (1.203980823s including waiting)
  Normal   Created         3m12s (x3 over 4m38s)  kubelet            Created container web-test
  Normal   Pulled          3m12s                  kubelet            Successfully pulled image "nginx:latest" in 1.235603358s (1.235662318s including waiting)
  Normal   Started         3m11s (x3 over 4m38s)  kubelet            Started container web-test
  Normal   Killing         2m8s (x3 over 4m38s)   kubelet            Stopping container web-test
  Normal   SandboxChanged  2m7s (x3 over 4m37s)   kubelet            Pod sandbox changed, it will be killed and re-created.
  Warning  BackOff         111s (x5 over 3m26s)   kubelet            Back-off restarting failed container web-test in pod web-test_default(139f68e9-4105-4ad3-9d7e-4a36dc31cb99)
  Normal   Pulling         98s (x4 over 4m40s)    kubelet            Pulling image "nginx:latest"
  Normal   Pulled          97s                    kubelet            Successfully pulled image "nginx:latest" in 1.248218462s (1.248230332s including waiting)

Если требуется выполнить команду внутри контейнера, то можно ее вызвать вот так

kubectl exec web-test date

Так же можно попасть в сам контейнер, с помощью интерактивного режима

kubectl exec -ti web-test sh

Логи Pod можно смотреть вот так

kubectl logs web-test

И логи будут довольно маленькие, это нормально

Чтобы получить доступ к Pod локально, можно пробросить порт

kubectl port-forward web-test 7788:80

И удалить Pod можно вот этой командой

kubectl delete pods web-test

Ручками запускать Pod мы научились. А что если его надо запускать иногда, но команды писать, не хочется, да и запоминать надо. Тут придут на помощь манифест файлы. Такие файлы пишутся в yaml-формате. И опишем, просто запуск nginx в нем. Имя файла я дам web-test.yaml

apiVersion : v1
kind: Pod # что мы создаем, это Pod
metadata:
  name: web-test # имя Pod
spec: # от сюда уже описывается контейнер, который мы хотим запустить в Pod
  containers:
    - name : container-nginx # имя контейнера
      image: nginx:latest # image, который мы будем использовать
      ports: # порты которые нужно запустить
        - containerPort: 80

Сохранили файл, комментарии можно не сохранять, я их описал для понимания того, что мы делаем

Запускаем kubectl apply -f ./web-test.yaml. Это все тоже что мы и делали руками, но только в файле

Удалить, то, что мы запускали из этого файла можно вот так kubectl delete -f ./web-test.yaml. Если у вас в кластере, таких манифест файлов много, то данная команда распарсит файл и удалит именно те Pod, которые описаны в этом файле, не больше и не меньше

Приведу еще один пример манифест файла, в котором есть сразу 2 контейнера, на одном Pod. Мы помним, что это плохая практика, но случаи бывают разные, так же разберем, еще новые параметры

apiVersion : v1
kind: Pod
metadata:
  name: MyApp
  labels:
    env: prod
    app: main
    tier: frontend
    owner: IvanIvanov
spec:
  containers:
    - name : nginx-web
      image: nginx:latest
      ports:
        - containerPort: 80

    - name : tg-send
      image: tomcat:8.5
      ports:
        - containerPort: 4444

И таким образом можно добавлять большое количество контейнеров в Pod, ну и labels - нужны, чтобы передавать ENV-переменные, некоторое описание, для чего данный Pod запускается и прочее. Еще labels всегда рекомендую заполнять, потому как k8s внутри себя обрабатывает эти данные и связывает объекты с labes. Это очень помогает понять в работе, какой Deployments или Service, для чего запущен, чтобы не перепутать и случайно не удалить ту часть кластера, что трогать было не нужно.

Ну и еще с помощью манифест файлов можно обновлять контейнеры внутри Pod, tg-send контейнер, например обновился с 8.5 до 8.5.89. В манифесте, заменяем версию и еще раз выполняем kubectl apply -f ./web-test.yaml, ну и откатываться можно по этой же логике. Самое главное, что Pod при обновлении контейнеров не будет уничтожен, он будет работать. nginx-web не заденет, а вот tg-send стянется с репозитория новая версия и пересоздаст контейнер с ней

tg-send и tomcat в данном примере были взяты только как пример, это не боевой манифест

kubectl describe pods web-test - и если мы еще раз выполним эту команду, то увидим, что появились наши labels

Попробуйте поднять 2 Pod, с разным количеством контейнеров и получить к ним доступ через port-forward

Если уронить один из Node кластера, на котором запущены Pods, поднимутся ли Pods на втором Node? Нет, потому что нет Autoscaling, для этого и был создан Deployments

Deployments

Перед началом работы, стоит удалить все Pods, которые мы создавали выше. И начнем с создания нового Deploymet из терминала

kubectl create deployment web-test --image=nginx:latest --port=80

Получаем статус по Deployments в нашем кластере

kubectl get deploy

NAME       READY   UP-TO-DATE   AVAILABLE   AGE
web-test   1/1     1            1           91s

Посмотрим Pods kubectl get pods

NAME                        READY   STATUS    RESTARTS      AGE
web-test-869f499b74-rbxjp   1/1     Running   1 (25s ago)   95s

Давайте посмотрим, что нам покажет более подробная инфа о созданном Pod

kubectl describe pods web-test-869f499b74-rbxjp

Name:             web-test-869f499b74-rbxjp
Namespace:        default
Priority:         0
Service Account:  default
Node:             k8s-node-02/192.168.99.56
Start Time:       Mon, 29 May 2023 14:00:32 +0000
Labels:           app=web-test
                  pod-template-hash=869f499b74
Annotations:      <none>
Status:           Running
IP:               10.244.2.26
IPs:
  IP:           10.244.2.26
Controlled By:  ReplicaSet/web-test-869f499b74
Containers:
  nginx:
    Container ID:   containerd://5bea31593912c0d7466fb43479b3242c94a44ec148e356167cbc31ed02fe0314
    Image:          nginx:latest
    Image ID:       docker.io/library/nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Mon, 29 May 2023 14:03:11 +0000
    Last State:     Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Mon, 29 May 2023 14:01:45 +0000
      Finished:     Mon, 29 May 2023 14:02:55 +0000
    Ready:          True
    Restart Count:  2
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-bj7jq (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  kube-api-access-bj7jq:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
QoS Class:                   BestEffort
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type     Reason          Age                  From               Message
  ----     ------          ----                 ----               -------
  Normal   Scheduled       4m11s                default-scheduler  Successfully assigned default/web-test-869f499b74-rbxjp to k8s-node-02
  Normal   Pulled          4m9s                 kubelet            Successfully pulled image "nginx:latest" in 1.204357661s (1.204405051s including waiting)
  Normal   Pulled          2m58s                kubelet            Successfully pulled image "nginx:latest" in 1.213935935s (1.213944135s including waiting)
  Normal   Killing         108s (x2 over 3m1s)  kubelet            Stopping container nginx
  Normal   SandboxChanged  107s (x2 over 3m)    kubelet            Pod sandbox changed, it will be killed and re-created.
  Warning  BackOff         106s (x2 over 107s)  kubelet            Back-off restarting failed container nginx in pod web-test-869f499b74-rbxjp_default(f1cf8095-2a76-424f-a672-56a627406afe)
  Normal   Pulling         94s (x3 over 4m10s)  kubelet            Pulling image "nginx:latest"
  Normal   Created         93s (x3 over 4m9s)   kubelet            Created container nginx
  Normal   Pulled          93s                  kubelet            Successfully pulled image "nginx:latest" in 1.236149903s (1.236157173s including waiting)
  Normal   Started         92s (x3 over 4m9s)   kubelet            Started container nginx

Много чего не поменялось, а если подробно глянем на сам Deploy

kubectl describe deployments web-test

Name:                   web-test
Namespace:              default
CreationTimestamp:      Mon, 29 May 2023 14:00:32 +0000
Labels:                 app=web-test
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               app=web-test
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=web-test
  Containers:
   nginx:
    Image:        nginx:latest
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Progressing    True    NewReplicaSetAvailable
  Available      True    MinimumReplicasAvailable
OldReplicaSets:  <none>
NewReplicaSet:   web-test-869f499b74 (1/1 replicas created)
Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  6m49s  deployment-controller  Scaled up replica set web-test-869f499b74 to 1

Тут можно увидеть, что Deploy уже указал свои labels, количество реплик, стратегию обновления, указывает количество контейнеров и т.д. Сейчас создадим Scaling нашего Deployment, то есть мы будем запускать несколько Pods

kubectl scale deployment web-test --replicas 2

Теперь если запросить количество Pods kubectl get pods

NAME                        READY   STATUS    RESTARTS        AGE
web-test-869f499b74-hhlw5   1/1     Running   2 (98s ago)     3m5s
web-test-869f499b74-rbxjp   1/1     Running   5 (2m27s ago)   13m

kubectl get deploy - смотрим на наш Deploy

NAME       READY   UP-TO-DATE   AVAILABLE   AGE
web-test   1/2     2            1           14m

Все, репликация работает. И у нас появился новый объект ReplicaSet, после того, как мы объявили новый Deployment в кластере

kubectl get rs

NAME                  DESIRED   CURRENT   READY   AGE
web-test-869f499b74   2         2         2       16m

Теперь мы можем удалить один Pod и он автоматически поднимется

kubectl delete pods web-test-869f499b74-hhlw5

Затем снова смотрим на Pods kubectl get pods

NAME                        READY   STATUS    RESTARTS        AGE
web-test-869f499b74-dgnqn   1/1     Running   0               6s
web-test-869f499b74-rbxjp   1/1     Running   6 (4m52s ago)   19m

Как можно заметить, Pod поднялся автоматически, и мгновенно, если бы у нас было больше серверов в кластере, то можно полностью убить сервер, Deployments автоматом тут же поднимет еще Pods на других доступных Nodes

Хорошо, мы научились делать Scaling, всегда будет работать 2 Pods. Научимся делать AutoScale

kubectl autoscale deployment web-test --min=1 --max=2 --cpu-percent=80 - и создается еще один объект HPA (Horizontal Pod Autoscaling), минимальное и максимальное количество Pods думаю понятно, а вот –cpu-percent, определяется в процентах. Когда процессор будет утилизироваться, на более чем 80%, то HPA будет поднимать еще Pods на других Nodes, чтобы распределить нагрузку.

kubectl get hpa - получим статут нашего HPA

NAME       REFERENCE             TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
web-test   Deployment/web-test   2%/80%          1         2         2          14m

Перед тем, как обновлять можем посмотреть историю обновлений нашего Deployment

kubectl rollout history deployment/web-test

deployment.apps/web-test
REVISION  CHANGE-CAUSE
1         <none>

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

kubectl rollout status deployment/web-test - а вот тут можно смотреть за статусом обновлений

Перед обновлением, нам нужно взять точное название контейнера, из Deployment

kubectl descriube deployment web-test

Name:                   web-test
Namespace:              default
CreationTimestamp:      Mon, 29 May 2023 14:00:32 +0000
Labels:                 app=web-test
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               app=web-test
Replicas:               2 desired | 2 updated | 2 total | 1 available | 1 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  app=web-test
  Containers:
-   nginx:
    Image:        nginx:latest
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Progressing    True    NewReplicaSetAvailable
  Available      False   MinimumReplicasUnavailable
OldReplicaSets:  <none>
NewReplicaSet:   web-test-869f499b74 (2/2 replicas created)
Events:          <none>

Красным выделено то, что нам нужно из этого вывода. Это именно имя контейнера в нашем Deployment

kubectl set image deployment/web-test nginx=nginx:latest мы запускаем обновление контейнера

kubectl rollout status deployment/web-test ждем ответ

deployment "web-test" successfully rolled out как только мы получаем эту строку, завершается обновление контейнеров в Pods

Теперь обратимся к истории обновлений kubectl rollout history deployment/web-test, тут будут сохраняться истории команды обновления

REVISION  CHANGE-CAUSE
1         kubectl set image deployment/web-test nginx=nginx:latest

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

kubectl rollout undo deployment/web-test - эта команда повернет на предыдущую версию

kubectl rollout undo deployment/web-test --to-revision=2 - эта команда повернет на ту версию, которая была записана в истории

Если в продакшене вы всегда будете указывать вместо версии, latest, и хотите обновляться каждый раз на latest версию, то можно использовать такую команду

kubectl rollout restart deployment/web-test

Теперь поучимся писать манифест файлы. Создадим файл deployment-web-test.yaml

apiVersion : apps/v1
kind: Deployment # инициализируем Deployment
metadata:
  name: web-test-deployment-autoscaling # навзвание нашего деплоймента
  labels:
    app: web-test-deployment-autoscaling
    env: prod
    owner: IvanIvanov
spec:
  selector:
    matchLabels: # описываем с какими Pods, нащ Deployment будет работать
      project: web-site
  template: # далее описываем наши Pods для Deployment
    metadata:
      labels: # данные labels, описываются для подов
        project: web-site
    spec: # описываем контейнеры
      containers:
        - name: web-test
          image: nginx:latest
          ports:
            - containerPort: 80

---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler # инициализируем HPA
metadata:
  name: web-test-autoscaling
spec:
  scaleTargetRef: # описываем для какого Deployment описывается HPA
    apiVersion: apps/v1
    kind: Deployment
    name: web-test-deployment-autoscaling
  minReplicas: 1 # минимально количество Pods
  maxReplicas: 2 # максимальное количество Pods
  metrics: # по каким параметрам будет выполняется HPA
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

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

Services

Переходим к Services, тут будет все проще, потому как мы будем получать доступ к нашим контейнерам. На данный момент существует 4 вида сервисов

  • ClusterIP - создается всегда, и если параметр доступа не обозначен, то ClusterIP будет использоваться по-умолчанию. Работает только внутри кластера
  • NodePort - определенный порт, на всех k8s Worker Nodes будет данный порт открыт для доступа к приложению
  • ExternalName - DNS CNAME запись, обычно используется в облаках (AWS, Azure и т.д.)
  • LoadBalancer - Доступен только в облачных кластерах (AWS, Azure и т.д.)

Перед началом работы требуется создать Deployment

kubectl create deployment web-test --image nginx:latest

Создадим реплики

kubectl scale deployment web-test --replicas 2

И посмотрим, запустилось ли

kubectl get pods

Контейнеры запущены, теперь попробуем получить доступ к ним через ClusterIP

kubectl expose deployment web-test --type=ClusterIP --port 80

kubectl get services или kubectl get svc- получим статус по Services

NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP   2d
web-test     ClusterIP   10.102.199.240   <none>        80/TCP    15s

kubernetes - работает по-умолчанию

web-test - и тот, что мы создали

Внутри кластера (Master или Worker Nodes) мы можем выполнить команду

curl http://10.102.199.240 и получим дефолтную страницу Nginx, при повторном обращении на этот же IP, k8s может забрасывать вас на разные Worker Nodes, потому как автоматически включается балансировка нагрузки

Теперь удалим наш созданный Service, но Deployment при этом не затрагивается

kubectl delete service web-test

Создадим теперь NodePort

kubectl expose deployment web-test --type=NodePort --port 80

kubectl get svc

NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        2d
web-test     NodePort    10.104.85.34   <none>        80:31405/TCP   10s

Как видно из вывода, у нас есть и ClusterIP, потому как он работает всегда и появился порт 80:31405/TCP. На всех Nodes кластера был создан порт 31405, к которому можно обратиться и получить доступ к нашему приложению. Даже можно получить доступ через ноды, на которых не крутится Pods, можно даже обратиться через Master

kubectl describe nodes | grep "IP" - можно будет отфильтровать вывод, чтобы получить InternalIP и ExternalIP, для доступа. ExternalIP может и не появится, все зависит от того, облачный k8s или нет

ExternalName - создавать можно тока через создание CNAME записи для домена, в нашем случае это не будет работать, потому как у нас была локальная установка

LoadBalancer - так же как и ExternalName не подходит для нашего теста

Поучимся создавать манифест файл, с созданием Pods, Deployments, Services и т.д. Файл назовем web-test.yaml

apiVersion : apps/v1
kind: Deployment
metadata:
  name: web-test-deployment-autoscaling
  labels:
    app: web-test-deployment
    env: prod
    owner: IvanIvanov
spec:
  selector:
    matchLabels:
      project: MyApp
  template:
    metadata:
      labels:
        project: MyApp   # Сервис будет искать эти метки, они важны
    spec:
      containers:
        - name: web-test
          image: nginx:latest
          ports:
            - containerPort: 80

---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-test-autoscaling
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-test-deployment-autoscaling
  minReplicas: 1
  maxReplicas: 2
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

---
apiVersion: v1
kind: Service # инициализируем Service
metadata:
  name: my-autoscaling-pod-service # даем ему название
  labels: # задаем ему labels
     env: prod
     owner: IvanIvanov
spec:
  selector:
    project: MyApp # выбираем с какими Pods будет работать наш Service
  ports: # указываем порты
    - name: app-listener
      protocol: TCP
      port: 80
      targetPort: 80
  type: NodePort # указываем тип Service

Ingress

До этого момента мы создавали кластер без Ingress контроллера. Он служит для того, чтобы создавать один Service, который будет принимать на себя трафик и уже с помощью LoadBalancer отправлять конечного пользователя на нужный внутренний Service на ClusterIP, и уже в конечном итоге трафик пользователя дойдет до нужного Pod

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

Для Service ingress можно подключить множество доменов/поддоменов, которые будут привязаны к вашему Services ClusterIP и даже можно привязывать к отдельным страницам вашего домена, что значительно упрощает жизнь. Ingress Controller проверяет свои правила, и направляет пользователя на нужное приложение, эти правила мы как раз и создаем Ingress Rules. В целом это вся задача Ingress, которую он должен выполнять.

Ingress контроллеров довольно много, и выбрать нужный потребуется от задач, которые стоят перед вами. Есть хорошая большая табличка, которая описывает большинство Ingress контроллеров.

Попробуем для примера установить Ingress Contour. Установка очень простая и одной командой

Переходим на сайт Contour и находим команду запуска

kubectl apply -f https://projectcontour.io/quickstart/contour.yaml

Можно проверить, завершилась ли установка

kubectl get pods -n projectcontour -o wide

Попробуем задеплоить пустой Nginx, и подключим его в нашему Ingress для примера

kubectl create deployment main --image=nginx:latest --port=80

kubectl create deployment web-test --image=tomcat:latest --port=8080

Создаем scaling

kubectl scale deployment main --replicas 2

kubectl scale deployment web-test --replicas 2

Дождемся окончания создания подов

kubectl get pods

Теперь нам нужно создать Services с ClusterIP

kubectl expose deployment main --port=80

kubectl expose deployment web-test --port=8080

kubectl get services -o wide

И все готово для подключения к Ingress. Создавать подобные правила лучше всего через манифест-файл, дадим ему имя ingress.yaml

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress-paths

spec:
  rules:
  - host: domain.ru
    http:
      paths:
      - path: "/page1"
        backend:
          serviceName: main
          servicePort: 80

      - path: "/page2"
        backend:
          serviceName: web-test
          servicePort: 80
          
  - host: www.domain.ru
    http:
      paths:
        - backend:
           serviceName: main
           servicePort: 80

kubectl apply -f ingress.yaml - применяем правила для Ingress

Состояние Ingress контроллера можно смотреть вот этими командами

kubectl get ingress && kubectl describe ingress

Ну и чтобы удалить полностью Ingress Controller Contour

kubectl delete ns projectcontour

Helm

Представим наш k8s кластер и мы хотим задеплоить приложение которое описано в манифест файлах например

~/k8s/AppMainProd/service.yaml

~/k8s/AppMainProd/deployment.yaml

На деле одно приложение может содержать не два файла, то есть не два объекта k8s, а значительно больше

Деплоим мы его вот такими командами

kubectl apply -f ~/k8s/AppMainProd/deployment.yaml

kubectl apply -f ~/k8s/AppMainProd/service.yaml

Предположим, что это приложение нужно поднять еще раз, только уже с другими параметрами, например создать Dev-облако, для разработчиков. Мы копируем папку ~/k8s/AppMainProd и называем ее ~/k8s/AppMainDev, а содержимое переписываем под новые условия использования например так

kubectl apply -f ~/k8s/AppMainDev/deployment.yaml

kubectl apply -f ~/k8s/AppMainDev/service.yaml

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

Чтобы не допускать такой ситуации и создали Helm. Наше приложение можно упаковать в Helm Chart

~/k8s/AppMain-HelmChart/templates/service.yaml
~/k8s/AppMain-HelmChart/templates/deployment.yaml
~/k8s/AppMain-HelmChart/Chart.yaml
~/k8s/AppMain-HelmChart/values.yaml

Как можно увидеть наши файлы service.yaml и deployment.yaml помещаются в папку templates - то есть в нее мы складываем все наши объекты кластера k8s

Файл values.yaml в этом файле указываем для значения по умолчанию на разные переменные, при создании кластера, через Helm, templates/service.yaml и остальные файлы будут обращаться к values.yaml

Файл Chart.yaml “технический” файл в котором мы будем указывать инфу о данном чарте

Вот и все, сейчас ниже будут содержимое каждого файла для Helm Chart

Файл AppMain-HelmChart/templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-service
  labels:
    env  : prod
    owner: IvanIvanov
spec:
  selector:
    project: {{ .Release.Name }}    # Selecting PODS with those Labels
  ports:
    - name      : {{ .Release.Name }}-listener
      protocol  : TCP
      port      : 80  # Port on Load Balancer
      targetPort: 80  # Port on POD
  type: LoadBalancer

Файл AppMain-HelmChart/templates/deployment.yaml

apiVersion : apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-deployment
  labels:
    app : {{ .Release.Name }}-deployment
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      project: {{ .Release.Name }}
  template:
    metadata:
      labels:
        project: {{ .Release.Name }}   # Service will look for those PODS Labels!!!
    spec:
      containers:
        - name : {{ .Release.Name }}-web
          image: {{ .Values.container.image }}
          ports:
            - containerPort: 80

Файл AppMain-HelmChart/Chart.yaml

apiVersion: v2
name       : App-HelmChart
description: My Helm chart for Kubernetes
type       : application
version    : 0.1.0   # This is the Helm Chart version
appVersion : "1.2.3" # This is the version of the application being deployed

keywords:
  - apache
  - http
  - https

maintainers:
  - name : Ivan Ivanov
    email: [email protected]
    url  : ivaaan.ru

Файл AppMain-HelmChart/values.yaml

# Default Values for my Helm Chart

container:
  image: nginx:latest

replicaCount: 2

Разберем переменные файла AppMain-HelmChart/templates/deployment.yaml

...
metadata:
  name: {{ .Release.Name }}-deployment
  labels:
    app : {{ .Release.Name }}-deployment
...

.Release.Name - в Helm есть переменная Release.Name, которую Helm создаст автоматически, чтобы при повторном деплое у вас не рушилась первая версия приложения

...
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
...

.Values.replicaCount - данная переменная берется из файла AppMain-HelmChart/values.yaml, значение которого будет равно 2 (обратите внимание на файл values.yaml и все станет понятно)

...
          image: {{ .Values.container.image }}
...

.Values.container.image - Docker Image, который будет использоваться во время деплоя, так же будет взят из файла AppMain-HelmChart/values.yaml

Деплоится вся эта конструкция вот такой командой

helm install App AppMain-HelmChart/ - helm install [название чарта] [папка]

helm install App AppMain-HelmChart/ -f prod_values.yaml - -f prod_values.yaml - содержит переменные для Prod

helm install App AppMain-HelmChart/ --set container.image=apache:latest - сменить переменную в файле values.yaml не редактируя его и таких переменных может быть больше

Переходим на сайт Helm и находим команду установки. На деле это один батник. Helm можно установить вам на ПК, с которого вы управляете кластером

Можно вызнать команду helm create Project - создаст пустой Chart, где можно будет посмотреть дефолтную структуру файлов и много полезного)

Посмотреть, что было задеплоино через Helm можно вот так

helm list

А так же для текущего Deployment увеличить например количество реплик с 2 до 4, для примера будем использовать наш Helm Chart

helm upgrade App AppMain-HelmChart/ --set replicaCount=4

Так же еще текущий Chart можно запаковать в архив вот такой командой

helm package AppMain-HelmChart/ и хранить его, например как версию приложения в своих репозиториях. Helm сохранит данный файл и к нему можно будет обращаться для деплоя не распаковывая

helm install App123 App-HelmChart-0.1.0.tgz

В Helm можно подключать публичные репозитории, где лежит куча чартов например для деплоя Zabbix

helm search hub zabbix

Заключение

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

Анонсы и еще больше информации в Telegram-канале