Recursos Gerenciados (Managed Resources)
Um recurso gerenciado representa qualquer recurso externo provisionado e controlado por um provider do Crossplane. Recursos externos, como aqueles em provedores de nuvem, são mapeados para recursos gerenciados dentro do Kubernetes, permitindo seu gerenciamento declarativo através de objetos nativos do Kubernetes.
O Crossplane oferece duas abordagens para trabalhar com recursos gerenciados:
- Recursos individuais: Criar e gerenciar recursos gerenciados individuais diretamente.
- Compositions (Composições): Definir conjuntos de recursos gerenciados que são provisionados e gerenciados como uma única unidade lógica.
A própria equipe do Crossplane recomenda começar trabalhando com recursos individuais para compreender melhor o funcionamento do sistema e, à medida que ganhar mais experiência, avançar para o uso de composições.
Todo recurso gerenciado possui uma política de deleção associada que define o comportamento quando o recurso gerenciado é excluído:
- deletionPolicy: Delete (Padrão - Exclua o recurso externo ao excluir o recurso gerenciado)
- deletionPolicy: Orphan (Deixe o recurso externo ao excluir o recurso gerenciado)
forProvider
Olhando o yaml abaixo de define um ec2 na aws.
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Instance
metadata:
name: crossplane-test
spec:
# O spec.forProviderde um recurso gerenciado é mapeado para os parâmetros do recurso externo.
# o forProvider é considerado a fonte da verdade. Depois do recurso criado no provider, se um usuário fizer uma alteração dentro do console da Web de um provedor, o Crossplane reverterá essa alteração de volta para o que está configurado no forProvider.
forProvider:
region: us-east-1
ami: ami-02a89066c48741345
subnetId: subnet-0a056609984bfbb94
instanceType: t2.micro
providerConfigRef:
name: default
Muitas vezes um resource precisa se referenciar a outro resource como por exemplo o subnetId que definimos acima. Esse recurso pode ser feito manualmente ou por inferência.
Uma vez que instalamos o
provider aws upboundque usa apiversion da upbound, vamos utiliza o github deles https://github.com/upbound/provider-aws/tree/main para alguns exemplos
Vamos criar uma VPC e depois uma subnet usando esta VPC.
apiVersion: ec2.aws.upbound.io/v1beta1
kind: VPC
metadata:
name: crossplane-vpc
spec:
forProvider:
region: us-east-1
cidrBlock: 10.0.0.0/16
enableDnsSupport: true
providerConfigRef:
name: default
Vamos aplicar o manifesto acima e conferir
kubectl apply -f vpc.yaml
vpc.ec2.aws.upbound.io/crossplane-vpc created
kubectl get vpcs.ec2.aws.upbound.io
NAME READY SYNCED EXTERNAL-NAME AGE
crossplane-vpc True True vpc-0bbe67dde157bb143 15m
# Faça um describe e veja o que temos
kubectl describe vpcs.ec2.aws.upbound.io crossplane-vpc
O que nos importava ali era ter uma VPC para criar uma subnet referenciando pelo seu nome que será automaticamente substituído pelo seu external name, observe.
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
name: crossplane-subnet1
spec:
forProvider:
region: us-east-1
availabilityZone: us-east-1a
# Aqui utilizamos a referência pelo nome, que irá nos trazer o ID
vpcIdRef:
name: crossplane-vpc
cidrBlock: 10.0.10.0/24
providerConfigRef:
name: default
---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
name: crossplane-subnet2
spec:
forProvider:
region: us-east-1
availabilityZone: us-east-1b
# Aqui utilizamos direto o ID sem fazer referência
vpcId: vpc-0647cd3d73b2245b5
cidrBlock: 10.0.20.0/24
providerConfigRef:
name: default
---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: Subnet
metadata:
name: crossplane-subnet3
spec:
forProvider:
region: us-east-1
availabilityZone: us-east-1b
# Aqui selecionamos usando labels
vpcIdSelector:
matchLabels:
name: crossplane-vpc
cidrBlock: 10.0.30.0/24
providerConfigRef:
name: default
---
E vamos aplicar...
❯ kubectl get subnets.ec2.aws.upbound.io
NAME READY SYNCED EXTERNAL-NAME AGE
crossplane-subnet1 True True subnet-021e79c108d979bee 6m10s
crossplane-subnet2 True True subnet-06194392f1196f39e 86s
Entendemos as referências, mas observaram que sempre precisamos definir o bloco providerConfigRef?
Se fizermos uma única composição criando as VPCs e as subnets num único manifesto, podemos ter somente um providerConfigRef.
O que aconteceria se tentássemos deletar uma VPC, que tem uma subnet dentro? Lembra que no próprio console da AWS, não seria possível deletar sem antes remover as subnets.
E se mudassemos a região de uma subnet? Não conseguiria aplicar, pois esse campo é imutável.
O Crossplane trata o recurso gerenciado como a fonte da verdade por padrão; espera ter todos os valores, spec.forProvider incluindo os opcionais. Se não for fornecido, o Crossplane preencherá os campos vazios com os valores atribuídos pelo provedor. Por exemplo, considere campos como regione availabilityZone. Você pode especificar apenas a região e deixar o provedor de nuvem escolher a zona de disponibilidade. Nesse caso, se o provedor atribuir uma zona de disponibilidade, o Crossplane usará esse valor para preencher o spec.forProvider.availabilityZonecampo.
Annotations
Algumas anotações são criadas pelo próprio crossplane nos recursos.
| Anotação | Definição |
|---|---|
| crossplane.io/external-name | O nome do recurso gerenciado dentro do Provedor. Por padrão o nome dado é o mesmo valor do name no metadata, mas é possível ter um nome específico. É de boa prática manter o mesmo. |
| crossplane.io/external-create-pending | Criado automaticamente a data/hora de quando o Crossplane começou a criar o recurso gerenciado. |
| crossplane.io/external-create-succeeded | Criado automaticamente a data/hora de quando o Provedor criou com êxito o recurso gerenciado. |
| crossplane.io/external-create-failed | Criado automaticamente de data/hora de quando o Provedor não conseguiu criar o recurso gerenciado. |
| crossplane.io/paused | Indica que o Crossplane não está reconciliando este recurso. Leia a anotação de pausa para obter mais detalhes. |
| crossplane.io/composition-resource-name | Para recursos gerenciados criados por uma Composição, esse é o resources.namevalor da Composição. |
Essas annotations são criadas automaticamente caso aconteçam.
Criando um cluster RDS por exemplo na AWS, sem instância, podemos observar o uso da annotation para expor o nome que queremos. Esse exemplo está na pasta resource para ser aplicado.
apiVersion: rds.aws.upbound.io/v1beta1
kind: Cluster
metadata:
# Esse seria o nome se não tivessemos usado a annotation abaixo
name: clusterdb
annotations:
crossplane.io/external-name: cluster-db-custom-name
spec:
forProvider:
region: us-east-1
engine: aurora-postgresql
masterUsername: cpadmin
masterPasswordSecretRef:
name: clusterdb-password
namespace: crossplane-system
key: password
skipFinalSnapshot: true
writeConnectionSecretToRef:
name: rds-clusterdb-secret
namespace: crossplane-system
---
apiVersion: v1
kind: Secret
metadata:
name: clusterdb-password
namespace: crossplane-system
type: Opaque
stringData:
password: TestPass0!
kubectl apply -f rds.yaml
kubectl get cluster.rds
NAME READY SYNCED EXTERNAL-NAME AGE
clusterdb True True cluster-db-custom-name 4m7s
kubectl describe cluster.rds clusterdb
Name: clusterdb
Namespace:
Labels: <none>
## Veja as annotations. Como não falhou não tem a annotation failed nem paused.
Annotations: crossplane.io/external-create-pending: 2023-10-24T13:53:58Z
crossplane.io/external-create-succeeded: 2023-10-24T13:53:58Z
crossplane.io/external-name: cluster-db-custom-name
upjet.crossplane.io/provider-meta:
{"e2bfb730-ecaa-11e6-8f88-34363bc7c4c0":{"create":7200000000000,"delete":7200000000000,"update":7200000000000}}
...

