refactor-module

Par hashicorp · agent-skills

Transformez des configurations Terraform monolithiques en modules réutilisables et maintenables, en suivant les principes de conception de modules de HashiCorp et les bonnes pratiques de la communauté.

npx skills add https://github.com/hashicorp/agent-skills --skill refactor-module

Skill: Refactorer Module

Overview

Cette skill guide les agents IA dans la transformation de configurations Terraform monolithiques en modules réutilisables et maintenables en suivant les principes de conception de modules de HashiCorp et les meilleures pratiques communautaires.

Capability Statement

L'agent analysera le code Terraform existant et le refactorisera systématiquement en modules bien structurés avec :

  • Des contrats d'interface clairs (variables et outputs)
  • Une encapsulation et une abstraction appropriées
  • Versioning et documentation
  • Des frameworks de test
  • Un chemin de migration pour l'état existant

Prérequis

  • Configuration Terraform existante à refactoriser
  • Compréhension des dépendances entre ressources
  • Accès au fichier d'état actuel (pour la planification de migration)
  • Connaissance des patterns de module registry

Paramètres d'entrée

Paramètre Type Requis Description
source_directory string Oui Chemin de la configuration Terraform existante
module_name string Oui Nom du nouveau module
abstraction_level string Non "simple", "intermediate", "advanced" (par défaut : intermediate)
preserve_state boolean Oui Maintenir la compatibilité d'état
target_registry string Non Module registry cible (local, private, public)

Étapes d'exécution

1. Phase d'analyse

**Identifier les candidats au refactoring**
- Grouper les ressources par fonction logique
- Identifier les patterns répétés
- Mapper les dépendances entre ressources
- Détecter les couplages de configuration
- Analyser les patterns d'utilisation de variables

**Évaluation de la complexité**
- Compter les relations entre ressources
- Mesurer la profondeur de propagation des variables
- Identifier les références entre ressources
- Évaluer la complexité de migration d'état

2. Conception de module

Conception d'interface

# Définir un contrat d'entrée clair
variable "network_config" {
  description = "Paramètres de configuration réseau"
  type = object({
    cidr_block         = string
    availability_zones = list(string)
    enable_nat         = bool
  })

  validation {
    condition     = can(cidrhost(var.network_config.cidr_block, 0))
    error_message = "Le bloc CIDR doit être un CIDR IPv4 valide."
  }
}

# Définir un contrat de sortie
output "vpc_id" {
  description = "ID du VPC créé"
  value       = aws_vpc.main.id
}

output "private_subnet_ids" {
  description = "Liste des IDs de subnets privés"
  value       = { for k, v in aws_subnet.private : k => v.id }
}

Stratégie d'encapsulation

**À inclure dans le module :**
- Ressources étroitement couplées (VPC + subnets)
- Ressources avec cycle de vie partagé
- Configuration avec frontières claires

**À garder séparé :**
- Préoccupations transversales (monitoring, tagging)
- Ressources avec cycles de vie différents
- Configurations spécifiques au provider

3. Transformation de code

Avant : Configuration monolithique

# main.tf (monolithique)
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "production-vpc"
    Environment = "prod"
  }
}

resource "aws_subnet" "public_1" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "public-subnet-1"
    Type = "public"
  }
}

resource "aws_subnet" "public_2" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1b"

  tags = {
    Name = "public-subnet-2"
    Type = "public"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "production-igw"
  }
}

# ... plus de ressources de subnet et routing répétitives

Après : Structure modulaire

# modules/vpc/main.tf
locals {
  subnet_count = length(var.availability_zones)
}

resource "aws_vpc" "main" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = var.enable_dns_hostnames
  enable_dns_support   = var.enable_dns_support

  tags = merge(
    var.tags,
    {
      Name = var.name
    }
  )
}

resource "aws_subnet" "public" {
  for_each = var.create_public_subnets ? toset(var.availability_zones) : []

  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.cidr_block, 8, index(var.availability_zones, each.value))
  availability_zone       = each.value
  map_public_ip_on_launch = true

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-public-${each.value}"
      Type = "public"
    }
  )
}

resource "aws_internet_gateway" "main" {
  count  = var.create_public_subnets ? 1 : 0
  vpc_id = aws_vpc.main.id

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-igw"
    }
  )
}

# modules/vpc/variables.tf
variable "name" {
  description = "Préfixe de nom pour toutes les ressources"
  type        = string
}

