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не запустится с неполным конфигом.