Két évig én voltam az a fickó, aki adatbázisokat provizionál. Minden hétfő reggel ugyanaz a Slack üzenet: “Hé, kéne egy Postgres a new service-hez.” Megnyitottam a Terraformot, másolás, három változó átírása, plan, approval, apply. Húsz perc az életemből, el. Szorozd meg négy csapattal, és érezni fogod.

Aztán beüzemeltem a Crossplane-t Compositions-szel, és most a fejlesztők maguk csinálják egyetlen YAML fájllal. Íme, hogyan jutottam el idáig, és mi tört el közben.

Miért nem maradtam a Terraformnál?

A Terraform működik. Nem azért írom ezt, hogy leégessem. De önkiszolgálásra van egy alapvető problémája: a fejlesztőknek kell hozzáférés a state-hez, a provider credentialökhoz és a CI pipeline-hoz, ami futtatja a terraform apply-t. Ez rengeteg bizalmi felület valakinek, aki csak egy adatbázist akar.

A Crossplane megfordítja ezt. A Kubernetes klaszterben fut controllerek formájában. A fejlesztő létrehoz egy custom resource-t, a Crossplane összerakja belőle a valódi cloud resource-okat. Ugyanaz a GitOps workflow, amit az appjaikhoz már használnak.

Crossplane telepítés

Helmen keresztül futtatom, mert a marketplace operátornak gondjai voltak az OPA policy-inkkel:

helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

helm install crossplane crossplane-stable/crossplane \
  --namespace crossplane-system \
  --create-namespace \
  --set args='{"--enable-usages"}' \
  --version 1.19.0

Az --enable-usages flag fontos. Nélküle egy Composition törlése árván hagyhat cloud resource-okat. Ezt a drága utat tanultam meg, amikor egy gyakornok törölt egy CompositeResourceDefinition-t, és 14 nyomon nem követett RDS instance futott egy hétig.

AWS Provider beállítás

apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-rds
spec:
  package: xpkg.upbound.io/upbound/provider-aws-rds:v1.18.0
  runtimeConfigRef:
    name: irsa-config

IRSA-t (IAM Roles for Service Accounts) használok statikus credentialök helyett. A runtime config így néz ki:

apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
  name: irsa-config
spec:
  deploymentTemplate:
    spec:
      selector: {}
      template:
        spec:
          serviceAccountName: crossplane-provider-aws
          containers:
            - name: package-runtime
              args:
                - --poll=1m

Egy buktató: a provider podoknak kell az eks.amazonaws.com/role-arn annotáció a saját ServiceAccountjukon, nem a Crossplane rendszer SA-ján. Egy délutánt töltöttem “AccessDenied” hibák debugolásával emiatt.

A Composition: RDS becsomagolva

Itt lesz érdekes. A Composition lényegében egy sablon, ami egy egyszerű, fejlesztőknek szánt API-t térképez le a mögöttes komplex cloud resource-ra.

Először definiáljuk, mit lát a fejlesztő (az XRD):

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresinstances.database.dedico.hu
spec:
  group: database.dedico.hu
  names:
    kind: XPostgresInstance
    plural: xpostgresinstances
  claimNames:
    kind: PostgresInstance
    plural: postgresinstances
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                size:
                  type: string
                  enum: ["small", "medium", "large"]
                  description: "small=db.t3.micro, medium=db.t3.small, large=db.t3.medium"
                teamName:
                  type: string
              required:
                - size
                - teamName

A fejlesztők pólóméret alapján választanak és megadják a csapatnevet. Ennyi. Nincs instance class memorizálás, nincs subnet group config, nincs parameter group.

Aztán a Composition leképezi ezeket az egyszerű inputokat valódi AWS resource-okra:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: postgres-on-aws
  labels:
    provider: aws
spec:
  compositeTypeRef:
    apiVersion: database.dedico.hu/v1alpha1
    kind: XPostgresInstance
  resources:
    - name: rds-instance
      base:
        apiVersion: rds.aws.upbound.io/v1beta2
        kind: Instance
        spec:
          forProvider:
            engine: postgres
            engineVersion: "16.4"
            dbSubnetGroupName: shared-private
            vpcSecurityGroupIds:
              - sg-0abc123def456
            publiclyAccessible: false
            storageEncrypted: true
            autoMinorVersionUpgrade: true
            backupRetentionPeriod: 7
            deletionProtection: true
      patches:
        - type: FromCompositeFieldPath
          fromFieldPath: spec.size
          toFieldPath: spec.forProvider.instanceClass
          transforms:
            - type: map
              map:
                small: db.t3.micro
                medium: db.t3.small
                large: db.t3.medium
        - type: FromCompositeFieldPath
          fromFieldPath: spec.size
          toFieldPath: spec.forProvider.allocatedStorage
          transforms:
            - type: map
              map:
                small: "20"
                medium: "50"
                large: "100"
        - type: FromCompositeFieldPath
          fromFieldPath: spec.teamName
          toFieldPath: spec.forProvider.tags.Team
    - name: rds-password
      base:
        apiVersion: secretstores.crossplane.io/v1alpha1
        kind: VaultSecret
        spec:
          forProvider:
            path: database/creds

