×

7.4. Деплой инфраструктуры в DevSpace через Terraform

 
Гайд для DevOps-инженеров, которые хотят управлять ресурсами в кластере DevSpace декларативно через Terraform. Целевая аудитория — инженер уровня middle с базовым опытом Kubernetes и Terraform.
 
  • DevSpace выставляет наружу стандартный Kubernetes API: https://api.devspace.cloupard.io:443. API-сервер использует внутренний Kubernetes CA — CA-сертификат встроен в выданный вам kubeconfig.
 
  • Аутентификация через OIDC (Keycloak), а не через статические kubeconfig’и или сервис-аккаунты — единый identity-слой, работа через SSO.
 
  • Внутри своего namespace тенанта вы создаёте не произвольные Kubernetes-ресурсы, а CozyStack-specific объекты из API-группы apps.cozystack.io/v1alpha1 (Kubernetes, Postgres, Kafka, Redis, VirtualMachine, …). RBAC в DevSpace настроен так, что прямой kubectl apply -f deployment.yaml в ваш namespace вернёт Forbidden.
 
  • В Terraform используется один провайдер — alekc/kubectl. Все ресурсы (CozyStack-specific и любые другие) деплоятся единообразно через kubectl_manifest. Аутентификация — через стандартный exec-плагин с kubelogin (он же kubectl oidc-login).
 
Что вы получаете от провайдера для доступа в DevSpace.
Что
Значение
Готовый kubeconfig
один файл — там уже CA, endpoint, имя вашего тенанта и exec-блок
API endpoint
https://api.devspace.cloupard.io:443
OIDC issuer URL
https://keycloak.devspace.cloupard.io/realms/cozy
OIDC client ID
kubernetes
CA сертификат API
встроен в kubeconfig в виде certificate-authority-data (base64)
Имя вашего тенанта
например tenant-kzj4to5r4t — виден в дашборде DevSpace
Логин/пароль в Keycloak
выдаётся вместе с аккаунтом
 
УСТАНОВКА ИНСТРУМЕНТОВ
 
На рабочей машине (или CI-раннере) понадобится три инструмента: Terraform, kubectl и kubelogin.
 
TERRAFORM
 
Требуется версия ≥ 1.5. Не ставьте через apt/yum/brew без проверки — там часто лежит устаревший Terraform или OpenTofu под тем же именем. Установка из официального источника HashiCorp.
 
macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
 
Linux (Debian/Ubuntu)
wget -O- https://apt.releases.hashicorp.com/gpg | \
  sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
 
Linux (RHEL/Fedora)
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo dnf install -y terraform
 
Windows
choco install terraform
# или скачайте zip с https://developer.hashicorp.com/terraform/install и положите в PATH
 
Проверка
terraform version
# должно быть >= Terraform v1.5.0
 
Не путайте Terraform с OpenTofu. OpenTofu — открытый форк (бинарь называется tofu). Для целей этого гайда подходят оба, но синтаксис примеров — терраформовский. Если вы установили tofu — просто заменяйте terraform на tofu в командах.
 
kubectl
 
Любая актуальная версия. Инструкции: https://kubernetes.io/docs/tasks/tools/.
 
Проверка:
kubectl version --client
 
kubelogin (OIDC credential plugin)
 
Это именно int128/kubelogin (он же kubectl oidc-login), а не одноимённый Azure/kubkubectl oidc-login --version
elogin от Microsoft (тот заточен под AAD).
# macOS
brew install int128/kubelogin/kubelogin
 
# Linux (через krew — рекомендуется)
kubectl krew install oidc-login
 
# Linux вручную
curl -LO https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
sudo mv kubelogin /usr/local/bin/
 
# Windows
choco install kubelogin
 
Проверка:
kubectl oidc-login --version
 
БАЗОВЫЙ KUBECONFIG ДЛЯ ИНТЕРАКТИВНОЙ РАБОТЫ
 
Kubeconfig выдаётся провайдером — собирать его руками не нужно. Выглядит он так:
apiVersion: v1
clusters:
- cluster:
    server: https://api.devspace.cloupard.io:443
    certificate-authority-data: LS0tLS1CRUdJTi...   # внутренний Kubernetes CA, base64
  name: cluster
contexts:
- context:
    cluster: cluster
    namespace: tenant-kzj4to5r4t
    user: keycloak
  name: tenant-kzj4to5r4t
current-context: tenant-kzj4to5r4t
users:
- name: keycloak
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      command: kubectl
      args:
      - oidc-login
      - get-token
      - --oidc-issuer-url=https://keycloak.devspace.cloupard.io/realms/cozy
      - --oidc-client-id=kubernetes
      - --skip-open-browser
 
Ключевые моменты:
  • certificate-authority-data обязателен. API-сервер представляется внутренним Kubernetes CA, который не входит в системные trust store’ы — без этого поля любой клиент получит x509: certificate signed by unknown authority.
     
  • --oidc-client-secret отсутствует, потому что клиент kubernetes public.
     
  • --skip-open-browser означает: kubelogin не пытается открыть браузер сам, а печатает URL в stdout. Удобно для серверов без графики и для случаев, когда terraform запускается из другого терминала.
 
