Обзор

werf поддерживает импорт конфигурационных файлов из внешних источников с помощью механизма includes.

Это удобно, когда у вас много однотипных приложений и вы хотите сопровождать и использовать конфигурацию централизованно.

Что можно импортировать

Поддерживается импорт следующих типов файлов:

  • Основной файл конфигурации (werf.yaml), шаблоны (.werf/**/*.tmpl), а также произвольные файлы, которые используются при шаблонизации.
  • Helm-чарты и отдельные файлы.
  • Dockerfile и .dockerignore.

Что нельзя импортировать

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

  • werf-includes.yaml
  • werf-includes.lock
  • werf-giterminism.yaml

Как работает механизм includes

  1. Вы описываете внешние источники и импортируемые файлы в werf-includes.yaml.
  2. Фиксируете версии внешних источников с помощью команды werf includes update, которая создаёт werf-includes.lock. Lock-файл необходим для воспроизводимости сборок и развертываний.
  3. Все команды werf (например, werf build, werf converge) будут учитывать этот lock-файл и работать с конфигурацией в соответствии с правилами наложения.

Правила наложения

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

  1. Локальные файлы проекта всегда имеют приоритет над импортируемыми.
  2. Если файл присутствует в нескольких includes, будет использоваться версия из источника, который расположен ниже по списку includes в werf-includes.yaml (от общего к частному).

Пример применения правил наложения

Допустим в локальном репозитории есть файл c, а также следующая конфигурация:

# werf-includes.yaml
includes:
  - git: repo1 # импортирует a, b, c
    branch: main
    add: /
    to: /
  - git: repo2 # импортирует b, c
    branch: main
    add: /
    to: /

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

.
├── a      # из repo1
├── b      # из repo2
├── c      # из локального репо

Подключение конфигурации из внешних источников

Ниже приведён пример конфигурации внешних источников (полное описание директив вы можете найти на соответствующей странице werf-includes.yaml):

# werf-includes.yaml
includes:
  - git: https://github.com/werf/werf
    branch: main
    add: /docs/examples/includes/common
    to: /
    includePaths:
      - .werf

После конфигурации необходимо зафиксировать версии внешних зависимостей. Для этого можно использовать команду werf includes update, после вызова которой, в корне проекта будет создан файл werf-includes.lock:

includes:
  - git: https://github.com/werf/werf
    branch: main
    commit: 21640b8e619ba4dd480fedf144f7424aa217a2eb

ВАЖНО. Согласно политикам гитерминизма, файлы werf-includes.yaml и werf-includes.lock должны быть закомичены. При конфигурации и отладке для удобства предлагается использовать флаг --dev.

Пример использования внешних источников при конфигурации однотипных приложений

Предположим вы решили централизованно обслуживать конфигурацию приложений в вашей организации, сохраняя общую конфигурацию в одном источнике и конфигурацию под каждый тип проекта в других (в одном или нескольких Git-репозиториях — на ваше усмотрение).

Конфигурация типового проекта в таком случае могла бы выглядеть так:

  • Проект:
.helm
backend
frontend
werf-includes.lock
werf-includes.yaml
werf.yaml
backend:
  limits:
    cpu: 100m
    memory: 256Mi

frontend:
  limits:
    cpu: 50m
    memory: 48Mi

// dummy

const express = require('express');
const app = express();

