Outsider's Dev Story

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

Crossplane #1 - 소개

연초부터 그룹스터디도 하고 하면서 Kubernetes도 공부하지만 오퍼레이터도 계속 보고 있는데 Kuberentes를 이미 쓰고 있는 상황에서 Kubernetes의 오케스트레이션을 이용하는 접근에 대해서 계속 고민하게 되는 것 같다. 아직은 잘 몰라서 오퍼레이터를 열심히 쓰고 있거나 하진 않은데 오퍼레이터를 써서 뭔가를 관리할 때와 외부에서 관리할 때의 장단점을 고민해 볼 일이 많아졌다.

작년에 나온 AWS Controllers for Kubernetes(이하 ACK)도 이를 사용해서 애플리케이션에 필요한 AWS 리소스를 Kubernetes에서 관리할 수 있을까 고민해 봤다. 물론 ACK를 막상 살펴보니 내가 필요한 리소스는 아직 개발되지 않았고 프로젝트도 꽤 초기 상태로 보여서 사용하지는 않았다.

그러고 있다가 어느 날 Crossplane이라는 프로젝트를 알게 되었고 이 프로젝트가 내가 궁금해하던 것과 비슷한 접근을 한다는 것을 알게 되었다. 프로젝트 이름은 몇 번 본 적 있지만, 원래는 뭐 하는 프로젝트인지도 잘 몰랐다.

Crossplane

Crossplane은 2018은 upbound 회사에서 만든 프로젝트고 2020년 7월 CNCFSandbox 프로젝트가 되었다. 현재 Crossplne을 Incubating 프로젝트로 올리는 투표가 진행 중가 진행 중이므로 곧 Incubating 프로젝트가 될지도 모르겠다. CNCF 졸업 프로젝트이면서 Kubernetes용 스토리지인 Rook을 만든 팀이 Crossplane도 만들었다고 한다.

현재 GitHub 저장소의 스타 수가 3,700개로 높은 편은 아니라고 생각한다. Crossplane을 테스트하려고 자료를 찾아볼 때도 홈페이지의 내용 외에는 별로 찾아볼 수가 없어서 아직 사용자가 많지는 않아 보였다. Go 언어로 작성되었고 현재 1.3.1이 최신 버전이다.

Crossplane 홈페이지

Crossplane은 애플리케이션에서 필요한 인프라스트럭처를 Kubernetes에서 직접 관리할 수 있게 해주는 프로젝트이다.

Kubernetes를 확장해서 사용하므로 Kubernetes 생태계를 그대로 이용할 수 있으면 인프라를 관리할 방법을 제공한다.

Crossplane 설치

말로는 잘 이해가 안 될 수 있으니 설치를 해보자. 홈페이지에도 시작하기 문서가 있지만 바로 패키지를 이용해서 안내하고 있어서 구조를 잘 이해하기 어려워서 내 나름대로 이해하기 쉽게 살펴봤다.

먼저 Crossplane을 사용할 Kubernetes 클러스터가 필요하다. 나 같은 경우는 테스트용 AWS EKS에 Kubernetes 1.21 클러스터를 사용했다. 이 클러스터에 Crossplane을 설치한 crossplane-system 네임스페이스를 생성한다.

$ kubectl create namespace crossplane-system
namespace/crossplane-system created

Crossplane이 Helm으로 배포되고 있으므로 이를 이용해서 쉽게 설치할 수 있다. Helm 차트를 가져온다.

$ helm repo add crossplane-stable https://charts.crossplane.io/stable
"crossplane-stable" has been added to your repositories

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "crossplane-stable" chart repository
...Successfully got an update from the "gocd" chart repository
Update Complete. ⎈Happy Helming!⎈

이 차트를 이용해서 crossplane을 설치한다. 최신 버전인 1.3.1을 사용했다.

$ helm install crossplane --namespace crossplane-system crossplane-stable/crossplane --version 1.3.1
NAME: crossplane
LAST DEPLOYED: Fri Aug 27 16:24:21 2021
NAMESPACE: crossplane-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Release: crossplane

