В этой главе мы рассмотрим основные ресурсы Kubernetes для развертывания приложений и обеспечения доступа к ним изнутри и снаружи кластера.

Подготовка окружения

Подготовьте рабочее окружение согласно инструкциями главы “Подготовка окружения”, если это ещё не сделано.

Рабочее окружение работало, но перестало? Инструкции из этой главы не работают? Может помочь:

Работает ли Docker?

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим Docker:

sudo systemctl restart docker

Убедимся, что Docker запустился:

sudo systemctl status docker

Результат выполнения команды, если Docker успешно запущен:

● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2021-06-24 13:05:17 MSK; 13s ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 2013888 (dockerd)
      Tasks: 36
     Memory: 100.3M
     CGroup: /system.slice/docker.service
             └─2013888 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

dockerd[2013888]: time="2021-06-24T13:05:16.936197880+03:00" level=warning msg="Your kernel does not support CPU realtime scheduler"
dockerd[2013888]: time="2021-06-24T13:05:16.936219851+03:00" level=warning msg="Your kernel does not support cgroup blkio weight"
dockerd[2013888]: time="2021-06-24T13:05:16.936224976+03:00" level=warning msg="Your kernel does not support cgroup blkio weight_device"
dockerd[2013888]: time="2021-06-24T13:05:16.936311001+03:00" level=info msg="Loading containers: start."
dockerd[2013888]: time="2021-06-24T13:05:17.119938367+03:00" level=info msg="Loading containers: done."
dockerd[2013888]: time="2021-06-24T13:05:17.134054120+03:00" level=info msg="Daemon has completed initialization"
systemd[1]: Started Docker Application Container Engine.
dockerd[2013888]: time="2021-06-24T13:05:17.148493957+03:00" level=info msg="API listen on /run/docker.sock"

Теперь проверим, что Docker доступен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Перезагружали компьютер после подготовки окружения?

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

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

Теперь запустите команду в фоновом PowerShell-терминале и не закрывайте его:

minikube tunnel --cleanup=true

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start --namespace werf-guide-app

Выставим Namespace по умолчанию, чтобы не указывать его при каждом вызове kubectl:

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

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

Случайно удаляли Namespace приложения?

Если вы непреднамеренно удалили Namespace приложения, то необходимо выполнить следующие команды, чтобы продолжить прохождение руководства:

kubectl create namespace werf-guide-app
kubectl create secret docker-registry registrysecret \
  --docker-server='https://index.docker.io/v1/' \
  --docker-username='<имя пользователя Docker Hub>' \
  --docker-password='<пароль пользователя Docker Hub>'

Результат успешного выполнения команды:

namespace/werf-guide-app created
secret/registrysecret created
Ничего не помогло, окружение или инструкции по-прежнему не работают?

Если ничего не помогло, то пройдите главу “Подготовка окружения” с начала, подготовив новое окружение с нуля. Если и это не помогло, тогда, пожалуйста, расскажите о своей проблеме в нашем Telegram или оставьте Issue на GitHub, и мы обязательно вам поможем.

Подготовка репозитория

Используем уже существующий репозиторий с приложением (выполните в Bash/PowerShell):

cd ~/werf-guide/app

Возникли проблемы с репозиторием? Попробуйте инструкции на вкладке “Начинаю проходить руководство с этой главы” выше.

Подготовим новый репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
if (-not (Test-Path ~/werf-guide/guides)) {
  git clone https://github.com/werf/werf-guides $env:HOMEPATH/werf-guide/guides
}

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -Recurse -Force ~/werf-guide/app
cp -Recurse -Force ~/werf-guide/guides/examples/basic/005_kubernetes_basics ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

Выполним следующий набор команд в Bash:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
test -e ~/werf-guide/guides || git clone https://github.com/werf/werf-guides ~/werf-guide/guides

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -rf ~/werf-guide/app
cp -rf ~/werf-guide/guides/examples/basic/005_kubernetes_basics ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

Шаблоны, манифесты и ресурсы

