Guide d'implémentation des actions du fournisseur Terraform
Aperçu
Les actions Terraform permettent les opérations impératives pendant le cycle de vie Terraform. Les actions sont des fonctionnalités expérimentales qui permettent d'effectuer des opérations du fournisseur à des événements de cycle de vie spécifiques (avant/après création, mise à jour, suppression).
Références :
Configuration de la première action
Lors de l'ajout de la première action à un fournisseur qui n'en a jamais eu, plusieurs étapes d'échafaudage ponctuelles sont requises :
- Implémenter
ProviderWithActions— ajouter une méthodeActions()au fournisseur qui retourne[]func() action.Action. - Définir
ActionDatadansConfigure— la méthodeConfiguredu fournisseur doit définirresp.ActionData = vaux côtés des affectations existantesResourceData,DataSourceDataetEphemeralResourceData. - Créer le type de base
ActionWithConfigure— si le fournisseur utilise des types de base imbriqués (par ex.ResourceWithConfigure), créer un type équivalentActionWithConfigureimplémentantaction.ConfigureRequest/action.ConfigureResponse. - Variantes d'assistant de schéma d'action — si le fournisseur injecte des attributs de schéma courants (par ex.
namespace) via des fonctions d'assistant, des variantes de schéma d'action sont nécessaires car les typesaction/schemadiffèrent des typesresource/schema.
Structure des fichiers
La plupart des fournisseurs conservent les actions aux côtés des ressources dans le package du fournisseur :
internal/provider/
├── <action_name>_action.go # Implémentation de l'action
└── <action_name>_action_test.go # Tests d'action
(Les fournisseurs multi-services volumineux utilisent des packages internal/service/<service>/ à la place — suivre la disposition du dépôt cible.)
La documentation se trouve avec les autres docs générées :
docs/actions/
└── <action_name>.md # Documentation destinée aux utilisateurs
(Certains anciens fournisseurs volumineux écrivent manuellement website/docs/actions/<name>.html.markdown — correspondre au dépôt.)
Définition du schéma d'action
Les actions utilisent le Terraform Plugin Framework avec un modèle de schéma standard :
func (a *actionType) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
// Paramètres de configuration requis
"resource_id": schema.StringAttribute{
Required: true,
Description: "ID de la ressource à utiliser",
},
// Paramètres optionnels avec defaults
"timeout": schema.Int64Attribute{
Optional: true,
Description: "Délai d'expiration de l'opération en secondes",
Default: int64default.StaticInt64(1800),
Computed: true,
},
},
}
}
Problèmes courants du schéma
Accordez une attention particulière à la définition du schéma - problèmes courants après un premier brouillon :
-
Incompatibilités de types
- Les structs de modèle utilisent
types.String/types.Int64et les schémas utilisenttypes.StringTypedegithub.com/hashicorp/terraform-plugin-framework/types— ne mélanger les types d'autres packages - Certains fournisseurs volumineux superposent leur propre package de type personnalisé (par ex.
fwtypesinterne de terraform-provider-aws) ; dans un tel dépôt, suivre sa convention de manière cohérente au lieu des types simples
- Les structs de modèle utilisent
-
Types d'éléments List/Map
// MAUVAIS - ElementType manquant "items": schema.ListAttribute{ Optional: true, } // CORRECT "items": schema.ListAttribute{ Optional: true, ElementType: types.StringType, } -
Computed vs Optional
- Les attributs avec des defaults doivent être à la fois
Optional: trueetComputed: true - Ne pas marquer les entrées d'action comme
Computedsauf si elles ont des defaults
- Les attributs avec des defaults doivent être à la fois
-
Imports de validateurs
// Assurer les imports appropriés "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" -
Attribut Région/Fournisseur (fournisseurs multi-régions, par ex. AWS)
- Utiliser la gestion de région partagée du fournisseur quand elle existe
- Ne pas redéfinir manuellement la configuration au niveau du fournisseur dans un schéma d'action
-
Attributs imbriqués
- Utiliser les types d'objets imbriqués appropriés pour les structures complexes
- S'assurer que les types imbriqués sont correctement définis
Liste de vérification de validation du schéma
Avant de soumettre, vérifier :
- [ ] Tous les attributs ont des descriptions
- [ ] Les attributs List/Map ont ElementType défini
- [ ] Les validateurs sont importés et appliqués correctement
- [ ] La struct de modèle utilise les types de framework corrects
- [ ] Les attributs optionnels avec des defaults sont marqués Computed
- [ ] Le code compile sans erreurs de type
- [ ] Exécuter
go buildpour identifier les incompatibilités de type
Méthode Invoke d'action
La méthode Invoke contient la logique d'action :
func (a *actionType) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
var data actionModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// a.client a été stocké par Configure (à partir de req.ProviderData), le même
// modèle que les ressources utilisent.
resp.SendProgress(action.InvokeProgressEvent{Message: "Démarrage de l'opération..."})
// Implémenter la logique d'action avec gestion des erreurs
// Utiliser context pour la gestion du délai d'expiration
// Interroger la fin si opération asynchrone
resp.SendProgress(action.InvokeProgressEvent{Message: "Opération terminée"})
}
Exigences clés de l'implémentation
1. Rapports de progression
- Utiliser
resp.SendProgress(action.InvokeProgressEvent{...})pour les mises à jour en temps réel - Fournir des messages de progression significatifs lors d'opérations longues
- Mettre à jour la progression à des jalons clés
- Inclure le temps écoulé pour les opérations longues
2. Gestion du délai d'expiration
- Toujours inclure un paramètre de délai d'expiration configurable (défaut : 1800 s)
- Utiliser
context.WithTimeout()pour les appels API - Gérer les erreurs de délai d'expiration gracieusement
- Valider les plages de délai d'expiration (généralement 60-7200 secondes)
3. Gestion des erreurs
- Ajouter des diagnostics avec
resp.Diagnostics.AddError() - Fournir des messages d'erreur clairs avec contexte
- Inclure les détails d'erreur API le cas échéant
- Mapper les types d'erreur du fournisseur à des messages conviviaux
- Documenter tous les cas d'erreur possibles
Exemple de gestion des erreurs :
// Gérer les erreurs spécifiques
var notFound *types.ResourceNotFoundException
if errors.As(err, ¬Found) {
resp.Diagnostics.AddError(
"Ressource non trouvée",
fmt.Sprintf("La ressource %s n'a pas été trouvée", resourceID),
)
return
}
// Gestion générique des erreurs
resp.Diagnostics.AddError(
"Opération échouée",
fmt.Sprintf("Impossible de terminer l'opération pour %s : %s", resourceID, err),
)
4. Intégration du SDK du fournisseur
- Utiliser le client API stocké au moment de Configure (
a.client), partagé avec les ressources et sources de données - Gérer la pagination pour les opérations de liste
- Implémenter la logique de retry pour les défaillances transitoires
- Utiliser les types d'erreur appropriés
5. Validation des paramètres
- Utiliser les validateurs framework pour la validation des entrées
- Valider l'existence des ressources avant les opérations
- Vérifier les paramètres en conflit
- Valider par rapport aux exigences de nommage du fournisseur
6. Interrogation et attente
Pour les opérations qui nécessitent d'attendre la fin, interroger sur un ticker sous une deadline de contexte, en signalant la progression au fur et à mesure. (Vous pouvez également utiliser retry.StateChangeConf de github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry, la même primitive waiter que les ressources utilisent.)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
// Interroger rapidement, signaler lentement : les événements de progression traversent le protocol plugin, donc
// les étrangler au lieu d'en émettre un par interrogation.
start := time.Now()
var lastProgress time.Time
for {
res, err := findResource(ctx, a.client, id)
if err != nil {
resp.Diagnostics.AddError("Erreur lors de l'interrogation de l'opération", fmt.Sprintf("vérification du statut de %s : %s", id, err))
return
}
switch res.Status {
case "AVAILABLE", "COMPLETED":
resp.SendProgress(action.InvokeProgressEvent{Message: "Opération terminée"})
return
case "CREATING", "PENDING":
if time.Since(lastProgress) >= 30*time.Second {
lastProgress = time.Now()
resp.SendProgress(action.InvokeProgressEvent{
Message: fmt.Sprintf("Statut : %s, Temps écoulé : %v", res.Status, time.Since(start).Round(time.Second)),
})
}
default:
resp.Diagnostics.AddError("Opération échouée", fmt.Sprintf("%s est entré dans un statut inattendu %q", id, res.Status))
return
}
select {
case <-ctx.Done():
resp.Diagnostics.AddError("Opération expirée", fmt.Sprintf("%s ne s'est pas terminé dans %v", id, timeout))
return
case <-ticker.C:
}
}
Modèles d'action courants
Opérations par lot
- Traiter les éléments par lots configurables
- Signaler la progression par lot
- Gérer les défaillances partielles gracieusement
- Supporter les paramètres de préfixe/filtre
Exécution de commande
- Soumettre la commande et obtenir l'ID d'opération
- Interroger le statut de fin
- Récupérer et signaler la sortie
- Gérer le délai d'expiration lors de l'interrogation
- Valider l'existence des ressources avant l'exécution
Invocation de service
- Invoquer le service avec les paramètres
- Attendre la fin (si synchrone)
- Retourner la sortie/résultats
- Gérer les erreurs spécifiques au service
Changements d'état de ressource
- Valider l'état actuel
- Appliquer le changement d'état
- Interroger l'état cible
- Gérer les états transitoires
Soumission de travail asynchrone
- Soumettre le travail avec configuration
- Obtenir l'ID du travail
- Éventuellement attendre la fin
- Signaler le statut du travail
Déclencheurs d'action
Les actions sont invoquées via les blocs de cycle de vie action_trigger dans les configurations Terraform. Un bloc action autonome sans déclencheur correspondant est déclaré mais jamais exécuté.
Syntaxe HCL
Les paramètres d'action doivent être enveloppés dans un bloc config {}. Les références de déclencheur utilisent le préfixe action., et actions est une liste. Les événements sont des identifiants nus, pas des chaînes entre guillemets.
action "provider_service_action" "name" {
config {
parameter = value
}
}
resource "terraform_data" "trigger" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.provider_service_action.name]
}
}
}
Événements de déclencheur disponibles
Événements supportés (à partir de Terraform 1.14) :
before_create- Avant la création de la ressourceafter_create- Après la création de la ressourcebefore_update- Avant la mise à jour de la ressourceafter_update- Après la mise à jour de la ressource
Non supportés (à partir de Terraform 1.14 ; vérifier les notes de version actuelles) :
before_destroy- Non disponible (causera une erreur de validation)after_destroy- Non disponible (causera une erreur de validation)
Tester les actions
Tests d'acceptation
- Tester l'invocation d'action avec des paramètres valides
- Tester les scénarios de délai d'expiration
- Tester les conditions d'erreur
- Vérifier les changements d'état du fournisseur
- Tester la signalisation de la progression
- Tester avec des paramètres personnalisés
- Tester l'invocation basée sur déclencheur
Modèle de test
func TestAccExampleAction_basic(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_14_0),
},
Steps: []resource.TestStep{
{
Config: testAccActionConfig_basic(),
ConfigStateChecks: []statecheck.StateCheck{
// vérifier l'effet observable de l'action sur la
// ressource de déclenchement
},
},
},
})
}
Nettoyage des tests avec fonctions de balayage
Les actions invoquées dans les tests peuvent laisser des ressources réelles derrière elles ; enregistrer des sweepers (liste → filtrer les noms avec préfixe de test → supprimer) pour que les ressources fuites soient nettoyables. Les sweepers ne sont pas spécifiques à une action — utiliser la skill provider-test-patterns (si disponible) pour le modèle de fonction de balayage, l'enregistrement, TestMain et l'ordre des dépendances.
Utiliser terraform_data comme déclencheur sans opération
terraform_data peut servir de ressource de déclenchement sans opération pour les tests d'action qui n'ont pas besoin d'infrastructure réelle. Ceci est précieux pour les tests de cas d'erreur et de validation :
resource "terraform_data" "trigger" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.provider_service_action.test]
}
}
}
action "provider_service_action" "test" {
config {
param = "invalid-value"
}
}
Utiliser PostApplyFunc pour vérifier les effets secondaires
Les actions ne produisent pas d'état qui peut être vérifiée avec resource.TestCheckResourceAttr. Utiliser PostApplyFunc sur resource.TestStep pour interroger l'API après l'application et confirmer que l'action a produit l'effet secondaire attendu :
Steps: []resource.TestStep{
{
Config: testConfig,
PostApplyFunc: func() {
// interroger l'API pour vérifier que l'effet secondaire de l'action s'est produit
},
},
},
Meilleures pratiques de test
Prérequis spécifiques au service
- Toujours vérifier les prérequis spécifiques au service qui doivent être remplis avant que les actions puissent réussir
- Documenter les prérequis dans la documentation de l'action et les configurations de test
Correspondance de modèle d'erreur
- Terraform enveloppe les erreurs d'action avec contexte supplémentaire
- Utiliser des modèles regex flexibles :
regexp.MustCompile(\(?s)Error Title.*key phrase`)`
Modèles de test non applicables aux actions
- Les actions se déclenchent sur des événements de cycle de vie, pas sur la réapplication de configuration
- Tests Before/After Destroy : Non supportés à partir de Terraform 1.14
Exécution des tests
Vérifier la compilation en premier, puis exécuter le test d'acceptation ciblé :
go test -c -o /dev/null ./internal/provider
TF_ACC=1 go test ./internal/provider -run TestAccExampleAction_ -timeout 60m
Utiliser la skill run-acceptance-tests (si disponible) pour la configuration des variables d'environnement, le débogage des tests échouant et les exécutions de sweeper.
Normes de documentation
Générer la documentation d'action avec tfplugindocs là où le fournisseur l'utilise (utiliser la skill provider-docs, si disponible, pour ce workflow). Chaque page de documentation d'action doit inclure :
-
Frontmatter (mises en page héritées écrites manuellement uniquement)
--- subcategory: "Service Name" layout: "provider" page_title: "Provider: provider_service_action" description: |- Description brève de ce que fait l'action. --- -
En-tête avec avertissements
- Avis sur statut Beta/Alpha concernant le statut expérimental
- Avertissement sur les conséquences involontaires potentielles
- Lien vers la documentation du fournisseur
-
Exemple d'utilisation
- Exemple d'utilisation basique
- Utilisation avancée avec toutes les options
- Exemple basé sur déclencheur avec
terraform_data - Exemples de cas d'usage réels
-
Référence des arguments
- Lister tous les arguments requis et optionnels
- Inclure les descriptions et les defaults
- Noter toutes les règles de validation
-
Linting de documentation (outillage optionnel)
- Si le dépôt utilise
terrafmt, exécuterterrafmt fmtavant la soumission et vérifier avecterrafmt diff
- Si le dépôt utilise
Format d'entrée de journal des modifications (convention spécifique au fournisseur)
Certains fournisseurs (par ex. terraform-provider-aws) suivent les notes de version avec go-changelog : un fichier par PR dans un répertoire .changelog/. Vérifier le guide CONTRIBUTING du dépôt cible ; ignorer si le dépôt ne l'utilise pas.
.changelog/<pr_number>.txt
Format du contenu :
action/provider_service_action: Brève description de l'action
Liste de vérification avant soumission
Avant de soumettre votre implémentation d'action :
- [ ] Le code compile :
go build -o /dev/null . - [ ] Les tests compilent :
go test -c -o /dev/null ./internal/provider - [ ] Le code est formaté :
gofmt(oumake fmtdu dépôt) - [ ] La documentation est générée ou formatée selon la convention du dépôt
- [ ] L'entrée de journal des modifications est créée (si le dépôt l'utilise)
- [ ] Le schéma utilise les types corrects
- [ ] Tous les attributs List/Map ont ElementType
- [ ] Les mises à jour de progression sont implémentées pour les opérations longues
- [ ] Les messages d'erreur incluent le contexte et les identifiants de ressource
- [ ] La documentation inclut plusieurs exemples
- [ ] La documentation inclut les prérequis et avertissements
Références
- Documentation du Terraform Plugin Framework
- Développement de fournisseur Terraform
- terraform-plugin-framework GitHub
- terraform-plugin-testing
- Writing a Terraform Action (blog)
- Implémentations de référence :
terraform-provider-tfe(action_query_run.go,action_query_run_test.go),terraform-provider-vault(action_rotate_root.go)