variable "cidr_block" {
  description = "Bloc CIDR du VPC"
  type        = string

  validation {
    condition     = can(cidrhost(var.cidr_block, 0))
    error_message = "Doit être un bloc CIDR IPv4 valide."
  }
}

variable "availability_zones" {
  description = "Liste des zones de disponibilité"
  type        = list(string)
}

variable "create_public_subnets" {
  description = "Créer des subnets publics"
  type        = bool
  default     = true
}

variable "enable_dns_hostnames" {
  description = "Activer les noms d'hôte DNS dans le VPC"
  type        = bool
  default     = true
}

variable "enable_dns_support" {
  description = "Activer le support DNS dans le VPC"
  type        = bool
  default     = true
}

variable "tags" {
  description = "Tags à appliquer à toutes les ressources"
  type        = map(string)
  default     = {}
}

# modules/vpc/outputs.tf
output "vpc_id" {
  description = "ID du VPC"
  value       = aws_vpc.main.id
}

output "vpc_cidr_block" {
  description = "Bloc CIDR du VPC"
  value       = aws_vpc.main.cidr_block
}

output "public_subnet_ids" {
  description = "Correspondance des zones de disponibilité aux IDs de subnets publics"
  value       = { for k, v in aws_subnet.public : k => v.id }
}

output "internet_gateway_id" {
  description = "ID de la passerelle Internet"
  value       = try(aws_internet_gateway.main[0].id, null)
}

# Configuration racine utilisant le module
module "vpc" {
  source = "./modules/vpc"

  name               = "production"
  cidr_block         = "10.0.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

  tags = {
    Environment = "production"
    ManagedBy   = "Terraform"
  }
}

4. Migration d'état

Générer un plan de migration

# migration.tf
# Utiliser des blocs moved pour refactoring d'état (Terraform 1.1+)

moved {
  from = aws_vpc.main
  to   = module.vpc.aws_vpc.main
}

moved {
  from = aws_subnet.public_1
  to   = module.vpc.aws_subnet.public["us-east-1a"]
}

moved {
  from = aws_subnet.public_2
  to   = module.vpc.aws_subnet.public["us-east-1b"]
}

moved {
  from = aws_internet_gateway.main
  to   = module.vpc.aws_internet_gateway.main[0]
}

Migration d'état manuelle (pré-1.1)

# Générer les commandes de migration d'état
terraform state mv aws_vpc.main module.vpc.aws_vpc.main
terraform state mv aws_subnet.public_1 'module.vpc.aws_subnet.public["us-east-1a"]'
terraform state mv aws_subnet.public_2 'module.vpc.aws_subnet.public["us-east-1b"]'
terraform state mv aws_internet_gateway.main 'module.vpc.aws_internet_gateway.main[0]'

5. Documentation de module

# Module VPC

## Overview
Crée un VPC avec des subnets publics et privés configurables sur plusieurs zones de disponibilité.

## Fonctionnalités
- Déploiement de subnets multi-AZ
- Configuration optionnelle de NAT gateway
- Intégration VPC Flow Logs
- Allocation CIDR personnalisable

## Utilisation

