Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Crossplane #2 - Configuration

이 글은 Crossplane #1 - 소개에서 이어진 글이다.

컴포짓 리소스

Crossplane에는 컴포짓 리소스라고 프로바이더의 CRD를 조합해서 새로운 리소스를 만들어주는 기능을 제공하는데 이를 컴포짓 리소스(XR)이라고 한다. 참고로 Crossplane에서는 Composite Resource를 XR이라고 줄여 부르고 Crossplane Resource Model(XRM)이라고 부르는데 이미 Kubernetes에 CRD(Custom Resource Definition)이 있으므로 겹치는걸 피하고자 X를 사용한 것이다.

  • Composition: 리소스를 구성해서 고수준의 컴포짓 리소스로 구성하는 방법을 정하는 핵심 Crossplane API 타입이다. 즉, Composition은 누군가 컴포짓 리소스 X를 생성하면 리소스 Y와 Z를 생성해야 한다고 Crossplane에게 알려준다.
  • 컴포짓 리소스(Composite Resource, XR): Crossplane을 사용해서 정의한 API 타입으로 작성자의 의도에 따라 임의의 이름이 될 수 있지만, 보통은 앞에 Composite를 접두사로 붙여준다.
  • 컴포짓 리소스 클레임(Composite Resource Claim, XRC): 각 클레임의 타입은 컴포짓 리소스의 타입에 대응된다.
  • 컴포짓 리소스 데피니션(Composite Resource Definition, XRD): 컴포짓 리소스와 클레임의 새로운 타입을 정의한다.
  • 패키지: 컴포짓 리소스와 클레임의 새로운 타입을 지원해서 Crossplane을 확장한다. Crossplane에는 두 가지 타입의 패키지가 있는데 프로바이더와 컨피그레이션이다.
  • 컨피그레이션(Configuration) : XRD와 Composition의 기념적인 그룹을 설치해서 Crossplane을 확장한다.

Crossplane 문서에서 가져온 그림을 살펴보자.

Crossplane의 composition 컨셉

여기 보면 CompositeMySQLIntance라는 컴포짓 리소스가 정의되어 있고 여기서는 각 클라우드 프로바이더의 Composition을 사용하고 있고 이 컴포짓 리소스에는 RDSInstanceDBSubnetGroup 리소스가 포함되어 있다. 이 리소스는 클라우드 범위내에 있으니 특정 네임스페이스에 속한 리소스가 아니라는 의미이다.

네임스페이스 영역을 보면 MySQLIntance가 컴포짓 리소스 클레임이다. 애플리케이션에서 MySQL이 필요해서 이 클레임을 통해서 요청을 보내면 CompositeMySQLIntance 컴포짓 리소스가 MySQL을 구성해서 돌려주면 연결 시크릿을 사용해서 애플리케이션이 데이터베이스에 접속할 수 있게 된다.

이 관계는 PV(PersistentVolume)와 PVC(PersistentVolumeClaim)와 비슷한 관계로 이해하면 된다. 리소스를 내부 정책에 따라 구성해서 논리적인 리소스 단위를 만들어서 컴포짓 리소스로 등록해 두면 각 서비스에서 필요한 리소스를 클레임으로 요청해서 만들어진 리소스를 사용하게 되는 것이다. Crossplane의 설명에 따르면 VPC 처럼 네임스페이스에 한정되지 않은 AWS의 권한을 서비스 팀에 주지 않고 클레임만 제공해서 리소스를 구성해 줄 수 있다고 얘기하고 있다.

CompositePostgreSQLInstance 예제

컨피그레이션을 만들려면 다음 3개의 파일이 필요하다. 솔직히 XR, XRD과 Composition을 패키지해서 컨피그레이션을 만드는 등 너무 다양한 용어가 나와서 아직 좀 헷갈린다.

  • crossplane.yaml: 컨피그레이션에 대한 메타데이터
  • definition.yaml: XRD
  • composition.yaml: Composition

꼭 하나의 XRD와 Compositon일 필요는 없고 여러 XRD와 Composition 조합도 가능하다. 테스트 목적으로 crossplane.yaml 없이 XRD와 Composition을 클러스터에 직접 생성할 수도 있지만 패키징해서 배포하려면 crossplane.yaml 파일이 필요하다.

일단 postgres-config 폴더를 만든다.

$ mkdir postgres-config

CompositeResourceDefinition

