State Refresh
What is State Refresh?
State refresh updates Terraform’s view of resources to match actual infrastructure state by calling the Read operation.
When refresh happens:
- Before every
terraform plan - Before every
terraform apply - During
terraform refreshcommand - After import
- After create, update operations
┌──────────────────────────────────────────┐
│ State Refresh Flow │
├──────────────────────────────────────────┤
│ │
│ 1. Read state file │
│ { id: "db-12345", storage_gb: 100 } │
│ │
│ 2. Call Read() for each resource │
│ GET /api/databases/db-12345 │
│ │
│ 3. Update state with API response │
│ { id: "db-12345", storage_gb: 200 } │
│ │
│ 4. Continue with plan/apply │
│ │
└──────────────────────────────────────────┘
Read Operation Fundamentals
Basic Read Implementation
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// 1. Get current state
var state DatabaseResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
id := state.ID.ValueString()
tflog.Debug(ctx, "Reading database", map[string]any{
"id": id,
})
// 2. Fetch from API
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
if client.IsNotFoundError(err) {
// Resource deleted outside Terraform
tflog.Info(ctx, "Database not found, removing from state", map[string]any{
"id": id,
})
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Read Failed",
fmt.Sprintf("Could not read database %s: %s", id, err.Error()),
)
return
}
// 3. Update state with current values
state.Name = types.StringValue(db.Name)
state.Engine = types.StringValue(db.Engine)
state.EngineVersion = types.StringValue(db.EngineVersion)
state.StorageGB = types.Int64Value(int64(db.StorageGB))
state.Port = types.Int64Value(int64(db.Port))
state.Endpoint = types.StringValue(db.Endpoint)
state.Status = types.StringValue(db.Status)
// Convert tags
if len(db.Tags) > 0 {
tagsMap := make(map[string]attr.Value)
for k, v := range db.Tags {
tagsMap[k] = types.StringValue(v)
}
tagValue, _ := types.MapValue(types.StringType, tagsMap)
state.Tags = tagValue
} else {
state.Tags = types.MapNull(types.StringType)
}
// 4. Save updated state
resp.State.Set(ctx, state)
tflog.Debug(ctx, "Successfully read database", map[string]any{
"id": db.ID,
"status": db.Status,
"storage_gb": db.StorageGB,
})
}
Handling Deleted Resources
Resource Not Found
When a resource is deleted outside Terraform, remove it from state:
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
db, err := r.client.GetDatabase(ctx, state.ID.ValueString())
if err != nil {
if client.IsNotFoundError(err) {
// ✅ Remove from state
resp.State.RemoveResource(ctx)
tflog.Warn(ctx, "Database not found, removed from state", map[string]any{
"id": state.ID.ValueString(),
})
return
}
// Other errors
resp.Diagnostics.AddError("Read Failed", err.Error())
return
}
// Update state normally...
}
User Experience:
$ terraform plan
mycloud_database.db: Refreshing state... [id=db-12345]
Terraform detected the following changes made outside of Terraform:
# mycloud_database.db has been deleted
- resource "mycloud_database" "db" {
- id = "db-12345" -> null
- name = "test-db" -> null
- storage_gb = 100 -> null
}
Terraform will perform the following actions:
# mycloud_database.db will be created
+ resource "mycloud_database" "db" {
+ id = (known after apply)
+ name = "test-db"
+ storage_gb = 100
}
Never Error on Not Found
// ❌ Bad: Error when not found
if client.IsNotFoundError(err) {
resp.Diagnostics.AddError(
"Database Not Found",
"Database was deleted",
)
return
}
// ✅ Good: Remove from state
if client.IsNotFoundError(err) {
resp.State.RemoveResource(ctx)
return
}
Why? Terraform needs to know the resource is gone to plan recreation.
Refreshing All Attributes
Update Every Attribute
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
db, err := r.client.GetDatabase(ctx, state.ID.ValueString())
if err != nil {
if client.IsNotFoundError(err) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Read Failed", err.Error())
return
}
// ✅ Update ALL attributes from API
state.ID = types.StringValue(db.ID) // Even if ID doesn't change
state.Name = types.StringValue(db.Name) // May have changed
state.Engine = types.StringValue(db.Engine) // Usually immutable
state.EngineVersion = types.StringValue(db.EngineVersion) // May have upgraded
state.StorageGB = types.Int64Value(int64(db.StorageGB)) // May have scaled
state.Port = types.Int64Value(int64(db.Port)) // May have changed
state.Endpoint = types.StringValue(db.Endpoint) // May have changed
state.Status = types.StringValue(db.Status) // Changes frequently
state.MultiAZ = types.BoolValue(db.MultiAZ) // May have toggled
// Update complex types
state.Tags = convertTags(db.Tags)
state.SecurityGroups = convertList(db.SecurityGroupIDs)
resp.State.Set(ctx, state)
}
Don’t Skip “Immutable” Fields
// ❌ Bad: Assuming fields never change
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// ...
// Don't skip engine - what if API changed it?
// state.Engine = ... (skipped)
state.StorageGB = types.Int64Value(int64(db.StorageGB))
resp.State.Set(ctx, state)
}
// ✅ Good: Update everything
state.Engine = types.StringValue(db.Engine)
state.EngineVersion = types.StringValue(db.EngineVersion)
state.StorageGB = types.Int64Value(int64(db.StorageGB))
Handling Null and Empty Values
String Attributes
// Handle optional strings that may be empty
if db.Description == "" {
state.Description = types.StringNull()
} else {
state.Description = types.StringValue(db.Description)
}
// Handle pointer fields
if db.MasterUsername != nil {
state.MasterUsername = types.StringValue(*db.MasterUsername)
} else {
state.MasterUsername = types.StringNull()
}
Maps
// Handle tags that may be empty
if len(db.Tags) > 0 {
tagsMap := make(map[string]attr.Value)
for k, v := range db.Tags {
tagsMap[k] = types.StringValue(v)
}
mapValue, diags := types.MapValue(types.StringType, tagsMap)
resp.Diagnostics.Append(diags...)
state.Tags = mapValue
} else {
state.Tags = types.MapNull(types.StringType)
}
Lists
// Handle lists that may be empty
if len(db.SecurityGroupIDs) > 0 {
elements := make([]attr.Value, len(db.SecurityGroupIDs))
for i, id := range db.SecurityGroupIDs {
elements[i] = types.StringValue(id)
}
listValue, diags := types.ListValue(types.StringType, elements)
resp.Diagnostics.Append(diags...)
state.SecurityGroupIDs = listValue
} else {
state.SecurityGroupIDs = types.ListNull(types.StringType)
}
Refreshing Nested Objects
Single Nested Object
type DatabaseResourceModel struct {
ID types.String `tfsdk:"id"`
BackupConfig types.Object `tfsdk:"backup_config"`
}
type BackupConfigModel struct {
Enabled types.Bool `tfsdk:"enabled"`
RetentionDays types.Int64 `tfsdk:"retention_days"`
PreferredWindow types.String `tfsdk:"preferred_window"`
}
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
db, err := r.client.GetDatabase(ctx, state.ID.ValueString())
if err != nil {
// Handle error...
return
}
// Update nested object
if db.BackupConfig != nil {
backupConfigAttrs := map[string]attr.Value{
"enabled": types.BoolValue(db.BackupConfig.Enabled),
"retention_days": types.Int64Value(int64(db.BackupConfig.RetentionDays)),
"preferred_window": types.StringValue(db.BackupConfig.PreferredWindow),
}
objectValue, diags := types.ObjectValue(
map[string]attr.Type{
"enabled": types.BoolType,
"retention_days": types.Int64Type,
"preferred_window": types.StringType,
},
backupConfigAttrs,
)
resp.Diagnostics.Append(diags...)
state.BackupConfig = objectValue
} else {
state.BackupConfig = types.ObjectNull(
map[string]attr.Type{
"enabled": types.BoolType,
"retention_days": types.Int64Type,
"preferred_window": types.StringType,
},
)
}
resp.State.Set(ctx, state)
}
List of Objects
type LoadBalancerResourceModel struct {
ID types.String `tfsdk:"id"`
Listeners types.List `tfsdk:"listeners"`
}
func (r *LoadBalancerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state LoadBalancerResourceModel
req.State.Get(ctx, &state)
lb, err := r.client.GetLoadBalancer(ctx, state.ID.ValueString())
if err != nil {
// Handle error...
return
}
// Update list of listener objects
listenerObjectType := types.ObjectType{
AttrTypes: map[string]attr.Type{
"port": types.Int64Type,
"protocol": types.StringType,
"certificate_arn": types.StringType,
},
}
if len(lb.Listeners) > 0 {
listenerObjects := make([]attr.Value, len(lb.Listeners))
for i, listener := range lb.Listeners {
certARN := types.StringNull()
if listener.CertificateARN != "" {
certARN = types.StringValue(listener.CertificateARN)
}
objValue, diags := types.ObjectValue(
listenerObjectType.AttrTypes,
map[string]attr.Value{
"port": types.Int64Value(int64(listener.Port)),
"protocol": types.StringValue(listener.Protocol),
"certificate_arn": certARN,
},
)
resp.Diagnostics.Append(diags...)
listenerObjects[i] = objValue
}
listValue, diags := types.ListValue(listenerObjectType, listenerObjects)
resp.Diagnostics.Append(diags...)
state.Listeners = listValue
} else {
state.Listeners = types.ListNull(listenerObjectType)
}
resp.State.Set(ctx, state)
}
API Error Handling
Transient Errors
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
db, err := r.client.GetDatabase(ctx, state.ID.ValueString())
if err != nil {
// Not found - remove from state
if client.IsNotFoundError(err) {
resp.State.RemoveResource(ctx)
return
}
// Throttling - return error
if client.IsThrottlingError(err) {
resp.Diagnostics.AddError(
"API Rate Limited",
"Too many requests. Wait a moment and try again.",
)
return
}
// Permission denied
if client.IsUnauthorizedError(err) {
resp.Diagnostics.AddError(
"Permission Denied",
fmt.Sprintf("Insufficient permissions to read database %s", state.ID.ValueString()),
)
return
}
// Network error - generic message
resp.Diagnostics.AddError(
"Read Failed",
fmt.Sprintf("Could not read database: %s", err.Error()),
)
return
}
// Update state normally...
}
Handling Partial Responses
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
db, err := r.client.GetDatabase(ctx, state.ID.ValueString())
if err != nil {
// Handle errors...
return
}
// Some fields may be nil during provisioning
state.ID = types.StringValue(db.ID)
state.Status = types.StringValue(db.Status)
// Endpoint not available until status = "available"
if db.Endpoint != "" {
state.Endpoint = types.StringValue(db.Endpoint)
} else {
state.Endpoint = types.StringNull()
}
// Port may not be set during creation
if db.Port > 0 {
state.Port = types.Int64Value(int64(db.Port))
} else {
// Keep previous value or set null
if state.Port.IsNull() {
state.Port = types.Int64Null()
}
// else: keep existing value
}
resp.State.Set(ctx, state)
}
Refresh Performance
Minimize API Calls
// ✅ Good: Single API call fetches all data
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
// One API call gets everything
db, err := r.client.GetDatabase(ctx, state.ID.ValueString())
// Update all attributes from single response
state.Name = types.StringValue(db.Name)
state.Tags = convertTags(db.Tags)
state.Status = types.StringValue(db.Status)
resp.State.Set(ctx, state)
}
// ❌ Bad: Multiple API calls
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state DatabaseResourceModel
req.State.Get(ctx, &state)
id := state.ID.ValueString()
// Multiple API calls!
db, _ := r.client.GetDatabase(ctx, id)
tags, _ := r.client.GetDatabaseTags(ctx, id) // Extra call
status, _ := r.client.GetDatabaseStatus(ctx, id) // Extra call
state.Name = types.StringValue(db.Name)
state.Tags = convertTags(tags)
state.Status = types.StringValue(status)
resp.State.Set(ctx, state)
}
Batch Reads When Possible
If your API supports batch operations:
// Note: This is advanced and usually not needed
func (p *Provider) RefreshResources(ctx context.Context, ids []string) (map[string]*Database, error) {
// Batch API call
return p.client.BatchGetDatabases(ctx, ids)
}
Testing Read Operation
Test Refresh
func TestDatabaseResource_Read(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
// Create resource
Config: testAccDatabaseConfig("test-db", 100),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("mycloud_database.test", "storage_gb", "100"),
),
},
{
// Simulate external change
PreConfig: func() {
// Manually update storage via API
client.UpdateDatabase(ctx, "test-db", UpdateDatabaseInput{
StorageGB: 200,
})
},
// Refresh should detect change
Config: testAccDatabaseConfig("test-db", 100),
Check: resource.ComposeTestCheckFunc(
// State shows API value (200), plan will show drift
resource.TestCheckResourceAttr("mycloud_database.test", "storage_gb", "200"),
),
ExpectNonEmptyPlan: true, // Drift detected
},
},
})
}
Test Not Found
func TestDatabaseResource_Read_NotFound(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
// Create resource
Config: testAccDatabaseConfig("test-db", 100),
},
{
// Delete externally
PreConfig: func() {
client.DeleteDatabase(ctx, "test-db")
},
// Refresh should remove from state
Config: testAccDatabaseConfig("test-db", 100),
// Resource will be recreated
ExpectNonEmptyPlan: true,
},
},
})
}
Manual Refresh Command
terraform refresh
Users can explicitly refresh without planning:
# Refresh all resources
$ terraform refresh
mycloud_database.db: Refreshing state... [id=db-12345]
# State updated, no changes applied
When to use:
- Verify state matches reality
- Update state after manual changes
- Debugging state issues
Note: terraform refresh is being deprecated in favor of:
terraform apply -refresh-only
Best Practices
1. Always Handle Not Found
// ✅ Good: Remove from state
if client.IsNotFoundError(err) {
resp.State.RemoveResource(ctx)
return
}
// ❌ Bad: Return error
if client.IsNotFoundError(err) {
resp.Diagnostics.AddError("Not Found", "Resource deleted")
return
}
2. Update All Attributes
// ✅ Good: Update everything
state.Name = types.StringValue(db.Name)
state.Engine = types.StringValue(db.Engine)
state.StorageGB = types.Int64Value(int64(db.StorageGB))
state.Status = types.StringValue(db.Status)
// ... all attributes
// ❌ Bad: Selective updates
state.StorageGB = types.Int64Value(int64(db.StorageGB))
// Skipping other attributes!
3. Handle Null Values
// ✅ Good: Proper null handling
if db.Description == "" {
state.Description = types.StringNull()
} else {
state.Description = types.StringValue(db.Description)
}
// ❌ Bad: Empty string instead of null
state.Description = types.StringValue(db.Description) // "" not null
4. Log Refresh Operations
// ✅ Good: Log for debugging
tflog.Debug(ctx, "Refreshing database", map[string]any{
"id": state.ID.ValueString(),
})
db, err := r.client.GetDatabase(ctx, state.ID.ValueString())
tflog.Info(ctx, "Refreshed database", map[string]any{
"id": db.ID,
"status": db.Status,
})
5. Minimize API Calls
// ✅ Good: One API call
db, err := r.client.GetDatabase(ctx, id) // Returns all data
// ❌ Bad: Multiple calls
db, _ := r.client.GetDatabase(ctx, id)
tags, _ := r.client.GetTags(ctx, id)
status, _ := r.client.GetStatus(ctx, id)
Key Takeaways
- ✅ Read refreshes state before plan/apply
- ✅ Always update ALL attributes from API
- ✅ Remove from state when not found (don’t error)
- ✅ Handle null and empty values correctly
- ✅ Update nested objects and lists completely
- ✅ Handle API errors appropriately
- ✅ Minimize API calls in Read
- ✅ Log refresh operations
- ✅ Test refresh and not-found scenarios
- ✅ Read is called automatically, users rarely call refresh manually
What’s Next?
Learn how to detect and handle configuration drift.
Previous: ← Resource Import
Last updated: July 1, 2026