Файлы, упомянутые в главе

  • .helm/templates/deployment.yaml
  • .helm/templates/ingress.yaml
  • .helm/templates/service.yaml
  • .helm/values.yaml
  • .helm/secret-values.yaml

Для того, чтобы приложение заработало в Kubernetes, необходимо описать инфраструктуру приложения как код (IaC). В нашем случае потребуются следующие объекты Kubernetes: Pod, Service и Ingress.

Конфигурацию для Kubernetes нужно шаблонизировать. Один из популярных инструментов для такой шаблонизации — это Helm, его движок встроен в werf. Помимо этого, werf предоставляет возможности работы с секретными значениями, а также дополнительные Go-шаблоны для интеграции собранных образов.

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

Составление конфигов инфраструктуры

На сегодняшний день Helm — один из самых удобных (и самых распространённых) способов, которым можно описать свой деплой в Kubernetes. Он позволяет устанавливать готовые чарты с приложениями прямо из репозитория: введя одну команду, можно развернуть в своем кластере готовый Redis, PostgreSQL, RabbitMQ… Кроме того, Helm можно использовать для разработки собственных чартов, применяя удобный синтаксис для шаблонизации выката ваших приложений.

По этим причинам он был встроен в werf для решения соответствующих задач.

Что делать, если вы не работали с Helm?

Не будем вдаваться в подробности разработки YAML-манифестов с помощью Helm для Kubernetes. Если у вас есть вопросы о том, как именно описываются объекты Kubernetes, советуем посетить страницы документации Kubernetes о концепциях и документацию Helm по разработке шаблонов.

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

В случае затруднений убедитесь, что вы:

  • понимаете, как работает indent;
  • понимаете, что такое конструкция tuple;
  • понимаете, как Helm работает с хэш-массивами;
  • очень внимательно следите за пробелами в YAML.

Итак, для работы рассматриваемого приложения в среде Kubernetes понадобится:

  • описать сущности Deployment (он породит в кластере Pod) и Service;
  • направить трафик на приложение, настроив роутинг в кластере с помощью сущности Ingress;
  • не забыть создать отдельную сущность Secret, которая позволит Kubernetes скачивать собранные образа из Registry.

Создание Pod’а

Для того, чтобы в кластере появился Pod с нашим приложением, мы создадим объект Deployment. У создаваемого Pod будет один контейнер — basicapp. Укажем, как этот контейнер будет запускаться.

Здесь и далее будут показаны только фрагменты файлов. Если вам не знаком синтаксис Kubernetes-объектов и вы не можете дополнить приведённые сниппеты самостоятельно — обязательно сверяйтесь с файлами в репозитории.

      containers:
      - name: basicapp
        command: ["node","/app/app.js"]
{{ tuple "basicapp" . | include "werf_container_image" | indent 8 }}
containers: - name: basicapp command: ["node","/app/app.js"] {{ tuple "basicapp" . | include "werf_container_image" | indent 8 }}

Обратите внимание на вызов werf_container_image. Данная функция генерирует ключи image и imagePullPolicy со значениями, необходимыми для соответствующего контейнера Pod’а, что позволяет гарантировать перевыкат контейнера тогда, когда это нужно.

А в чём проблема?

Kubernetes не знает ничего об изменении контейнера: он действует на основании описания объектов и сам выкачивает образы из Registry. Поэтому Kubernetes’у нужно в явном виде сообщать, что делать.

werf складывает собранные образы в Registry с разными именами — в зависимости от выбранной стратегии тегирования и деплоя (подробнее это разобрано в главе про CI). Как следствие, в описание контейнера нужно пробрасывать правильный путь до образа, а также дополнительные аннотации, связанные со стратегией деплоя.

Подробнее - можно посмотреть в документации.

Для корректной работы приложения ему нужно узнать переменные окружения.

Например, для Node.js это DEBUG.

        env:
        - name: "DEBUG"
          value: "True"
<...>
{{ tuple "basicapp" . | include "werf_container_env" | indent 8 }}
env: - name: "DEBUG" value: "True" <...> {{ tuple "basicapp" . | include "werf_container_env" | indent 8 }}

