Guide d'implémentation des ressources Terraform Provider
Présentation
Ce guide couvre le développement de ressources Terraform Provider et de sources de données en utilisant Terraform Plugin Framework. Les ressources représentent des objets d'infrastructure que Terraform gère via des opérations Create, Read, Update et Delete (CRUD).
Références :
Structure des fichiers
Les ressources suivent la structure standard du package service :
internal/service/<service>/
├── <resource_name>.go # Implémentation de la ressource
├── <resource_name>_test.go # Tests d'acceptation
├── <resource_name>_data_source.go # Source de données (le cas échéant)
├── find.go # Fonctions de recherche
├── exports_test.go # Exports de test
└── service_package_gen.go # Enregistrement auto-généré
Structure de documentation :
website/docs/r/
└── <service>_<resource_name>.html.markdown # Documentation de ressource
website/docs/d/
└── <service>_<resource_name>.html.markdown # Documentation de source de données
Structure de ressource
Modèle SDKv2
func ResourceExample() *schema.Resource {
return &schema.Resource{
CreateWithoutTimeout: resourceExampleCreate,
ReadWithoutTimeout: resourceExampleRead,
UpdateWithoutTimeout: resourceExampleUpdate,
DeleteWithoutTimeout: resourceExampleDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringLenBetween(1, 255),
},
"arn": {
Type: schema.TypeString,
Computed: true,
},
"tags": tftags.TagsSchema(),
"tags_all": tftags.TagsSchemaComputed(),
},
CustomizeDiff: verify.SetTagsDiff,
}
}
Modèle Plugin Framework
type resourceExample struct {
framework.ResourceWithConfigure
}
func (r *resourceExample) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_example"
}
func (r *resourceExample) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": framework.IDAttribute(),
"name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthBetween(1, 255),
},
},
"arn": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
Opérations CRUD
Opération Create
func (r *resourceExample) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data resourceExampleModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
conn := r.Meta().ExampleClient(ctx)
input := &example.CreateExampleInput{
Name: data.Name.ValueStringPointer(),
}
output, err := conn.CreateExample(ctx, input)
if err != nil {
resp.Diagnostics.AddError(
"Error creating Example",
fmt.Sprintf("Could not create example %s: %s", data.Name.ValueString(), err),
)
return
}
data.ID = types.StringPointerValue(output.Id)
data.ARN = types.StringPointerValue(output.Arn)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
Opération Read
func (r *resourceExample) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data resourceExampleModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
conn := r.Meta().ExampleClient(ctx)
output, err := findExampleByID(ctx, conn, data.ID.ValueString())
if tfresource.NotFound(err) {
resp.Diagnostics.AddWarning(
"Resource not found",
fmt.Sprintf("Example %s not found, removing from state", data.ID.ValueString()),
)
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.AddError(
"Error reading Example",
fmt.Sprintf("Could not read example %s: %s", data.ID.ValueString(), err),
)
return
}
data.Name = types.StringPointerValue(output.Name)
data.ARN = types.StringPointerValue(output.Arn)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
Opération Update
func (r *resourceExample) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state resourceExampleModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
conn := r.Meta().ExampleClient(ctx)
if !plan.Description.Equal(state.Description) {
input := &example.UpdateExampleInput{
Id: plan.ID.ValueStringPointer(),
Description: plan.Description.ValueStringPointer(),
}
_, err := conn.UpdateExample(ctx, input)
if err != nil {
resp.Diagnostics.AddError(
"Error updating Example",
fmt.Sprintf("Could not update example %s: %s", plan.ID.ValueString(), err),
)
return
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
Opération Delete
func (r *resourceExample) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data resourceExampleModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
conn := r.Meta().ExampleClient(ctx)
_, err := conn.DeleteExample(ctx, &example.DeleteExampleInput{
Id: data.ID.ValueStringPointer(),
})
if tfresource.NotFound(err) {
return
}
if err != nil {
resp.Diagnostics.AddError(
"Error deleting Example",
fmt.Sprintf("Could not delete example %s: %s", data.ID.ValueString(), err),
)
return
}
}
Conception du schéma
Types d'attributs
| Type Terraform | Type Framework | Cas d'usage |
|---|---|---|
string |
schema.StringAttribute |
Noms, ARN, ID |
number |
schema.Int64Attribute, schema.Float64Attribute |
Compteurs, tailles |
bool |
schema.BoolAttribute |
Drapeaux de fonctionnalité |
list |
schema.ListAttribute |
Collections ordonnées |
set |
schema.SetAttribute |
Éléments uniques non ordonnés |
map |
schema.MapAttribute |
Paires clé-valeur |
object |
schema.SingleNestedAttribute |
Configuration imbriquée complexe |
Modificateurs de plan
// Forcer le remplacement lors d'un changement de valeur
stringplanmodifier.RequiresReplace()
// Préserver la valeur inconnue pendant le plan
stringplanmodifier.UseStateForUnknown()
// Modificateur de plan personnalisé
stringplanmodifier.RequiresReplaceIf(
func(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) {
// Logique personnalisée
},
"description",
"description markdown",
)
Validateurs
// Validateurs de chaîne
stringvalidator.LengthBetween(1, 255)
stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9-]+$`), "must be lowercase alphanumeric with hyphens")
stringvalidator.OneOf("option1", "option2", "option3")
// Validateurs Int64
int64validator.Between(1, 100)
int64validator.AtLeast(1)
int64validator.AtMost(1000)
// Validateurs de liste
listvalidator.SizeAtLeast(1)
listvalidator.SizeAtMost(10)
Attributs sensibles
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(8),
},
}
Gestion d'état
Gestion de ressource introuvable
func findExampleByID(ctx context.Context, conn *example.Client, id string) (*example.Example, error) {
input := &example.GetExampleInput{
Id: &id,
}
output, err := conn.GetExample(ctx, input)
if err != nil {
var notFound *types.ResourceNotFoundException
if errors.As(err, ¬Found) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}
return nil, err
}
if output == nil || output.Example == nil {
return nil, tfresource.NewEmptyResultError(input)
}
return output.Example, nil
}
Attendre les changements d'état de ressource
func waitExampleCreated(ctx context.Context, conn *example.Client, id string, timeout time.Duration) (*example.Example, error) {
stateConf := &retry.StateChangeConf{
Pending: []string{"CREATING", "PENDING"},
Target: []string{"ACTIVE", "AVAILABLE"},
Refresh: statusExample(ctx, conn, id),
Timeout: timeout,
}
outputRaw, err := stateConf.WaitForStateContext(ctx)
if output, ok := outputRaw.(*example.Example); ok {
return output, err
}
return nil, err
}
func statusExample(ctx context.Context, conn *example.Client, id string) retry.StateRefreshFunc {
return func() (interface{}, string, error) {
output, err := findExampleByID(ctx, conn, id)
if tfresource.NotFound(err) {
return nil, "", nil
}
if err != nil {
return nil, "", err
}
return output, string(output.Status), nil
}
}
Tests
Test d'acceptation de base
func TestAccExampleResource_basic(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "provider_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckExampleDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccExampleConfig_basic(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckExampleExists(ctx, resourceName),
resource.TestCheckResourceAttr(resourceName, "name", rName),
resource.TestCheckResourceAttrSet(resourceName, "arn"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
Test de disparition
func TestAccExampleResource_disappears(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "provider_example.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckExampleDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccExampleConfig_basic(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckExampleExists(ctx, resourceName),
acctest.CheckResourceDisappears(ctx, acctest.Provider, ResourceExample(), resourceName),
),
ExpectNonEmptyPlan: true,
},
},
})
}
Fonctions d'aide aux tests
func testAccCheckExampleExists(ctx context.Context, name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
conn := acctest.Provider.Meta().(*conns.Client).ExampleClient(ctx)
_, err := findExampleByID(ctx, conn, rs.Primary.ID)
return err
}
}
func testAccCheckExampleDestroy(ctx context.Context) resource.TestCheckFunc {
return func(s *terraform.State) error {
conn := acctest.Provider.Meta().(*conns.Client).ExampleClient(ctx)
for _, rs := range s.RootModule().Resources {
if rs.Type != "provider_example" {
continue
}
_, err := findExampleByID(ctx, conn, rs.Primary.ID)
if tfresource.NotFound(err) {
continue
}
if err != nil {
return err
}
return fmt.Errorf("Example %s still exists", rs.Primary.ID)
}
return nil
}
}
Exécution des tests
# Compiler les tests
go test -c -o /dev/null ./internal/service/<service>
# Exécuter les tests d'acceptation
TF_ACC=1 go test ./internal/service/<service> -run TestAccExample -v -timeout 60m
# Exécuter avec une version spécifique du provider
TF_ACC=1 go test ./internal/service/<service> -run TestAccExample -v
# Exécuter le sweeper pour nettoyer
TF_ACC=1 go test ./internal/service/<service> -sweep=<region> -v
Gestion des erreurs
Modèles d'erreur courants
// Gérer les erreurs API spécifiques
var notFound *types.ResourceNotFoundException
if errors.As(err, ¬Found) {
// La ressource n'existe pas
}
var conflict *types.ConflictException
if errors.As(err, &conflict) {
// Conflit d'état de ressource
}
var throttle *types.ThrottlingException
if errors.As(err, &throttle) {
// Limité en débit - le SDK gère la tentative
}
Diagnostics
// Ajouter une erreur
resp.Diagnostics.AddError(
"Error creating resource",
fmt.Sprintf("Could not create resource: %s", err),
)
// Ajouter un avertissement
resp.Diagnostics.AddWarning(
"Resource modified outside Terraform",
"Resource was modified outside of Terraform, state may be inconsistent",
)
// Ajouter une erreur d'attribut
resp.Diagnostics.AddAttributeError(
path.Root("name"),
"Invalid name",
"Name must be lowercase alphanumeric",
)
Normes de documentation
Documentation de ressource
---
subcategory: "Service Name"
layout: "provider"
page_title: "Provider: provider_example"
description: |-
Manages an Example resource.
---
# Resource: provider_example
Manages an Example resource.
## Example Usage
### Basic Usage
\```hcl
resource "provider_example" "example" {
name = "my-example"
}
\```
## Argument Reference
* `name` - (Required) Name of the example.
* `description` - (Optional) Description of the example.
## Attribute Reference
* `id` - ID of the example.
* `arn` - ARN of the example.
## Import
Example can be imported using the ID:
\```
$ terraform import provider_example.example example-id-12345
\```
Checklist pré-soumission
- [ ] Le code compile sans erreurs
- [ ] Tous les tests passent localement
- [ ] La ressource a toutes les opérations CRUD implémentées
- [ ] L'import est implémenté et testé
- [ ] Le test de disparition est inclus
- [ ] La documentation est complète avec exemples
- [ ] Les messages d'erreur sont clairs et actionnables
- [ ] Les attributs sensibles sont marqués
- [ ] Les modificateurs de plan sont appropriés
- [ ] Les validateurs couvrent les cas limites