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>