Сохраните файл как ~/.kube/devspace.yaml и проверьте доступ:
export KUBECONFIG=~/.kube/devspace.yaml
 
# первый запрос — kubelogin напечатает URL, откройте его в браузере, залогиньтесь
kubectl auth whoami
 
# посмотрите доступные вам CozyStack-ресурсы
kubectl api-resources --api-group=apps.cozystack.io
 
При первом вызове kubelogin напечатает URL Keycloak. После успешного входа инструмент закеширует ID-token в ~/.kube/cache/oidc-login/. Дальше все запросы (включая terraform) будут переиспользовать кеш, пока токен валиден.
 
Если работаете через SSH без браузера — добавьте --grant-type=authcode-keyboard в args. kubelogin напечатает URL, который вы открываете на локальной машине, и попросит вставить код обратно в терминал.
 
TERRAFORM: ПОДКЛЮЧЕНИЕ ПРОВАЙДЕРА
 
Для всех ресурсов используется один провайдер — kubectl от alekc. Он умеет применять произвольные YAML-манифесты и корректно работает как с обычными Kubernetes-ресурсами, так и с aggregated API DevSpace (apps.cozystack.io/v1alpha1).
Есть два равноправных подхода к конфигурации — переиспользовать выданный kubeconfig напрямую или сконфигурировать провайдер явными полями.
 
Вариант 1 (рекомендуемый): через config_path
 
Самый простой и устойчивый к изменениям способ — указать путь к kubeconfig’у, который выдал провайдер DevSpace. Terraform сам прочитает оттуда endpoint, CA, namespace и exec-блок
terraform {
  required_version = ">= 1.5"
  required_providers {
    kubectl = {
      source  = "alekc/kubectl"
      version = "~> 2.0"
    }
  }
}
 
provider "kubectl" {
  config_path      = "~/.kube/devspace.yaml"
  config_context   = "tenant-kzj4to5r4t"
  load_config_file = true
}
 
Плюсы: если провайдер обновит CA или endpoint — обновится kubeconfig, ваш Terraform-код менять не нужно.
 
Вариант 2: явная конфигурация провайдера
 
Если нужно держать всё в HCL (например, чтобы не таскать файлы в CI) — продублируйте содержимое kubeconfig в провайдере
provider "kubectl" {
  host                   = "https://api.devspace.cloupard.io:443"
  cluster_ca_certificate = base64decode(var.cluster_ca_data)
  load_config_file       = false
 
  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "kubectl"
    args = [
      "oidc-login", "get-token",
      "--oidc-issuer-url=https://keycloak.devspace.cloupard.io/realms/cozy",
      "--oidc-client-id=kubernetes",
      "--skip-open-browser",
    ]
  }
}
 
variable "cluster_ca_data" {
  description = "base64-encoded CA certificate (значение поля certificate-authority-data из kubeconfig)"
  type        = string
  sensitive   = true
}
 
В terraform.tfvars
cluster_ca_data = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t..."  # из вашего kubeconfig
 
Как работает exec-блок
 
При каждом запросе к API провайдер запускает указанную команду. Команда должна вернуть JSON формата ExecCredential. kubelogin get-token ровно это и делает: берёт кеш-токен из ~/.kube/cache/oidc-login/, если он валиден — отдаёт сразу; если нет — открывает браузер (или показывает URL для authcode-keyboard) и получает новый.
 
Поэтому terraform plan / terraform apply будут работать без диалогов после первой аутентификации.
 
Откуда Terraform берёт провайдер
 
source = "alekc/kubectl" — это сокращённая запись. Terraform разворачивает её в полный адрес registry.terraform.io/alekc/kubectl. registry.terraform.io — официальный публичный реестр HashiCorp; для провайдеров он работает примерно как Docker Hub для образов.
 
ДЕПЛОЙ РЕСУРСОВ
 
Что можно деплоить
 
Внутри namespace вашего тенанта вы создаёте объекты DevSpace. Команда для просмотра доступных типов
kubectl api-resources --api-group=apps.cozystack.io
 
Типичный список (зависит от версии DevSpace)
Kind
Plural
Что это
Kubernetes
kuberneteses
managed Kubernetes-кластер (control-plane через Kamaji + worker-ноды как VM)
Postgres
postgreses
managed PostgreSQL (CloudNativePG)
Kafka
kafkas
managed Kafka (Strimzi)
Redis
redises
managed Redis
MariaDB
mariadbs
managed MariaDB
ClickHouse
clickhouses
managed ClickHouse
VirtualMachine
virtualmachines
виртуальная машина (KubeVirt)
VMDisk
vmdisks
диск для VM
 
DevSpace-ресурсы через Terraform
 
Все ресурсы — через kubectl_manifest. Содержимое любого манифеста можно либо собрать в HCL через yamlencode({...}), либо положить отдельным YAML-файлом и подключить через heredoc / file().
 