Mit csinálnak a fejlesztők a gyakorlatban?

Egy fejlesztő, aki adatbázist akar, ezt hozza létre az appja GitOps repójában:

apiVersion: database.dedico.hu/v1alpha1
kind: PostgresInstance
metadata:
  name: user-service-db
  namespace: team-payments
spec:
  size: small
  teamName: payments

Pusholja, az ArgoCD szinkronizálja, a Crossplane felveszi, és 5 perc múlva fut egy RDS instance, a connection string pedig bekerül egy Kubernetes Secretbe a namespace-ükben.

Nincs Slack üzenet. Nincs ticket. Nincs várakozás rám.

Amik félrementek

1. probléma: A Composition drift detection lassú. A Crossplane intervallumonként pollozza a cloud providereket (alapból 1 perc). Ha valaki módosít egy RDS instance-t az AWS konzolon, akár egy percbe is telhet, mire észreveszi és visszaállítja. Nekünk ez belefért, de ha szorosabb drift detectionre van szükséged, csökkentsd a --poll intervallumot. Csak figyelj az API rate limitekre.

2. probléma: A törlés sorrendje számít. Volt egy Compositionünk, ami RDS instance-t és security groupot is létrehozott. Amikor egy fejlesztő törölte a claimjét, a Crossplane megpróbálta a security groupot az RDS instance előtt törölni. A security group törlés elbukott, mert még használatban volt, és az egész beragadt egy törlési loopba. Megoldás: a usages feature használata a függőségek deklarálásához.

apiVersion: apiextensions.crossplane.io/v1alpha1
kind: Usage
metadata:
  name: rds-uses-sg
spec:
  of:
    apiVersion: ec2.aws.upbound.io/v1beta1
    kind: SecurityGroup
    resourceRef:
      name: my-sg
  by:
    apiVersion: rds.aws.upbound.io/v1beta2
    kind: Instance
    resourceRef:
      name: my-rds

3. probléma: Provider verziófrissítések CRD-ket törhetnek. Amikor a provider-aws-rds-t v1.14-ről v1.16-ra frissítettem, két mező nevet változtatott. Az összes létező managed resource “field not found” hibákat kezdett dobálni. Azóta mindig staging klaszterben tesztelem a provider frissítéseket, és éles környezetben pontos verziókat pinnelek.

Költségkontroll

A pólóméret modell remek guardrail. Senki sem tud véletlenül egy db.r6g.4xlarge-ot felhúzni, mert nincs benne az enumban. De azért raktunk egy Kyverno policyt is második rétegként:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: limit-postgres-size
spec:
  validatingAdmissionPolicy: false
  rules:
    - name: max-size-per-namespace
      match:
        any:
          - resources:
              kinds:
                - PostgresInstance
      validate:
        message: "Nem-production namespace-ek csak 'small' vagy 'medium' méreteket használhatnak"
        deny:
          conditions:
            all:
              - key: "{{request.object.spec.size}}"
                operator: Equals
                value: "large"
              - key: "{{request.namespace}}"
                operator: AnyNotIn
                value:
                  - prod-*

Megérte?

Három hónap után: heti 15+ infra kérésből lett talán 2 (edge case-ek, ahol valaki a standard méreteken kívül esik). A fejlesztők boldogabbak, mert nem kell várniuk. Én boldogabb vagyok, mert a platformra tudok koncentrálni, ahelyett hogy élő Terraform futtatógép lennék.

A setup nagyjából két hét valódi munkába került. Ennek nagy része az IRSA rendberakása és a Compositionök tesztelése különböző hibaforgatókönyvek ellen. Ha már futtatod a Kubernetest és GitOps-ot használsz, a Crossplane hozzáadása természetes következő lépés.

Egy tanács: kezd egy resource típussal. Csináld meg jól a Compositiont, dokumentáld le, hagyd, hogy a csapatok használják egy hónapig. Aztán bővíts. Az első napon teljes önkiszolgáló katalógust építeni recept a félig kész absztrakciókra, amikben senki nem bízik.