\`\`\`hcl
module "vpc" {
  source = "./modules/vpc"

  name               = "my-vpc"
  cidr_block         = "10.0.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b"]

  create_public_subnets  = true
  create_private_subnets = true
  enable_nat_gateway     = true

  tags = {
    Environment = "production"
  }
}
\`\`\`

## Prérequis

| Nom | Version |
|-----|---------|
| terraform | >= 1.5.0 |
| aws | ~> 5.0 |

## Entrées

| Nom | Description | Type | Par défaut | Requis |
|-----|-------------|------|-----------|--------|
| name | Préfixe de nom pour les ressources | `string` | n/a | oui |
| cidr_block | Bloc CIDR du VPC | `string` | n/a | oui |
| availability_zones | Liste des AZ | `list(string)` | n/a | oui |

## Sorties

| Nom | Description |
|-----|-------------|
| vpc_id | Identifiant VPC |
| public_subnet_ids | Correspondance des IDs de subnets publics |
| private_subnet_ids | Correspondance des IDs de subnets privés |

## Exemples

Voir le répertoire [examples/](./examples/) pour des exemples d'utilisation complets.

6. Test

Utiliser la skill terraform-test

Test File: Un fichier .tftest.hcl ou .tftest.json contenant la configuration de test et les blocs run qui valident votre configuration Terraform.

Test Block: Bloc de configuration optionnel qui définit les paramètres au niveau du test (disponible depuis Terraform 1.6.0).

Run Block: Définit un scénario de test unique avec des variables optionnelles, des configurations de provider et des assertions. Chaque fichier de test nécessite au moins un bloc run.

Assert Block: Contient les conditions qui doivent s'évaluer à true pour que le test passe. Les assertions échouées causent l'échec du test.

Mock Provider: Simule le comportement du provider sans créer d'infrastructure réelle (disponible depuis Terraform 1.7.0).

Test Modes: Les tests s'exécutent en mode apply (par défaut, crée une infrastructure réelle) ou en mode plan (valide la logique sans créer de ressources).

Structure de fichier

Les fichiers de test Terraform utilisent l'extension .tftest.hcl ou .tftest.json et sont généralement organisés dans un répertoire tests/. Utilisez des conventions de nommage claires pour distinguer les tests unitaires (mode plan) des tests d'intégration (mode apply) :

my-module/
├── main.tf
├── variables.tf
├── outputs.tf
└── tests/
    ├── unit_test.tftest.hcl      # Test unitaire (mode plan)
    └── integration_test.tftest.hcl  # Test d'intégration (mode apply - crée des ressources réelles)

Patterns de refactoring

Pattern 1 : Groupement de ressources

Extraire les ressources associées en modules cohésifs :

  • Networking (VPC, Subnets, Route Tables)
  • Compute (ASG, Launch Templates, Load Balancers)
  • Data (RDS, ElastiCache, S3)

Pattern 2 : Stratification de configuration

# Module de base avec defaults
module "vpc_base" {
  source = "./modules/vpc-base"
  # Entrées minimales requises
}

# Wrapper spécifique à l'environnement
module "vpc_prod" {
  source = "./modules/vpc-production"
  # Hérite de la base, ajoute la config spécifique à prod
}

Pattern 3 : Composition

# Petits modules ciblés
module "vpc" {
  source = "./modules/vpc"
}

module "security_groups" {
  source = "./modules/security-groups"
  vpc_id = module.vpc.vpc_id
}

module "application" {
  source     = "./modules/application"
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnet_ids
  sg_ids     = module.security_groups.app_sg_ids
}

Pièges courants

1. Sur-abstraction

# ❌ Ne pas créer de modules trop génériques
variable "resources" {
  type = map(map(any))  # Trop flexible, difficile à valider
}

# ✅ Utiliser des interfaces spécifiques et typées
variable "database_config" {
  type = object({
    engine         = string
    instance_class = string
  })
}

2. Couplage étroit

# ❌ Ne pas coupler les modules par des références directes
# module A
output "instance_id" { value = aws_instance.app.id }

# module B (dans la même config)
resource "aws_eip" "app" {
  instance = module.a.instance_id  # Couplage étroit
}

# ✅ Passer les dépendances via le module racine
module "compute" {
  source = "./modules/compute"
}

resource "aws_eip" "app" {
  instance = module.compute.instance_id
}

3. Erreurs de migration d'état

Toujours tester la migration en non-production d'abord :

# Créer un plan pour vérifier l'absence de changements après migration
terraform plan -out=migration.tfplan

# Examiner attentivement
terraform show migration.tfplan

# Appliquer seulement si le plan montre aucun changement
terraform apply migration.tfplan

Stratégie de contrôle de version

# Utiliser le versioning sémantique pour les modules
module "vpc" {
  source  = "git::https://github.com/org/terraform-modules.git//vpc?ref=v1.2.0"
  version = "~> 1.2"
}

# Épingler à des versions spécifiques en production
# Utiliser des plages de version en développement

Critères de succès

  • [ ] Le module a une responsabilité unique et bien définie
  • [ ] Toutes les variables ont des descriptions et des types
  • [ ] Les règles de validation empêchent les configurations invalides
  • [ ] Les outputs fournissent suffisamment d'informations pour les consommateurs
  • [ ] La documentation inclut des exemples d'utilisation
  • [ ] Les tests vérifient le comportement du module
  • [ ] La migration d'état s'est déroulée sans recréation de ressources
  • [ ] Aucune différence de plan après refactoring

Skills associées

Ressources

Historique des révisions

Version Date Changements
1.0.0 2025-11-07 Définition initiale de la skill

Skills similaires