Chart Name: crossplane
Chart Description: Crossplane is an open source Kubernetes add-on that enables platform teams to assemble infrastructure from multiple vendors, and expose higher level self-service APIs for application teams to consume.
Chart Version: 1.3.1
Chart Application Version: 1.3.1

Kube Version: v1.21.2-eks-0389ca3

helm list로 잘 설치되었는지 확인할 수 있다.

$ helm list -n crossplane-system
NAME        NAMESPACE         REVISION  UPDATED                               STATUS    CHART             APP VERSION
crossplane  crossplane-system 1         2021-08-27 16:24:21.765252 +0900 KST  deployed  crossplane-1.3.1  1.3.1

이 Helm 차트가 어떤 리소스를 설치했는지 확인해 보자. 크게 crossplanecrossplane-rbac-manager Deployment가 배포된 것을 볼 수 있다.

$ kubectl -n crossplane-system get all
NAME                                           READY   STATUS            RESTARTS   AGE
pod/crossplane-755d979f9-kq8hn                 0/1     PodInitializing   0          13s
pod/crossplane-rbac-manager-79754c5467-bb2rb   1/1     Running           0          13s

NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/crossplane                1/1     1            1           13s
deployment.apps/crossplane-rbac-manager   1/1     1            1           13s

NAME                                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/crossplane-755d979f9                 1         1         1       13s
replicaset.apps/crossplane-rbac-manager-79754c5467   1         1         1       13s

로컬에서 crossplane을 이용할 수 있도록 crossplane CLI를 설치한다.

$ $ curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh
kubectl plugin downloaded successfully! Run the following commands to finish installing it:

sudo mv kubectl-crossplane /Users/outsider/bin
kubectl crossplane --help

Visit https://crossplane.io to get started. 
Have a nice day! 

설치가 완료되면 kubectl에서 kubectl crossplane 명령어를 사용할 수 있다.

$ kubectl crossplane --help
Usage: kubectl crossplane <command>

A command line tool for interacting with Crossplane.

Flags:
  -h, --help       Show context-sensitive help.
  -v, --version    Print version and quit.
      --verbose    Print verbose logging statements.

Commands:
  build configuration
    Build a Configuration package.

  build provider
    Build a Provider package.

  install configuration <package> [<name>]
    Install a Configuration package.

  install provider <package> [<name>]
    Install a Provider package.

  update configuration <name> <tag>
    Update a Configuration package.

  update provider <name> <tag>
    Update a Provider package.

  push configuration <tag>
    Push a Configuration package.

  push provider <tag>
    Push a Provider package.

Run "kubectl crossplane <command> --help" for more information on a command.


프로바이더

인프라를 관리해야 하므로 Terraform처럼 프로바이더가 존재한다. 사용할 인프라의 프로바이더를 설치해서 해당 프로바이더의 리소스를 관리할 수 있는 구조로 다음과 같은 프로바이더가 존재하고 해당 클라우드의 기능은 프로바이더에서 제공한다고 생각하면 된다.

AWS 프로바이더 설치

여기선 AWS를 사용할 것이므로 AWS 프로바이더를 설치한다. 현재 최신 버전인 v0.19.0을 설치했다.

# provider.yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws
spec:
  package: "crossplane/provider-aws:v0.19.0"

이 파일을 적용한다.

$ kubectl -n crossplane-system apply -f provider.yaml
provider.pkg.crossplane.io/provider-aws created

설치한 pkg를 확인해 보면 INSTALLED, HEALTHY가 정상인 걸 볼 수 있다.

$ kubectl -n crossplane-system get pkg
NAME                                      INSTALLED   HEALTHY   PACKAGE                           AGE
provider.pkg.crossplane.io/provider-aws   True        True      crossplane/provider-aws:v0.19.0   18s


프로바이더 구성

AWS 프로바이더를 설정했으니 여기에 ProviderConfig를 생성해야 한다. 여러 계정의 인프라를 사용할 수도 있고 목적에 따라 권한 등을 나눌 수도 있으니 예를 들어 alpha/production 같은 ProviderConfig를 다수 만들어서 원하는 구성을 이용할 수 있다.