werf при деплое использует YAML-манифесты, описывающие Kubernetes-ресурсы. Эти манифесты получаются из Helm-шаблонов, которые лежат в .helm/templates и .helm/charts.

Helm-шаблон, описывающий Pod (один из Kubernetes-ресурсов), мог бы лежать в .helm/templates/pod.yaml и выглядеть так:

apiVersion: v1
kind: Pod
metadata:
  name: standalone-pod
spec:
  containers:
  - name: main
    image: {{ $.Values.image }}  # Helm-шаблонизация, позволяет параметризовать имя образа для контейнера
    command: ["tail", "-f", "/dev/null"]

Перед деплоем этот Helm-шаблон с помощью werf преобразовался бы в манифест, который выглядел бы так:

apiVersion: v1
kind: Pod
metadata:
  name: standalone-pod
spec:
  containers:
  - name: main
    image: alpine  # если пользователь в качестве имени образа указал "alpine"
    command: ["tail", "-f", "/dev/null"]

А уже во время деплоя этот манифест стал бы ресурсом Pod в Kubernetes-кластере. Посмотреть, как этот ресурс выглядел бы в кластере, можно было бы с помощью команды kubectl get:

kubectl get pod standalone-pod --output yaml

Так бы выглядел результат:

apiVersion: v1
kind: Pod
metadata:
  name: standalone-pod
  namespace: default
spec:
  containers:
  - name: main
    image: alpine
    command: ["tail", "-f", "/dev/null"]
    ...
status:
  phase: Running
  podIP: 172.17.0.7
  startTime: "2021-06-02T13:17:47Z"

Запуск приложений

Использование ресурса Pod — простейший способ запустить в Kubernetes один или несколько контейнеров. В манифесте Pod’а описываются контейнеры и их конфигурация. Но на практике сами по себе Pod’ы стараются не деплоить — вместо них деплоят Controller’ы, которые создают и управляют Pod’ами за вас. Один из таких контроллеров — Deployment. Создавая Pod’ы с помощью Deployment, вы сильно упрощаете управление Pod’ами.

Вот некоторые возможности, которые предоставляет Deployment (и которых нет у Pod’ов самих по себе):

  • При автоматическом или ручном удалении Pod’а в Deployment’е этот Pod будет перезапущен;
  • Большая часть конфигурации Pod’а не может быть обновлена. Чтобы обновить его конфигурацию, Pod нужно пересоздать. Конфигурацию же Pod’а в Deployment’е можно обновлять без пересоздания Deployment’а;
  • Обновление конфигурации Pod’ов происходит без простоя: при обновлении часть Pod’ов со старой конфигурацией остаётся активной до тех пор, пока новые Pod’ы не начнут успешно запускаться;
  • Одним Deployment’ом можно запускать нескольких Pod’ов сразу, в том числе на разных Node’ах.

Разные контроллеры имеют разные возможности, связанные с созданием и управлением Pod’ами. Вот основные контроллеры и их практическое применение:

  • Deployment — стандарт для деплоя stateless-приложений;
  • StatefulSet — стандарт для деплоя stateful-приложений;
  • DaemonSet — для деплоя приложений, которые должны быть запущены только по одному экземпляру на каждом узле (агенты для логирования, мониторинга);
  • Job — для запуска разовых задач в Pod’ах (например, миграции базы данных);
  • CronJob — для многоразового запуска задач в Pod’ах по расписанию (например, регулярная очистка чего-либо).

Получить список ресурсов определенного типа в кластере можно с помощью всё той же команды kubectl get (в данном примере команды последовательно возвратят список всех Pod, Deployment, StatefulSet, Job и CronJob из всех namespace):

