provider-resources

Par hashicorp · agent-skills

Implémentez des ressources et des sources de données pour les providers Terraform en utilisant le Plugin Framework. À utiliser lors du développement d'opérations CRUD, de la conception de schémas, de la gestion d'état et des tests d'acceptation pour les ressources d'un provider.

npx skills add https://github.com/hashicorp/agent-skills --skill provider-resources

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, &notFound) {
            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, &notFound) {
    // 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

Références

Skills similaires