ProviderConfig에서 구성할 AWS 엑세스 키와 시크릿 키를 creds.conf라는 파일로 저장한다.

$ AWS_PROFILE=default && echo -e "[default]\naws_access_key_id = $(aws configure get aws_access_key_id --profile $AWS_PROFILE)\naws_secret_access_key = $(aws configure get aws_secret_access_key --profile $AWS_PROFILE)" > creds.conf

creds.conf 파일을 사용해서 시크릿을 만든다.

$ kubectl -n crossplane-system create secret generic aws-creds --from-file=creds=./creds.conf
secret/aws-creds created

ProviderConfig는 다음과 같이 작성한다. 여기에는 구성의 이름(default라는 이름으로 만들었다)을 지정하고 방금 만든 aws-creds라는 시크릿을 참조하도록 구성한다.

# providerconfig.yaml
apiVersion: aws.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: creds

이 파일을 적용한다.

$ kubectl -n crossplane-system apply -f providerconfig.yaml
providerconfig.aws.crossplane.io/default created

프로바이더가 설정된 것을 볼 수 있다.

$ kubectl -n crossplane-system get provider
NAME                                       AGE
providerconfig.aws.crossplane.io/default   12

AWS 프로바이더에는 AWS 리소스를 사용할 수 있도록 CRD(CustomResourceDefinition)가 포함되어 있어서 프로바이더를 설치할 때 CRD도 같이 설치된다.

$ kubectl -n crossplane-system get crd
NAME                                                       CREATED AT
activities.sfn.aws.crossplane.io                           2021-08-27T07:26:18Z
addresses.ec2.aws.crossplane.io                            2021-08-27T07:26:21Z
apimappings.apigatewayv2.aws.crossplane.io                 2021-08-27T07:26:22Z
... 중략 ...
subnets.ec2.aws.crossplane.io                              2021-08-27T07:26:20Z
tables.dynamodb.aws.crossplane.io                          2021-08-27T07:26:20Z
vpccidrblocks.ec2.aws.crossplane.io                        2021-08-27T07:26:17Z
vpclinks.apigatewayv2.aws.crossplane.io                    2021-08-27T07:26:18Z
vpcs.ec2.aws.crossplane.io                                 2021-08-27T07:26:21Z

기본으로 띄워놓은 테스트용 EKS라서 권한 권리는 따로 안 하고 있는데 프로바이더 구성에 AWS 시크릿을 제공했음에도 AWS 인프라를 관리할 때 권한이 필요했다.

노드 role에 추가한 IAM 권한

