Aller plus loin avec Crossplane: Compositions et fonctions

Sommaire

Avec l'émergence du Platform engineering, on assiste à une évolution vers la création de solutions dites "self-service" à destination des développeurs. Cette approche permet une standardisation des pratiques DevOps, une meilleure expérience pour les développeurs, et une réduction de la charge cognitive liée à la gestion des outils.

Crossplane, un projet sous l'égide de la Cloud Native Computing Foundation (CNCF) vise à devenir le framework incontournable pour créer des plateformes Cloud Natives. Dans mon premier article sur Crossplane, j'ai présenté cet outil et expliqué comment il utilise les principes GitOPs pour l'infrastructure, permettant ainsi de créer un cluster GKE.

Le projet, qui fête maintenant ses 5 ans 🎂🎉, a gagné en maturité et s'est enrichi de nouvelles fonctionnalités au fil du temps.

Dans cet article, nous explorerons certaines fonctionnalités clés de Crossplane, avec un intérêt particulier pour les compositions functions qui génèrent un vif intérêt au sein de la communauté. Allons-nous assister à un tournant décisif pour le projet ?

🎯 Notre objectif

La documentation de Crossplane est très bien fournie, nous allons donc passer rapidement sur les concepts de base pour se concentrer sur un cas d'usage concret: Déployer Harbor sur un cluster EKS en suivant les recommandations en terme de haute disponibilité.

Harbor

Harbor, issue aussi de la CNCF, est une solution de gestion d'artefacts de conteneurs centrée sur la sécurité. Son rôle principal est de stocker, signer et analyser les vulnérabilités des images de conteneurs. Harbor dispose d'un contrôle d'accès fin, d'une API ainsi que d'une interface web afin de permettre aux équipes de dev d'y accéder et gérer leurs images simplement.

La disponibilité d'Harbor dépend principalement de ses composants avec des données persistantes (stateful). L'utilisateur est responsable de leur mise en œuvre, qui doit être adaptée à l'infrastructure cible. L'article présente les options choisies pour un niveau de disponibilité optimal.

Harbor
  • Redis déployé avec le chart Helm de Bitnami en mode "master/slave"
  • Les artefacts sont stockés dans un bucket AWS S3
  • Une instance RDS pour la base de données PostgreSQL

Nous allons maintenant explorer comment Crossplane facilite le provisionnement d'une base de données (RDS), en offrant un niveau d'abstraction simple, exposant uniquement les options nécessaires. 🚀

🏗️ Prérequis

Avant de pouvoir construire nos compositions, nous devons préparer le terrain car certaines opérations préalables sont nécessaires. Ces étapes sont réalisées dans un ordre bien précis:

Install workflow
  1. Déploiement du contrôlleur Crossplane en utilisant le chart Helm.
  2. Installation des providers et de leur configuration.
  3. Déploiement de diverses configurations faisant usage des providers installés préalablement. Notamment les Compositions et les Composition Functions.
  4. Déclarations de Claims pour consommer les Compositions.

Ces étapes sont traduites en dépendances Flux, et peuvent être consultées ici.

Les sources

Toutes les actions réalisées dans cet article proviennent de ce dépôt git

On peut y trouver de nombreuses sources qui me permettent de construire mes articles de blog. N'hésitez pas à me faire des retours, ouvrir des issues si nécessaire ... 🙏

📦 Les compositions

Pour le dire simplement, une Composition dans Crossplane est un moyen d'agrèger et de gérer automatiquement plusieurs ressources dont la configuration peut s'avérer parfois complexe.

Elle utilise l'API de Kubernetes pour définir et orchestrer non seulement des éléments d'infrastructure tels que le stockage et le réseau, mais aussi de nombreux autres composants (se référer à la liste des providers). Cette méthode offre aux développeurs une interface simplifiée, représentant une couche d'abstraction qui masque les détails techniques plus complexes de l'infrastructure sous-jacente.

Pour atteindre mon objectif, qui est de créer une base de données RDS lors du déploiement de l'application Harbor, j'ai d'abord recherché s'il existait un exemple pertinent. Pour ce faire, j'ai utilisé le marketplace d'Upbound, où l'on peut trouver de nombreuses Compositions pouvant servir de point de départ.

En me basant sur la composition configuration-rds, j'ai souhaité y ajouter les éléments suivants:

  • 🔑 Permettre aux pods d'accéder à l'instance.
  • ▶️ Création d'un service de type ExternalName avec un nom prédictible qui peut être utilisé dans la configuration de Harbor
  • 💾 Création de bases de données et des rôles qui en seront propriétaires.

