consul-template в deploy job GitLab CI

Published: 2026-05-22

consul-template обычно описывают как демон, который следит за Vault и перезаписывает конфиг-файлы в реальном времени. Мы используем его иначе: как одноразовый рендерер во время helm upgrade. Схема такая: скачать чарт из Artifactory, отрендерить секреты из Vault в директорию config/ чарта с помощью HCL-шаблонов, а потом запустить helm upgrade, чтобы отрендеренные файлы попали в ConfigMap и Secret внутри кластера.


Почему не ESO или Vault Agent?

Когда мы это настраивали, ESO в проекте ещё не был доступен. Vault Agent подставляет секреты при запуске пода — подходит для долгоживущих сервисов, но добавляет sidecar-контейнер к каждому поду. Подход с рендерингом на стороне CI держит кластер «тупым»: Helm-чарт просто использует файлы из config/, никакой зависимости от Vault в рантайме.

Компромисс: секреты рендерятся в чарт при деплое и запекаются в ConfigMap (для несекретного конфига) или Kubernetes Secret (для учётных данных). Если значение в Vault изменится, нужно перезапустить пайплайн, чтобы его подхватить.


Структура чарта

my-service/
  Chart.yaml
  values.yaml
  values-Development.yaml
  values-Production.yaml
  config/
    appsettings.json        # consul-template HCL — рендерится при деплое
    appsettings.ENV.json    # переопределения для конкретной среды
  crt/
    YC-CA.pem               # рендерится из Vault
  files/
    config.hcl              # исходный consul-template шаблон для appsettings.json
    config-user.hcl         # шаблон переопределений для среды
    config-crt.hcl          # шаблон CA-сертификата
  templates/
    configmap.yaml
    deployment.yaml
    ...

Файлы в config/ и crt/результат рендеринга: их нет в git, они создаются через consul-template -once в CI job и затем встраиваются в ConfigMap через helm upgrade.


HCL-шаблоны

files/config.hcl — файл consul-template, который читает из Vault KV v2 и рендерит корректный JSON:

{{ with $env := env "APP_ENV" }}
{{ $prj := "my-service" }}
{
  "ConnectionStrings": {
    "DefaultConnection": "{{ $path := printf "Microservices/%s/%s/system" $prj $env }}{{ with secret $path }}{{ .Data.data.connection_string }}{{ end }}"
  },
  "RabbitMq": {
    "Host":     "{{ with secret (printf "common-secret/%s/" $env) }}{{ .Data.data.rabbit_hostname }}{{ end }}",
    "Port":     "{{ with secret (printf "common-secret/%s/" $env) }}{{ .Data.data.rabbit_port }}{{ end }}",
    "Username": "{{ with secret (printf "common-secret/%s/" $env) }}{{ .Data.data.rabbit_username }}{{ end }}",
    "Password": "{{ with secret (printf "common-secret/%s/" $env) }}{{ .Data.data.rabbit_password }}{{ end }}"
  },
  "Logging": {
    "MinimumLevel": {
      {{- if eq $env "Production" }}
      "Default": "Warning"
      {{- else }}
      "Default": "Debug"
      {{- end }}
    }
  }
}
{{ end }}

Два паттерна путей Vault:

  • Microservices/<service>/<env>/system — секреты конкретного сервиса (URL базы, API-ключи)
  • common-secret/<env>/ — общая инфраструктура (RabbitMQ, Elastic, эндпоинт APM)

files/config-user.hcl выгружает весь KV-секрет user как форматированный JSON:

{{ with $env := env "APP_ENV" }}
{{ $prj := "my-service" }}
{{ $path := printf "Microservices/%s/%s/user" $prj $env }}{{ with secret $path }}{{ .Data.data | toJSONPretty }}{{ end }}
{{ end }}

files/config-crt.hcl достаёт YC root CA из общего пути Vault:

{{ with $env := env "APP_ENV" }}
{{ with secret (printf "common-secret/%s/" $env) }}{{ .Data.data.yc_ca }}{{ end }}
{{ end }}

Deploy job