app.get('/api/hello', (req, res) => {
  res.json({ message: 'Hello from backend!' });
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Backend running on port ${port}`);
});

{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

<!DOCTYPE html>
<html>
<head><title>Frontend</title></head>
<body>
  <h1>Hello from Frontend</h1>
  <button onclick="callApi()">Call Backend</button>
  <p id="output"></p>

  <script>
    async function callApi() {
      const res = await fetch('$BACKEND_URL');
      const data = await res.json();
      document.getElementById('output').innerText = data.message;
    }
  </script>
</body>
</html>

includes:
    - git: https://github.com/werf/werf
      branch: main
      commit: 792c7631d2abbae8dc0acb252905f740d98c7585

includes:
  - git: https://github.com/werf/werf
    branch: main
    add: /docs/examples/includes/werf-common
    to: /
  - git: https://github.com/werf/werf
    branch: main
    add: /docs/examples/includes/common_js
    to: /

project: myapp
configVersion: 1
build:
  platform:
    - linux/amd64
  imageSpec: {{ include "imagespec" (dict "appName" "myapp") | nindent 4 }}
cleanup: {{ include "cleanup" (dict "mainBranchName" "main") | nindent 2 }}
---
{{ include "image-backend" (dict) }}
---
{{ include "image-frontend" (dict "backendUrl" "http://backend:3000") }}

  • Источник с общей конфигурацией:
.werf
{{- define "cleanup" }}
{{- $mainBranchName := default "main" .mainBranchName}}
keepImagesBuiltWithinLastNHours: 1
keepPolicies:
  - references:
      branch: /.*/
      limit:
        last: 20
        in: 168h
        operator: And
    imagesPerReference:
      last: 1
      in: 168h
      operator: And
  - references:
      branch: "{{ $mainBranchName }}"
    imagesPerReference:
      last: 1
{{- end }}

{{- define "imagespec" }}
{{- $appName := default "defaultapp" .appName}}
clearHistory: true
config:
  labels:
    example.io/app: "{{ $appName }}"
{{- end }}

  • Источник со специфичной для вашего типа проекта конфигурацией:
.helm
.werf
.dockerignore
backend.Dockerfile
frontend.Dockerfile
**/dummy.ini
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: backend
          image: {{ $.Values.werf.image.backend }}
          ports:
            - containerPort: 3000
          resources:
            requests:
              cpu: {{ $.Values.backend.limits.cpu }}
              memory: {{ $.Values.backend.limits.memory}}
            limits:
              cpu: {{ $.Values.backend.limits.cpu }}
              memory: {{ $.Values.backend.limits.memory }}
        - name: frontend
          image: {{ $.Values.werf.image.frontend }}
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: {{ $.Values.frontend.limits.cpu }}
              memory: {{ $.Values.frontend.limits.memory}}
            limits:
              cpu: {{ $.Values.frontend.limits.cpu }}
              memory: {{ $.Values.frontend.limits.memory }}

apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
    - name: http
      port: 80

backend:
  limits:
    cpu: 100m
    memory: 256Mi

frontend:
  limits:
    cpu: 50m
    memory: 48Mi

{{- define "image-backend" }}
image: backend
dockerfile: backend.Dockerfile
{{- end }}

{{- define "image-frontend" }}
{{- $backendUrl := default "http://example.org" .backendUrl }}
image: frontend
dockerfile: frontend.Dockerfile
args:
  BACKEND_URL: "{{ $backendUrl }}"
{{- end }}

FROM node:18-alpine
WORKDIR /app
COPY backend/ /app/
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

FROM nginx:alpine
ARG BACKEND_URL
ENV BACKEND_URL=${BACKEND_URL}
WORKDIR /usr/share/nginx/html
COPY frontend/index.html .
RUN envsubst < index.html > index.tmp.html && \
    mv index.tmp.html index.html

Обновление includes

Детерминированное

Существует два способа обновления версий includes:

  • Командой werf includes update. Данная команда обновит все includes на HEAD соответствующего референса (branch или tag).
  • Редактирование файла werf-includes.lock вручную или с помощью таких как инструментов как dependabot, renovate и прочих.

Автообновление (не рекомендовано)

Если необходимо использовать последние HEAD-версии без lock-файла, можно использовать опцию --allow-includes-update, а так же явно разрешить ее использование в werf-giterminism.yaml:

includes:
  allowIncludesUpdate: true

ВАЖНО. Мы не рекомендуем использование данного подхода, так как он может нарушать воспроизводимость сборок и развертываний.

Отладка

Получение списка файлов проекта с учётом источников

werf includes ls-files .helm

Пример вывода:

PATH                                             SOURCE
.helm/Chart.yaml                                 https://github.com/werf/werf
.helm/charts/backend/Chart.yaml                  https://github.com/werf/werf
.helm/charts/backend/templates/deployment.yaml   https://github.com/werf/werf
.helm/charts/backend/templates/service.yaml      https://github.com/werf/werf
.helm/charts/frontend/Chart.yaml                 https://github.com/werf/werf
.helm/charts/frontend/templates/deployment.yaml  https://github.com/werf/werf
.helm/charts/frontend/templates/service.yaml     https://github.com/werf/werf
.helm/requirements.lock                          https://github.com/werf/werf
.helm/values.yaml                                local

Получение содержимого файла проекта с учётом источников

werf includes get-file .helm/charts/backend/templates/deployment.yaml

Пример вывода:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: 
  annotations:
    werf.io/weight: "30"
spec:
  selector:
    matchLabels:
      app: 
  template:
    metadata:
      labels:
        app: 
    spec:
      containers:
        - name: backend
          image: 
          ports:
            - containerPort: 3000
          resources:
            requests:
              cpu: 
              memory: 
            limits:
              cpu: 
              memory: 

Использование локальных репозиториев

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

# werf-includes.yaml
includes:
  - git: /local/repo
    branch: main
    add: local-dev
    to: /
    includePaths:
      - /.helm