definition.yaml 파일에 CompositeResourceDefinition를 정의한다.

# definition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: compositepostgresqlinstances.database.example.org
spec:
  group: database.example.org
  names:
    kind: CompositePostgreSQLInstance
    plural: compositepostgresqlinstances
  claimNames:
    kind: PostgreSQLInstance
    plural: postgresqlinstances
  connectionSecretKeys:
    - username
    - password
    - endpoint
    - port
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              parameters:
                type: object
                properties:
                  storageGB:
                    type: integer
                required:
                  - storageGB
            required:
              - parameters

여기서 kindPostgreSQLInstance가 되고 API 타입은 compositepostgresqlinstances.database.example.org로 임의로 지정했다. 자세히 보면 CRD와 형태가 똑같은 것을 볼 수 있는데 spec에서 데이터베이스 스토리지 용량인 storageGB를 받도록 정의했다.

Composition

composition.yaml 파일에 Composition을 정의한다.

# composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: compositepostgresqlinstances.aws.database.example.org
  labels:
    provider: aws
    vpc: playground
spec:
  writeConnectionSecretsToNamespace: crossplane-system
  compositeTypeRef:
    apiVersion: database.example.org/v1alpha1
    kind: CompositePostgreSQLInstance
  resources:
    - name: rdsinstance
      base:
        apiVersion: database.aws.crossplane.io/v1beta1
        kind: RDSInstance
        spec:
          forProvider:
            region: ap-northeast-2
            dbInstanceClass: db.t2.small
            masterUsername: masteruser
            engine: postgres
            engineVersion: "12"
            skipFinalSnapshotBeforeDeletion: true
            publiclyAccessible: true
            VPCSecurityGroupIDRefs:
              - name: vpc-abdce1234
            VPCSecurityGroupIDs:
              - sg-01234abcd
          writeConnectionSecretToRef:
            namespace: crossplane-system
      patches:
        - fromFieldPath: "metadata.uid"
          toFieldPath: "spec.writeConnectionSecretToRef.name"
          transforms:
            - type: string
              string:
                fmt: "%s-postgresql"
        - fromFieldPath: "spec.parameters.storageGB"
          toFieldPath: "spec.forProvider.allocatedStorage"
      connectionDetails:
        - fromConnectionSecretKey: username
        - fromConnectionSecretKey: password
        - fromConnectionSecretKey: endpoint
        - fromConnectionSecretKey: port

컴포짓 리소스의 생성 요청이 왔을 때 어떤 리소스를 구성할지 여기서 정의한다. resources.base 부분을 보면 앞에서 작성했던 프로바이더의 CRD의 작성과 같은 걸 알 수 있다. 여러 리소스를 만들 수 있지만 여기서는 RDSInstance을 사용해서 엔진 버전과 VPC, 보안 그룹 등을 지정해서 RDS를 띄우게 했다. VPC 등은 원하는 값으로 수정해야 한다. patches에서는 클레임의 파라미터로 받을 storageGBspec.forProvider.allocatedStorage의 값으로 지정하고 uid를 변환해서 name으로 사용하게 설정했다.

메타데이터

패키징하기 위해 crossplane.yamlConfiguration을 작성하고 ownPostgres라는 이름을 지정했다.

# crossplane.yaml
apiVersion: meta.pkg.crossplane.io/v1alpha1
kind: Configuration
metadata:
  name: ownPostgres
  annotations:
    provider: aws
    vpc: playground
spec:
  crossplane:
    version: ">=v1.0.0-0"
  dependsOn:
    - provider: crossplane/provider-aws
      version: ">=v0.14.0"

여기서 vpc: playground는 사용할 VPC의 이름이다.

Configuration 패키지 설치

이 Configuration을 빌드한 후에 OCI 이미지로 배포해서 클러스터에 설치할 수 있다. 테스트 목적으로 배포할 수도 있지만, 패키지로 설치하려면 OCI 이미지로 배포해야 한다.

위 파일이 있는 디렉터리에서 crossplane CLI를 이용해서 컨피그레이션을 빌드한다.

$ kubectl crossplane build configuration

빌드하면 해당 폴더에 ownostgres-a2843c6b92f7.xpkg 같은 파일이 생성된다.

OCI 이미지를 배포하기 위해 Docker Hub에 레파지토리를 만들었다고 하고 docker에 로그인한다.

$ docker login
Authenticating with existing credentials...
Login Succeeded

