Перейти к содержимому
HoginHogin
Назад

Crossplane 2.0 и скорый EOL v1.20: облачная инфраструктура через GitOps вместо Terraform-стейта

9 мин чтения

Десять лет стандартом управления облаком был 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: правда об инфраструктуре в трёх местах

У Terraform три хранилища истины: Git держит желаемое состояние (.tf-файлы), стейт-файл — что было создано и с какими id (обычно S3 с локом в DynamoDB), облако — как всё выглядит на самом деле. Совпадают эти три копии ровно в момент terraform apply. Дальше они расходятся: инженер поправил security group в консоли — plan узнает об этом только при следующем запуске, а между запусками pipeline в большом проекте проходит день.

Terraform: три копии правды и ручные правки в консоли

Болезненные следствия знакомы каждому, кто жил рядом с большим стейтом:

Модель 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 — это самая большая переработка с первого релиза:

Crossplane 2.0: конвейер композиции — рендер функцией, создание ресурса провайдер-функцией

Приятный бонус — CLI: crossplane beta render рендерит композицию локально без кластера, crossplane beta validate проверяет XR против политик. Миграцию с них и начинайте: сначала отрендерили, потом применили.

Terraform против Crossplane v2

TerraformCrossplane 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 держит облако равным кластеру. Получается двойной контур выравнивания с одной точкой правды.

GitOps поверх Crossplane: Argo CD выравнивает кластер, 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, переписывать всё разом не нужно.

  1. Ядро — helm upgrade на v2; убедитесь, что контроллеры и функции поднялись;
  2. Композиции — режим mode: Resources больше не развивается: переведите на pipeline. Временно спасает function-patch-and-transform, целевое состояние — go-templating или KCL;
  3. Провайдеры — заменяйте монолиты на provider functions по одной композиции; ресурс, созданный managed resource, «импортируйте» в функцию, созвучив external-name, — иначе получите дубль;
  4. 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 мы разбирали отдельно.


Поделиться:

Следующая статья
GPU в Kubernetes без device plugins: DRA — как теперь правильно выделять ускорители