Последние несколько месяцев я пристально слежу за тем, как растет и меняется Flux CD. Для тех, кто с ним еще не сталкивался: Flux CD — это набор GitOps-контроллеров для Kubernetes. Он непрерывно следит за Git-репозиторием и приводит кластер к тому состоянию, которое там описано — ставит Helm-чарты, применяет Kustomize-оверлеи, подтягивает образы и секреты. Git становится единственным источником правды, а любое изменение инфраструктуры превращается в коммит: его можно отревьюить, откатить и проследить по истории.
Flux хорош модульностью. Это не монолит, а набор небольших контроллеров — source-controller, kustomize-controller, helm-controller и другие, — каждый решает свою узкую задачу и расширяется через CRD. Благодаря этому экосистема легко прирастает сторонними контроллерами. Один из них, Tofu Controller, и станет героем этой статьи.
У Flux есть более известный конкурент — ArgoCD, тоже GitOps-инструмент для Kubernetes, но с собственным веб-интерфейсом и архитектурой вокруг него. Мне ближе подход именно Flux: он нативно живёт внутри кластера, дружит с автоматизацией через CLI и не навязывает обязательный UI.
Расскажу, как поднимаю облачную инфраструктуру связкой из трех инструментов: Flux CD, Flux Tofu Controller и Terraform-провайдера для Basis Dynamix. Сразу разложу роли, чтобы дальше было понятно, кто за что отвечает:
Flux CD ставит весь процесс на рельсы GitOps: то, что описано в репозитории, автоматически и непрерывно разворачивается в кластере.
Flux Tofu Controller — развитие известного Weave TF-controller — умеет запускать Terraform и OpenTofu прямо внутри Kubernetes. Он берет Terraform-код из git-репозитория, выполняет plan и apply, хранит state и следит за дрейфом конфигурации — и все это через привычные CRD.
Terraform-провайдер для Basis Dynamix — прослойка между Terraform и облачной платформой Basis Dynamix, которая создает в ней реальные ресурсы.
Под «инфраструктурой» я имею в виду ресурсы в облаке Basis Dynamix. В примере ниже это виртуальная машина, но точно так же можно создавать сети, диски, балансировщики, что угодно. Все они заказываются декларативно прямо из Git, а Kubernetes остается единственной точкой управления и выходить за его пределы не нужно.
Почему Terraform в кластере?
Вопрос: а зачем нести Terraform в k8s, если есть Crossplane или Cluster API? Мне самому очень нравится Crossplane, и как-нибудь я расскажу про опыт работы с ним отдельно. Но такой популярности, как у Terraform (Tofu), он пока не набрал. А предложенный здесь подход гибче: ресурсы Terraform можно создавать хоть контроллером в кластере, хоть локально, а кластер заодно решает проблему совместного использования plan и state.
Есть и человеческий фактор. Очень много специалистов, для которых HCL остается основным языком описания инфраструктуры. Подход, о котором я рассказываю, позволяет им остаться в привычной экосистеме Terraform и переиспользовать уже накопленные наработки, а не переучиваться на инструмент вроде Crossplane с его собственной моделью ресурсов.
С Cluster API история другая. Он, на мой взгляд, создан для полной автоматизации, когда человека в цепочке принятия решений просто нет. Манифесты для него писать непросто: чтобы поднять даже один кластер, нужно согласованно описать целое дерево связанных объектов: Cluster, MachineDeployment, инфраструктурные ресурсы провайдера, шаблоны bootstrap и control-plane — и все они ссылаются друг на друга. Ошибка в одном месте ломает всю цепочку, а отлаживать такие связки тяжело.
Даже различия между версиями v1beta1 и v1beta2 весьма существенны, а в следующих релизах многое из этого станет deprecated. Как создатель провайдера для Cluster API могу сказать: контракты между ресурсами в спецификации сложные. Каждый ресурс обязан выставлять строго определенный набор полей и статусов. Например, status.ready, spec.providerID, failureDomains, на которые опираются другие контроллеры. Стоит провайдеру нарушить хотя бы одно ожидаемое поле в нужный момент и reconcile останавливается, причем причина не всегда очевидна из логов. Этим контрактам, к слову, посвящен отдельный раздел документации Cluster API.
В общем, дальше — простые и гибкие инструменты. Весь код примера лежит в репозитории gitops-infra.
Поднимаем тестовый кластер
Начнем с kind — локального Kubernetes в Docker. Ставим актуальную версию:
sudo curl -fsSL -o /usr/local/bin/kind https://kind.sigs.k8s.io/dl/v0.32.0/kind-linux-amd64 chmod +x /usr/local/bin/kind kind version
Создаем кластер:
# clusters/local.yaml kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 name: gitops-test nodes: - role: control-plane
kind create cluster --config clusters/local.yaml
Устанавливаем Flux CD
Ставим Flux CLI, если его еще нет:
curl -s https://fluxcd.io/install.sh | sudo bash
Дальше — bootstrap в репозиторий:
flux bootstrap git \ --url=ssh://git@github.com/beastlex-cyber/gitops-infra \ --branch=main \ --private-key-file=/home/user/.ssh/id_rsa_new \ --path=clusters/gitops-test
Проверяем, что все поднялось:
flux get all
Собираем кастомный раннер для Tofu Controller
Базовый раннер в поставке достаточно гибкий, но кастомный провайдер Terraform в него придется внедрять самостоятельно. Как собрать такой раннер, хорошо разобрано в этой дискуссии — я в целом шел по ее мотивам.
Вот что делает Dockerfile ниже, шаг за шагом:
берет за основу официальный базовый образ раннера tf-runner нужной версии;
временно переключается на root (USER 0:0), чтобы иметь право ставить пакеты;
скачивает и кладет в /usr/local/bin нужную версию бинарника Terraform;
копирует entrypoint.sh и required_providers.tf, а затем выполняет terraform init — так базовые провайдеры (kubernetes, helm) заранее оседают в кэше TF_PLUGIN_CACHE_DIR, и раннеру не приходится ходить за ними в сеть при каждом запуске;
скачивает провайдер Basis Dynamix (decort) из внутреннего репозитория и кладет бинарник в /opt/decort-provider;
формирует файл /etc/terraformrc с блоком dev_overrides, который заставляет Terraform брать провайдер basis/decort/decort из локальной папки, а не искать его в публичном registry — там его просто нет;
возвращает файлы непривилегированному пользователю 65532 и переключается обратно на него — контейнер не должен работать от root;
назначает entrypoint.sh точкой входа.
# images/Dockerfile ARG BASE_RUNNER_VERSION=v0.16.4 FROM ghcr.io/flux-iac/tf-runner:${BASE_RUNNER_VERSION}-base USER 0:0 ARG TF_VERSION=1.15.6 ARG DECORT_PROVIDER_VERSION=4.11.3 ARG TARGETARCH=amd64 RUN wget https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_${TARGETARCH}.zip \ && unzip terraform_${TF_VERSION}_linux_${TARGETARCH}.zip \ && mv -v terraform /usr/local/bin/terraform \ && chmod +x /usr/local/bin/terraform \ && rm -f terraform_${TF_VERSION}_linux_${TARGETARCH}.zip COPY entrypoint.sh required_providers.tf / RUN chmod +x /entrypoint.sh ENV TF_PLUGIN_CACHE_DIR="/opt/terraform.d/plugin-cache" RUN mkdir -p /opt/terraform.d/plugin-cache \ && terraform init \ && rm -rf required_providers.tf /.terraform /.terraform.lock.hcl /LICENSE.txt # Устанавливаем наш провайдер Dynamix RUN mkdir -p /opt/decort-provider \ && wget -q https://repository.basistech.ru/BASIS/terraform-provider-decort/releases/download/${DECORT_PROVIDER_VERSION}/terraform-provider-decort_${DECORT_PROVIDER_VERSION}_linux_${TARGETARCH}.zip \ && unzip -q terraform-provider-decort_${DECORT_PROVIDER_VERSION}_linux_${TARGETARCH}.zip -d /tmp/decort-provider \ && mv /tmp/decort-provider/bin/* /opt/decort-provider/terraform-provider-decort \ && chmod +x /opt/decort-provider/terraform-provider-decort \ && rm -rf terraform-provider-decort_${DECORT_PROVIDER_VERSION}_linux_${TARGETARCH}.zip /tmp/decort-provider # Меняем настройки terraform, чтобы он не пытался отыскать наш провайдер в официальном репозитории RUN echo 'provider_installation {' > /etc/terraformrc \ && echo ' dev_overrides {' >> /etc/terraformrc \ && echo ' "basis/decort/decort" = "/opt/decort-provider"' >> /etc/terraformrc \ && echo ' }' >> /etc/terraformrc \ && echo ' direct {}' >> /etc/terraformrc \ && echo '}' >> /etc/terraformrc ENV TF_CLI_CONFIG_FILE=/etc/terraformrc RUN chown -R 65532:65532 /opt/terraform.d /opt/decort-provider \ && rm -rf /opt/providers \ && ln -s /opt/terraform.d/plugin-cache /opt/providers USER 65532:65532 ENTRYPOINT ["/entrypoint.sh"]
В репозитории рядом лежат еще два файла — images/entrypoint.sh и images/required_providers.tf. Оба простые:
entrypoint.sh — точка входа контейнера. Запускает tf-runner через tini, крошечный init-процесс, который корректно обрабатывает сигналы (например, SIGTERM при остановке пода) и подбирает «зомби»-процессы, чтобы они не накапливались внутри контейнера.
required_providers.tf — минимальный Terraform-конфиг, который объявляет провайдеры kubernetes и helm. Нужен только на этапе сборки: по нему terraform init заранее скачивает эти провайдеры в кэш, и раннеру не приходится тянуть их из интернета во время реальной работы.
Собираем образ:
docker build -t tf-runner-decort:v0.16.4 images/
и загружаем в kind:
kind load docker-image tf-runner-decort:v0.16.4 --name gitops-test
Убедиться, что образ действительно загрузился, можно так:
docker exec -it gitops-test-control-plane crictl images | grep tf-runner
Ставим Tofu Controller через HelmRelease
Создаем манифесты нужных ресурсов в clusters/gitops-test:
# clusters/gitops-test/helmrepository.yaml apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: tofu-controller namespace: flux-system spec: interval: 1h url: https://flux-iac.github.io/tofu-controller
# clusters/gitops-test/helmrelease.yaml apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: tofu-controller namespace: flux-system spec: interval: 1h chart: spec: chart: tofu-controller sourceRef: kind: HelmRepository name: tofu-controller namespace: flux-system version: ">=0.16.4" values: image: tag: v0.16.4 runner: image: # здесь используем наш собранный образ repository: tf-runner-decort tag: v0.16.4 pullPolicy: IfNotPresent grpc: maxMessageSize: 30
Ждем, пока Flux CD развернет ресурсы в кластер, и проверяем, что в указанном namespace появился под контроллера:
kubectl get pods -n flux-system
Создаем ресурсы через Terraform прямо в кластере
Данные учетной записи Basis Dynamix в репозитории хранить нельзя, поэтому секрет создаем руками:
kubectl create secret generic decort-cred -n flux-system \ --from-literal=DECORT_APP_ID=<your_app_id> \ --from-literal=DECORT_APP_SECRET=<your_app_secret>
Дальше — манифест ресурса Terraform. Новый GitRepository заводить не нужно: переиспользуем тот, что уже создан при flux bootstrap:
# clusters/gitops-test/terraform-vm.yaml apiVersion: infra.contrib.fluxcd.io/v1alpha2 kind: Terraform metadata: name: decort-vm namespace: flux-system spec: interval: 1m approvePlan: auto # возможны варианты с ручным апрувом path: ./infra sourceRef: kind: GitRepository name: flux-system namespace: flux-system varsFrom: - kind: Secret name: decort-cred # тот, что создали выше
Дальше остается ждать, пока Tofu Controller развернет ресурсы, — и посматривать в его логи.
Что получилось в итоге
Получился полностью декларативный конвейер. В репозитории лежит описание виртуальной машины на Terraform (infra/main.tf), а Flux CD вместе с Tofu Controller сами разворачивают ее в облаке Basis Dynamix. Через RBAC в кластере легко выдать ответственным инженерам права на создание ресурсов Terraform, чтение логов контроллера, а заодно на чтение plan и state.
Теперь, чтобы что-то поменять в инфраструктуре — добавить диск, поднять еще одну ВМ, поправить параметры существующей, — достаточно сделать коммит в Git. Контроллер подхватит изменения, выполнит plan и apply, а заодно будет постоянно следить за дрейфом состояния, возвращая ресурсы к тому виду, что описан в репозитории. Можно, конечно, продолжать гонять terraform apply руками с ноутбука — но тогда теряется весь смысл GitOps: ревью через pull request, история изменений и возможность откатить неудачный apply одним revert-коммитом.
По сути — self-service облачная инфраструктура, управляемая через привычный Git-процесс, без ручного запуска terraform с локальной машины.
Комментарии (4)

yaroher
17.07.2026 15:48Подскажите пожалуйста, есть ли опыт запуска например workflow рядом с поднятой инфраструктурой? Например если задача поднять условные 3 машины через tf и запустить на них полезную нагрузку в после опустить?
redbeardster
Большой молодец!