yaml.deploy:
  image: registry.example.com/ci-images/vault-cli:1.0.1
  stage: deploy
  script:
    # 1. Скачать чарт из Helm registry
    - helm repo add charts https://artifacts.example.com/repository/helm/
    - helm repo update
    - helm fetch charts/${CHART_NAME} --untar

    # 2. Аутентификация в Vault через GitLab JWT
    - export VAULT_TOKEN="$(vault write -field=token auth/jwt/login
        role=access-${APP_ENV} jwt=$CI_JOB_JWT)"
    - vault kv get -field=kubeconfig_${NAMESPACE} common-secret/${APP_ENV} > kube_config
    - chmod 600 kube_config

    # 3. Рендеринг секретов в директорию чарта
    - consul-template -template="${CHART_NAME}/files/config-crt.hcl:${CHART_NAME}/crt/YC-CA.pem" -once
    - consul-template -template="${CHART_NAME}/files/config-user.hcl:${CHART_NAME}/config/appsettings.ENV.json" -once
    - consul-template -template="${CHART_NAME}/files/config.hcl:${CHART_NAME}/config/appsettings.json" -once

    # 4. Деплой с отрендеренными файлами в chart/config/
    - helm upgrade --install --wait \
        --kubeconfig kube_config \
        ${INSTANCE} ${CHART_NAME} \
        -f ${CHART_NAME}/values-${APP_ENV}.yaml \
        -n ${NAMESPACE} \
        --set aspnetcoreEnvironment=${APP_ENV} \
        --set image.tag=${CI_COMMIT_SHORT_SHA}

Флаг -template src:dst принимает HCL-источник и пишет отрендеренный результат по пути назначения. Vault-токен consul-template читает из переменной $VAULT_TOKEN автоматически.


Как чарт использует отрендеренные файлы

В templates/configmap.yaml чарт читает config/appsettings.json через Files.Get:

yamlapiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "my-service.fullname" . }}-app-settings
data:
  appsettings.json: |
{{ tpl (.Files.Get "config/appsettings.json") . | indent 4 }}
  appsettings.{{ .Values.aspnetcoreEnvironment }}.json: |
{{ tpl (.Files.Get "config/appsettings.ENV.json") . | indent 4 }}

CA-сертификат попадает в отдельный ConfigMap, монтируемый в /etc/ssl/certs/:

yamlapiVersion: v1
kind: ConfigMap
data:
  YC-CA.pem: |
{{ .Files.Get "crt/YC-CA.pem" | indent 4 }}

Rolling update при изменении конфига

В Deployment добавляется аннотация с контрольной суммой, чтобы поды перезапускались при изменении ConfigMap:

yamlspec:
  template:
    metadata:
      annotations:
        checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        checksum/configmap-ca: {{ include (print $.Template.BasePath "/capem-configmap.yaml") . | sha256sum }}

Helm вычисляет sha256sum содержимого ConfigMap. Если содержимое изменилось между двумя helm upgrade — меняется хеш шаблона пода, и Kubernetes запускает rolling restart.


Отладка ошибок рендеринга

bash# Локальная проверка рендеринга consul-template
export VAULT_ADDR=https://vault.test.example.com
export VAULT_TOKEN=<ваш-dev-токен>
export APP_ENV=Development

consul-template -template="files/config.hcl:/tmp/appsettings.json" -once -log-level=debug
cat /tmp/appsettings.json

Если consul-template зависает (не завершается с -once) — значит, путь к секрету не существует: блок {{ with secret }} ждёт данных бесконечно. Проверьте путь в Vault:

bashvault kv get Microservices/my-service/Development/system

Ограничения

  • Если значение в Vault изменилось без нового деплоя — поды держат старое значение до следующего запуска пайплайна. Для часто ротируемых учётных данных лучше взять External Secrets Operator или Vault Agent.
  • Отрендеренный config/appsettings.json никогда не попадает в git — он существует только во временной файловой системе CI-раннера во время deploy job.
  • consul-template -once завершается с ненулевым кодом, если Vault недоступен или у токена нет прав. Так и задумано: helm upgrade не запустится с неполным конфигом.