❓ Comment cette Composition serait-elle alors utilisée si, par exemple, un développeur souhaite disposer d'une base de données? Il suffit de déclarer une Claim qui représente le niveau d'abstraction exposé aux utilisateurs.

tooling/base/harbor/sqlinstance.yaml

 1apiVersion: cloud.ogenki.io/v1alpha1
 2kind: SQLInstance
 3metadata:
 4  name: xplane-harbor
 5  namespace: tooling
 6spec:
 7  parameters:
 8    engine: postgres
 9    engineVersion: "15"
10    size: small
11    storageGB: 20
12    databases:
13      - owner: harbor
14        name: registry
15    passwordSecretRef:
16      namespace: tooling
17      name: harbor-pg-masterpassword
18      key: password
19  compositionRef:
20    name: xsqlinstances.cloud.ogenki.io
21  writeConnectionSecretToRef:
22    name: xplane-harbor-rds

ici nous constatons que cela se limite à une simple ressource avec peu de paramètres pour exprimer nos souhaits:

  • Une instance PostgreSQL en version 15 sera créée
  • La type d'instance de celle-ci est laissé à l'appréciation de l'équipe plateforme (les mainteneurs de la composition). Dans la Claim ci-dessus nous souhaitons une "petite" instance, qui est traduit par la composition en db.t3.small.

infrastructure/base/crossplane/configuration/sql-instance-composition.yaml

1transforms:
2  - type: map
3    map:
4      large: db.t3.large
5      medium: db.t3.medium
6      small: db.t3.small
  • Le mot de passe de l'utilisateur master est extrait d'un secret harbor-pg-masterpassword, généré via un External Secret.
  • Une fois l'instance créée, les détails pour la connexion sont stockés dans un secret xplane-harbor-rds

C'est là que nous pouvons pleinement apprécier la puissance des Compositions Crossplane! En effet, de nombreuses ressources sont générées de manière transparente, comme illustré par le schéma suivant :