나 같은 경우는 EKS의 워커 노드용 EC2에 부여된 IAM 역할에 AmazonS3FullAccess, IAMFullAccess, AmazonRDSFullAccess`를 부여해서 권한 문제를 해결했다. 실제로 사용할 때는 권한 제어도 테스트해봐야겠지만 여기서는 Crossplane의 기능을 이해하는 게 목적이라서 여기에 권한을 최대한 주고 테스트했다.

매니지드 리소스

Crossplane에서 매니지드 리소스는 Kubernetes 밖의 프로바이더 리소스를 의미하고 위에서 얘기한 CRD를 사용해서 리소스를 생성하면 매니지드 리소스가 된다. 프로바이더 API 문서를 참고하면 이 CRD의 필드의 사용 방법을 알 수 있다.(문서가 아직 친절하진 않다.)

AWS 프로바이더 문서

프로바이더의 CRD는 비슷하게 생겼는데 spec에는 다음 4개의 필드가 있다.

  • forProvider: 이 필드는 프로바이더에 제공되는 필드다. 여기에 포함된 필더는 프로바이더 즉, AWS가 리소스를 만들 때 이 필드의 값을 사용한다.
  • writeConnectionSecretToRef: 생성한 매니지드 리소스에 관한 연결 정보(엔드포인트나, 이름 등)를 저장할 시크릿을 저장한다. 생성한 리소스의 정보를 시크릿에 저장하고 서비스에서 사용할 Pod에서 이 시크릿을 연결해서 정보를 사용하는 구조이다.
  • providerConfigRef: 어떤 ProviderConfig를 사용할지 지정하고 생략할 시 default 구성을 사용한다.
  • deletionPolicy: 매니지드 리소스를 삭제했을 때 실제 클라우드의 리소스를 어떻게 할지를 정한다. Delete로 지정하면 매니지드 리소스를 지울 때 클라우드의 리소스도 지우고 Orphan으로 지정하면 클라우드 리소스는 놔두고 Kubernetes 안에서만 지운다.

S3 Bucket 생성하기

실제로 이 CRD를 직접 사용해보자. crossplane-system은 인프라팀에서 관리하는 네임스페이스이므로 서비스에서의 사용을 가정하고 app-demo 네임스페이스를 만들어 보자.

$ kubectl create namespace app-demo
namespace/app-demo created

프로바이더에서 제공한 Bucket CRD를 사용해서 리소스를 선언한다. 자세한 사용 방법은 문서에서 볼 수 있다.

# s3.yaml
apiVersion: s3.aws.crossplane.io/v1beta1
kind: Bucket
metadata:
  name: outsider-crossplane-demo
  namespace: app-demo
spec:
  deletionPolicy: Delete
  forProvider:
    acl: private
    locationConstraint: ap-northeast-2
  providerConfigRef:
    name: default
  writeConnectionSecretToRef:
    name: s3-info
    namespace: app-demo

이 리소스를 지우면 S3 버킷도 삭제되도록 deletionPolicyDelete로 지정했고 forProvider에서 S3 버킷의 acllocationConstraint를 지정했다. 여기서 locationConstraint는 필수 필드이다. providerConfigRef에서 구성한 ProviderConfig를 참조하도록 했고 S3를 생성한 뒤의 연결 정보는 s3-info라는 시크릿에 작성하도록 했다.

$ kubectl -n app-demo apply -f s3-claim.yaml
bucket.s3.aws.crossplane.io/outsider-crossplane-demo created

리소스 적용이 끝난 뒤 AWS 콘솔에 가면 outsider-crossplane-demo라는 버킷이 생성된 것을 볼 수 있다.

생성된 S3 버킷

Crossplane이 얘기하는 매니지드 리소스 동작의 특징이 있다.

  • 지속적인 조정(continuous reconciliation): Kubernetes의 등록된 desired state를 실제 상태와 맞추기 위해서 지속해서 조정한다. 그래서 실제 인프라의 상태가 바뀌면 Crossplane이 이를 desired state로 되돌린다. 즉, 위 S3 버킷을 AWS 웹 콘솔에서 태그를 추가하거나 권한 등 다른 설정을 바꾸고 몇 분 뒤면 다시 Crossplane이 desired state로 맞추므로 변경된 내용이 복원된다. 그래서 spec에 지정한 내용이 인프라에 대한 유일한 source of truth이다.
  • 불변 속성(immutable properties): 인프라에는 프로바이더가 변경을 허락하지 않는 속성들이 있다. 예를 들어 어떤 서비스는 생성한 뒤 리전을 변경하는 것이 불가능하다. Terraform은 이런 속성을 바꾸기 위해 리소스를 제거한 뒤 다시 만들지만 Crossplane은 desired state는 Kubernetes 클러스터에 등록되겠지만 이를 프로바이더에 적용하다가 오류를 반환할 것이다. Crossplane에서 리소스가 지워지는 상황은 deletePolicyDelete일 때 매니지드 리소스를 지웠을 때뿐이다. 이렇게 동작하는 이유는 자동으로 조정이 일어나는 만큼 의도치 않게 리소스가 지워지는 경우를 막기 위한 정책일 거로 생각한다.
  • 지연 초기화(late initialization): 이 부분은 몇 번 테스트를 해봤지만, 정확히 기능을 파악하진 못했는데 문서대로라면 프로바이더의 CRD에서는 선택적인 필드이지만 이 속성의 기본값을 프로바이더가 선택하는 경우가 있다. 이럴 때 spec에 사용자가 지정하지 않았으므로 빈값으로 등록했다가 프로바이더가 기본값을 지정하면 이 값을 가져와서 state에 채워준다고 한다.
  • 삭제: 매니지드 리소스의 삭제 요청을 받으면 컨트롤러가 바로 프로바이더의 리소스도 삭제 정차를 시작했다. 클라우드의 인프라가 실제로 삭제되었다는 것을 컨트롤러가 확인한 후에 매니지드 리소스를 지우기 때문에 매니지드 리소스가 지워지면 클라우드의 리소스도 지워졌다는 것을 확신할 수 있다.

생성된 리소스를 확인해 보면 bucketsecret이 생성된 것을 알 수 있고 Bucket은 READY, SYNCED 상태가 정상인 걸 알 수 있다.

$ kubectl -n app-demo get all,secret,bucket
NAME                         TYPE                                  DATA   AGE
secret/default-token-dtmh5   kubernetes.io/service-account-token   3      48m
secret/s3-info               connection.crossplane.io/v1alpha1     2      28m

NAME                                                   READY   SYNCED   AGE
bucket.s3.aws.crossplane.io/outsider-crossplane-demo   True    True     28m

매지니드 리소스는 kubectl get managed로 한꺼번에 볼 수도 있다.

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

연결 정보가 보관된 시크릿을 살펴보면 endpointregion이 등록된 것을 볼 수 있다.

$ kubectl -n app-demo describe secrets s3-info
Name:         s3-info
Namespace:    app-demo
Labels:       <none>
Annotations:  <none>

Type:  connection.crossplane.io/v1alpha1

Data
====
endpoint:  24 bytes
region:    14 bytes

저장된 시크릿 값을 확인해 보면 버킷 이름과 리전이 등록된 것을 알 수 있다. 버킷이 간단하긴 하지만 ARN 등이 없는 건 아쉽다.

$ kubectl -n app-demo get secrets s3-info -o 'go-template={{index .data "endpoint"}}' | base64 -d
outsider-crossplane-demo

$ kubectl -n app-demo get secrets s3-info -o 'go-template={{index .data "region"}}' | base64 -d
ap-northeast-2


기존 S3 버킷 임포트

Crossplane을 사용하더라도 이미 프로비저닝해서 사용하고 있던 리소스가 있을 수 있고 이를 임포트해서 Crossplane의 관리하에 넣을 수 있다.

# s3-import.yaml
apiVersion: s3.aws.crossplane.io/v1beta1
kind: Bucket
metadata:
  name: outsider-crossplane-import-test
  namespace: app-demo
  annotations:
    crossplane.io/external-name: my-test-bucket
spec:
  deletionPolicy: Orphan
  forProvider:
    locationConstraint: ap-northeast-2
  providerConfigRef:
    name: default
  writeConnectionSecretToRef:
    name: s3-import-info
    namespace: app-demo

이 YAML처럼 crossplane.io/external-name 어노테이션으로 기존 버킷 이름을 지정하면 아까와 달리 outsider-crossplane-import-test라는 버킷을 생성하지 않고 기존 버킷을 임포트해서 가져온다. crossplane.io/external-name은 Kubernetes에서의 name과 실제 클라우드에서 표시되는 이름(예를 들어 웹 콘솔)을 다르게 하는 용도로도 사용되기 때문에 crossplane.io/external-name의 이름으로 된 리소스가 있으면 임포트하고 없으면 새로 생성하면서 이름을 ``crossplane.io/external-name`의 값으로 사용한다.

