Kubernetes
December 11, 2022

Kubernetes in Yandex Cloud

Прежде чем приступать к изучению Kubernetes в YC необходимо ответить на ряд вопросов.

  1. Что такое Kubernetes?
  2. Зачем он используется и в чем профит от его использования?
  3. Что такое микросервисная архитектура и в чем её отличие от монолита?
  4. Какие варианты развертывания кластера Kubernetes существуют?
  5. В чем плюс использования Kubernetes в облаке?
  6. Какой плюс использования Managed решения по сравнению со своей инсталляцией?
  7. Плюсы от использования решения mk8s в Yandex Cloud?
  8. Ради чего стоит изучать тему Kubernetes?
  9. Подготавливаем инфраструктуру для разворачивания кластера k8s в YC.

Поехали!

Что же такое Kubernetes, или как часто ещё пишут k8s.

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

Теперь поговорим подробнее про контейнеры. Для этого вкратце взглянем на причины их появления и генезис виртуализации. При всех плюсах виртуализации у неё есть один существенный недостаток - не оптимальное расходование ресурсов. Предположим есть две ВМ и некоторый набор приложений на каждой из них. Но помимо этих приложений на ВМ будут находиться и ОС, которые будут потреблять существенное количество CPU/RAM. Можно было бы переместить приложения на одну ВМ. Но это может быть не всегдя возможно или не безопасно.

В 2013 году компания Docker представила одноимённый продукт для работы с контейнерами. С его помощью можно в пределах одной ВМ и соответственно одной ОС хранить разные приложения несовместимые в обычном случае. Это уменьшило накладные ресурсы на работу нескольких ОС.

Docker->Docker Compose->Kubernetes.

Итак, docker позволил хранить на одной ВМ разные приложения в специальных изолированных областях - контейнерах. Где помимо самого приложения находятся все необходимые для его запуска библиотеки и зависимости. Также появилась возможность быстро создавать приложение не тратя время на его компиляцию/инсталляцию. Это решение пришлось в первую очередь по душе программистам, которым нужно было бы протестировать какую-то фичу. Создал контейнер с приложением, протестировал фичу, убил контейнер.

Но разработчики данного решения решили пойти дальше. А что если не просто упаковывать приложения в контейнеры, но и научить их взаимодействовать между собой? Так появилось решение Docker Compose. И микросервисный подход к разработке ПО. С его помощью можно уже настроить взаимодействие нескольких приложений друг с другом. Но и у этого решения есть недостаток. Все сервисы, запущенные с посощью docker compose, будут работать только на одной ВМ. И если она перезагрузится/выйдет из строя, то и сервис в этом контейнере будет недоступен. Здесь и выходит на сцену k8s, который позвляет размещать приложения на разных ВМ.

Зачем используется k8s и в чем профит от его использования.

  1. Возможность масштабировать приложение в зависимости от нагрузки;
  2. Независимость работы приложения от работоспособности ВМ;
  3. Независимость работы приложения от характеристик ВМ*;
  4. Автоматическое восстановление работы приложения в случае его сбоя;
  5. Цена поддержания инфраструктуры резко сокращается.

Существуют и другие оркестраторы помимо k8s. Это nomad, docker swarm. Но их комьюнити не сильно развито. А docker swarm вообще перестал поддерживаться и стал deprecated. Что же касается k8s, то количество контрибуций находится на втором месте в мире после Linux.

В чём отличия микросервисной и монолитной архитектур при создании ПО.

При рассмотрении темы кубера нельзя не упомятнуть про микросервисную архитектуру. Это подход при разработке ПО, когда решение разбивается на самостоятельные куски, которые можно поддерживать вполне себе отдельно от других компонентов.

В чём же плюсы такого подхода:

  1. Каждая команда может заниматься своим куском кода и апгрейдить его независимо от других команд;
  2. Это позволяет постоянно совершенствовать продукт;
  3. Уменьшает время на поиск ошибок в коде;
  4. Уменьшает время компиляции.

Приведу пример:
Есть решение для интернет коммерции. В качестве веб сервера используется Nginx, для хранения кеша (например, корзины) используется Redis, в качестве постоянного хранилища используется MongoDB и в качестве брокера используется Kafka. И логика, которая это всё будет связывать написана на python+js.
Каждое из этих решений можно конфигурировать/дорабатывать отдельно.

