В этой статье мы рассмотрим, как с помощью Open Source-утилиты werf собрать Docker-образ простейшего приложения и развернуть его в кластере Kubernetes, а также с легкостью накатывать изменения в его коде и инфраструктуре.

Мы поговорим об общих принципах работы с werf при использовании ее разработчиками, поэтому в качестве примера приложения используем небольшой эхо-сервер на основе shell-скрипта, который будет возвращать в ответ на запрос по адресу /ping строку Hello, werfer!. В следующих материалах будет рассмотрена работа и с «настоящими» приложениями, основанными на распространенных фреймворках на разных языках, но для начала сфокусируемся на общем подходе к разработке с использованием утилиты werf.

NB. Все файлы тестового приложения можно посмотреть и скачать в этом репозитории. Статья подготовлена на основе недавно анонсированного самоучителя werf.

В качестве кластера Kubernetes мы используем minikube, что позволит легко и быстро попробовать поработать с werf прямо на рабочем компьютере.

werf

Для тех, кто слышит это название впервые, поясним, что werf — это CLI-утилита, организующая полный цикл доставки приложения в Kubernetes. Она использует Git как единый источник, хранящий код и конфигурацию приложения. Каждый коммит — это состояние приложения, которое во время доставки werf синхронизирует с container registry, дособирая несуществующие слои конечных образов, а затем с приложением в Kubernetes, перевыкатывая изменившиеся ресурсы. Также werf позволяет очищать container registry от неактуальных артефактов по уникальному алгоритму, опирающемуся на историю Git и пользовательские политики.

Почему werf? Что в ней такого полезного и особенного? Основная ее фишка — объединение привычных для разработчиков и DevOps-/SRE-инженеров инструментов, таких как Git, Docker, container registry, CI-система, Helm и Kubernetes под одной утилитой. Тесная интеграция компонентов совместно с заложенными в процесс работы лучшими практиками, накопленными нашей компанией за годы работы с кубернетизацией приложений разных клиентов и запуском их в Kubernetes, делает werf достойным претендентом для доставки вашего приложения в Kubernetes.

Подготовка системы

Перед началом работы установите в вашу систему последнюю стабильную версию werf (v1.2 из канала обновлений stable), воспользовавшись официальной документацией.

Все команды и действия, приводимые в статье, актуальны для операционной системы Linux, в частности Ubuntu 20.04.03. При работе с другими ОС, такими как Windows или macOS, команды аналогичны, но могут встречаться небольшие особенности для каждой из этих ОС. (Найти уже адаптированные инструкции для этих систем можно в разделе «Первые шаги» нашего самоучителя.)

Сборка образа

Для начала нужно написать сам скрипт, который будет представлять собой наше «приложение». Создадим каталог, в котором будем работать (у нас это каталог app в домашнем каталоге пользователя):

mkdir app

Создадим внутри каталога скрипт hello.sh, в котором опишем простую логику ответа на запрос:

#!/bin/sh

RESPONSE="Hello, werfer!"

while true; do
  printf "HTTP/1.1 200 OK\n\n$RESPONSE\n" | ncat -lp 8000
done

Инициализируем в созданном каталоге новый Git-репозиторий и закоммитим первые изменения — созданный скрипт:

cd ~/app
git init
git add .
git commit -m initial

Т.к. собираться и работать наше приложение будет в Docker, создадим рядом со скриптом Dockerfile, в котором будет описана логика сборки приложения в образ:

FROM alpine:3.14
WORKDIR /app

# Устанавливаем зависимости приложения
RUN apk add --no-cache --update nmap-ncat

# Добавляем в образ созданный скрипт для запуска эхо-сервера 
# и устанавливаем разрешение на выполнение
COPY hello.sh .
RUN chmod +x hello.sh

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

project: werf-first-app
configVersion: 1

---
image: app
dockerfile: Dockerfile

Уже готовое состояние репозитория с нужными сейчас файлами также можно забрать из этого каталога репозитория werf/first-steps-example.

Теперь мы готовы собрать наше приложение. Обратите внимание, что перед сборкой нужно зафиксировать все изменения в репозитории проекта (созданные нами Dockerfile и т. д.), поэтому для начала выполним следующие команды:

git add .
git commit -m FIRST

Запустим сборку приложение командой:

werf build

При успешной сборке увидим примерно следующий лог:

┌ ⛵ image app
│ ┌ Building stage app/dockerfile
│ │ app/dockerfile  Sending build context to Docker daemon  4.096kB
│ │ app/dockerfile  Step 1/13 : FROM alpine:3.14
│ │ app/dockerfile   ---> 0a97eee8041e
│ │ app/dockerfile  Step 2/13 : WORKDIR /app
│ │ app/dockerfile   ---> Running in d4c535c0d754
│ │ app/dockerfile  Removing intermediate container d4c535c0d754
│ │ app/dockerfile   ---> 5a2a81813edc
│ │ app/dockerfile  Step 3/13 : RUN apk add --no-cache --update nmap-ncat
│ │ app/dockerfile   ---> Running in ce40513872fc
│ │ app/dockerfile  fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
│ │ app/dockerfile  fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz
│ │ app/dockerfile  (1/3) Installing lua5.3-libs (5.3.6-r0)
│ │ app/dockerfile  (2/3) Installing libpcap (1.10.0-r0)
│ │ app/dockerfile  (3/3) Installing nmap-ncat (7.91-r0)
│ │ app/dockerfile  Executing busybox-1.33.1-r6.trigger
│ │ app/dockerfile  OK: 6 MiB in 17 packages
│ │ app/dockerfile  Removing intermediate container ce40513872fc
│ │ app/dockerfile   ---> 8fffbbd2f295
│ │ app/dockerfile  Step 4/13 : COPY hello.sh .
│ │ app/dockerfile   ---> 157c374b6224
│ │ app/dockerfile  Step 5/13 : RUN chmod +x hello.sh
│ │ app/dockerfile   ---> Running in e603d67b6a34
│ │ app/dockerfile  Removing intermediate container e603d67b6a34
│ │ app/dockerfile   ---> e6d8a8dcd424
│ │ app/dockerfile  Step 6/13 : LABEL werf=werf-first-app
│ │ app/dockerfile   ---> Running in 42b37bd0b1cd
│ │ app/dockerfile  Removing intermediate container 42b37bd0b1cd
│ │ app/dockerfile   ---> 2f27cc2d99bc
│ │ app/dockerfile  Step 7/13 : LABEL werf-cache-version=1.2
│ │ app/dockerfile   ---> Running in 2e003ef0ec0a
│ │ app/dockerfile  Removing intermediate container 2e003ef0ec0a
│ │ app/dockerfile   ---> f55707dc38e6
│ │ app/dockerfile  Step 8/13 : LABEL werf-docker-image-name=f4059163-28e5-44fe-a53a-a110ed27db2d
│ │ app/dockerfile   ---> Running in 4240a40d6032
│ │ app/dockerfile  Removing intermediate container 4240a40d6032
│ │ app/dockerfile   ---> 1ae5fc802a07
│ │ app/dockerfile  Step 9/13 : LABEL werf-image=false
│ │ app/dockerfile   ---> Running in 610b776441c8
│ │ app/dockerfile  Removing intermediate container 610b776441c8
│ │ app/dockerfile   ---> 1c9066dcea1f
│ │ app/dockerfile  Step 10/13 : LABEL werf-project-repo-commit=4959e91153d14e07133806398a001814b76f83bd
│ │ app/dockerfile   ---> Running in ec973f81f08d
│ │ app/dockerfile  Removing intermediate container ec973f81f08d
│ │ app/dockerfile   ---> c938b7f22f96
│ │ app/dockerfile  Step 11/13 : LABEL werf-stage-content-digest=2c3a4d5dd59c112c3cba8d1ef5590dc16a618489bdcaf276f551f8ef
│ │ app/dockerfile   ---> Running in 9acc7cd494df
│ │ app/dockerfile  Removing intermediate container 9acc7cd494df
│ │ app/dockerfile   ---> 4568493d8f34
│ │ app/dockerfile  Step 12/13 : LABEL werf-stage-digest=11b3f1b95dd6a39a9473a34ba0615c513d799be7e67e48a2deeed05c
│ │ app/dockerfile   ---> Running in ac1bf708647a
│ │ app/dockerfile  Removing intermediate container ac1bf708647a
│ │ app/dockerfile   ---> 1b4d163b491c
│ │ app/dockerfile  Step 13/13 : LABEL werf-version=v1.2.40
│ │ app/dockerfile   ---> Running in a1f9e286d870
│ │ app/dockerfile  Removing intermediate container a1f9e286d870
│ │ app/dockerfile   ---> eb03f3a2c600
│ │ app/dockerfile  Successfully built eb03f3a2c600
│ │ app/dockerfile  Successfully tagged a0e1fc05-970b-4b4c-bb8c-31c51a1a991a:latest
│ │ ┌ Store stage into :local
│ │ └ Store stage into :local (0.02 seconds)
│ ├ Info
│ │      name: werf-first-app:11b3f1b95dd6a39a9473a34ba0615c513d799be7e67e48a2deeed05c-1638357228902
│ │        id: eb03f3a2c600
│ │   created: 2021-12-01 14:13:48.839066578 +0300 MSK
│ │      size: 6.0 MiB
│ └ Building stage app/dockerfile (7.45 seconds)
└ ⛵ image app (7.47 seconds)