$ kubectl -n app-demo apply -f s3-import.yaml
bucket.s3.aws.crossplane.io/outsider-crossplane-import-test created

이 CRD를 적용한 뒤에 조회해 보면 ARN이 arn: arn:aws:s3:::my-test-bucket으로 등록된 것을 볼 수 있고 새 버킷이 만들어지는 대신 기존 버킷과 연결된 것을 알 수 있다.

$ kubectl -n app-demo get bucket.s3.aws.crossplane.io/outsider-crossplane-import-test -o yaml
apiVersion: s3.aws.crossplane.io/v1beta1
kind: Bucket
metadata:
  annotations:
    crossplane.io/external-name: my-test-bucket
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"s3.aws.crossplane.io/v1beta1","kind":"Bucket","metadata":{"annotations":{"crossplane.io/external-name":"my-test-bucket"},"name":"outsider-crossplane-import-test"},"spec":{"deletionPolicy":"Orphan","forProvider":{"locationConstraint":"ap-northeast-2"},"providerConfigRef":{"name":"default"},"writeConnectionSecretToRef":{"name":"s3-import-info","namespace":"app-demo"}}}
  creationTimestamp: "2021-08-28T06:47:15Z"
  finalizers:
  - finalizer.managedresource.crossplane.io
  generation: 1
  managedFields:
  - apiVersion: s3.aws.crossplane.io/v1beta1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .: {}
          f:crossplane.io/external-name: {}
          f:kubectl.kubernetes.io/last-applied-configuration: {}
      f:spec:
        .: {}
        f:deletionPolicy: {}
        f:forProvider:
          .: {}
          f:locationConstraint: {}
        f:providerConfigRef:
          .: {}
          f:name: {}
        f:writeConnectionSecretToRef:
          .: {}
          f:name: {}
          f:namespace: {}
    manager: kubectl-client-side-apply
    operation: Update
    time: "2021-08-28T06:47:15Z"
  - apiVersion: s3.aws.crossplane.io/v1beta1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:finalizers:
          .: {}
          v:"finalizer.managedresource.crossplane.io": {}
      f:status:
        .: {}
        f:atProvider:
          .: {}
          f:arn: {}
        f:conditions: {}
    manager: crossplane-aws-provider
    operation: Update
    time: "2021-08-28T06:47:19Z"
  name: outsider-crossplane-import-test
  resourceVersion: "466753"
  uid: a2eab42e-36d5-462b-af9f-e706725c888f