Обратите также внимание на функцию werf_container_env: с её помощью werf вставляет в описание объекта служебные переменные окружения.

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

Helm — шаблонизатор, который поддерживает множество инструментов для подстановки значений. Один из центральных способов — подставлять значения из файла values.yaml. Наша конструкция могла бы иметь вид:

        env:
<...>
        - name: "DEBUG_2"
          value: "{{ .Values.isDebug }}"
env: <...> - name: "DEBUG_2" value: "{{ .Values.isDebug }}"

… или даже более сложный — для того, чтобы значение основывалось на текущем окружении:

        env:
<...>
        - name: "DEBUG"
          value: "{{ pluck .Values.global.env .Values.app.isDebug | first | default .Values.app.isDebug._default }}"
env: <...> - name: "DEBUG" value: "{{ pluck .Values.global.env .Values.app.isDebug | first | default .Values.app.isDebug._default }}"
app:
  isDebug:
    _default: "true"
    production: "false"
    testing: "true"
app: isDebug: _default: "true" production: "false" testing: "true"

При запуске приложения в Kubernetes логи необходимо отправлять в stdout и stderr — это нужно для простого сбора логов, например, через filebeat, а также для того, чтобы не разрастались Docker-образы запущенных приложений.

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

console.log("I will goto the STDOUT");
console.error("I will goto the STDERR");

Либо, если вы хотите получать логи об HTTP-запросах, можно использовать morgan:

var morgan = require("morgan");
app.use(morgan("combined"));

Доступность Pod’а

Для того, чтобы запросы извне попали к нам в приложение, нужно открыть порт у Pod’а, создать объект Service и привязать его к Pod’у, а также создать объект Ingress.

Что за объект Ingress и как он связан с балансировщиком?

Возможна коллизия терминов:

  • Есть Ingress в смысле NGINX Ingress Controller, который работает в кластере и принимает входящие извне запросы.
  • А ещё есть объект Ingress, который фактически описывает настройки для NGINX Ingress Controller.

В статьях и бытовой речи оба этих термина зачастую называют «Ingress», так что догадываться нужно по контексту.

Наше приложение работает на стандартном порту 3000откроем порт Pod’у:

        ports:
        - containerPort: 3000
          protocol: TCP
ports: - containerPort: 3000 protocol: TCP

Затем пропишем Service, чтобы к Pod’у могли обращаться другие приложения кластера:

---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Chart.Name }}
spec:
  selector:
    app: {{ .Chart.Name }}
  ports:
  - name: http
    port: 3000
    protocol: TCP
--- apiVersion: v1 kind: Service metadata: name: {{ .Chart.Name }} spec: selector: app: {{ .Chart.Name }} ports: - name: http port: 3000 protocol: TCP

Обратите внимание на поле selector у Service: он должен совпадать с аналогичным полем у Deployment. Ошибки в этой части — самая частая проблема с настройкой маршрута до приложения.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}
spec:
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Chart.Name }} spec: selector: matchLabels: app: {{ .Chart.Name }}

Как убедиться, что выше всё сделано правильно?

Мы будем деплоить приложение в Kubernetes позже, но если после деплоя у вас возникли проблемы в этом месте, то вернитесь сюда и проведите проверку, описанную ниже.

Попробуйте получить endpoint сервиса в нужном вам окружении. Если в нем будет фигурировать IP Pod’а — всё настроено правильно. А если нет, то проверьте еще раз, совпадают ли поля selector у Service и Deployment.

Название endpoint совпадает с названием сервиса, т.е. пример нужной команды:

kubectl -n <название окружения> get ep {{ .Chart.Name }}

После этого можно настраивать роутинг на Ingress. Укажем, на какой домен, путь, сервис и порт направлять запросы:

  rules:
  - host: mydomain.io
    http:
      paths:
      - path: /
        backend:
          serviceName: {{ .Chart.Name }}
          servicePort: 3000
rules: - host: mydomain.io http: paths: - path: / backend: serviceName: {{ .Chart.Name }} servicePort: 3000

Разное поведение в разных окружениях

Некоторые настройки хочется видеть разными в разных окружениях. К примеру, домен, на котором будет открываться приложение, должен быть либо staging.mydomain.io, либо mydomain.io — в зависимости от того, куда мы задеплоились.

