Первые шаги с werf: собираем и деплоим простое приложение в Kubernetes

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

image-loader.svg

Мы поговорим об общих принципах работы с 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«а в системе нет:


404 Not Found

404 Not Found


nginx

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.

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

© Habrahabr.ru