Crossplane의 Managed Resource만으로 운영하면 앱 팀이 VPC, 서브넷, 보안 그룹, RDS를 각각 선언해야 한다. 인프라 세부사항을 알아야 하고, 잘못 설정할 여지가 많다. Composition은 이 복잡성을 인프라 팀이 흡수하고 앱 팀에게 단순한 인터페이스를 제공하는 메커니즘이다.

“PostgreSQL 데이터베이스 하나 주세요. 스몰 사이즈로.”

이 한 줄 요청이 VPC 피어링, 서브넷 선택, 보안 그룹, 파라미터 그룹, RDS 인스턴스, 백업 설정을 자동으로 처리하게 만드는 것이 Composition의 목적이다.

3계층 구조

XRD (CompositeResourceDefinition)
  ← 인프라 팀이 "어떤 리소스 타입을 제공할지" 정의

XR (CompositeResource)
  ← Composition으로 실제 리소스들이 생성되는 중간 오브젝트

Claim
  ← 앱 팀이 네임스페이스에서 "이 타입의 리소스를 요청"

XRD — 인터페이스 정의

어떤 커스텀 리소스 타입을 제공할지 정의한다. Claim이 가질 수 있는 파라미터(spec)와 연결 정보(connectionSecretKeys)를 선언한다.

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xpostgresqlinstances.platform.example.com
spec:
  group: platform.example.com
  names:
    kind: XPostgreSQLInstance
    plural: xpostgresqlinstances
  claimNames:                          # 앱 팀이 쓰는 Claim 타입 이름
    kind: PostgreSQLInstance
    plural: postgresqlinstances
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              parameters:
                type: object
                properties:
                  storageGB:
                    type: integer
                    default: 20
                  size:
                    type: string
                    enum: ["small", "medium", "large"]
                    default: small
                  region:
                    type: string
                    default: ap-northeast-2
  connectionSecretKeys:
  - host
  - port
  - username
  - password
  - database

Composition — 어떻게 만들지 정의

XRD로 정의한 타입을 실제로 어떤 Managed Resource 조합으로 만들지 구현한다.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xpostgresqlinstances.aws.platform.example.com
spec:
  compositeTypeRef:
    apiVersion: platform.example.com/v1alpha1
    kind: XPostgreSQLInstance
  resources:
  - name: rdsinstance
    base:
      apiVersion: rds.aws.upbound.io/v1beta1
      kind: Instance
      spec:
        forProvider:
          region: ap-northeast-2
          engine: postgres
          engineVersion: "15.4"
          skipFinalSnapshot: true
          publiclyAccessible: false
          multiAz: false
          dbName: main
          username: admin
        writeConnectionSecretToRef:
          namespace: crossplane-system
    patches:
    - type: FromCompositeFieldPath
      fromFieldPath: spec.parameters.storageGB
      toFieldPath: spec.forProvider.allocatedStorage
    - type: FromCompositeFieldPath
      fromFieldPath: spec.parameters.size
      toFieldPath: spec.forProvider.instanceClass
      transforms:
      - type: map
        map:
          small:  db.t3.micro
          medium: db.t3.medium
          large:  db.r6g.large
    - type: FromCompositeFieldPath
      fromFieldPath: spec.parameters.region
      toFieldPath: spec.forProvider.region
  - name: rds-subnet-group
    base:
      apiVersion: rds.aws.upbound.io/v1beta1
      kind: SubnetGroup
      spec:
        forProvider:
          region: ap-northeast-2
          subnetIdRefs:
          - name: private-subnet-a
          - name: private-subnet-b

patches가 핵심이다. Claim의 spec.parameters.size: "small"spec.forProvider.instanceClass: "db.t3.micro"로 변환된다. 앱 팀은 인스턴스 타입 이름을 몰라도 된다.

Claim — 앱 팀의 요청

앱 팀이 자신의 네임스페이스에서 Claim을 만든다.

apiVersion: platform.example.com/v1alpha1
kind: PostgreSQLInstance
metadata:
  name: order-service-db
  namespace: order-service
spec:
  parameters:
    size: small
    storageGB: 50
  writeConnectionSecretToRef:
    name: db-credentials    # 연결 정보를 이 Secret에 저장

이게 전부다. VPC, 서브넷, 보안 그룹, 파라미터 그룹을 몰라도 된다. 인프라 팀이 정의한 small 규격대로 모든 것이 만들어진다.

kubectl get postgresqlinstances -n order-service
# NAME               READY   SYNCED   CONNECTION-SECRET   AGE
# order-service-db   True    True     db-credentials      5m

연결 정보가 db-credentials Secret에 자동으로 저장된다.

# 앱 Deployment에서 Secret 마운트
env:
- name: DB_HOST
  valueFrom:
    secretKeyRef:
      name: db-credentials
      key: host
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: db-credentials
      key: password

Platform Engineering

이 구조가 Platform Engineering의 핵심 패턴이다. 인프라 팀이 플랫폼(Composition, XRD)을 만들고, 앱 팀이 셀프서비스로 인프라를 프로비저닝한다. 인프라 팀의 보안/비용 정책이 Composition 안에 인코딩돼 있어, 앱 팀이 실수로 정책을 위반하기 어렵다.

트레이드오프

Composition 작성이 복잡하다. patches, transforms, patchSets 문법이 직관적이지 않고, 중첩 구조를 참조하는 FromCompositeFieldPath 표현식이 길어진다. 인프라 팀에서 이 YAML을 작성하고 유지하는 비용이 상당하다.

Composition이 여러 Managed Resource를 만들 때 일부가 실패하면 전체 롤백이 안 된다. 성공한 것들은 남아 있고, 실패한 것만 재시도된다. 인프라 부분 생성 상태가 생길 수 있어 정리가 까다롭다.

Upbound(Crossplane 메인 컨트리뷰터)의 Upbound Marketplace에서 검증된 Composition 예시를 볼 수 있고, 직접 작성 전에 참고하면 시간을 절약할 수 있다.