연초부터 그룹스터디도 하고 하면서 Kubernetes도 공부하지만 오퍼레이터도 계속 보고 있는데 Kuberentes를 이미 쓰고 있는 상황에서 Kubernetes의 오케스트레이션을 이용하는 접근에 대해서 계속 고민하게 되는 것 같다. 아직은 잘 몰라서 오퍼레이터를 열심히 쓰고 있거나 하진 않은데 오퍼레이터를 써서 뭔가를 관리할 때와 외부에서 관리할 때의 장단점을 고민해 볼 일이 많아졌다.
작년에 나온 AWS Controllers for Kubernetes(이하 ACK)도 이를 사용해서 애플리케이션에 필요한 AWS 리소스를 Kubernetes에서 관리할 수 있을까 고민해 봤다. 물론 ACK를 막상 살펴보니 내가 필요한 리소스는 아직 개발되지 않았고 프로젝트도 꽤 초기 상태로 보여서 사용하지는 않았다.
그러고 있다가 어느 날 Crossplane이라는 프로젝트를 알게 되었고 이 프로젝트가 내가 궁금해하던 것과 비슷한 접근을 한다는 것을 알게 되었다. 프로젝트 이름은 몇 번 본 적 있지만, 원래는 뭐 하는 프로젝트인지도 잘 몰랐다.
Crossplane
Crossplane은 2018은 upbound 회사에서 만든 프로젝트고 2020년 7월 CNCF의 Sandbox 프로젝트가 되었다. 현재 Crossplne을 Incubating 프로젝트로 올리는 투표가 진행 중가 진행 중이므로 곧 Incubating 프로젝트가 될지도 모르겠다. CNCF 졸업 프로젝트이면서 Kubernetes용 스토리지인 Rook을 만든 팀이 Crossplane도 만들었다고 한다.
현재 GitHub 저장소의 스타 수가 3,700개로 높은 편은 아니라고 생각한다. Crossplane을 테스트하려고 자료를 찾아볼 때도 홈페이지의 내용 외에는 별로 찾아볼 수가 없어서 아직 사용자가 많지는 않아 보였다. Go 언어로 작성되었고 현재 1.3.1이 최신 버전이다.
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 차트가 어떤 리소스를 설치했는지 확인해 보자. 크게 crossplane
과 crossplane-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 인프라를 관리할 때 권한이 필요했다.
나 같은 경우는 EKS의 워커 노드용 EC2에 부여된 IAM 역할에 AmazonS3FullAccess
, IAMFullAccess
, AmazonRDSFullAccess`를 부여해서 권한 문제를 해결했다. 실제로 사용할 때는 권한 제어도 테스트해봐야겠지만 여기서는 Crossplane의 기능을 이해하는 게 목적이라서 여기에 권한을 최대한 주고 테스트했다.
매니지드 리소스
Crossplane에서 매니지드 리소스는 Kubernetes 밖의 프로바이더 리소스를 의미하고 위에서 얘기한 CRD를 사용해서 리소스를 생성하면 매니지드 리소스가 된다. 프로바이더 API 문서를 참고하면 이 CRD의 필드의 사용 방법을 알 수 있다.(문서가 아직 친절하진 않다.)
프로바이더의 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 버킷도 삭제되도록 deletionPolicy
를 Delete
로 지정했고 forProvider
에서 S3 버킷의 acl
과 locationConstraint
를 지정했다. 여기서 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
라는 버킷이 생성된 것을 볼 수 있다.
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에서 리소스가 지워지는 상황은
deletePolicy
가Delete
일 때 매니지드 리소스를 지웠을 때뿐이다. 이렇게 동작하는 이유는 자동으로 조정이 일어나는 만큼 의도치 않게 리소스가 지워지는 경우를 막기 위한 정책일 거로 생각한다. - 지연 초기화(late initialization): 이 부분은 몇 번 테스트를 해봤지만, 정확히 기능을 파악하진 못했는데 문서대로라면 프로바이더의 CRD에서는 선택적인 필드이지만 이 속성의 기본값을 프로바이더가 선택하는 경우가 있다. 이럴 때
spec
에 사용자가 지정하지 않았으므로 빈값으로 등록했다가 프로바이더가 기본값을 지정하면 이 값을 가져와서 state에 채워준다고 한다. - 삭제: 매니지드 리소스의 삭제 요청을 받으면 컨트롤러가 바로 프로바이더의 리소스도 삭제 정차를 시작했다. 클라우드의 인프라가 실제로 삭제되었다는 것을 컨트롤러가 확인한 후에 매니지드 리소스를 지우기 때문에 매니지드 리소스가 지워지면 클라우드의 리소스도 지워졌다는 것을 확신할 수 있다.
생성된 리소스를 확인해 보면 bucket
과 secret
이 생성된 것을 알 수 있고 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
연결 정보가 보관된 시크릿을 살펴보면 endpoint
와 region
이 등록된 것을 볼 수 있다.
$ 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로 이어집니다.
Comments