kubectl get --all-namespaces pod
Посмотреть ответ
NAMESPACE           NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx       ingress-nginx-admission-create-8bgk7        0/1     Completed   0          11d
ingress-nginx       ingress-nginx-admission-patch-8fkgl         0/1     Completed   1          11d
ingress-nginx       ingress-nginx-controller-5d88495688-6lgx9   1/1     Running     1          11d
kube-system         coredns-74ff55c5b-hgzzx                     1/1     Running     1          13d
kube-system         etcd-minikube                               1/1     Running     1          13d
kube-system         kube-apiserver-minikube                     1/1     Running     1          13d
kube-system         kube-controller-manager-minikube            1/1     Running     1          13d
kube-system         kube-proxy-gtrcq                            1/1     Running     1          13d
kube-system         kube-scheduler-minikube                     1/1     Running     1          13d
kube-system         storage-provisioner                         1/1     Running     2          13d
kubectl get --all-namespaces deployment
Посмотреть ответ
NAMESPACE           NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
ingress-nginx       ingress-nginx-controller   1/1     1            1           11d
kube-system         coredns                    1/1     1            1           13d
kubectl get --all-namespaces statefulset
kubectl get --all-namespaces job
kubectl get --all-namespaces cronjob

Также можно получить полную конфигурацию любого ресурса в YAML-формате, если добавить в команду kubectl get опцию --output yaml, например:

kubectl -n ingress-nginx get deployment ingress-nginx-controller --output yaml

В ответ получим примерно следующее:

...
kind: Deployment
metadata:
  name: ingress-nginx-controller
...

Чаще всего вам придется сталкиваться с ресурсом Deployment, поэтому рассмотрим его подробнее. Про остальные типы контроллеров можно прочитать в документации к Kubernetes.

Deployment

Рассмотрим Deployment нашего приложения подробнее:

apiVersion: apps/v1
# Тип ресурса.
kind: Deployment
metadata:
  # Имя этого Deployment.
  name: werf-guide-app
spec:
  # Кол-во реплик этого Deployment.
  replicas: 1
  selector:
    matchLabels:
      # По этому лейблу Deployment находит обслуживаемые им Pods.
      app: werf-guide-app
  # Описание шаблона, на основе которого создаются Pods для этого Deployment.
  template:
    metadata:
      labels:
        # Лейблы создаваемых Pods.
        app: werf-guide-app
    spec:
      imagePullSecrets:
      # Имя секрета с Docker credentials для доступа к container registry.
      - name: registrysecret
      containers:
      # Имя первого контейнера.
      - name: app
        # Имя и тег образа контейнера.
        image: {{ .Values.werf.image.app }}
        # Основная команда контейнера, начнёт выполняться при его запуске.
        command: ["/app/start.sh"]
        # Порт, на котором будет слушать запускаемое приложение.
        ports:
        - containerPort: 8000
apiVersion: apps/v1 # Тип ресурса. kind: Deployment metadata: # Имя этого Deployment. name: werf-guide-app spec: # Кол-во реплик этого Deployment. replicas: 1 selector: matchLabels: # По этому лейблу Deployment находит обслуживаемые им Pods. app: werf-guide-app # Описание шаблона, на основе которого создаются Pods для этого Deployment. template: metadata: labels: # Лейблы создаваемых Pods. app: werf-guide-app spec: imagePullSecrets: # Имя секрета с Docker credentials для доступа к container registry. - name: registrysecret containers: # Имя первого контейнера. - name: app # Имя и тег образа контейнера. image: {{ .Values.werf.image.app }} # Основная команда контейнера, начнёт выполняться при его запуске. command: ["/app/start.sh"] # Порт, на котором будет слушать запускаемое приложение. ports: - containerPort: 8000

Более подробное описание Deployment доступно в официальной документации.

Развернём наше приложение:

werf converge --repo <имя пользователя Docker Hub>/werf-guide-app

Убедимся, что наш Deployment создался:

kubectl get deployment werf-guide-app

В ответ получим примерно следующее:

NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
werf-guide-app          1/1     1            1           25s

А теперь посмотрим на Pod, созданный нашим Deployment:

kubectl get pod -l app=werf-guide-app

В ответ отобразится примерно следующее:

NAME                             READY   STATUS    RESTARTS   AGE
werf-guide-app-8b748b85d-829j9   1/1     Running   0          25h

Service и Ingress