Пример 1 — managed PostgreSQL
locals {
  tenant_namespace = "tenant-kzj4to5r4t"
}
 
resource "kubectl_manifest" "pg" {
  yaml_body = yamlencode({
    apiVersion = "apps.cozystack.io/v1alpha1"
    kind       = "Postgres"
    metadata = {
      name      = "app-db"
      namespace = local.tenant_namespace
    }
    spec = {
      external     = false
      size         = "10Gi"
      replicas     = 2
      storageClass = "replicated"
      postgresql = {
        parameters = {
          max_connections = "100"
        }
      }
      users = {
        appuser = {
          password = var.pg_password
        }
      }
      databases = {
        appdb = {
          roles = {
            admin = ["appuser"]
          }
        }
      }
      backup          = { enabled = false }
      resourcesPreset = "nano"
    }
  })
}
 
variable "pg_password" {
  type      = string
  sensitive = true
}
 
Пример 2 — managed Kubernetes-кластер (если вам нужно запускать произвольные workload’ы — вы заказываете себе целый K8s-кластер внутри DevSpace, и уже там делаете что угодно)
resource "kubectl_manifest" "k8s" {
  yaml_body = yamlencode({
    apiVersion = "apps.cozystack.io/v1alpha1"
    kind       = "Kubernetes"
    metadata = {
      name      = "prod-cluster"
      namespace = local.tenant_namespace
    }
    spec = {
      version      = "1.31.0"
      storageClass = "replicated"
      controlPlane = {
        replicas          = 2
        apiServer         = { resourcesPreset = "small" }
        controllerManager = { resourcesPreset = "micro" }
        scheduler         = { resourcesPreset = "micro" }
        konnectivity = {
          server = { resourcesPreset = "micro" }
        }
      }
      nodeGroups = {
        md0 = {
          minReplicas  = 2
          maxReplicas  = 5
          instanceType = "u1.medium"
          diskSize     = "50Gi"
          roles        = ["ingress-nginx"]
        }
      }
      addons = {
        ingressNginx     = { enabled = true,  hosts = [] }
        certManager      = { enabled = false }
        fluxcd           = { enabled = false }
        monitoringAgents = { enabled = false }
      }
    }
  })
}
 
Это рекомендуемый паттерн: DevSpace-namespace для управления managed-сервисами, а свой K8s-кластер — для произвольной нагрузки.
 
ЗАПУСК И СОСТОЯНИЕ
 
terraform init
terraform plan
terraform apply
 
Хранение state’а
 
State содержит секреты (пароли БД, kubeconfig’и дочерних кластеров и т.д.) — локальный state допустим только для эксперимента. Рекомендуется хранить terraform.tfstate в любом внешнем S3-совместимом хранилище (Cloupard S3, AWS S3, MinIO и т.д.) с обязательным включением шифрования и version-locking — это стандартная практика.
 
ЧАСТЫЕ ПРОБЛЕМЫ
 
Симптом
Причина
Решение
x509: certificate signed by unknown authority
Не передан cluster_ca_certificate / certificate-authority-data
API использует внутренний Kubernetes CA — либо берите его из выданного kubeconfig’а, либо подкладывайте явно в provider
Unauthorized после успешного входа в Keycloak
Несовпадение client-id или issuer URL между провайдером и тем, что ждёт API-сервер
Сверьте флаги kubelogin с конфигом API-сервера (см. раздел 1)
forbidden: cannot create deployments.apps in namespace tenant-...
Попытка создать обычный Kubernetes-ресурс в namespace тенанта
По дизайну DevSpace вы создаёте объекты из apps.cozystack.io/v1alpha1. Если нужен Deployment — поднимите managed K8s-кластер (раздел 5.2) и деплойте туда
forbidden: User "alice" cannot list ... in namespace "tenant-..."
Пользователь аутентифицирован, но не в нужной Keycloak-группе
Попросите администратора DevSpace добавить вас в группу tenant-<name>-admin
failed to call OIDC provider: ... connection refused
Нет сетевого доступа к Keycloak
Откройте egress на keycloak.devspace.cloupard.io:443
kubectl oidc-login: open browser failed
Headless-окружение
Используйте --grant-type=authcode-keyboard
Error: namespaces "tenant-..." not found
Тенант ещё не создан администратором
Запросите создание тенанта у провайдера DevSpace
invalid_client от Keycloak
Передан --oidc-client-secret, но клиент kubernetes в DevSpace public
Уберите флаг --oidc-client-secret
failed to look up GVK [apps.cozystack.io/v1alpha1, Kind=...] among available CRDs: customresourcedefinitions ... is forbidden
Использован kubernetes_manifest из hashicorp/kubernetes — он на этапе plan делает LIST всех CRD на cluster scope, чего у тенанта нет (и не должно быть)
Используйте kubectl_manifest из провайдера alekc/kubectl — см. раздел 4
kubectl get crd | grep apps.cozystack.io ничего не показывает
apps.cozystack.io — это aggregated API, а не CRD
Используйте kubectl api-resources --api-group=apps.cozystack.io
 
ССЫЛКИ