Comprehensive documentation and best practices for building Terraform providers with terraform-plugin-framework (v1.17.0). Covers providers, resources, schemas, types, validators, testing, and common pitfalls.
Overall
score
97%
Design resource and data source schemas with attributes, blocks, nested structures, and modifiers.
Schemas define the structure of resources and data sources. They specify:
func (r *PetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages a pet resource",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Unique identifier",
Computed: true,
},
"name": schema.StringAttribute{
Description: "Pet name",
Required: true,
},
},
}
}func (d *PetDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Fetches pet data",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Pet ID to lookup",
Required: true,
},
"name": schema.StringAttribute{
Description: "Pet name",
Computed: true,
},
},
}
}Attributes: map[string]schema.Attribute{
// String
"name": schema.StringAttribute{
Required: true,
},
// Int64
"age": schema.Int64Attribute{
Optional: true,
},
// Float64
"weight": schema.Float64Attribute{
Optional: true,
},
// Bool
"active": schema.BoolAttribute{
Optional: true,
},
// Number (arbitrary precision)
"price": schema.NumberAttribute{
Optional: true,
},
}Attributes: map[string]schema.Attribute{
// List of strings
"tags": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
},
// Set of strings
"roles": schema.SetAttribute{
ElementType: types.StringType,
Optional: true,
},
// Map of string to string
"labels": schema.MapAttribute{
ElementType: types.StringType,
Optional: true,
},
}Attributes: map[string]schema.Attribute{
// Single nested object
"address": schema.SingleNestedAttribute{
Optional: true,
Attributes: map[string]schema.Attribute{
"street": schema.StringAttribute{
Required: true,
},
"city": schema.StringAttribute{
Required: true,
},
"zipcode": schema.StringAttribute{
Optional: true,
},
},
},
// List of nested objects
"contacts": schema.ListNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"email": schema.StringAttribute{
Required: true,
},
"phone": schema.StringAttribute{
Optional: true,
},
},
},
},
// Set of nested objects
"permissions": schema.SetNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"resource": schema.StringAttribute{
Required: true,
},
"action": schema.StringAttribute{
Required: true,
},
},
},
},
// Map of nested objects
"metadata": schema.MapNestedAttribute{
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"value": schema.StringAttribute{
Required: true,
},
"description": schema.StringAttribute{
Optional: true,
},
},
},
},
}For values with unknown structure at schema definition time:
"config": schema.DynamicAttribute{
Description: "Arbitrary configuration",
Optional: true,
},See Type System for details on working with dynamic types.
Attributes: map[string]schema.Attribute{
// Required: Must be set by user
"name": schema.StringAttribute{
Required: true,
},
// Optional: May be set by user
"description": schema.StringAttribute{
Optional: true,
},
// Computed: Set by provider (read-only for user)
"id": schema.StringAttribute{
Computed: true,
},
// Optional + Computed: User can set or provider computes
"status": schema.StringAttribute{
Optional: true,
Computed: true,
},
}Rules:
Required, Optional, Computed must be trueOptional and ComputedRequired and ComputedMark attributes as sensitive to mask in logs:
"api_key": schema.StringAttribute{
Required: true,
Sensitive: true, // Masked as (sensitive value) in logs
},Mark attributes as deprecated:
"old_field": schema.StringAttribute{
Optional: true,
DeprecationMessage: "Use new_field instead. old_field will be removed in v2.0.0",
},Provide default values for optional attributes:
import "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults"
"timeout": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(30),
},
"enabled": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"environment": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("production"),
},type EnvironmentDefault struct{}
func (d EnvironmentDefault) Description(ctx context.Context) string {
return "Defaults to 'dev' in development, 'prod' in production"
}
func (d EnvironmentDefault) MarkdownDescription(ctx context.Context) string {
return "Defaults to `dev` in development, `prod` in production"
}
func (d EnvironmentDefault) DefaultString(ctx context.Context, req defaults.StringRequest, resp *defaults.StringResponse) {
if os.Getenv("APP_ENV") == "production" {
resp.PlanValue = types.StringValue("prod")
} else {
resp.PlanValue = types.StringValue("dev")
}
}
// Use in schema
"environment": schema.StringAttribute{
Optional: true,
Computed: true,
Default: EnvironmentDefault{},
},Add validation logic to attributes:
import "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
import "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.LengthAtMost(100),
},
},
"port": schema.Int64Attribute{
Required: true,
Validators: []validator.Int64{
int64validator.Between(1, 65535),
},
},
"protocol": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("http", "https", "tcp", "udp"),
},
},
}See Validators for comprehensive validation patterns.
Control plan behavior for computed attributes:
import "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
import "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
// Use state value if unknown during plan
stringplanmodifier.UseStateForUnknown(),
},
},
"region": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
// Require replacement if region changes
stringplanmodifier.RequiresReplace(),
},
},
}See Plan Modifiers for details.
Blocks are repeatable nested configuration sections. Use when users need multiple instances:
func (r *ServerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Blocks: map[string]schema.Block{
"disk": schema.ListNestedBlock{
Description: "Disks attached to server",
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"size": schema.Int64Attribute{
Required: true,
},
"type": schema.StringAttribute{
Optional: true,
},
},
},
},
},
}
}Usage in HCL:
resource "example_server" "web" {
name = "web-server"
disk {
name = "boot"
size = 50
type = "ssd"
}
disk {
name = "data"
size = 500
type = "hdd"
}
}Blocks: map[string]schema.Block{
// List: Ordered, repeatable
"volume": schema.ListNestedBlock{
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"size": schema.Int64Attribute{Required: true},
},
},
},
// Set: Unordered, repeatable, unique
"tag": schema.SetNestedBlock{
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"key": schema.StringAttribute{Required: true},
"value": schema.StringAttribute{Required: true},
},
},
},
// Single: Non-repeatable
"logging": schema.SingleNestedBlock{
Attributes: map[string]schema.Attribute{
"enabled": schema.BoolAttribute{Required: true},
"level": schema.StringAttribute{Optional: true},
},
},
}| Feature | Nested Attributes | Blocks |
|---|---|---|
| HCL Syntax | attr = { ... } | block { ... } |
| Repetition | List/Set/Map attribute | Block repetition |
| Use Case | Simple nesting | Complex repeatable config |
| Assignability | Can assign from variables | Cannot assign from variables |
Use nested attributes for simple nested data:
resource "example_server" "web" {
config = {
memory = 4096
cpus = 2
}
}Use blocks for repeatable configuration:
resource "example_server" "web" {
disk {
size = 50
}
disk {
size = 100
}
}resp.Schema = schema.Schema{
Description: "Manages a server instance",
MarkdownDescription: "Manages a server instance.\n\n" +
"Servers are virtual machines with configurable resources.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "Server name",
MarkdownDescription: "Server name. Must be unique within the account.",
Required: true,
},
},
}Description: Plain text for CLI helpMarkdownDescription: Markdown for generated documentationIndicate schema version for compatibility tracking:
resp.Schema = schema.Schema{
Version: 1, // Increment when making schema changes
// ...
}type ServerModel struct {
Name types.String `tfsdk:"name"`
Disks []DiskModel `tfsdk:"disk"`
}
type DiskModel struct {
Name types.String `tfsdk:"name"`
Size types.Int64 `tfsdk:"size"`
Type types.String `tfsdk:"type"`
}
func (r *ServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan ServerModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
// Access values
name := plan.Name.ValueString()
// Iterate blocks
for _, disk := range plan.Disks {
diskName := disk.Name.ValueString()
diskSize := disk.Size.ValueInt64()
// ...
}
}func (r *ServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// ... create server via API ...
state := ServerModel{
ID: types.StringValue(server.ID),
Name: types.StringValue(server.Name),
Disks: make([]DiskModel, len(server.Disks)),
}
for i, disk := range server.Disks {
state.Disks[i] = DiskModel{
Name: types.StringValue(disk.Name),
Size: types.Int64Value(disk.Size),
Type: types.StringValue(disk.Type),
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}"id": schema.StringAttribute{
Description: "Unique identifier",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},"region": schema.StringAttribute{
Description: "Deployment region (immutable)",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},"status": schema.StringAttribute{
Description: "Server status (defaults to 'running')",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("running"),
},"password": schema.StringAttribute{
Description: "Admin password",
Required: true,
Sensitive: true,
},"tags": schema.MapAttribute{
Description: "Resource tags",
ElementType: types.StringType,
Optional: true,
},Usage:
resource "example_server" "web" {
name = "web-1"
tags = {
environment = "production"
team = "platform"
}
}"network": schema.SingleNestedAttribute{
Description: "Network configuration",
Required: true,
Attributes: map[string]schema.Attribute{
"vpc_id": schema.StringAttribute{
Required: true,
},
"subnet_id": schema.StringAttribute{
Required: true,
},
"security_groups": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
},
},
},Safe to add optional or computed attributes:
// Version 1
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{Required: true},
}
// Version 2 - safe
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{Required: true},
"description": schema.StringAttribute{Optional: true}, // New optional
}Mark as deprecated first:
// Version 2
"old_field": schema.StringAttribute{
Optional: true,
DeprecationMessage: "Use new_field instead",
},
// Version 3 - remove after grace period
// (remove old_field from schema)Avoid changing required/optional/computed:
// BAD: Breaks existing configs
"name": schema.StringAttribute{
Required: true, // Was: Optional: true
}
// GOOD: Add new attribute
"name_v2": schema.StringAttribute{
Required: true,
},
"name": schema.StringAttribute{
Optional: true,
DeprecationMessage: "Use name_v2",
},Continue to Type System to learn about framework types, conversions, and null/unknown handling.