이제 컨피그레이션을 Docker Hub에 푸시한다.

$ kubectl crossplane push configuration outsideris/crossplane-test:v0.5

Docker Hub에 잘 배포된 걸 확인할 수 있다.

Docker Hub에 배포된 이미지

$ kubectl crossplane install configuration outsideris/crossplane-test:v0.5
configuration.pkg.crossplane.io/outsideris-crossplane-test created

확인해 보면 잘 설치된 걸 확인할 수 있다.

$ kubectl get pkg
NAME                                      INSTALLED   HEALTHY   PACKAGE                           AGE
provider.pkg.crossplane.io/provider-aws   True        True      crossplane/provider-aws:v0.19.0   25h

NAME                                                         INSTALLED   HEALTHY   PACKAGE                           AGE
configuration.pkg.crossplane.io/outsideris-crossplane-test   True        True      outsideris/crossplane-test:v0.5   31s


PostgreSQLInstance 클레임

인프라팀이 RDS에 PostgreSQL을 생성할 수 있는 컨피그레이션을 만들어 주었으니 이제 각 서비스 쪽에서 PostgreSQL을 생성해서 사용해야 한다. 앞에서 설명한 대로 이는 컴포짓 리소스 클레임(XRC)을 이용한다.

# postgres-claim.yaml
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
  name: my-db
  namespace: app-demo
spec:
  parameters:
    storageGB: 20
  compositionSelector:
    matchLabels:
      provider: aws
      vpc: playground
  writeConnectionSecretToRef:
    name: db-conn

앞에서 작성한 CompositeResourceDefinition에 대응되는 PostgreSQLInstance 클레임이다.

$ kubectl -n app-demo apply -f postgres-claim.yaml
postgresqlinstance.database.example.org/my-db created

이를 적용하고 나면 상태에서 해당 클레임의 리소스가 생성 중인 것을 볼 수 있다.

$ kubectl get crossplane -l crossplane.io/claim-name=my-db
NAME                                                       READY   SYNCED   STATE      ENGINE     VERSION   AGE
rdsinstance.database.aws.crossplane.io/my-db-msps6-g4gfs   False   True     creating   postgres   12.7      35s

AWS 웹 콘솔에서도 생성 중인 것을 볼 수 있다.

AWS 웹 콘솔에서 생성된 PostgreSQL RDS

RDS라 준비되는데 시간이 꽤 걸리는데 준비가 끝나면 상태가 available이 된걸 볼 수 있다.

$ kubectl -n app-demo get crossplane -l crossplane.io/claim-name=my-db
NAME                                                       READY   SYNCED   STATE       ENGINE     VERSION   AGE
rdsinstance.database.aws.crossplane.io/my-db-msps6-g4gfs   True    True     available   postgres   12.7      13m

관련해서 클레임과 이 클레임과 연결된 composite, 매니지드 리소스를 확인해 볼 수 있다.

$ kubectl -n app-demo get claim
NAME    READY   CONNECTION-SECRET   AGE
my-db   True    db-conn             14m

$ kubectl -n app-demo get composite
NAME          READY   COMPOSITION                                             AGE
my-db-msps6   True    compositepostgresqlinstances.aws.database.example.org   14m

$ kubectl -n app-demo get managed
NAME                                                          READY   SYNCED   AGE
bucket.s3.aws.crossplane.io/outsider-crossplane-demo          True    True     165m
bucket.s3.aws.crossplane.io/outsider-crossplane-import-test           True     126m

NAME                                                       READY   SYNCED   STATE       ENGINE     VERSION   AGE
rdsinstance.database.aws.crossplane.io/my-db-msps6-g4gfs   True    True     available   postgres   12.7      14m

생성된 db-conn 시크릿을 확인하면 데이터베이스에 연결할 수 있는 정보가 저장된 걸 알 수 있다.

$ kubectl -n app-demo describe secret db-conn
Name:         db-conn
Namespace:    app-demo
Labels:       <none>
Annotations:  <none>

Type:  connection.crossplane.io/v1alpha1

Data
====
endpoint:  63 bytes
password:  27 bytes
port:      4 bytes
username:  10 bytes

서비스용 Pod에서는 이 시크릿을 연결해서 생성한 RDS에 접근할 수 있게 된다.

Crossplane 삭제

테스트가 끝났으면 crossplane을 제거해야 한다.