Au bout de quelques minutes, toutes les ressources sont créées. (ℹ️ La CLI Crossplane permet désormais de nombreuses opérations, notamment visualiser les ressources d'une Composition. Elle est dénommée crank pour la différencier du binaire crossplane qui est le binaire qui tourne sur Kubernetes. )

 1kubectl get xsqlinstances
 2NAME                  SYNCED   READY   COMPOSITION                     AGE
 3xplane-harbor-jmdhp   True     True    xsqlinstances.cloud.ogenki.io   8m32s
 4
 5crank beta trace xsqlinstances.cloud.ogenki.io xplane-harbor-jmdhp
 6NAME                                                    SYNCED   READY   STATUS
 7XSQLInstance/xplane-harbor-jmdhp                        True     True    Available
 8├─ SecurityGroupIngressRule/xplane-harbor-jmdhp-n785k   True     True    Available
 9├─ SecurityGroup/xplane-harbor-jmdhp-8jnhc              True     True    Available
10├─ Object/external-service-xplane-harbor                True     True    Available
11├─ Object/providersql-xplane-harbor                     True     True    Available
12├─ Database/registry                                    True     True    Available
13├─ Role/harbor                                          True     True    Available
14├─ Instance/xplane-harbor-jmdhp-whv4g                   True     True    Available
15└─ SubnetGroup/xplane-harbor-jmdhp-fjfth                True     True    Available

Et Harbor devient accessible grâce à Cilium et Gateway API (Vous pouvez jeter un oeil à un précédent post sur le sujet 😉)

EnvironmentConfig

Les EnvironmentConfigs permettent d'utiliser des variables spécifiques au cluster local. Ces éléments de configuration sont chargés en mémoire et peuvent ensuite être utilisés dans la composition.

Étant donné que le cluster EKS est créé avec Opentofu, nous stockons ses propriétés par le biais de variables Flux. (plus d'infos sur les variables de substitution ici)

infrastructure/base/crossplane/configuration/environmentconfig.yaml

 1apiVersion: apiextensions.crossplane.io/v1alpha1
 2kind: EnvironmentConfig
 3metadata:
 4  name: eks-environment
 5data:
 6  clusterName: ${cluster_name}
 7  oidcUrl: ${oidc_issuer_url}
 8  oidcHost: ${oidc_issuer_host}
 9  oidcArn: ${oidc_provider_arn}
10  accountId: ${aws_account_id}
11  region: ${region}
12  vpcId: ${vpc_id}
13  CIDRBlock: ${vpc_cidr_block}
14  privateSubnetIds: ${private_subnet_ids}

Ces variables peuvent ensuite être utilisées dans les Compositions via la directive FromEnvironmentFieldPath. Par exemple pour permettre aux pods d'accéder à notre instance RDS, nous autorisons le CIDR du VPC:

infrastructure/base/crossplane/configuration/irsa-composition.yaml

 1- name: SecurityGroupIngressRule
 2  base:
 3    apiVersion: ec2.aws.upbound.io/v1beta1
 4    kind: SecurityGroupIngressRule
 5    spec:
 6      forProvider:
 7        cidrIpv4: ""
 8  patches:
 9...
10    - fromFieldPath: CIDRBlock
11      toFieldPath: spec.forProvider.cidrIpv4
12      type: FromEnvironmentFieldPath

⚠️ A l'heure où j'écris cet article, la fonctionnalité est toujours en alpha.

🛠️ Les fonctions de compositions

Les fonctions de compositions (Composition Functions) représentent une évolution significative pour le développement de Compositions. En effet, la méthode traditionnelle de patching dans une composition présentait certaines limitations, telles que l'incapacité à utiliser des conditions, des boucles dans le code, ou à exécuter des fonctions avancées (ex: calcul de sous-réseaux, vérification de l'état des ressources externes...).

Les Composition Functions permettent de lever ces limites et sont en réalité des programmes qui étendent les capacités de templating des ressources au sein de Crossplane. Elles sont rédigées dans n'importe quel langage de programmation, offrant ainsi une souplesse et une puissance presque infinies lors de la définition des compositions. Cela permet désormais des tâches complexes telles que les transformations conditionnelles, les itérations, et les opérations dynamiques.

Ces fonctions sont exécutées de manière séquentielle (mode Pipeline), chaque fonction manipulant et transformant les ressources, puis transmettant le résultat à la fonction suivante, ouvrant ainsi la porte à des combinaisons puissantes.

Mais revenons à notre composition RDS 🔍 ! Celle-ci utilise, en effet, cette nouvelle façon de définir des Compositions et est composée de 3 étapes:

infrastructure/base/crossplane/configuration/sql-instance-composition.yaml

 1apiVersion: apiextensions.crossplane.io/v1
 2kind: Composition
 3metadata:
 4  name: xsqlinstances.cloud.ogenki.io
 5...
 6spec:
 7  mode: Pipeline
 8...
 9  pipeline:
10    - step: patch-and-transform
11      functionRef:
12        name: function-patch-and-transform
13...
14    - step: sql-go-templating
15      functionRef:
16        name: function-go-templating
17...
18    - step: ready
19      functionRef:
20        name: function-auto-ready
  1. La syntaxe de la première étape patch-and-transform vous parait probablement familière 😉. Il s'agit en effet de la méthode de patching traditionnelle de Crossplane mais cette fois ci, elle est exécutée en tant que fonction dans le Pipeline.
  2. La seconde étape consiste à faire appel à la fonction function-go-templating dont nous allons parler un peu plus loin.
  3. Enfin la dernière étape utilise la fonction function-auto-ready qui permet de vérifier que la ressource composite (XR) est prête. C'est à dire que l'ensemble des ressources qui la compose ont atteint l'état Ready
Migration

Si vous avez déjà des Compositions dans le format précédent (Patch & Transforms), il existe un super outil qui permet de migrer vers le mode Pipeline: crossplane-migrator

  • Installer crossplane-migrator
1go install github.com/crossplane-contrib/crossplane-migrator@latest
  • Puis lancer la commande suivante qui permettra d'obtenir le bon format dans compostion-pipeline.yaml
1crossplane-migrator new-pipeline-composition --function-name crossplane-contrib-function-patch-and-transform -i composition.yaml -o composition-pipeline.yaml

ℹ️ Cet outil devrait être ajouté à la CLI Crossplane en version 1.15

🐹 Du Go Template dans Crossplane

Comme évoqué précédemment, la puissance des Composition functions réside principalement dans le fait que n'importe quel langage peut être utilisé. Il est notamment possible de générer des ressources à partir de templates Go. Cela n'est pas si différent de l'écriture de Charts Helm.

Il suffit de faire appel à la fonction et lui fournir en entrée le template permettant de générer des ressources Kubernetes. Dans la composition SQLInstance, les YAMLs sont générés directement en ligne mais il est aussi possible de charger des fichiers locaux (source: Filesystem)

 1    - step: sql-go-templating
 2      functionRef:
 3        name: function-go-templating
 4      input:
 5        apiVersion: gotemplating.fn.crossplane.io/v1beta1
 6        kind: GoTemplate
 7        source: Inline
 8        inline:
 9          template: |
10          ...          

Ensuite c'est à vous de jouer! Par exemple il y a peu de différence pour générer une base de données MariaDB ou PostgreSQL et nous pouvons donc formuler des conditions comme la suivante:

1{{- $apiVersion := "" }}
2{{- if eq $parameters.engine "postgres" }}
3  {{- $apiVersion = "postgresql.sql.crossplane.io/v1alpha1" }}
4{{- else }}
5  {{- $apiVersion = "mysql.sql.crossplane.io/v1alpha1" }}
6{{- end -}}

Cela m'a aussi permit de définir une liste de bases de données ainsi que le propriétaire associé

 1apiVersion: cloud.ogenki.io/v1alpha1
 2kind: SQLInstance
 3metadata:
 4...
 5spec:
 6  parameters:
 7...
 8    databases:
 9      - owner: owner1
10        name: db1
11      - owner: owner2
12        name: db2

Puis d'utiliser des boucles Golang pour les créer en utilisant le provider SQL.

 1{{- range $parameters.databases }}
 2---
 3apiVersion: {{ $apiVersion }}
 4kind: Database
 5metadata:
 6  name: {{ .name | replace "_" "-" }}
 7  annotations:
 8    {{ setResourceNameAnnotation (print "db-" (replace "_" "-" .name)) }}
 9spec:
10...
11{{- end }}

Il est même possible de développer une logique plus complexe dans des fonctions go template avec les directives habituelles define et include. Voici un extrait des exemples disponibles dans le repo de la fonction

1{{- define "labels" -}}
2some-text: {{.val1}}
3other-text: {{.val2}}
4{{- end }}
5...
6labels:
7  {{- include "labels" $vals | nindent 4}}
8...

Enfin nous pouvons tester la Composition et afficher le rendu du template avec la commande suivante:

1crank beta render tooling/base/harbor/sqlinstance.yaml infrastructure/base/crossplane/configuration/sql-instance-composition.yaml infrastructure/base/crossplane/configuration/function-go-templating.yaml

Comme on peut le voir, les possibilités sont décuplées grâce à la capacité de construire des ressources en utilisant un langage de programmation. Cependant, il est également nécessaire de veiller à ce que la composition reste lisible et maintenable sur le long terme. Nous assisterons probablement à l'émergence de bonnes pratiques à mesure que nous gagnons en expérience sur l'utilisation de ces fonctions.

💭 Dernières remarques

Lorsqu'on parle d'Infrastructure As Code, Terraform est souvent le premier nom qui nous vient à l'esprit. Cet outil, soutenu par une vaste communauté et qui dispose d'un écosystème bien établi, reste un choix de premier ordre. Mais il est intéressant de se demander comment Terraform a évolué face aux nouveaux paradigmes apportés par Kubernetes. Nous avons abordé cette question dans notre article sur terraform controller. Depuis, vous avez sûrement remarqué le petit séisme causé par la décision de Hashicorp de passer sous licence BSL. Cette évolution a suscité de nombreuses réactions et a peut-être influencé la stratégie et la roadmap d'autres solutions...

Il est difficile de dire si c'est une réaction directe, mais récemment, Crossplane a mis à jour sa charte pour élargir son champ d'action à l'ensemble de l'écosystème (providers, functions), notamment en intégrant le projet Upjet sous l'égide de la CNCF. L'objectif de cette démarche est de renforcer la gouvernance des projets associés et d'améliorer, en fin de compte, l'expérience des développeurs.

Personnellement, j'utilise Crossplane depuis un moment pour des cas d'usage spécifiques. Je l'ai même mis en production dans une entreprise, utilisant une composition pour définir des permissions spécifiques pour les pods sur EKS (IRSA). Nous avions également restreint les types de ressources qu'un développeur pouvait déclarer.

❓ Alors, que penser de cette nouvelle expérience avec Crossplane?

Il faut le dire, les Composition Functions offrent de larges perspectives et on peut s'attendre à voir de nombreuses fonctions apparaître en 2024 🚀

Toutefois, à mon avis, il est crucial que les outils de développement et d'exploitation se perfectionnent pour favoriser l'adoption du projet. Par exemple, une interface web ou un plugin k9s seraient utiles.

Pour un débutant souhaitant développer une composition ou une fonction, le premier pas peut sembler difficile. La validation d'une composition n'est pas simple, et les exemples à suivre ne sont pas très nombreux. On espère que le marketplace s'enrichira avec le temps.

Cela dit, ces préoccupations ont été prises en compte par la communauté de Crossplane, notamment par le SIG Dev XP dont il faut féliciter l'effort et qui mène actuellement un travail important. 👏

Je vous encourage à suivre de près l'évolution du projet dans les prochains mois 👀, et à tenter l'expérience Crossplane pour vous faire votre propre idée. Pour ma part, je suis particulièrement intéressé par la fonction CUElang, un langage sur lequel je prévois de me pencher prochainement.

🔖 References

Traductions: