Aller au contenu

Terraform/OpenTofu m’ont fait perdre du temps

Disclaimer : Terraform/OpenTofu me font plus gagner de temps que l’inverse !

Pré-ambule

Je suis utilisateur d’OpenTofu depuis l’annonce du changement de licence appliquée à l’utilisation de Terraform. Cependant, ce qui est traité dans cet article impacte bien les deux solutions.

Pour rappel, les récentes levées de bouclier depuis l’application de la nouvelle licence d’utilisation de Terraform proviennent essentiellement de l’incertitude et du maintien de son flou des conditions applicables mais aussi sur le risque de voir à nouveau la licence changer.

HashiCorp, l’éditeur de Terraform, a adopté la Business Source License (BSL/BUSL) courant 2023 (HashiCorp adopts Business Source License, Aug 10 2023, Armon Dadgar), laissant derrière eux la Mozilla Public License v2.0 (MPL 2.0). Cette license porte donc dans ces conditions un flou quant à votre possible utilisation concurrentielle de Terraform vis-à-vis des services offerts par HashiCorp (notamment Terraform Cloud).

En tant qu’intervenant externe, SLASH MNT pourrait par exemple être vu comme concurrent direct à HashiCorp car nous pouvons fournir un service facturé sur, par exemple, la mise en place de l’automatisation d’une infrastructure tout en utilisant l’outil Terraform.

Tous ces événements ayant attirés l’attention des plus grands groupes, il fallu peu de temps pour qu’OpenTofu soit fork du projet Terraform à sa dernière version livrées sous MPL 2.0. Aujourd’hui OpenTofu est soutenu par la Linux Foundation, organisme à but non-lucratif dont l’objectif est de promouvoir l’utilisation d’outils et services “vendor-neutral” (éviter le verrouillage du marché).

Sachant cela, pour éviter toutes mésaventures et désagréments à nos clients, nous proposons de prime abord l’utilisation d’OpenTofu qui, quant à lui, est proposé sous licence Mozilla Public License 2.0. Comme l’était autrefois Terraform !

Le début des problèmes

N.B. : j’ai un gros passif de développeur soucieux de la maintenabilité de mon code, passant par la factorisation par exemple.

Ma problématique du jour : concevoir et développer une infrastructure générique, capable de se lancer sur de multiples cloud providers. Pour transparence, l’infrastructure-as-code a été imaginée et développée d’abord pour OVHcloud. Un second cas d’usage s’est présenté mais chez Open Telekom Cloud.

Les deux cloud providers utilisent Openstack pour fournir leur infrastructure : un point commun qui peut faire toute la différence.

OVH a pris le parti de développer son provider Terraform sur tous les aspects en surcouche ou supplémentaires à Openstack, et donc de laisser le provider Openstack faire le reste. L’avantage est donc qu’il y a une adhérence minimale à OVH.
En ce qui concerne Open Telekom Cloud (OTC), c’est… différent ! Nous nous rendons compte qu’OTC a plutôt cherché à créer son provider Terraform par dessus celui d’Openstack. Personnellement, c’est un choix qui me déplait, et ce à plusieurs égards, mais il y a peut-être une implémentation différente que celle d’Openstack ?

Par rapport à mon besoin, ceci amène un frein notable : l’infrastructure-as-code de mon infrastructure générique doit définir deux implémentations bien disctinctes, engendrant un arbre de dépendances divergeant :

  • OVH :
    • Provider OVH
    • Provider Openstack
    • Provider AWS (pour tous les aspects S3 / Object Storage)
  • Open Telekom Cloud :
    • Provider Open Telekom Cloud

Comme dit plus tôt, en tant qu’ingénieur qui cherche à factoriser ses productions, voilà ce que je pouvais imaginer faire :

L’idée est qu’en fonction du Cloud Provider défini (et configuré), Terraform/OpenTofu viendrait piocher les modules dont il a besoin.

Malheureusement cette approche vient à l’encontre du design même de l’outil Terraform/OpenTofu. Terraform/OpenTofu crée un plan d’exécution décrivant l’infrastructure qu’il va créer, mettre à jour ou détruire en fonction de l’infrastructure existante et de votre configuration. Pour y parvenir, il génère un graphe de dépendance de toutes les ressources qui doivent être créées ou modifiées.

Pour l’ensemble du code Terraform/OpenTofu qui suit, prenons pour acquis la définition suivante des providers :

p_opentelekomcloud.tf
provider "opentelekomcloud" {
  access_key  = var.providers.opentelekomcloud.access_key
  secret_key  = var.providers.opentelekomcloud.secret_key
  tenant_name = var.providers.opentelekomcloud.tenant_name
  domain_name = var.providers.opentelekomcloud.domain_name
  auth_url    = var.providers.opentelekomcloud.auth_url
}
HCL

p_ovh.tf
provider "aws" {
  region     = var.providers.ovh.awsregion
  access_key = var.providers.ovh.aws.access_key
  secret_key = var.providers.ovh.aws.

  skip_credentials_validation = true
  skip_requesting_account_id = true

  # The regions are probably unknown to AWS hence skipping is needed.
  skip_region_validation = true

  endpoints {
    s3 = var.providers.ovh.s3_endpoint
  }
}

provider "ovh" {
  endpoint           = var.providers.ovh.endpoint
  application_key    = var.providers.ovh.application_key
  application_secret = var.providers.ovh.application_secret
  consumer_key       = var.providers.ovh.consumer_key
}

provider "openstack" {
  auth_url     = var.providers.ovh.openstack.auth_url
  region       = var.providers.ovh.openstack.region
  tenant_name  = var.providers.ovh.openstack.tenant_name
  user_name    = var.providers.ovh.openstack.user_name
  password     = var.providers.ovh.openstack.password
  delayed_auth = true
}
HCL

Maintenant, imaginons le plan suivant, où l’on souhaite gérer un cluster Kubernetes managé où l’on délègue la logique d’implémentation de ce que serait un “managed_kubernetes” selon le provider sélectionné :

r_infrastructure.tf
module "managed_kubernetes" {
  source = "./modules/${var.provider}/managed-kubernetes"
  
  name = "my-cluster"
}
HCL

Si vous utilisez cette syntaxe, Terraform/OpenTofu va très rapidement vous indiquer qu’il n’est pas capable d’utiliser de variable dans son mot clés source !

Alors comment faire ?

Seconde tentative avec l’ajout d’un module intermédiare dont l’intérêt est d’utiliser les ressources adéquates, en fonction du provider

r_infrastructure.tf
module "managed_kubernetes" {
  source = "./modules/managed-kubernetes"
  provider = var.provider # let's say `ovh` or `otc`
  
  name = "my-cluster"
}
HCL

modules/managed-kubernetes/main.tf
variable "name" {
  type = string
  description = "The cluster name."
}

variable "provider" {
  type = string
  description = "The Public Cloud provider name (`ovh` or `otc`)."
}

module "otc_managed_kubernetes" {
  source = "./otc"
  count  = var.provider == "otc" ? 1 : 0 # Nous ne voulons pas appeler le module si le provider choisi est "ovh"

  name = var.name
}

module "ovh_managed_kubernetes" {
  source = "./ovh"
  count  = var.provider == "ovh" ? 1 : 0 # Nous ne voulons pas appeler le module si le provider choisi est "otc"

  name = var.name
}

locals {
  delegated_module = var.provider == "ovh" ? module.ovh_managed_kubernetes : module.otc_managed_kubernetes
}

output "cluster_id" {
  value = local.delegated_module[0].cluster_id
}
HCL

modules/managed-kubernetes/otc/main.tf
variable "name" {
  type = string
  description = "The cluster name."
}

resource "opentelekomcloud_cce_cluster_v3" "cluster" {
  name = var.name
}

output "cluster_id" {
  value = opentelekomcloud_cce_cluster_v3.cluster.cluster_id
}
HCL

modules/managed-kubernetes/ovh/main.tf
variable "name" {
  type = string
  description = "The cluster name."
}

resource "ovh_cloud_project_kube" "cluster" {
  name = var.name
}

output "cluster_id" {
  value = ovh_cloud_project_kube.cluster.cluster_id
}
HCL

Déjà, nous constatons la “laideur” de l’implémentation, de part ces conditions et index à utiliser. Mais au delà de ça, pour revenir sur le paragraphe où nous décrivons le fonctionnement de Terraform/OpenTofu, la création d’un graphe de dépendance dans ce contexte là ajoutera à la fois le module ./modules/managed-kubernetes/otc ET ./modules/managed-kubernetes/ovh même si l’un des deux n’est pas réellement utilisé.

C’est la ligne de code var.provider == "ovh" ? module.ovh_managed_kubernetes : module.otc_managed_kubernetes qui en est la cause !

Cela implique que Terraform/OpenTofu demandera des informations de connexions des deux providers bien que nous soyons dans un cas où seul un des deux est réellement appelé.

Un début de solution

Nous constatons à tout cela qu’il est nécessaire de séparer a minima les plans selon le provider.

OpenTofu v1.9.0 vient ici nous aider un peu car cette dernière version apporte la fonctionnalité d’exclusions de ressources. Sans cela, il faudrait totalement séparer les plans dans deux dossiers distincts (car Terraform/OpenTofu lit tous les fichiers .tf du dossier ciblé) jusqu’à même devoir séparer, par exemple, en deux repositories différents.

r_ovh.tf
module "ovh_managed_kubernetes" {
  source = "./modules/ovh/managed-kubernetes"
  
  name = "my-cluster"
}
HCL

r_otc.tf
module "otc_managed_kubernetes" {
  source = "./modules/otc/managed-kubernetes"
  
  name = "my-cluster"
}
HCL

Puis, via le CLI, appeler :

$ tofu plan -exclude module.otc_managed_kubernetes
Bash

La suite ?

Tout ça nous amène à une conclusion plutôt négative car je n’ai pas réussi à atteindre l’objectif désiré. La maintenabilité est réduite et la factorisation quasi nulle. Il s’agit là d’une limitation de Terraform/OpenTofu.

Ma tentative d’adaption Terraform/OpenTofu en structurant les modules pour sélectionner dynamiquement les ressources en fonction du provider a été vaine. Comme dit plus tôt, la nature même du fonctionnement de Terraform/OpenTofu, qui génère un graphe de dépendance global, a posé problème : même si un provider n’était pas utilisé, ses ressources étaient tout de même prises en compte, rendant la factorisation difficile et impactant la maintenabilité du code.

La rigidité du modèle de dépendances de Terraform/OpenTofu empêche une séparation efficace des configurations sans compromettre la maintenabilité.

OpenTofu v1.9.0 apporte une amélioration avec la gestion des exclusions, mais ne résout pas entièrement le problème. Cette expérience souligne la nécessité d’une évolution des outils d’infrastructure-as-code vers une plus grande flexibilité pour mieux répondre aux enjeux du multi-cloud.