При монолитной архитектуре продукт компилируется целиком. Иными словами при возникновении на этом этапе какой-то ошибки влечёт за собой долгий её поиск. К решениям, использующим монолитную архитектуру можно отнести: 1C Предприятие, Microsoft Office, etc.

Выбор микросервисной архитектуры при проектировании софта диктуется потребностью рынка. Лучше первым выпустить продукт на рынок и далее его допиливать, чем выпустить сразу хорошо, но через несколько лет. С большой долей вероятности рынок в этом случае не получится отыграть.

История про кубер - это история про микросервисную архитектуру.

Варианты развёртывания кластера Kubernetes.

Существуют три варианта развёртывания кластера Kubernetes:

  1. На голом железе. В этом случае придётся вручную поднимать ОС, ВМ и настраивать кластер своими руками.
  2. Использовать облако для развёртывания кластера. Но использовать при этом свою собственную инсталляцию.
  3. Использовать managed решение, предоставляемое облаком.

Плюсы использования Kubernetes в облаке.

  1. Возможность изменения характеристик воркер нод, входящих в кластер, на лету. То есть без даунтайма.
  2. Возможность использовать балансировку запросов извне на воркеры.
  3. Поддержка ВМ и сети специалистами облака. Здесь стоит отметить, что на 100% это не избавит от проблем. Просто сделает этот шанс меньше.

Вообще плюсы от облака - это возможность изменять ресурсы по мере их надобности.

Плюсы использования managed решения.

  1. Всю работу по обновлению и поддержанию mk8s облако берёт на себя.
  2. Оказывает поддержку по этому решению.

Но есть и минусы:

Сделать какие-то вещи под себя (кастомизировать) не получится.

Плюсы от использования решения mk8s в Yandex Cloud.

В данном случае имеет смысл сравнивать с аналогичными решениями от AWS, GCP, OCI, Azure.

YC выигрывает по следующим параметрам:

  1. Цена.
  2. Ориентированность на российского потребителя.

Если же сравнивать с российскими аналогами такими как SberCloud, MTS cloud, VK Cloud, Selectel, Timeweb, то решение от YC появилось исторически раньше. И другие вендоры являются догоняющими.

Зачем вам изучать Kubernetes?

Также как и облака, это наиболее динамично развивающийся сегмент IT индустрии. Это интересно и прибыльно. Это возможность пойти работать в технологическую компанию на интересный проект и за немалые деньги.

Практика!

В начале практики подготовим всю необходимую в дальнейшем инфраструктуру.

Подготовка инфраструктуры.

На этом этапе мы создадим необходимые ресурсы в облаке для работы с кластером: SA, Network, Subnet, SG, ключ KMS. Также установим утилиту kubectl для работы с кластером.

export FOLDER=$(yc config get folder-id)
export ZONE=ru-central1-d

Установим docker по следующему мануалу: https://docs.docker.com/engine/install/ubuntu/

Создадим SA и сгенерируем для него ключи.

yc iam service-account create \
   --name kuber-sa \
   --description "Main service account" \
   --async
yc iam service-account list | grep kuber-sa

Теперь добавим ему необходимых прав доступа.

Добавим созданному аккаунту необходимых прав доступа.

export SA=$(yc iam service-account get kuber-sa --format json | jq -r '.id')
yc resource folder add-access-binding $FOLDER --role editor --subject serviceAccount:$SA
yc resource folder list-access-bindings --id $FOLDER | grep $SA
| editor                           | serviceAccount

Теперь сгенерируем необходимые для доступов ключи.

mkdir sa_key && cd sa_key
yc iam key create --service-account-id $SA --output key.json
yc iam access-key create --service-account-id $SA --format=json > stat-key-to-s3.json
ls -l

Создадим Network, Subnet, SG.

Network:

yc vpc network create \
   --folder-id $FOLDER \
   --name institute \
   --description "Institute network" \
   --async
export NETWORK=$(yc vpc network get institute --format json | jq -r '.id')

Subnet:

yc vpc subnet create \
  --folder-id $FOLDER \
  --name sub-d \
  --description "Subnet zone d" \
  --network-id $NETWORK \
  --zone $ZONE \
  --range 10.130.0.0/24 \
  --async

Запомним идентификатор подсети для зоны ru-central1-d:

export SUBNET=$(yc vpc subnet get sub-d --format json | jq -r '.id')

SG:

yc vpc sg create --name k8s-institute \
"--rule" "description=access all egress port,direction=egress,from-port=1,to-port=65535,protocol=any,v4-cidrs=[0.0.0.0/0]" \
"--rule" "description=access 22 port,direction=ingress,port=22,protocol=tcp,v4-cidrs=[0.0.0.0/0]" \
"--rule" "description=access 443 port,direction=ingress,port=443,protocol=tcp,v4-cidrs=[0.0.0.0/0]" \
"--rule" "description=access 6443 port,direction=ingress,port=6443,protocol=tcp,v4-cidrs=[0.0.0.0/0]" \
"--rule" "description=balancer port,direction=ingress,from-port=1,to-port=65535,protocol=tcp,v4-cidrs=[198.18.235.0/24],v4-cidrs=[198.18.248.0/24]" \
"--rule" "description=service port,direction=ingress,from-port=30000,to-port=32767,protocol=tcp,v4-cidrs=[0.0.0.0/0]" \
"--rule" "description=ICMP,direction=ingress,protocol=icmp,v4-cidrs=[10.0.0.0/8],v4-cidrs=[192.168.0.0/16]" \
"--rule" "description=PODtoService,direction=ingress,from-port=1,to-port=65535,protocol=any,v4-cidrs=[10.1.0.0/16],v4-cidrs=[10.2.0.0/16]" \
"--rule" "description=Self,direction=ingress,from-port=1,to-port=65535,protocol=any,predefined=self_security_group" \
--network-id $NETWORK --description "k8s access" --folder-id $FOLDER --async
export SG=$(yc vpc sg get k8s-institute --format json | jq -r '.id')

Создадим ключ шифрования KMS.

yc kms symmetric-key create \
--folder-id $FOLDER \
--name k8s-institute \
--labels institute=k8s \
--description "k8s symmetric key" \
--default-algorithm aes-256 \
--deletion-protection \
--rotation-period 24h \
--async
export KMS=$(yc kms symmetric-key get k8s-institute --format json | jq -r ".id")

Установка kubectl.

https://kubernetes.io/ru/docs/tasks/tools/install-kubectl/

Производится на вашем ноутбуке.

На этом подготовительный этап завершился!


Установка кластера Kubernetes.

Создадим кластер k8s с cni Calico.

yc k8s cluster create \
--folder-id $FOLDER \
--name institute-kuber \
--description "k8s institute" \
--network-id $NETWORK \
--zone $ZONE \
--subnet-id $SUBNET \
--public-ip \
--release-channel rapid \
--version 1.28 \
--cluster-ipv4-range 10.1.0.0/16 \
--service-ipv4-range 10.2.0.0/16 \
--auto-upgrade=false \
--security-group-ids $SG \
--enable-network-policy \
--node-service-account-id $SA \
--service-account-id $SA \
--kms-key-id $KMS \
--daily-maintenance-window start=22:00,duration=10h \
--async

SSH

Копируем ключ ssh в текстовый файлик. У меня это /Users/cameda/ssh-pairs.txt.

Создание масштабируемой нод группы для кластера.

yc k8s node-group create \
--folder-id $FOLDER \
--name cam-auto-group \
--cluster-name institute-kuber \
--description "autoupgrade group" \
--metadata serial-port-enable=1 \
--metadata-from-file=ssh-keys=/Users/cameda/ssh-pairs.txt \
--location zone=$ZONE \
--platform standard-v3 \
--preemptible \
--memory 8 \
--cores 2 \
--core-fraction 100 \
--disk-type network-ssd \
--disk-size 96 \
--network-acceleration-type standard \
--network-interface security-group-ids=$SG,subnets=$SUBNET,ipv4-address=nat \
--version 1.28 \
--container-runtime containerd \
--auto-scale min=1,max=3,initial=1 \
--auto-upgrade \
--auto-repair \
--max-expansion 2 \
--max-unavailable 1 \
--daily-maintenance-window start=22:00,duration=5h \
--allowed-unsafe-sysctls net.ipv4.tcp_timestamps \
--async

Готово! Теперь можно подключиться к кластеру и немного осмотреться внутри.

Для подключения необходимо зайти в UI и нажать на кнопку Подключиться. Появившуюся строчку копируем в консоль.

Посмотрим что внутри кластера.

kubectl get ns
kubectl get po -A
kubectl api-resources
kubectl get no
kubectl get svc -A
kubectl get events -A
kubectl describe po ip-masq-agent-f48gk -n kube-system

Осмотримся снаружи кластера.

