Ressources
Terraform import : reprendre une infrastructure AWS existante sans la recréer
Reprendre une infrastructure AWS existante avec terraform import : inventaire des sous-objets, blocs import, premier plan lu comme contrôle et state protégé.
L'équipe CTN Solutions · publié le · 14 min de lecture
Base d'expérience : Laboratoire reproductible de CTN Solutions du 8 octobre 2026 contre un émulateur local de l'API AWS (moto 5.2.3), et documentation officielle HashiCorp et du fournisseur AWS. Aucun compte AWS réel, aucune référence client.
Faits vérifiés sur les sources primaires le
Une infrastructure AWS construite à la main, ou livrée par un prestataire, peut contenir des ressources que personne n'a déclarées dans Terraform. Reprendre le compte revient alors à décider, objet par objet, ce qui passe sous Terraform, ce qui reste hors périmètre, et comment vérifier qu'aucune ressource n'est recréée par erreur. La commande terraform import ne tranche pas cette décision à elle seule : elle rattache un objet réel à une adresse du state, sans dire si la configuration décrit fidèlement cet objet.
L'import ne migre aucune donnée et ne garantit pas que le prochain apply conservera l'existant. Nous découpons donc la reprise en trois temps : découvrir ce qui existe, importer dans le state, puis modifier la configuration seulement après acceptation du premier plan. Cet article décrit ce découpage, le contrôle à chaque étape et la protection du state. Il ne couvre ni IAM, ni politiques de bucket, ni réseau complet (routes, passerelles), ni données.
Le fil conducteur est un exemple fictif : un VPC, un sous-réseau, un groupe de sécurité et un bucket S3 créés en ligne de commande, sans aucune configuration Terraform.
Cartographier ce qui existe avant de décider qui le gère
Importer une ressource, c'est lui donner un gestionnaire unique. Avant toute configuration, la matrice doit indiquer pour chaque objet : le compte, la région, le propriétaire, le mécanisme qui le crée aujourd'hui (console, CLI, autre state, CloudFormation) et l'adresse Terraform cible.
Dans un laboratoire local du 8 octobre 2026, nous avons créé ces objets par CLI, puis les avons inventoriés. Nous avons utilisé Terraform 1.16.5 et le fournisseur hashicorp/aws en version 6.68.0, contre un émulateur local de l'API AWS, moto 5.2.3, et non contre un compte AWS réel. Tous les identifiants et noms sont fictifs. Ce que nous observons vient donc de cet émulateur : sur AWS réel, les valeurs renvoyées par l'API peuvent différer, ce qui modifie certains messages de validation.
| Ressource | Identifiant fictif | Mécanisme actuel | Adresse Terraform cible |
|---|---|---|---|
| VPC | vpc-328508c45d80b3772 | commande CLI create-vpc | aws_vpc.principal |
| Sous-réseau | subnet-4c14804774336e153 | commande CLI create-subnet | aws_subnet.a |
| Groupe de sécurité | sg-6fba0f2746cbef60d | create-security-group, puis authorize-security-group-ingress | aws_security_group.web |
| Bucket S3 | exemple-fictif-reprise-bucket | s3api create-bucket, put-bucket-versioning, put-bucket-tagging | aws_s3_bucket.reprise |
L'inventaire doit lister les sous-objets, pas seulement les ressources visibles dans la console. Dans le laboratoire, la liste de départ comptait quatre lignes, et la configuration finale ajoutait trois adresses : deux règles de groupe de sécurité (entrée HTTPS et sortie) et le versioning du bucket. Une règle de groupe de sécurité s'importe par son propre identifiant de règle, préfixé sgr-, distinct de l'identifiant du groupe. Le versioning, lui, s'importe par le nom du bucket.
Il faut aussi distinguer ce qui doit être importé de ce qui doit seulement être lu. Une source de données lit un objet du fournisseur sans le créer ni le modifier, et Terraform ne peut faire que des opérations de lecture sur elle : elle sert à référencer un objet existant, pas à le reprendre. Enfin, désignez pour la durée de la reprise la personne qui peut modifier chaque ressource en console. Une modification faite à la main pendant l'import ressortira au plan suivant comme une dérive ou une suppression.
Choisir un import lisible et une configuration minimale
Deux chemins existent. La commande terraform import n'écrit que dans le state : il faut avoir écrit à la main le bloc resource avant, et elle ne génère aucune configuration. La documentation recommande le bloc import pour générer la configuration et pour importer plusieurs ressources. Un bloc import s'écrit dans la configuration et se planifie comme une action ordinaire, avant d'être appliqué.
Un bloc import porte deux informations : l'adresse de destination, qui doit correspondre à un bloc resource déclaré, et l'identifiant exigé par le fournisseur.
import {
to = aws_vpc.principal
id = "vpc-328508c45d80b3772"
}Depuis Terraform 1.7.0, for_each permet d'étendre un bloc import à plusieurs instances. Depuis 1.12.0, l'argument identity peut remplacer id, les deux étant exclusifs. Le laboratoire n'utilise ni l'un ni l'autre.
La génération de configuration existe aussi : terraform plan -generate-config-out=PATH écrit le HCL des ressources listées dans un bloc import sans configuration. Terraform la présente comme expérimentale. Dans le laboratoire, le premier essai a affiché ce message :
Warning: Config generation is experimental. Generating configuration during import is currently experimental, and the generated configuration format may change in future versions.Le plan annonçait Plan: 2 to import, 0 to add, 0 to change, 0 to destroy. pour les deux ressources sans configuration, mais la validation échouait sur cinq erreurs concernant le VPC et le sous-réseau générés. Le fichier était écrit malgré l'échec. Nous avons retiré neuf arguments à la main, dont des arguments de zone et de plage IPAM, pour obtenir une validation propre. Le fichier généré présentait trois autres défauts :
- le groupe de sécurité portait ses règles en ligne, alors que le fournisseur déconseille de combiner règles en ligne et ressources de règles séparées ;
- aucune ressource de versioning n'était générée pour le bucket ;
- le fichier est, selon la documentation, la meilleure estimation de Terraform, à relire avant d'être versionné.
Les erreurs de validation dépendent de valeurs renvoyées par l'API. Sur AWS réel, elles peuvent donc être différentes.
Un identifiant erroné échoue plus clairement, mais le message se lit mal. Le laboratoire a testé quatre fautes : identifiant de groupe inexistant, nom de bucket mal orthographié, nom du groupe à la place de son identifiant, identifiant de sous-réseau donné pour un groupe. Les quatre ont produit la même erreur :
Error: Cannot import non-existent remote objectLe plan affichait en plus les ressources non importées comme 1 to add, ce qui est trompeur. Il faut donc lire l'erreur avant le compte du plan.
Lire le premier plan comme un contrôle de non-régression
Le premier plan qui suit des imports ne doit annoncer que des imports. Chaque autre ligne est une décision à expliquer avant tout apply.
| Ligne du plan | Ce qu'elle signifie | Conduite |
|---|---|---|
| Import attendu : « will be imported », sans différence | l'objet existant correspond à la configuration | accepter après revue |
| Mise à jour inattendue : modification sur place d'un objet importé (étiquette, règle, versioning) | la configuration ne décrit pas exactement l'existant | expliquer la différence ; ne pas appliquer pour faire passer le plan |
| Remplacement : « must be replaced » ou « forces replacement » | Terraform détruirait l'objet réel puis le recréerait | arrêter le lot et comparer l'attribut en cause |
| Suppression : élément marqué d'un signe moins, ou valeur ramenée à null | la configuration retire un élément existant | arrêter : la configuration est incomplète |
Le récapitulatif ne compte pas les remplacements à part : un remplacement apparaît comme un ajout et une destruction. Dans le laboratoire, une configuration écrite rapidement, sans étiquettes ni règles, annonçait Plan: 4 to import, 1 to add, 3 to change, 1 to destroy. Le « 1 to add » et le « 1 to destroy » formaient un seul remplacement : le groupe de sécurité dont la description différait (« exemple fictif web » contre « web »), avec la mention « this will destroy the imported resource ». Les trois variantes et la configuration finale donnent le tableau suivant.
| Configuration du laboratoire | Plan annoncé | Lecture |
|---|---|---|
| Variante A : sans étiquettes ni règles | Plan: 4 to import, 1 to add, 3 to change, 1 to destroy. | un remplacement du groupe de sécurité |
| Variante B : sans règle HTTPS ni versioning déclarés | Plan: 4 to import, 0 to add, 0 to change, 0 to destroy. | plan propre, mais sous-objets non décrits |
| Variante C : ingress = [] et egress = [] | Plan: 4 to import, 0 to add, 1 to change, 0 to destroy. | retrait de la règle entrante et de la règle sortante |
| Configuration finale, règles et versioning déclarés | Plan: 7 to import, 0 to add, 0 to change, 0 to destroy. | après application, second plan sans changement |
Le cas B est le plus trompeur. Un plan propre ne prouve rien sur ce qui n'est pas déclaré : Terraform ne compare que ce qu'il connaît. La liste des sous-objets de l'inventaire sert donc de contrôle séparé, chaque règle et chaque versioning recevant une adresse ou une exclusion écrite. Le cas C montre l'inverse : déclarer une liste vide retire ce qui existe. À la création d'un groupe de sécurité dans un VPC, le fournisseur supprime la règle de sortie « allow all » qu'AWS ajoute par défaut. Sur le groupe importé du laboratoire, cette règle n'a pas été retirée tant que egress n'était pas déclaré (variante B) ; seule la liste vide egress = [] l'a retirée (variante C).
Décider de chaque ligne du premier plan
Le premier plan après les blocs import est trié en quatre décisions, dont deux arrêtent le lot.
Schéma défilable horizontalement ; sa version textuelle complète suit.
Lire le schéma sous forme textuelle
- Le premier plan qui suit les blocs import est lu ligne par ligne.
- Un import sans différence est accepté après revue.
- Une modification sur place est expliquée, et on n'applique pas pour faire passer le plan.
- Un remplacement annoncé arrête le lot.
- Un élément retiré arrête le lot, parce que la configuration est incomplète.
Le contrôle ne suffit pas pour les modifications faites hors Terraform après l'import. Dans le laboratoire, nous avons ajouté par CLI une seconde règle entrante (TCP 22) sur le groupe de sécurité, dont les règles étaient des ressources séparées. Le plan annonçait No changes, car la règle inconnue n'est pas vue par le plan classique. Seul terraform plan -refresh-only la montrait dans l'attribut ingress. Une étiquette ajoutée à la main, en revanche, apparaissait au plan : Plan: 0 to add, 1 to change, 0 to destroy. Un contrôle de dérive doit donc inclure un refresh-only, et les règles posées hors Terraform doivent être recherchées explicitement.
Enfin, ne mélangez pas import et modernisation dans le même lot. Un lot qui combine les deux ne permet plus de savoir quelle ligne du plan vient de quelle décision. Dans le laboratoire, la configuration corrigée annonçait Plan: 7 to import, 0 to add, 0 to change, 0 to destroy. ; l'application a importé sept ressources, et le second plan a répondu No changes. Your infrastructure matches the configuration.
Protéger le state et organiser les accès pendant la reprise
Le state relie chaque adresse Terraform à un objet réel. Le protéger, c'est empêcher deux écritures concurrentes, garder une copie récupérable et n'avoir qu'un gestionnaire par objet. Récupérer un fichier d'état et restaurer une ressource sont deux opérations distinctes.
Le backend S3 propose un verrouillage natif depuis Terraform 1.10.0. Il s'active par use_lockfile = true, dont le défaut est false, et crée à côté de l'état un fichier de verrou <clé>.tflock qui doit être couvert par les contrôles d'accès. Le verrouillage par DynamoDB est déprécié selon la documentation du backend, qui annonce son retrait dans une version mineure future. La documentation recommande fortement d'activer le versioning du bucket d'état, pour récupérer un état supprimé ou écrasé par erreur.
Si le verrou ne peut pas être obtenu, Terraform ne poursuit pas l'opération. L'option -lock-timeout réessaie pendant une durée donnée ; la documentation ne fixe pas de valeur par défaut, et le laboratoire a utilisé cinq secondes. Dans le laboratoire, un apply restait en attente de confirmation et tenait le verrou : un fichier terraform.tfstate.tflock apparaissait à côté de l'état. Un second plan avec -lock-timeout=5s a échoué après cinq secondes :
Error: Error acquiring the state lockLe détail de la réponse S3 (une précondition refusée, code 412) vient de l'émulateur et peut différer sur AWS réel. Le fichier de verrou disparaissait à la fin de l'opération détentrice. L'option -lock=false passe sans verrou ; la documentation la juge dangereuse si d'autres commandes peuvent s'exécuter sur le même espace de travail. Elle ne doit pas servir à contourner une attente.
force-unlock sert à lever son propre verrou lorsque le déverrouillage automatique a échoué, mais la documentation avertit que déverrouiller pendant qu'un autre opérateur tient le verrou peut créer plusieurs écrivains. Ce n'est pas une réponse standard à un plan bloqué : il faut d'abord identifier le détenteur du verrou, indiqué dans le bloc de diagnostic, et vérifier que son opération est terminée.
Deux états ne doivent jamais gérer le même objet. Dans le laboratoire, un second dossier, avec un état local distinct, a importé le même groupe de sécurité sous une autre adresse. Terraform n'a émis aucun avertissement. Chaque état a ensuite voulu défaire la modification de l'autre : l'étiquette Team ajoutée par un état était retirée par l'autre, et chaque apply de l'un défaisait le précédent apply de l'autre. Le plan de destruction du second état annonçait la suppression d'un groupe utilisé par le premier. L'inventaire doit donc porter une colonne « gestionnaire », avec un seul nom par ligne.
La commande historique terraform import ADRESSE ID écrit aussitôt dans le state. Dans le laboratoire, elle a répondu Import successful! sans aperçu, et la différence, une étiquette absente, n'est apparue qu'au plan suivant. Le bloc import, au contraire, est planifié avant d'être appliqué.
Pour sortir un objet du state sans le détruire, le bloc removed avec destroy = false est disponible depuis Terraform 1.7.0. Il faut savoir que prevent_destroy fait échouer les plans qui détruiraient l'objet, mais ne protège pas si le bloc de configuration est retiré.
Transmettre un dossier d'exploitation utilisable
Une reprise n'est terminée que lorsque la personne qui intervient ensuite peut rejouer le plan sans aide. Le dossier de transmission contient donc les commandes reproductibles, les versions épinglées, la liste des ressources adoptées avec leur adresse et leur identifiant (sans secret), les exclusions écrites, le prochain plan attendu et le propriétaire de la pipeline et des accès au state.
Épinglez la version de Terraform sur une 1.16.x précise, et le fournisseur AWS sur une version exacte : le laboratoire a utilisé 1.16.5 et = 6.68.0. Le changelog de Terraform 1.16.1 corrige un bug où des blocs import étaient ignorés quand plusieurs blocs visaient des instances différentes d'une ressource utilisant for_each ou count. Une version exacte permet de savoir quelle correction est en vigueur.
La reprise terminée n'est pas le refactoring. Découper la configuration en modules, changer de backend ou réorganiser les adresses sont autant de lots distincts, chacun avec son propre premier plan. Pour le périmètre et l'ordre des chantiers, l'article Platform engineering pour une scale-up : par où commencer sans sur-ingénierie pose la question en amont.
Une fois la reprise faite, la question change : qui exploite, qui intervient, selon quel contrat. Externaliser l'exploitation cloud et Kubernetes : que confier, à qui et avec quel contrat traite ce point. Lorsque la reprise doit être cadrée avec nous, la page reprise d'une infrastructure cloud existante décrit la démarche proposée.
Le tableau suivant résume la décision à prendre à chaque étape.
| Étape | Sortie attendue | Condition d'acceptation |
|---|---|---|
| Inventaire | matrice par ressource et par sous-objet, avec propriétaire et gestionnaire unique | chaque ligne a une adresse ou une exclusion écrite |
| Import | un bloc import par adresse, identifiant vérifié contre la matrice | toute erreur « Cannot import non-existent remote object » arrête le lot |
| Premier plan | quatre lignes classées | seuls des imports attendus ou des différences expliquées ; remplacement et suppression non prévus arrêtent le lot |
| Second plan | No changes après application | preuve de stabilité pour les objets décrits seulement ; les sous-objets non déclarés se vérifient à part |
Sources officielles et limites de lecture
Faits datés
Les pages officielles citées ont été relues le . Les résultats de laboratoire valent pour Terraform 1.16.5 et le fournisseur hashicorp/aws 6.68.0, face à l'émulateur moto 5.2.3.
Vérifié le
- L'émulateur n'est pas AWS. Valeurs par défaut, attributs calculés et textes d'erreur peuvent différer sur AWS réel ; les erreurs de validation du HCL généré et le détail du verrou S3 sont à reconfirmer sur un compte réel.
- Le laboratoire ne couvre ni IAM, ni politiques de bucket, ni instances, ni bases de données, ni routes. Il utilise un seul compte fictif et une seule région, sans concurrence réelle entre opérateurs.
- La génération de configuration est expérimentale selon Terraform : le HCL produit doit être relu et corrigé.
- Le comportement des règles de groupe de sécurité dépend du fournisseur en version 6.68.0 et peut évoluer.
- Les versions de Terraform et du fournisseur changent : relisez la documentation de la version effectivement utilisée.
- Aucune expérience client n'est décrite ici. Les résultats présentés viennent du laboratoire, et non d'une mission.
Sources
- https://developer.hashicorp.com/terraform/language/block/import
- https://developer.hashicorp.com/terraform/language/import
- https://developer.hashicorp.com/terraform/language/import/generating-configuration
- https://developer.hashicorp.com/terraform/cli/import
- https://developer.hashicorp.com/terraform/cli/commands/plan
- https://developer.hashicorp.com/terraform/language/state/locking
- https://developer.hashicorp.com/terraform/language/backend/s3
- https://developer.hashicorp.com/terraform/language/block/removed
- https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle
- https://developer.hashicorp.com/terraform/language/data-sources
- https://raw.githubusercontent.com/hashicorp/terraform/v1.5.0/CHANGELOG.md
- https://raw.githubusercontent.com/hashicorp/terraform/v1.7.0/CHANGELOG.md
- https://raw.githubusercontent.com/hashicorp/terraform/v1.10.0/CHANGELOG.md
- https://raw.githubusercontent.com/hashicorp/terraform/v1.12.0/CHANGELOG.md
- https://raw.githubusercontent.com/hashicorp/terraform/v1.16.5/CHANGELOG.md
- https://raw.githubusercontent.com/hashicorp/terraform-provider-aws/v6.68.0/website/docs/r/security_group.html.markdown
- https://raw.githubusercontent.com/hashicorp/terraform-provider-aws/v6.68.0/website/docs/r/s3_bucket.html.markdown
- https://raw.githubusercontent.com/hashicorp/terraform-provider-aws/v6.68.0/website/docs/r/vpc.html.markdown
- https://raw.githubusercontent.com/hashicorp/terraform-provider-aws/v6.68.0/website/docs/r/subnet.html.markdown
- https://raw.githubusercontent.com/hashicorp/terraform-provider-aws/v6.68.0/website/docs/r/vpc_security_group_ingress_rule.html.markdown
- https://raw.githubusercontent.com/hashicorp/terraform-provider-aws/v6.68.0/website/docs/r/s3_bucket_versioning.html.markdown