app-demo 같은 네임스페이스에 설치한 claim, composite, managed를 제거하고 설치한 리소스를 제거해야 한다. Helm 차트를 지워도 CRD는 제거해 주지 않으므로 관련 CRD는 따로 지워주어야 한다.(마지막 줄)

$ kubectl delete configuration.pkg outsideris-crossplane-test
$ kubectl delete provider.pkg crossplane-provider-aws
$ helm delete crossplane --namespace crossplane-system
$ kubectl delete namespace crossplane-system
$ kubectl get crd -o name | grep crossplane.io | xargs kubectl delete


Terraform과의 비교

아무래도 Infrastructure as Code에서 Terraform이 가장 많이 쓰이다 보니 Crossplane에서도 Crossplane vs Terraform라는 비교 글을 올린 게 있어서 읽어봤다.

  • Crossplane은 컨트롤 플레인이이지만 Terraform은 컨트롤 플레인의 인터페이스인 CLI 도구이다.
  • Terraform은 모노리식 apply 프로세스를 사용해서(한 번에 한 명만 apply 가능하다는 의미) 관리하는 인프라가 많아질수록 폴더를 세분화하길 권장하고 이때 상태 파일도 분리되는데 이렇게 되면 구성이 점점 복잡하게 된다. 반면 Crossplane은 XRM(Crossplane Resource Model)을 이용한 느슨한 결합과 eventual consistency를 사용하므로 협업을 쉽게 할 수 있고 전체 프로덕션 환경을 단일 데이터베이스로 운영할 수 있다.
  • Terraform은 모듈로 인프라를 추상화해서 제공하지만, 이는 개발팀을 좁은 범위의 운영팀처럼 대우하므로 개발팀도 Terraform과 HCL을 배워야 하며 엑세스 제어 수준은 높이지 못하고 구성의 추상화 수준만 높였다. Terraform의 모듈에 대응되는 것이 Crossplane에서는 XR인데 XR은 API로 노출되고 RBAC으로 권한 제어를 할 수 있다.
  • Terraform은 CLI로 실행할 때만 인프라를 조정하는 보수적인 온디멘드 접근을 하므로 교착상태가 만들어지지만 Crossplane은 항상 동작하는 제어 루프로 인프라를 원하는 구성과 맞추기 때문에 다른 사람이 Crossplane을 우회해서 인프라를 조정하지 못하게 한다.
  • Terraform은 API를 제공하지 않는다.

소감

Crossplane을 꽤 흥미로운 접근을 하고 있어서 많은 생각이 들었다. 당장 데이터베이스나 캐시처럼 데이터를 많이 가진 부분은 좀 걱정되지만 간단한 부분을 써볼 만할 것 같다. ...

Terraform을 무척 좋아하지만, 인프라가 복잡해졌을 때나 Terraform을 잘 사용하기 위해서 꽤 큰 노력(+힘듦)이 든다는 점도 이해하고 있다. 제일 피곤한 부분 중 하나는 웹 콘솔에서 바로 수정하는 등의 다양한 이유로 상태와 코드가 달라지는 경우가 많이 생기는데 이런 부분을 매번 해결하기 어렵다는 부분이다. 그래서 상태와 코드를 계속 감시해 주는 무언가가 필요하지 않을까? 같은 생각을 많이 하여서 Crossplane이 상태를 계속 맞춰주는 부분(문제 생길까 봐 무섭기도 하지만...)이 맘에 들었다.

Kubernetes를 많이 쓰면 많은 부분을 Kubernetes를 이용하는 것은 자연스러운 접근인 것 같다. Kubernetes가 오케스트레이션을 알아서 해주기 때문에 편하기도 하지만 Kubernetes를 쓰면서 많은 부분을 자동화하게 되었다면 배포나 운영, 모니터링 등 이미 구축해 놓은 시스템에 변경을 가하지 않고도 그대로 쓸 수 있다는 점도 큰 장점이라고 생각한다.

Terraform의 경우 많은 조직이 시행착오 끝에 어떻게 관리하면 좋은지에 대한 노하우가 많이 공유되어 있지만 Crossplane에는 아직 그런 커뮤니티가 없어서 사용하려면 직접 시행착오를 겪고 관리 방법을 고민해봐야 할 것 같다. 그래도 서비스에서 필요한 리소스를 같은 네임스페이스 관리하고 사용하는 점도 괜찮은 접근 같다.

2021/08/28 19:16 2021/08/28 19:16