yc k8s cluster get catspqurstbl7rfcuvfj
yc k8s cluster list-nodes catspqurstbl7rfcuvfj
yc k8s cluster list-operations catspqurstbl7rfcuvfj
yc k8s cluster list-node-groups catspqurstbl7rfcuvfj

Создадим deployment с nginx и получим к нему доступ извне.

Создадим отдельный namespace для нашей работы:

kubectl create ns prod

Deployment+Service

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cam-nginx
  namespace: prod
  labels: 
    app: nginx-prod 
    environment: prod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  strategy: 
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        imagePullPolicy: IfNotPresent
        ports: 
        - containerPort: 80
        resources: 
          requests: 
            cpu: 90m
            memory: 50Mi 
          limits:
            memory: 100Mi
      restartPolicy: Always
EOF
kubectl get po -n prod
kubectl get deploy -n prod
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  namespace: prod
  name: cam-nginx-service
spec:
  ports:
  # Порт сетевого балансировщика, на котором будут обслуживаться пользовательские запросы.
  - port: 80
    name: plaintext
    # Порт контейнера, на котором доступно приложение.
    targetPort: 80
  # Метки селектора, использованные в шаблоне подов при создании объекта Deployment.
  selector:
    app: nginx
  type: LoadBalancer
EOF
kubectl get svc -A

Проверяем поле EXTERNAL-IP и там хранится IP адрес по которому мы можем подключиться к сервису.

http://158.160.45.149/

curl -I http://158.160.45.149/
HTTP/1.1 200 OK
Server: nginx/1.23.2
Date: Sun, 11 Dec 2022 16:16:33 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Wed, 19 Oct 2022 07:56:21 GMT
Connection: keep-alive
ETag: "634fada5-267"
Accept-Ranges: bytes

Поскейлим приложение.

yc k8s cluster list-nodes catspqurstbl7rfcuvfj
kubectl scale deployment cam-nginx --replicas=20 -n prod

и ещё раз

kubectl scale deployment cam-nginx --replicas=5 -n prod
yc k8s cluster list-nodes catspqurstbl7rfcuvfj

3 ноды как было, так и осталось. Проблема здесь в том, что вверх нод группа скейлится легко, а вот вниз не очень. И это потому происходит, что на всех нодах кластера создаются в том числе и системные поды. Которые сами по себе не очень помирать любят. Поэтому их надо убить.

Для этого сначала надо ноду закардонить, чтобы более ничего на неё не шедулилось. Если не закардоним, то под опять пересоздастся после того как его удалили.

kubectl get no
NAME                        STATUS   ROLES    AGE    VERSION
cl1mhmf0r2lehrd3s0fv-amas   Ready    <none>   63m    v1.21.5
cl1mhmf0r2lehrd3s0fv-orul   Ready    <none>   120m   v1.21.5
cl1mhmf0r2lehrd3s0fv-yzec   Ready    <none>   63m    v1.21.5
kubectl cordon cl1mhmf0r2lehrd3s0fv-yzec

Посмотрим какие поды есть на этой ноде с помощью дескрайба:

kubectl describe no cl1mhmf0r2lehrd3s0fv-yzec

Вот кусок дескрайба:

Non-terminated Pods:          (5 in total)
  Namespace                   Name                         CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                         ------------  ----------  ---------------  -------------  ---
  kube-system                 calico-node-45wxp            250m (12%)    0 (0%)      0 (0%)           0 (0%)         64m
  kube-system                 ip-masq-agent-tpwtr          10m (0%)      0 (0%)      16Mi (1%)        0 (0%)         64m
  kube-system                 kube-proxy-d7lm8             100m (5%)     0 (0%)      0 (0%)           0 (0%)         64m
  kube-system                 npd-v0.8.0-xsrxf             20m (1%)      200m (10%)  20Mi (1%)        100Mi (7%)     64m
  kube-system                 yc-disk-csi-node-v2-sw5ff    30m (1%)      600m (31%)  96Mi (7%)        600Mi (47%)    64m

Удлим поды из этого списка:

kubectl delete po calico-node-45wxp -n kube-system
kubectl delete po ip-masq-agent-tpwtr -n kube-system
kubectl delete po npd-v0.8.0-xsrxf -n kube-system

После этого нода удалилась сама:

kubectl get no
NAME                        STATUS   ROLES    AGE    VERSION
cl1mhmf0r2lehrd3s0fv-amas   Ready    <none>   70m    v1.21.5
cl1mhmf0r2lehrd3s0fv-orul   Ready    <none>   127m   v1.21.5