В werf для этого существует три механики:

  1. Подстановка значений из values.yaml по аналогии с Helm.
  2. Проброс значений через аргумент --set при работе в CLI-режиме, по аналогии с Helm.
  3. Подстановка секретных значений из secret-values.yaml.

Вариант с values.yaml рассматривался ранее в главе “Создание Pod’а”.

Второй вариант подразумевает задание переменных через CLI. Например, в случае выполнения команды werf deploy --set "global.ci_url=mydomain.io" в YAML’ах будет доступно значение {{ .Values.global.ci_url }}.

Этот вариант удобен для проброса, например, имени домена для каждого окружения:

  rules:
  - host: {{ .Values.global.ci_url }}
rules: - host: {{ .Values.global.ci_url }}

Отдельная проблема — хранение и задание секретных переменных, например, учётных данных аутентификации для сторонних сервисов, API-ключей и т.п.

Так как werf рассматривает Git как единственный источник правды, правильно хранить секретные переменные там же. Чтобы делать это корректно, мы храним данные в шифрованном виде. Подстановка значений из этого файла происходит при рендере шаблона, который также запускается при деплое.

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

  • сгенерируйте ключ (werf helm secret generate-secret-key);
  • определите ключ в переменных окружения для приложения, в текущей сессии консоли (например, export WERF_SECRET_KEY=634f76ead513e5959d0e03a992372b8e);
  • пропишите полученный ключ в Variables для вашего репозитория в GitLab (раздел SettingsCI/CD), название переменной должно быть WERF_SECRET_KEY:

После этого можно задать секретные переменные access_key и secret_key, например, для работы с S3. Зайдите в режим редактирования секретных значений:

$ werf helm secret values edit .helm/secret-values.yaml

Откроется консольный текстовый редактор с данными в расшифрованном виде:

.helm/secret-values.yaml в расшифрованном виде копировать имя копировать текст
app:
  s3:
    access_key:
      _default: bNGXXCF1GF
    secret_key:
      _default: zpThy4kGeqMNSuF2gyw48cOKJMvZqtrTswAQ
app: s3: access_key: _default: bNGXXCF1GF secret_key: _default: zpThy4kGeqMNSuF2gyw48cOKJMvZqtrTswAQ

После сохранения значения в файле зашифруются и примут примерно такой вид:

.helm/secret-values.yaml в зашифрованном виде копировать имя копировать текст
app:
  s3:
    access_key:
      _default: 1000f82ff86a5d766b9895b276032928c7e4ff2eeb20cab05f013e5fe61d21301427
    secret_key:
      _default: 1000bee1b42b57e39a9cfaca7ea047de03043c45e39901b8974c5a1f275b98fd0ac2c72efbc62b06cad653ebc4195b680370dc9c04e88a8182a874db286d8360def6
app: s3: access_key: _default: 1000f82ff86a5d766b9895b276032928c7e4ff2eeb20cab05f013e5fe61d21301427 secret_key: _default: 1000bee1b42b57e39a9cfaca7ea047de03043c45e39901b8974c5a1f275b98fd0ac2c72efbc62b06cad653ebc4195b680370dc9c04e88a8182a874db286d8360def6

Доступ кластера к Registry

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

Сперва нужно создать API-ключ в GitLab: зайдите в настройки пользователя (Settings) и в разделе Personal Access Tokens создайте API-ключ с правами на read_registry. Корректнее всего создать отдельного служебного пользователя, чтобы не завязываться на персональный аккаунт.

Полученный ключ должен быть прописан в каждом пространстве имён в Kubernetes, куда осуществляется деплой, в виде объекта Secret. Сделать это можно, выполнив на master-узле кластера команду:

kubectl create secret docker-registry registrysecret -n <namespace> --docker-server=<registry_domain> --docker-username=<account_email> --docker-password=<account_password> --docker-email=<account_email>

Здесь:

  • <namespace> — название пространства имён в Kubernetes (например, werf-guided-project-production);
  • <registry_domain> — домен Registry (например, registry.gitlab.com);
  • <account_email> — email вашей учётной записи в GitLab;
  • <account_password> — созданный API-ключ.

