Десять лет стандартом управления облаком был Terraform: желаемое состояние в HCL, terraform plan, terraform apply, а вся правда об инфраструктуре — в стейт-файле. Crossplane предлагает противоположное: облачные ресурсы — это объекты Kubernetes, их создаёт kubectl, откатывает git revert, а соответствие кластера и облака поддерживает вечный reconcile-цикл, как у Deployment. В ноябре 2026 вместе с релизом Crossplane v2.5 заканчивается поддержка v1.20 — последней ветки 1.x. Тем, кто сидит на 1.x, пора переезжать; тем, кто смотрел на Crossplane издалека, — посмотреть заново: v2 починила главные боли 1.x.
Содержание
Открыть содержание
- Terraform: правда об инфраструктуре в трёх местах
- Crossplane 1.x: правильная модель, тяжёлая посадка
- Что изменилось в 2.0
- Terraform против Crossplane v2
- Что нужно, чтобы выдать S3-бакет одним Kind’ом
- Argo CD поверх Crossplane: два контура, один Git
- Миграция v1 → v2: порядок и грабли
- Когда Crossplane не нужен
- Как проверить, что всё работает
- Итог
Terraform: правда об инфраструктуре в трёх местах
У Terraform три хранилища истины: Git держит желаемое состояние (.tf-файлы), стейт-файл — что было создано и с какими id (обычно S3 с локом в DynamoDB), облако — как всё выглядит на самом деле. Совпадают эти три копии ровно в момент terraform apply. Дальше они расходятся: инженер поправил security group в консоли — plan узнает об этом только при следующем запуске, а между запусками pipeline в большом проекте проходит день.
Болезненные следствия знакомы каждому, кто жил рядом с большим стейтом:
- дрейф виден только в pipeline — между запусками
terraform planручные правки в консоли никто не отменяет, и «plan чистый с утра» не значит «plan чистый к вечеру»; - blast radius одного apply — стейт на окружение один: ошибочный рефакторинг модуля или кривой
-targetкладёт инфраструктуру всего проекта за один запуск; - сериализация через локи — два pipeline над одним стейтом не работают параллельно, и платформенная команда становится узким местом;
- стейт надо беречь — потерянный tfstate восстанавливают ручным импортом, а секреты в нём (строки подключения, ключи) — отдельная головная боль.
Модель Kubernetes решает это архитектурно: контроллер непрерывно сверяет наблюдаемое с желаемым и выравнивает разницу. Crossplane переносит этот цикл на облако.
Crossplane 1.x: правильная модель, тяжёлая посадка
В 1.x Crossplane работал так: провайдер (например, provider-aws) регистрирует в кластере CRD на каждый облачный ресурс — тысячи managed resources, по объекту на бакет, VPC или IAM-пользователя. Сверху — композиции: composite resource (XR) — ваш собственный Kind («StaticSite», «PostgresCluster»), который композиция разворачивает в набор managed resources. Похоже на терраформ-модули, только живьём и с reconcile.
Посадка оказалась тяжёлой: монолитный provider-aws тащил 800+ CRD — etcd и API-сервер проседали, а апгрейд провайдера был событием; XR были кластер-скоупом, а попытка завести их в namespace через claims запутала всех; композировать можно было только managed resources — приложение рядом с его инфраструктурой в один объект не собиралось. Вторая версия чинит именно это.
Что изменилось в 2.0
Crossplane 2.0 вышел в августе 2025 — это самая большая переработка с первого релиза:
- композиции из любых ресурсов — в composition теперь попадает что угодно: Deployment и Service, ConfigMap, CR от cert-manager или вашего оператора наравне с облачными ресурсами. Приложение и его облако — один объект: XR «сервис X» описывает и деплоймент, и его S3-бакет, и IAM-пользователя;
- provider functions — провайдер перестал быть отдельным контроллером с CRD и стал функцией в конвейере композиции. provider-aws-family создаёт любые AWS-ресурсы, вызывая API напрямую; фактическое состояние живёт в статусе XR, а не в тысячах объектов в etcd;
- именованные XR по умолчанию — XR живут в namespace, права на них выдаёт обычный RBAC, claims объявлены deprecated; кластерный скоуп остался явной опцией;
- конвейер функций — единственный путь — встроенный patch-and-transform объявлен deprecated, композиции собираются функциями: function-go-templating, function-kcl, function-cue;
- готовность любых ресурсов — композиция умеет ждать готовности Deployment’а так же, как раньше ждала managed resource.
Приятный бонус — CLI: crossplane beta render рендерит композицию локально без кластера, crossplane beta validate проверяет XR против политик. Миграцию с них и начинайте: сначала отрендерили, потом применили.
Terraform против Crossplane v2
| Terraform | Crossplane v2 | |
|---|---|---|
| Где правда | стейт-файл в S3 | объекты кластера |
| Когда меняется | по запуску pipeline | непрерывный reconcile |
| Дрейф | виден в plan | выравнивается сам |
| Доступ команды | CI + лок стейта | kubectl + RBAC |
| Откат | surgery над стейтом | git revert |
| Скорость мелочи | минуты: pipeline | секунды: reconcile |
| Экосистема модулей | огромная | скромнее, но растёт |
| Что требуется | бэкенд для стейта | кластер под контрол-плейн |
Что нужно, чтобы выдать S3-бакет одним Kind’ом
Цель: команда в своём namespace создаёт объект StaticSite — и получает S3-бакет для статики и IAM-пользователя с ключами, без тикета в платформенную команду. Нужны кластер (подойдёт managed), Crossplane v2 и Argo CD. Учётную запись AWS подключаем через ProviderConfig — как раньше в provider-aws, вплоть до IRSA/workload identity на service account.
1. Ядро и функции. Ставим Crossplane, затем две функции — go-templating для рендера и provider-aws-family для создания ресурсов:
# functions.yaml — версии зафиксируйте актуальные из реестра
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-go-templating
spec:
package: xpkg.upbound.io/upbound/function-go-templating:v0.14.0
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: provider-aws-family
spec:
package: xpkg.upbound.io/upbound/provider-aws-family:v1.0.0
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm install crossplane crossplane-stable/crossplane \
--namespace crossplane-system --create-namespace
kubectl apply -f functions.yaml
2. Kind и композиция. XRD объявляет API, Composition — как его собирать. Обратите внимание: в pipeline два шага, а CRD на Bucket и User не существуют — провайдер-функция берёт эти объекты из desired-состояния и зовёт AWS API сама:
# xrd.yaml — namespaced XR по умолчанию, claims не нужны
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: staticsites.platform.acme.io
spec:
group: platform.acme.io
names:
kind: StaticSite
plural: staticsites
defaultCompositionRef:
name: static-site-aws
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
region:
type: string
default: eu-central-1
bucketName:
type: string
# composition.yaml
apiVersion: apiextensions.crossplane.io/v2
kind: Composition
metadata:
name: static-site-aws
spec:
compositeTypeRef:
apiVersion: platform.acme.io/v1alpha1
kind: StaticSite
mode: Pipeline
pipeline:
- step: render
functionRef:
name: function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: Template
resources:
- name: bucket
base:
apiVersion: s3.aws.upbound.io/v2beta1
kind: Bucket
spec:
forProvider:
region: "{{ .param.spec.region }}"
- name: ci-user
base:
apiVersion: iam.aws.upbound.io/v2beta1
kind: User
spec:
forProvider:
name: "{{ .param.spec.bucketName }}-ci"
- name: ci-keys
base:
apiVersion: iam.aws.upbound.io/v2beta1
kind: AccessKey
spec:
forProvider:
user: "{{ .param.spec.bucketName }}-ci"
- step: provision # вход не нужен: провайдер-функция
functionRef: # забирает свои ресурсы из desired
name: provider-aws-family
3. Пользователь. Команда (или Argo CD) применяет XR в своём namespace:
# docs-acme-io.yaml
apiVersion: platform.acme.io/v1alpha1
kind: StaticSite
metadata:
name: docs-acme-io
namespace: team-docs
spec:
bucketName: docs-acme-io
writeConnectionSecretsToRef:
name: docs-acme-io-aws
Ключи из AccessKey попадут в connection details XR и запишутся в указанный Secret — монтируйте его в CI-раннер, который заливает статику. Политику «s3:PutObject на этот бакет» добавьте ресурсом UserPolicy, подставив ARN из статуса бакета.
Argo CD поверх Crossplane: два контура, один Git
XRD, композиции и XR лежат в Git; Argo CD держит кластер равным репозиторию, Crossplane держит облако равным кластеру. Получается двойной контур выравнивания с одной точкой правды.
Практика, сэкономившая многим неделю отладки:
- app-of-apps для инфры — корневое Application указывает на папку с Application-ами по окружениям, каждое — на свой путь в репо: инфраструктура живёт в том же дереве, что приложения;
- ServerSideApply — схемы XRD большие; без SSA Argo упирается в лимит
last-applied-configurationи молча не применяет объект; - sync waves — XRD и композиции применяйте волной раньше XR, иначе первый sync упадёт с «no matches for kind»;
- prune: false поначалу — удаление инфраструктурного Application с prune снесёт облако; включайте осознанно;
- selfHeal: true — ручной
kubectl editXR вернётся к Git, ручная правка бакета в консоли AWS — к желаемому состоянию reconcile-циклом Crossplane.
# infra-app.yaml — лист дерева app-of-apps
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: static-site-docs
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/acme/infra.git
targetRevision: main
path: crossplane/static-site
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
prune: false
selfHeal: true
syncOptions:
- ServerSideApply=true
- CreateNamespace=true
Миграция v1 → v2: порядок и грабли
Миграция спроектирована постепенной: легаси-провайдеры и managed resources работают на v2, переписывать всё разом не нужно.
- Ядро —
helm upgradeна v2; убедитесь, что контроллеры и функции поднялись; - Композиции — режим
mode: Resourcesбольше не развивается: переведите на pipeline. Временно спасает function-patch-and-transform, целевое состояние — go-templating или KCL; - Провайдеры — заменяйте монолиты на provider functions по одной композиции; ресурс, созданный managed resource, «импортируйте» в функцию, созвучив external-name, — иначе получите дубль;
- Claims → namespaced XR — переносите вместе с RBAC: именованным XR нужны права в namespace’ах команд, кластерный скоуп теперь только явный (
clusterScoped: true).
Чаще всего наступают на три грабли: API-версии apiextensions.crossplane.io/v2 у XRD и Composition при обновлении манифестов; deletionPolicy при смене провайдера — неосторожная миграция сносит то, что должна была унаследовать; и битые functionRef, если функции ещё не поднялись к моменту первого sync Argo CD.
Когда Crossplane не нужен
Один VPS, один облачный аккаунт, одна команда — контроль-плейн в Kubernetes оверсайз: вы будете обслуживать кластер ради одного бакета. Terraform в одном pipeline честнее и дешевле: меньше движущихся частей, ноль CRD, модули под любую задачу.
Crossplane окупается, когда: окружений и аккаунтов больше одного; команды потребляют инфраструктуру самообслуживанием — «мне нужен бакет» вместо тикета на три дня; и GitOps-контур для приложений уже есть, так что инфраструктуру логично держать в том же дереве.
Как проверить, что всё работает
kubectl get staticsites -n team-docs
kubectl describe staticsite docs-acme-io -n team-docs # условия каждого ресурса
crossplane beta trace staticsite/docs-acme-io -n team-docs
aws s3 ls | grep docs-acme-io
argocd app get static-site-docs # Synced + Healthy
Тест дрейфа: удалите бакет в консоли AWS — через reconcile-период Crossplane создаст его заново. Отредактируйте XR руками — selfHeal вернёт объект к Git. Оба контура закольцованы.
Итог
Один VPS — оставайтесь на Terraform, это дешевле и проще. Но когда инфраструктуры больше, чем на один облачный аккаунт, контроль-плейн внутри Kubernetes честнее и безопаснее стейт-файла в S3: желаемое состояние — объекты кластера с RBAC и историей, дрейф выравнивается непрерывно, откат — это git revert. Плата — кластер, который надо обслуживать, и экосистема помоложе терраформовой. Сидящим на 1.x выбора нет: EOL v1.20 в ноябре закрывает вопрос. А инфраструктура в Kubernetes открывает и соседние двери — Zero Trust-доступ к серверам через Teleport мы разбирали отдельно.