Running time 7.53 seconds

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

werf run app --docker-options="-ti --rm -p 8000:8000" -- /app/hello.sh

Рассмотрим подробнее команду запуска. В ней заданы параметры Docker’а при помощи опции --docker-options, а в самом конце указана команда для выполнения внутри контейнера через два дефиса.

Проверим, что все запустилось и работает как нужно. Перейдем в браузере по адресу http://127.0.0.1:8000/ping, либо запросим ответ в другом терминале при помощи утилиты curl:

curl http://127.0.0.1:8000/ping

В результате увидим строку Hello, werfer!, а в логах запущенного контейнера появится следующее:

GET /ping HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: curl/7.68.0
Accept: */*

Подготовка к деплою

Собрать приложение — половина дела, если не его треть. Ведь еще нужно его задеплоить на боевые серверы. Для этого давайте сэмулируем «продакшн» у себя на машине, поставив минимальный кластер Kubernetes и настроив его на работу с werf. Для этого мы сделаем следующее:

  • установим и запустим minikube — минимальный дистрибутив Kubernetes, который можно использоваться для простой и быстрой установки на рабочий ПК;

  • установим NGINX Ingress Controller — специальный компонент кластера, который отвечает за маршрутизацию запросов снаружи вовнутрь;

  • настроим файл /etc/hosts для доступа к кластеру по доменному имени приложения;

  • авторизуемся на Docker Hub и настроим секрет с нужными учетными данными;

  • непосредственно задеплоим приложение в K8s.

1. Установка и запуск minikube

Для начала установим minikube, следуя его официальной документации. Если у вас в системе он уже установлен, убедитесь, что его версия соответствует последней актуальной (v1.23.2 на момент публикации статьи).

Создадим Kubernetes-кластер с помощью minikube:

# удаляем существующий minikube-кластер, если он существует
minikube delete
# запускаем новый minikube-кластер
minikube start --driver=docker

Зададим пространство имен в Kubernetes (namespace) по умолчанию, чтобы не указывать его явно каждый раз при использовании kubectl (здесь мы только задаем имя по умолчанию, но не создаём сам namespace, это будет сделано ниже):

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

Если у вас не установлена kubectl, сделать это можно двумя способами:

  • Установить ее в систему отдельно, воспользовавшись официальной документацией.

  • Использовать поставляемый с minikube бинарник утилиты. Для этого достаточно выполнить команды:

alias kubectl="minikube kubectl --"
echo 'alias kubectl="minikube kubectl --"' >> ~/.bash_aliases

Если вы выбрали второй вариант, то при первом обращении к kubectl по созданному alias’у утилита будет выкачана и доступна к использованию.

Давайте проверим, что все прошло успешно, и посмотрим на список всех Pod’ов, запущенных в свежесозданном кластере:

kubectl get --all-namespaces pod

Pod — это абстрактный объект Kubernetes, представляющий собой группу из одного или нескольких контейнеров приложения и совместно используемых ресурсов для этих контейнеров.

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

NAMESPACE     NAME                               READY   STATUS    RESTARTS      AGE
kube-system   coredns-78fcd69978-qldbj           1/1     Running   0             14m
kube-system   etcd-minikube                      1/1     Running   0             14m
kube-system   kube-apiserver-minikube            1/1     Running   0             14m
kube-system   kube-controller-manager-minikube   1/1     Running   0             14m
kube-system   kube-proxy-5hrfd                   1/1     Running   0             14m
kube-system   kube-scheduler-minikube            1/1     Running   0             14m
kube-system   storage-provisioner                1/1     Running   1 (13m ago)   14m

Посмотрите внимательно на столбцы READY и STATUS: если все Pod’ы имеют статус Running, а их количество отображается в виде 1/1 (главное, чтобы число слева было равно числу справа), значит все прошло успешно, и наш кластер готов к использованию. Если картина не похожа на представленную выше, то попробуйте подождать и посмотреть чуть позже еще раз — возможно, что не все Pod’ы успели запуститься быстро, и в момент первого просмотра их состояние еще не было активным.

2. Установка NGINX Ingress Controller

Следующим шагом в подготовке будет установка и настройка Ingress-контроллера, основной задачей которого будет проброс внешних HTTP-запросов в наш кластер.

Установим его следующей командой:

minikube addons enable ingress

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

Если все прошло успешно, вы увидите сообщение о том, что аддон успешно установлен и активирован.

The 'ingress' addon is enabled

Немного подождем, чтобы он успел запуститься, и убедимся, что он работает:

kubectl -n ingress-nginx get pod

В результате будет отображено нечто похожее на картину ранее:

NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx-admission-create--1-xn4wr     0/1     Completed   0          6m19s
ingress-nginx-admission-patch--1-dzxt6      0/1     Completed   0          6m19s
ingress-nginx-controller-69bdbc4d57-bp27q   1/1     Running     0          6m19s

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

3. Внесение изменений в файл hosts

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

Для тестирования приложения мы будем использовать адрес werf-first-app.test. Убедимся, что команда minikube ip выдает валидный IP-адрес, выполнив ее в терминале. Если в результате вы видите сообщения, явно не похожие на IP-адрес (в моем случае это 192.168.49.2) — вернитесь на несколько шагов назад и пройдите установку и запуск кластера minikube еще раз.

Если все работает как надо, выполним следующую команду:

echo "$(minikube ip) werf-first-app.test" | sudo tee -a /etc/hosts

Проверить правильность выполнения можно, посмотрев на содержимое искомого файла — в самом конца должна появиться строка вида 192.168.49.2 werf-first-app.test

Давайте убедимся, что все сделанное нами выше работает как надо. Выполним запрос на адрес http://werf-first-app.test/ping с использованием утилиты curl:

curl http://werf-first-app.test/ping

Если все работает правильно — NGINX Ingress Controller вернет страницу с кодом 404, сообщающую, что такого endpoint’а в системе нет:

<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

4. Авторизация в Docker Hub

Для дальнейшей работы понадобится хранилище собираемых образов, и мы предлагаем использовать для этого приватный репозиторий на Docker Hub. Для удобства используем такое же имя, как у приложения — werf-first-app

Залогинимся на Docker Hub, выполнив следующую команду:

docker login
Username: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>
Password: <ПАРОЛЬ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>

Если введено правильно, увидим сообщение Login Succeeded.

5. Создание Secret для доступа к registry

Чтобы иметь возможность в процессе работы пользоваться приватным container registry для хранения образов, необходимо создать Secret с учетными данными для входа в registry. Здесь есть одна особенность — Secret должен располагаться в том же namespace’е, что и приложение.

Поэтому необходимо заранее создать namespace для нашего приложения:

kubectl create namespace werf-first-app

В результате должно отобразится сообщение о том, что новое пространство имен создано — namespace/werf-first-app created.

Далее создадим Secret с именем registrysecret:

kubectl create secret docker-registry registrysecret \
  --docker-server='https://index.docker.io/v1/' \
  --docker-username='<ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>' \
  --docker-password='<ПАРОЛЬ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>'

Все прошло успешно, если в результате выполнения команды отображается строка secret/registrysecret created. Если же по какой-то причине вы ошиблись при создании, то созданный секрет можно удалить командой kubectl delete secret registrysecret, а затем создать его заново.

Это  стандартный способ создания Secret из официальной документации Kubernetes.

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

Далее мы будем использовать созданный Secret для получения образов приложения из registry, указывая поле imagePullSecrets при конфигурации Pod’ов.

Деплой приложения в кластер

Для деплоя приложения в кластер необходимо подготовить Kubernetes-манифесты, которые описывают необходимые для работы ресурсы. Создавать их будем в формате Helm-чартов — пакетов Helm, содержащих все определения ресурсов, необходимых для запуска приложения, инструмента или службы внутри кластера Kubernetes.

Для нашего приложения понадобится три ресурса — Deployment, отвечающий за запуск приложений в контейнерах, а также Ingress и Service, отвечающие за доступ к запущенному приложению снаружи и изнутри кластера соответственно.

Для их создания в случае нашего приложения получится следующая структура файлов:

.
├── Dockerfile
├── .dockerignore
├── hello.sh
├── .helm
│   └── templates
│       ├── deployment.yaml
│       ├── ingress.yaml
│       └── service.yaml
└── werf.yaml

Мы создали скрытый каталог .helm, в который поместим упомянутые выше манифесты (их содержимое будет описано ниже) в подкаталоге templates. Обратите внимание: чтобы исключить эти файлы из контекста сборки Docker-образа, каталог с манифестами мы добавляем в файл .dockerignore:

/.helm/

Рассмотрим манифесты используемых ресурсов более подробно.

1. Deployment

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

apiVersion: apps/v1
kind: Deployment
metadata:
  name: werf-first-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: werf-first-app
  template:
    metadata:
      labels:
        app: werf-first-app
    spec:
      imagePullSecrets:
      - name: registrysecret
      containers:
      - name: app
        image: {{ .Values.werf.image.app }}
        command: ["/app/hello.sh"]
        ports:
        - containerPort: 8000

Здесь с помощью шаблонизации подставляется полное имя Docker-образа нашего приложения: {{ .Values.werf.image.app }}. Важно отметить, что для доступа к этому значению необходимо использовать имя компонента, которое используется в werf.yaml — в нашем случае это app

Полные имена собираемых образов, так же как и другие сервисные данные, werf автоматически добавляет в параметры Helm-чарта (.Values), они все доступны по ключу werf.

werf пересобирает образы только при изменениях в добавляемых файлах (используемых в Dockerfile-инструкциях COPY и ADD), а также при изменении конфигурации образа в werf.yaml. При пересборке изменяется и тег образа, что автоматически приводит к обновлению Deployment’а. Если же изменений в этих файлах нет, то образ приложения и связанный с ним Deployment останутся без изменений — значит на данный момент состояние приложения в кластере актуальное.

2. Service

Этот ресурс позволяет другим приложениям внутри кластера обращаться к нашему приложению. Выглядит он так:

apiVersion: v1
kind: Service
metadata:
  name: werf-first-app
spec:
  selector:
    app: werf-first-app
  ports:
  - name: http
    port: 8000

3. Ingress

В отличие от предыдущего ресурса Service Ingress позволяет открыть доступ к нашему приложению снаружи кластера. В нем мы указываем, на какой Service внутри Kubernetes нужно перенаправлять трафик, поступающий на публичный домен werf-first-app.test. Выглядит ресурс так:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: werf-first-app
spec:
  rules:
  - host: werf-first-app.test
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: werf-first-app
            port:
              number: 8000

Деплой приложения в Kubernetes

Зафиксируем в Git изменения конфигурации — добавленные ресурсы для деплоя в Kubernetes, — как делали в начале статьи:

git add .
git commit -m FIRST

Уже готовое состояние репозитория с нужными сейчас файлами также можно забрать из этого каталога репозитория werf/first-steps-example.

Запускаем деплой командой:

werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app

Если все прошло успешно — вы увидите примерно такой лог:

│ │ app/dockerfile  Successfully built 558e6bf23bb7
│ │ app/dockerfile  Successfully tagged 96c8cfca-d7db-4bf0-8351-cbdc0777c5b5:latest
│ │ ┌ Store stage into .../werf-first-app
│ │ └ Store stage into .../werf-first-app (16.85 seconds)
│ ├ Info
│ │      name: .../werf-first-app:11b3f1b95dd6a39a9473a34ba0615c513d799be7e67e48a2deeed05c-1638438736538
│ │        id: 558e6bf23bb7
│ │   created: 2021-12-02 12:52:16 +0300 MSK
│ │      size: 3.0 MiB
│ └ Building stage app/dockerfile (31.61 seconds)
└ ⛵ image app (39.62 seconds)

Release "werf-first-app" does not exist. Installing it now.

┌ Waiting for release resources to become ready
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                                 REPLICAS            AVAILABLE             UP-TO-DATE                           
│ │ werf-first-app                                                                                              1/1                 0                     1                                    
│ │ │   POD                                      READY          RESTARTS            STATUS                     ---                                                                            
│ │ └── first-app-559d4f59b-j5ffv                 0/1            0                   ContainerCreating          Waiting for: available 0->1                                                    
│ └ Status progress
│ 
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                                 REPLICAS            AVAILABLE             UP-TO-DATE                           
│ │ werf-first-app                                                                                              1/1                 0->1                  1                                    
│ │ │   POD                                      READY          RESTARTS            STATUS                                                                                                    
│ │ └── first-app-559d4f59b-j5ffv                 1/1            0                   ContainerCreating ->       
│ │                                                                                 Running                    
│ └ Status progress
└ Waiting for release resources to become ready (9.40 seconds)

NAME: werf-first-app
LAST DEPLOYED: Thu Dec  2 12:52:40 2021
NAMESPACE: werf-first-app
STATUS: deployed
REVISION: 1
TEST SUITE: None
Running time 60.24 seconds

Давайте убедимся, что все прошло успешно:

curl http://werf-first-app.test/ping

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

Hello, werfer!

Мы успешно задеплоили приложение в Kubernetes-кластер!

Внесение изменений в приложение

Давайте попробуем внести изменения в наше приложение и посмотреть, как werf его пересоберёт и повторно задеплоит в кластер.

Масштабирование

Наш веб-сервер запущен в Deployment’е web-first-app. Посмотрим, сколько реплик запущено:

$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
werf-first-app-559d4f59b-j5ffv   1/1     Running   0          146m

Сейчас реплика одна (смотрим на количество строк, начинающихся на werf-first-app). Вручную заменим их количество на четыре:

kubectl edit deployment werf-first-app

Откроется консольный текстовый редактор с текстом манифеста. Найдем там строку spec.replicas и заменим число реплик на четыре: spec.replicas=4. Подождем немного и снова посмотрим на количество запущенных реплик приложения:

$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
werf-first-app-559d4f59b-j5ffv   1/1     Running   0          172m
werf-first-app-559d4f59b-l4wxp   1/1     Running   0          10s
werf-first-app-559d4f59b-xrnxn   1/1     Running   0          10s
werf-first-app-559d4f59b-z48qm   1/1     Running   0          10s

Сейчас мы произвели масштабирование вручную, напрямую указав количество реплик внутри кластера, минуя при этом Git. Если сейчас снова запустим werf converge:

werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app

А затем снова посмотрим на количество реплик приложения:

$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
werf-first-app-559d4f59b-j5ffv   1/1     Running   0          175m

То увидим, что количество снова соответствует тому, что указано в Git-репозитории — в файле-манифесте, который мы не редактировали. Так получилось потому, что werf снова привела состояние кластера к тому, что описано в текущем Git-коммите. Этот принцип называется гитерминизмом (giterminism, от англ. Git + determinism).

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

Новое содержимое файла:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: werf-first-app
spec:
  # Меняем количество реплик на 4
  replicas: 4
  selector:
    matchLabels:
      app: werf-first-app
  template:
    metadata:
      labels:
        app: werf-first-app
    spec:
      imagePullSecrets:
      - name: registrysecret
      containers:
      - name: app
        image: {{ .Values.werf.image.app }}
        command: ["/app/hello.sh"]
        ports:
        - containerPort: 8000

Закоммитим изменения и пересоберем приложение командой:

werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app

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

$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
werf-first-app-559d4f59b-2fth5   1/1     Running   0          7m12s
werf-first-app-559d4f59b-7jqnv   1/1     Running   0          7m12s
werf-first-app-559d4f59b-j5ffv   1/1     Running   0          3h17m
werf-first-app-559d4f59b-n9n64   1/1     Running   0          7m12s

Давайте снова вернём одну реплику. Снова исправляем файл deployment.yaml, коммитим изменения и перезапускаем converge.

$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
werf-first-app-559d4f59b-j5ffv   1/1     Running   0          3h24m

Меняем само приложение

Сейчас наше приложение отвечает Hello, werfer!. Давайте изменим эту строку и перезапустим обновленное приложение в кластере. Открываем наш hello.sh и меняем строку ответа на любую другую, например на Hello, Habr users!:

#!/bin/sh

RESPONSE="Hello, Habr users!"

while true; do
  printf "HTTP/1.1 200 OK\n\n$RESPONSE\n" | ncat -lp 8000
done

Действуем по старой схеме — коммитим изменения, перезапускаем converge и смотрим результат: 

$ curl "http://werf-first-app.test/ping"
Hello, Habr users!

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

Выводы

В этой статье мы рассмотрели пример простейшего приложения, которое было собрано и задеплоено в кластер Kubernetes с помощью werf. 

Эта статья основана на главе «Первые шаги» нашего онлайн-самоучителя. Представляя её как максимально лаконичный практический tutorial, мы не стали останавливаться на некоторых теоретических вопросах, которые раскрыты в полном руководстве:  шаблоны и манифесты Kubernetes, основные ресурсы K8s для запуска приложений (Deployment, Service, Ingress), режимы работы werf и гитерминизм, использование Helm в werf и т. д. Ответы на них можно найти, проходя «Первые шаги» в полном самоучителе.

Надеемся, что эта статья поможет вам сделать ваши первые шаги с werf и приобрести немного опыта в части деплоя приложений в Kubernetes!

С любыми вопросами и предложениями ждем вас в комментариях к статье, а также в Telegram-каналe werf_ru, где уже более 700 участников и всегда рады помочь. Любым issues (и звёздам) всегда рад и GitHub-репозиторий werf.

P.S.

Читайте также в нашем блоге:

Комментарии (5)


  1. KotKotich
    24.12.2021 10:28
    +4

    Сколько же лишних абстракций, сложностей. То что раньше делалось на десятках мегабайт ( а еще раньше в килобайтах), теперь требует десятки гигабайт, а скоро и терабайтом не спасешься при таких подходах. KISS принцип напрочь забыт. (((. Зато стильно, модно, молодежно. На море на океане есть остров, на том острове дуб стоит, под дубом сундук зарыт, в сундуке — заяц, в зайце — утка, в утке — яйцо, в яйце — игла, — смерть Кощея. Но даже это не спасло Кощея от злого хакера))).


    1. zevssneg
      24.12.2021 13:09
      +1

      Полагаю что подобный образ типа Hello World! будет занимать килобайты и требовать килобайты же ОЗУ если написать его на C, а не на NodeJS. В чём проблема использовать инструмент, соответствующий задаче?


    1. Guzergus
      24.12.2021 19:57
      +3

      То что раньше делалось на десятках мегабайт ( а еще раньше в килобайтах), теперь требует десятки гигабайт, а скоро и терабайтом не спасешься при таких подходах

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

      Собственно, если разобраться в компонентах, я не нахожу ничего излишнего:
      1. Докер образ нужен для того, чтобы все зависимости приложения и окружение были прописаны явно и не менялись в зависимости от машины, i.e. сводим к минимуму «работает на моей машине».
      2. Докер registry, соответственно, нужен просто как хранилище этих образов.
      3. k8s отвечает за масштабирование приложения, следит за жизнью контейнеров, раскидывает их по нодам.
      4. Ингрес — по сути «мост» между внешним миром и внутренней сетью k8s.

      Если у вас есть на примере решение, которое обеспечивает те же характеристики, но в разы проще — пожалуйста, поделитесь. Я порядка 5 лет работал со связкой docker + k8s и многие вещи там делать невероятно легко, на мой взгляд.


    1. sundmoon
      25.12.2021 19:00


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

      Докер с кубом предлагают осилить столь же "лишние" абстракции (а теперь вот Флант с верфью подсуетился помочь с ними удобнее управиться). Опять же, чтобы запускать изящные миниатюрные штуковины.

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

      Так что да, абстракции не для всех. Ассемблер с его детализациями лично я уважаю, но издалека.


  1. sundmoon
    25.12.2021 02:04

    PS> werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app

    здесь по-прежнему хочет докера (десктоп винда).
    А можно обойтись без отдельного докера на хосте, раз уж под рукой миникуб в hyperv ?