В каждом Deployment также указывается имя секрета:

    spec:
      imagePullSecrets:
      - name: "registrysecret"
spec: imagePullSecrets: - name: "registrysecret"

Это каждый раз копировать руками?

В идеале проблема копирования секретов должна решаться на уровне платформы, но если это не сделано — можно прописать нужные команды в CI-процесс.

Вариант решения — завести секрет один раз в пространство имён kube-system, а затем в .gitlab-ci.yaml при деплое копировать этот секрет:

kubectl get ns ${CI_ENVIRONMENT_CUSTOM_SLUG:-${CI_ENVIRONMENT_SLUG}} || kubectl create namespace ${CI_ENVIRONMENT_CUSTOM_SLUG:-${CI_ENVIRONMENT_SLUG}}
kubectl get secret registrysecret -n kube-system -o json |
                      jq ".metadata.namespace = \"${CI_ENVIRONMENT_CUSTOM_SLUG:-${CI_ENVIRONMENT_SLUG}}\"|
                      del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.selfLink,.metadata.uid)" |
                      kubectl apply -f -

Отладка конфигов инфраструктуры и деплой в Kubernetes

После того, как написана основная часть конфигов, хочется проверить корректность конфигов и задеплоить их в Kubernetes. Для того, чтобы отрендерить конфиги инфраструктуры, требуются сведения об окружении, на которое будет произведён деплой, ключ для расшифровки секретных значений и т.п.

Если мы запускаем werf вне Gitlab CI, необходимо сделать несколько операций вручную прежде, чем werf сможет рендерить конфиги и деплоить в Kubernetes. А именно:

  • Вручную подключиться к GitLab Registry с помощью docker login (если это не было сделано ранее);
  • Установить переменную окружения WERF_IMAGES_REPO с путём до Registry (вида registry.mydomain.io/myproject);
  • Установить переменную окружения WERF_SECRET_KEY со значением, сгенерированным ранее в главе “Разное поведение в разных окружениях”;
  • Установить переменную окружения WERF_ENV с названием окружения, в которое будет осуществляться деплой. Вопрос разных окружений будет затронут подробнее в процессе создания CI-процесса, а сейчас — просто установим значение staging. Важно удалить эту переменную в финальном варианте деплоя: иначе деплой всегда будет идти в один и тот же namespace.

Если вы всё правильно сделали, уже должны корректно отрабатывать команды werf helm render и werf deploy. Примечание: при локальном запуске эти команды могут жаловаться на нехватку данных, которые в ином случае были бы проброшены из CI. Например, на данные о теге собранного образа. Это нормально.

Как вообще работает деплой?

werf (по аналогии с Helm) берет YAML-шаблоны, которые описывают объекты Kubernetes, и генерирует из них общий манифест. Манифест отдается в API Kubernetes, который на его основе вносит все необходимые изменения в кластер.

werf отслеживает, как Kubernetes вносит изменения, и сигнализирует о результатах в реальном времени. Всё это возможно благодаря встроенной в werf библиотеке kubedog. Уже сам Kubernetes занимается выкачиванием нужных образов из Registry и запуском их на нужных серверах с указанными настройками.

Запустите деплой и дождитесь успешного завершения:

werf deploy --stages-storage :local

А проверить, что приложение задеплоилось в кластер, можно с помощью kubectl. Должно получиться примерно следующее:

$ kubectl get namespace
NAME                                 STATUS               AGE
default                              Active               161d
werf-guided-project-production       Active               4m44s
werf-guided-project-staging          Active               3h2m

Как формируется имя namespace'а?

По шаблону [[ project ]]-[[ env ]], где [[ project ]] — имя проекта, а [[ env ]] — имя окружения. Подробнее можно почитать в документации.

При необходимости namespace можно переназначить.

$ kubectl -n example-1-staging get po
NAME                                 READY                STATUS   RESTARTS  AGE
werf-guided-project-9f6bd769f-rm8nz  1/1                  Running  0         6m12s
$ kubectl -n example-1-staging get ingress
NAME                                 HOSTS                ADDRESS  PORTS     AGE
werf-guided-project                  staging.mydomain.io           80        6m18s

А также вы должны увидеть сервис через браузер.