Terraform Operator: запуск terraform apply из Kubernetes
Published: 2026-04-24
Инфраструктура Yandex Cloud (VM, managed Kubernetes-кластеры, VPC) управляется через Terraform. Запуск terraform apply из GitLab CI работает, но требует передавать учётные данные через переменные и хранить state снаружи. GalleyBytes terraform-operator запускает Terraform как рабочую нагрузку в Kubernetes — state, учётные данные и выполнение живут прямо в кластере.
Оператор
GalleyBytes terraform-operator определяет CRD Terraform. Каждый объект описывает Terraform-модуль для запуска: исходный git-репозиторий, конфигурацию backend и частоту reconcile.
Разворачивается из custom/apps/terraform-operator/ через HelmRelease на кластере infra.
Установка:
bashkubectl apply -f https://raw.githubusercontent.com/GalleyBytes/terraform-operator/master/deploy/bundles/crd-bundle.yaml
Или как HelmRelease через Flux:
yamlapiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: terraform-operator
namespace: terraform-operator
spec:
chart:
spec:
chart: terraform-operator
sourceRef:
kind: HelmRepository
name: galleybytes
version: ">=0.1.0"
interval: 1h
Объект Terraform CRD
yamlapiVersion: tf.galleybytes.com/v1beta1
kind: Terraform
metadata:
name: infra-terraform
namespace: terraform-operator
spec:
terraformVersion: "1.5.5"
terraformModule:
source: "git::ssh://git@gitlab.example.com/docker/k8s/infra.git//terraform/infra?ref=main"
scmAuthMethods:
- host: gitlab.example.com
git:
ssh:
sshKeySecretRef:
name: flux-system
namespace: flux-system
key: identity
backend: |
terraform {
backend "local" {}
}
storageClassName: local-path
keepLatestPodsOnly: true
writeOutputsToStatus: true
env:
- name: YC_TOKEN
valueFrom:
secretKeyRef:
name: yc-token
key: YC_TOKEN
source — модуль берётся из того же infra git-репозитория, путь terraform/infra, ветка main. Оператор клонирует его перед каждым запуском.
scmAuthMethods — переиспользует SSH-ключ из secret flux-system, который Flux создаёт при bootstrap. Никакого отдельного управления учётными данными.
backend: local — Terraform state хранится на PVC (оператор создаёт его с storageClassName: local-path). Для production лучше переключиться на backend "s3" с объектным хранилищем.
writeOutputsToStatus: true — выходные значения Terraform (ID кластеров, VPC) записываются в поле .status объекта и становятся доступны другим контроллерам.
Токен для Terraform
Провайдер Yandex Cloud требует токен. Хранится как SealedSecret:
yamlapiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: yc-token
namespace: terraform-operator
spec:
encryptedData:
YC_TOKEN: AgBx...
Объект Terraform ссылается на него как на переменную окружения для пода-раннера.
RBAC
Оператору нужны права на создание подов и PVC:
yamlapiVersion: v1
kind: ServiceAccount
metadata:
name: terraform-operator
namespace: terraform-operator
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: terraform-operator
rules:
- apiGroups: ["tf.galleybytes.com"]
resources: ["*"]
verbs: ["*"]
- apiGroups: [""]
resources: ["pods", "pods/log", "persistentvolumeclaims", "secrets", "serviceaccounts"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: terraform-operator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: terraform-operator
subjects:
- kind: ServiceAccount
name: terraform-operator
namespace: terraform-operator
Структура директорий Terraform
terraform/
├── infra/ ← ресурсы YC для infra-кластера (VM, LB, VPC)
├── dev/ ← dev-кластер
├── test/ ← test-кластер
├── sre/ ← YC-кластер sre
├── loadgds/ ← YC-кластер loadgds
└── demo/ ← YC-кластер demo
Каждая директория — независимый корневой Terraform-модуль, и каждой управляет отдельный объект Terraform.
Поведение reconcile
Оператор запускает terraform plan и terraform apply по расписанию (по умолчанию: каждые 5 минут). Если plan не обнаруживает изменений — no-op. Если обнаружен дрифт — применяет автоматически.
keepLatestPodsOnly: true — сохраняет только последний под-раннер для просмотра логов.
Для немедленного reconcile:
bashkubectl annotate terraform infra-terraform \
-n terraform-operator \
tf.galleybytes.com/sync="true" \
--overwrite \
--context=infra-k8s
Просмотр статуса
bash# Статус объекта Terraform
kubectl get terraform -n terraform-operator --context=infra-k8s
# Детали с условиями
kubectl describe terraform infra-terraform -n terraform-operator --context=infra-k8s
# Логи последнего пода-раннера
kubectl logs \
-n terraform-operator \
-l terraform.galleybytes.com/name=infra-terraform \
--context=infra-k8s
# Выходные значения Terraform
kubectl get terraform infra-terraform \
-n terraform-operator \
-o jsonpath='{.status.outputs}' \
--context=infra-k8s | jq .
CI vs оператор: сравнение
| GitLab CI | Terraform Operator | |
|---|---|---|
| Запуск | git push / расписание | Kubernetes reconcile loop |
| Хранение state | Внешнее (S3 / HTTP) | PVC в кластере |
| Учётные данные | CI-переменные | Kubernetes secrets |
| Обнаружение дрифта | Вручную / по расписанию | Автоматически (каждые N минут) |
| История | Логи CI-задач | Логи подов + .status |
Оператор лучше для инфраструктуры, которая редко меняется, но требует контроля дрифта. CI-пайплайны лучше там, где каждое изменение проходит через PR с ревью.
Диагностика
Под-раннер завис в Init — проверить логи оператора:
bashkubectl logs -n terraform-operator -l app=terraform-operator
git clone падает — SSH-ключ не найден или неправильный namespace:
bashkubectl get secret flux-system -n flux-system -o yaml
Ошибка state lock — предыдущий под упал в процессе apply и оставил блокировку. Force-unlock:
bash# Зайти в debug-pod с примонтированным PVC
terraform force-unlock <LOCK_ID>