Подробнее про работу кластер автоскейлера можно почитать здесь:
https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler

Подключение к поду.

Посмотрим какие поды у нас сейчас есть:

kubectl get po -n prod
NAME                         READY   STATUS    RESTARTS   AGE
cam-nginx-6dc46855d8-84kp7   1/1     Running   0          141m
cam-nginx-6dc46855d8-9k64q   1/1     Running   0          124m
cam-nginx-6dc46855d8-hpljm   1/1     Running   0          124m
cam-nginx-6dc46855d8-vvw4r   1/1     Running   0          124m
cam-nginx-6dc46855d8-wn7t7   1/1     Running   0          141m

Удалим любой из них:

kubectl delete po cam-nginx-6dc46855d8-vvw4r -n prod

Посмотрим сколько сейчас подов:

kubectl get po -n prod
NAME                         READY   STATUS    RESTARTS   AGE
cam-nginx-6dc46855d8-84kp7   1/1     Running   0          143m
cam-nginx-6dc46855d8-9k64q   1/1     Running   0          126m
cam-nginx-6dc46855d8-hpljm   1/1     Running   0          126m
cam-nginx-6dc46855d8-nr2qk   1/1     Running   0          38s
cam-nginx-6dc46855d8-wn7t7   1/1     Running   0          143m

Их всё равно 5. Дело в том, что после того как мы убили под он довольно быстро создался заново.

Подключимся к любому из подов:

kubectl exec --tty --stdin -n prod cam-nginx-6dc46855d8-9k64q -- /bin/bash

Внутри пода будет примерно ничего. Не будет таких утилит как ping, ip, ss. Но можно кое-что установить:

apt -y install htop vim
htop
cd /bin && ls
nginx -t
nginx -v
exit

Marketplace Kubernetes

В Marketplace k8s мы рассмотрим одно решение для примера того как это работает. Это будет NodeLocalDNS. Это daemonset. Его функция - кешировать DNS запросы и экономить таким образом трафик и следовательно деньги. Также это сокращает время последующих запросов и хорошо ускоряет работу кластера.

Заходим в UI облака. Кластер->Marketplace. И выбираем там NodeLocalDNS. В качестве namespace указываем kube-system. После того как приложение перешло в статус Deployed, проверим как оно установилось.

kubectl get po -n kube-system

DaemonSet - это ресурс кластера k8s, который шедулит по одному поду на каждую ноду кластера.

Интеграция с Container Registry

До этого момента мы использовали стандартный образ из Docker Hub. Но если необходимо создать свой сайт, то лучше для этих целей создать свой собственный образ и запустить на его основе под в кубере.

Перед тем как создать свой собственный образ подготовим репозиторий для него. Репозиторий - это хранилище образов.

yc container registry create \
   --folder-id $FOLDER \
   --name pguti \
   --labels pguti=cr \
   --async

Посмотреть информацию о реестре можно так:

yc container registry list
yc container registry get pguti
export REGISTRY=$(yc container registry get pguti --format=json | jq -r ".id")

Аутентификация с помощью Docker Credential helper.

yc container registry configure-docker

Создадим Dockerfile и отправим его в реджистри.

mkdir -p ~/docker/nginx && cd ~/docker/nginx
touch index.html && echo 'Hello, Alex!' > index.html
vi nginx.dockerfile
FROM nginx:latest
LABEL maintainer="[email protected]"
RUN apt update && apt -y install htop mc dstat
COPY index.html /usr/share/nginx/html/index.html
docker build . -f nginx.dockerfile -t cr.yandex/$REGISTRY/nginx:a1
docker push cr.yandex/${REGISTRY}/nginx:a1

Создадим под на основе данного имиджа:

kubectl get po
kubectl run --attach cam-test --image cr.yandex/${REGISTRY}/nginx:a1
kubectl get po
kubectl logs cam-test
kubectl describe po cam-test # Здесь нам нужно уточнить адрес пода для проверки доступности сервиса в следующей части.
kubectl exec --tty --stdin cam-test — /bin/bash
dstat

Сервисный под.

kubectl run -i -t --rm --image amouat/network-utils test bash

Заходим внутрь пода.

curl 10.97.128.14
Hello, Alex!

Итого: мы создали под на основе подготовленного нами имиджа.

exit