С помощью Deployment’а мы можем развернуть наше stateless-приложение, но если пользователям или другим приложениям потребуется связываться с этим приложением изнутри или снаружи кластера, то в этом нам помогут два других ресурса: Ingress и Service.

Рассмотрим наш Ingress-ресурс:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  # Имя Ingress-ресурса.
  name: werf-guide-app
spec:
  rules:
  # Домен, запросы на который будут обрабатываться в paths ниже.
  - host: werf-guide-app
    http:
      paths:
      # Запросы с префиксом / (все запросы) перенаправятся на порт 8000 нашего Service.
      - path: /
        backend:
          serviceName: werf-guide-app
          servicePort: 8000
apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: annotations: kubernetes.io/ingress.class: nginx # Имя Ingress-ресурса. name: werf-guide-app spec: rules: # Домен, запросы на который будут обрабатываться в paths ниже. - host: werf-guide-app http: paths: # Запросы с префиксом / (все запросы) перенаправятся на порт 8000 нашего Service. - path: / backend: serviceName: werf-guide-app servicePort: 8000

И рассмотрим Service-ресурс:

apiVersion: v1
kind: Service
metadata:
  # Имя Service.
  name: werf-guide-app
spec:
  selector:
    # Этот Service перенаправит трафик на Pods с этим лейблом.
    app: werf-guide-app
  ports:
  - name: http
    # Трафик с 8000-го порта Service перенаправится на 8000-й порт Pod'а.
    port: 8000
apiVersion: v1 kind: Service metadata: # Имя Service. name: werf-guide-app spec: selector: # Этот Service перенаправит трафик на Pods с этим лейблом. app: werf-guide-app ports: - name: http # Трафик с 8000-го порта Service перенаправится на 8000-й порт Pod'а. port: 8000

Убедимся, что ресурс Service создан:

kubectl get service werf-guide-app

В ответ получим примерно следующее:

NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
werf-guide-app          ClusterIP   10.107.19.126   <none>        80/TCP    35s

Также убедимся, что ресурс Ingress создан:

kubectl get ingress werf-guide-app

В ответ получим примерно следующее:

NAME                    CLASS    HOSTS                               ADDRESS   PORTS   AGE
werf-guide-app          <none>   werf-guide-app                                80      3m21s

Если несколько упростить, то эти два ресурса позволят HTTP-пакетам, приходящим на NGINX Ingress Controller, у которых есть заголовок Host: werf-guide-app, быть перенаправленными на 8000-й порт Service’а werf-guide-app, а оттуда — на 8000-й порт одного из Pods нашего Deployment’а. В конфигурации по умолчанию Service будет перенаправлять запросы на все Pods Deployment’а поровну.

В общем случае схема взаимодействия между разными ресурсами внутри кластера выглядит следующим образом:

Обратимся к нашему приложению через Ingress:

curl http://werf-guide-app/ping

В ответ получим примерно следующее:

pong

При этом Service-ресурсы нужны не только для связи Ingress и приложения. Service-ресурсы также дают возможность ресурсам внутри кластера общаться между собой. При создании Service создается доменное имя <ServiceName>.<NamespaceName>.svc.cluster.local, доступное изнутри кластера. Также Service доступен и по более коротким доменным именам:

  • <ServiceName> — при обращении из того же Namespace,
  • <ServiceName>.<NamespaceName> — при обращении из другого Namespace.

Создадим новый контейнер, не имеющий отношения к нашему приложению:

kubectl run werf-temporary-deployment --image=alpine --rm -it -- sh

В запустившемся контейнере обратимся к нашему приложению через Service:

apk add curl  # Установим curl внутри контейнера.
curl http://werf-guide-app/ping  # Обратимся к одному из Pod'ов нашего приложения через Service.

В ответ получим:

pong

Использование Ingress-ресурсов — не единственный способ получить доступ к приложению снаружи кластера. Service’ы типа LoadBalancer и NodePort позволяют предоставить доступ к приложению снаружи и без Ingress’ов. Почитать подробнее про Service’ы можно в официальной документации. А про Ingress’ы — здесь.