spec:
  deletionPolicy: Orphan
  forProvider:
    locationConstraint: ap-northeast-2
  providerConfigRef:
    name: default
  writeConnectionSecretToRef:
    name: s3-import-info
    namespace: app-demo
status:
  atProvider:
    arn: arn:aws:s3:::my-test-bucket
  conditions:
  - lastTransitionTime: "2021-08-28T06:47:19Z"
    reason: ReconcileSuccess
    status: "True"
    type: Synced

실제 다양한 인프라를 관리할 때 crossplane.io/external-name만으로 충분하려나? 싶은 생각도 들지만, 현재로선 이렇게 동작한다.

인프라를 구성하다 보면 인프라끼리 값을 참조하는 일이 많이 생긴다. 예를 들면 S3 버킷을 만들고 그 버킷의 ARN을 정책에 사용하거나 Cloudfront의 오리진으로 지정하는 등의 일이 자주 일어난다. Crossplane에 의존성 관련 설명이 있기는 한데 문서가 너무 빈약해서 이런 경우 어떻게 사용하는지 아직 찾지 못했다.

IAM 역할 생성

IAMRole은 다음과 같이 선언해서 사용할 수 있다. 적용은 S3 버킷과 다를 게 없으므로 테스트로 만들어 본 YAML만 여기에 추가했다. 아직 Crossplane의 사용자층이 많지 않아서 다양한 클라우드 서비스의 manifest를 작성하는 방법은 아직 파악하지 못했다.

apiVersion: identity.aws.crossplane.io/v1beta1
kind: IAMRole
metadata:
  name: crossplane-test
  namespace: app-demo
spec:
  deletionPolicy: Delete
  forProvider:
    assumeRolePolicyDocument: |
      {
        "Version": "2012-10-17",
        "Statement": [{
          "Action": "sts:AssumeRole",
          "Effect": "Allow",
          "Sid": "",
          "Principal": {
            "Service": "ec2.amazonaws.com"
          }
        }]
      }
  providerConfigRef:
    name: default
  writeConnectionSecretToRef:
    name: test-iam
    namespace: app-demo


이 글은 Crossplane #2 - Configuration로 이어집니다.

2021/08/28 19:04 2021/08/28 19:04