É crítico entender primeiro como funciona os recursos gerenciados, mas como falei anteriormente, o uso de compositions que veremos mais adiante é uma maneiro muito melhor de definir e gerenciar os recusos.
providerConfigRef
O providerConfigRef informa ao Provider qual ProviderConfig usar ao criar o recurso gerenciado. Passamos o nome da configuração.
Cada recurso gerenciado pode fazer referência a diferentes ProviderConfigs. Isso permite que diferentes recursos gerenciados sejam autenticados com credenciais diferentes no mesmo Provedor.
Drift e Reconciliação
O que acontece se deletarmos um pod no Kubernetes que é controlado por um replicaset? Ele o recriará. Ele garante o estado desejado certo? Ele cuida para que o pod esteja sempre de pé, na quantidade correta.
O Crossplane é um controller certo? Então, se um recurso for deletado da cloud, mas estiver definido no Crossplane, ele o recriará. Verá que o recurso que ele tem que provisionar não se encontra mais e aplicará novamente. No Terraform, caso isso aconteça somente será detectada a diferença na próxima vez que for aplicado o projeto.
O Crossplane elevou o conceito de reconciliação e drift para muito além dos pods, mas para tudo!
Na verdade quando aplicamos vários manifestos de uma única vez o Crossplane executa todos ao mesmo tempo, não é igual o Terraform com o depends_on. Ele aplicará e à medida que ficar true, as referências serão corrigidas automaticamente.
ver os recursos gerenciados
kubectl get managed
NAME READY SYNCED EXTERNAL-NAME AGE
kafkatopic.confluent.crossplane.io/emx-test-composition-zn79p-w6wxd True True lkc-7nx291/emx_test-composition 3d16h
kafkatopic.confluent.crossplane.io/topic-emx-ninjas-test True True lkc-7nx291/emx_test-crossplane-topic-emx 11d
kafkatopic.confluent.crossplane.io/topic-emx-ninjas-test2 True True lkc-7nx291/emx_test-crossplane-topic-emx2 11d
NAME READY SYNCED AGE PATH WITH NAMESPACE
project.projects.gitlab.crossplane.io/test-project-crossplane True True 6d14h latamairlines/oper/emx/platform-engineering/port/test