Read Operation
What is the Read Operation?
The Read operation fetches the current state of a resource from your infrastructure and updates Terraform’s state file. It ensures that Terraform’s view matches reality.
When Read is called:
- ✅ During
terraform plan(refresh phase) - ✅ During
terraform refresh - ✅ After Create operation
- ✅ After Update operation
- ✅ Before Update operation
- ✅ Before Delete operation
Read must:
- Read current state from state file
- Fetch actual resource state from API
- Update state with current values
- Handle deleted resources
- Not modify the resource itself
Read Method Signature
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Implementation here
}
Parameters:
ctx: Context for cancellation and loggingreq: Contains current stateresp: Used to update state or remove resource
Basic Read Implementation
Step-by-Step
func (r *DatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Step 1: Get current state
var state DatabaseResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Step 2: Get resource ID
id := state.ID.ValueString()
// Step 3: Log the operation
tflog.Debug(ctx, "Reading database resource", map[string]any{
"id": id,
})
// Step 4: Fetch resource from API
db, err := r.client.GetDatabase(ctx, id)
// Step 5: Handle deleted resources
if err != nil {
if api.IsNotFoundError(err) {
tflog.Warn(ctx, "Database not found, removing from state", map[string]any{
"id": id,
})
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Failed to Read Database",
fmt.Sprintf("Could not read database %s: %s", id, err.Error()),
)
return
}
// Step 6: Update state with current values
state.Name = types.StringValue(db.Name)
state.Engine = types.StringValue(db.Engine)
state.EngineVersion = types.StringValue(db.EngineVersion)
state.InstanceClass = types.StringValue(db.InstanceClass)
state.StorageGB = types.Int64Value(int64(db.StorageGB))
state.Status = types.StringValue(db.Status)
state.Endpoint = types.StringValue(db.Endpoint)
state.UpdatedAt = types.StringValue(db.UpdatedAt)
// Step 7: Save updated state
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}
Handling Deleted Resources
When a resource is deleted outside Terraform, Read must 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)
id := state.ID.ValueString()
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
// Check if resource doesn't exist
if api.IsNotFoundError(err) {
tflog.Warn(ctx, "Database not found, removing from state", map[string]any{
"id": id,
})
// Remove from state
resp.State.RemoveResource(ctx)
return
}
// Other error - report it
resp.Diagnostics.AddError(
"Failed to Read Database",
fmt.Sprintf("Could not read database: %s", err.Error()),
)
return
}
// Resource exists, update state
state.Status = types.StringValue(db.Status)
resp.State.Set(ctx, &state)
}
What happens:
- Terraform detects resource is missing
- Next
terraform planshows resource needs to be recreated - User can apply to recreate it
Detecting Drift
Read is crucial for drift detection:
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 all attributes - Terraform will detect drift
state.Name = types.StringValue(db.Name)
state.InstanceClass = types.StringValue(db.InstanceClass)
state.StorageGB = types.Int64Value(int64(db.StorageGB))
// If user configured storage=100 but API shows storage=200,
// Terraform will detect drift and show in plan
resp.State.Set(ctx, &state)
}
Example drift:
# User configuration
resource "mycloud_database" "example" {
storage_gb = 100
}
If storage was manually changed to 200GB:
terraform plan
# mycloud_database.example will be updated in-place
~ resource "mycloud_database" "example" {
~ storage_gb = 200 -> 100
}
Handling Tags
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
}
// Convert API tags to Terraform map
if len(db.Tags) > 0 {
tagsMap := make(map[string]attr.Value)
for key, value := range db.Tags {
tagsMap[key] = types.StringValue(value)
}
mapValue, diags := types.MapValue(types.StringType, tagsMap)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
state.Tags = mapValue
}
} else {
// No tags - set to null
state.Tags = types.MapNull(types.StringType)
}
resp.State.Set(ctx, &state)
}
Handling Lists
type DatabaseResourceModel struct {
// ... other fields ...
Replicas types.List `tfsdk:"replicas"`
}
type ReplicaModel struct {
Region types.String `tfsdk:"region"`
InstanceClass types.String `tfsdk:"instance_class"`
Status types.String `tfsdk:"status"`
}
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
}
// Convert API replicas to Terraform list
if len(db.Replicas) > 0 {
replicas := make([]ReplicaModel, len(db.Replicas))
for i, r := range db.Replicas {
replicas[i] = ReplicaModel{
Region: types.StringValue(r.Region),
InstanceClass: types.StringValue(r.InstanceClass),
Status: types.StringValue(r.Status),
}
}
listValue, diags := types.ListValueFrom(ctx, types.ObjectType{
AttrTypes: map[string]attr.Type{
"region": types.StringType,
"instance_class": types.StringType,
"status": types.StringType,
},
}, replicas)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
state.Replicas = listValue
}
} else {
state.Replicas = types.ListNull(types.ObjectType{
AttrTypes: map[string]attr.Type{
"region": types.StringType,
"instance_class": types.StringType,
"status": types.StringType,
},
})
}
resp.State.Set(ctx, &state)
}
Handling Nested Objects
type DatabaseResourceModel struct {
// ... other fields ...
BackupConfig types.Object `tfsdk:"backup_config"`
}
type BackupConfigModel struct {
RetentionDays types.Int64 `tfsdk:"retention_days"`
Window types.String `tfsdk:"window"`
Enabled types.Bool `tfsdk:"enabled"`
}
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
}
// Convert API backup config to Terraform object
if db.BackupConfig != nil {
backupConfig := BackupConfigModel{
RetentionDays: types.Int64Value(int64(db.BackupConfig.RetentionDays)),
Window: types.StringValue(db.BackupConfig.Window),
Enabled: types.BoolValue(db.BackupConfig.Enabled),
}
objValue, diags := types.ObjectValueFrom(ctx, map[string]attr.Type{
"retention_days": types.Int64Type,
"window": types.StringType,
"enabled": types.BoolType,
}, backupConfig)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
state.BackupConfig = objValue
}
} else {
state.BackupConfig = types.ObjectNull(map[string]attr.Type{
"retention_days": types.Int64Type,
"window": types.StringType,
"enabled": types.BoolType,
})
}
resp.State.Set(ctx, &state)
}
Computed vs Configured Attributes
Update Only Computed Attributes
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 computed attributes (always update from API)
state.Status = types.StringValue(db.Status)
state.Endpoint = types.StringValue(db.Endpoint)
state.ARN = types.StringValue(db.ARN)
state.CreatedAt = types.StringValue(db.CreatedAt)
state.UpdatedAt = types.StringValue(db.UpdatedAt)
// ✅ Update configured attributes (detect drift)
state.Name = types.StringValue(db.Name)
state.InstanceClass = types.StringValue(db.InstanceClass)
state.StorageGB = types.Int64Value(int64(db.StorageGB))
resp.State.Set(ctx, &state)
}
Preserve User Configuration
For attributes where API normalization differs from user input:
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
}
// Example: API returns uppercase, but user provided lowercase
// Keep user's original format if functionally equivalent
// User configuration: engine_version = "14.5"
// API returns: "14.5.0"
// Option 1: Update to API value (detect drift)
state.EngineVersion = types.StringValue(db.EngineVersion)
// Option 2: Preserve user value if semantically same
// (Requires version comparison logic)
if !versionsEquivalent(state.EngineVersion.ValueString(), db.EngineVersion) {
state.EngineVersion = types.StringValue(db.EngineVersion)
}
resp.State.Set(ctx, &state)
}
Error Handling
Network 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 api.IsNotFoundError(err) {
resp.State.RemoveResource(ctx)
return
}
// Network/timeout errors - report but don't remove
if api.IsNetworkError(err) {
resp.Diagnostics.AddWarning(
"Network Error Reading Database",
fmt.Sprintf(
"Could not read database due to network error: %s\n\n"+
"The resource state has not been updated. "+
"This error is temporary and can be retried.",
err.Error(),
),
)
return
}
// Other errors
resp.Diagnostics.AddError(
"Failed to Read Database",
fmt.Sprintf("Could not read database: %s", err.Error()),
)
return
}
// Update state
resp.State.Set(ctx, &state)
}
Permission Errors
if err != nil {
if api.IsUnauthorizedError(err) {
resp.Diagnostics.AddError(
"Permission Denied",
fmt.Sprintf(
"Could not read database %s due to insufficient permissions.\n\n"+
"Verify that your API credentials have read access to this resource.",
state.ID.ValueString(),
),
)
return
}
}
Partial State Updates
If some attributes fail to read:
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()
// Fetch main resource
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
if api.IsNotFoundError(err) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Read Failed", err.Error())
return
}
// Update basic attributes
state.Name = types.StringValue(db.Name)
state.Status = types.StringValue(db.Status)
// Fetch tags separately (optional, may fail)
tags, err := r.client.GetDatabaseTags(ctx, id)
if err != nil {
// Don't fail entire Read, just log warning
tflog.Warn(ctx, "Could not fetch tags", map[string]any{
"id": id,
"error": err.Error(),
})
resp.Diagnostics.AddWarning(
"Could Not Fetch Tags",
fmt.Sprintf("Tags could not be retrieved: %s", err.Error()),
)
} else {
// Update tags if successful
state.Tags = convertTagsToMap(tags)
}
// Save state with whatever we could fetch
resp.State.Set(ctx, &state)
}
Complete Example
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
}
// 2. Get resource ID
id := state.ID.ValueString()
// 3. Check client
if r.client == nil {
resp.Diagnostics.AddError(
"Unconfigured Client",
"Expected configured API client.",
)
return
}
// 4. Log operation
tflog.Debug(ctx, "Reading database resource", map[string]any{
"id": id,
})
// 5. Fetch from API
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
// Handle not found
if api.IsNotFoundError(err) {
tflog.Warn(ctx, "Database not found, removing from state", map[string]any{
"id": id,
})
resp.State.RemoveResource(ctx)
return
}
// Handle other errors
resp.Diagnostics.AddError(
"Failed to Read Database",
fmt.Sprintf("Could not read database %s: %s", id, err.Error()),
)
return
}
// 6. Update state with current values
// Configured attributes (user-set)
state.Name = types.StringValue(db.Name)
state.Engine = types.StringValue(db.Engine)
state.EngineVersion = types.StringValue(db.EngineVersion)
state.InstanceClass = types.StringValue(db.InstanceClass)
state.StorageGB = types.Int64Value(int64(db.StorageGB))
// Computed attributes (API-generated)
state.Status = types.StringValue(db.Status)
state.Endpoint = types.StringValue(db.Endpoint)
state.ARN = types.StringValue(db.ARN)
state.CreatedAt = types.StringValue(db.CreatedAt)
state.UpdatedAt = types.StringValue(db.UpdatedAt)
// Tags (optional)
if len(db.Tags) > 0 {
tagsMap := make(map[string]attr.Value)
for key, value := range db.Tags {
tagsMap[key] = types.StringValue(value)
}
mapValue, diags := types.MapValue(types.StringType, tagsMap)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
state.Tags = mapValue
}
} else {
state.Tags = types.MapNull(types.StringType)
}
// 7. Save updated state
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
tflog.Debug(ctx, "Database read successfully", map[string]any{
"id": id,
"status": db.Status,
})
}
Best Practices
1. Always Handle Not Found
// ✅ Good: Remove from state if not found
if api.IsNotFoundError(err) {
resp.State.RemoveResource(ctx)
return
}
// ❌ Bad: Return error (prevents recreating)
if api.IsNotFoundError(err) {
resp.Diagnostics.AddError("Not Found", err.Error())
return
}
2. Update All Attributes
// ✅ Good: Update everything
state.Name = types.StringValue(db.Name)
state.Status = types.StringValue(db.Status)
state.Endpoint = types.StringValue(db.Endpoint)
// ❌ Bad: Update only some attributes
state.Status = types.StringValue(db.Status)
// Other attributes not updated - drift undetected!
3. Don’t Modify Resources
// ✅ Good: Read only
db, err := r.client.GetDatabase(ctx, id)
state.Status = types.StringValue(db.Status)
// ❌ Bad: Making changes in Read
if db.Status != "active" {
r.client.StartDatabase(ctx, id) // DON'T!
}
4. Log Debug Information
// ✅ Good: Debug logging
tflog.Debug(ctx, "Reading database", map[string]any{
"id": id,
})
tflog.Debug(ctx, "Database read successfully", map[string]any{
"id": id,
"status": db.Status,
})
5. Handle Temporary Errors Gracefully
// ✅ Good: Warning for temporary errors
if api.IsNetworkError(err) {
resp.Diagnostics.AddWarning(
"Temporary Network Error",
"State not updated. This error is temporary.",
)
return // Keep existing state
}
// ❌ Bad: Error for temporary issues
if api.IsNetworkError(err) {
resp.Diagnostics.AddError("Network Error", err.Error())
}
Common Mistakes
❌ Not Removing Deleted Resources
// Bad: Error instead of removing
if api.IsNotFoundError(err) {
resp.Diagnostics.AddError("Not Found", err.Error())
return
}
// Good: Remove from state
if api.IsNotFoundError(err) {
resp.State.RemoveResource(ctx)
return
}
❌ Modifying Resources in Read
// Bad: Making changes
func (r *DatabaseResource) Read(...) {
db, _ := r.client.GetDatabase(ctx, id)
// Don't do this in Read!
if db.Status != "active" {
r.client.UpdateDatabase(ctx, id, ...)
}
}
❌ Not Updating Configured Attributes
// Bad: Only updating computed values
state.Status = types.StringValue(db.Status)
state.Endpoint = types.StringValue(db.Endpoint)
// Drift in name, instance_class, etc. won't be detected!
// Good: Update everything
state.Name = types.StringValue(db.Name)
state.InstanceClass = types.StringValue(db.InstanceClass)
state.Status = types.StringValue(db.Status)
Key Takeaways
- ✅ Read synchronizes state with actual infrastructure
- ✅ Called during plan, refresh, and after CRUD operations
- ✅ Must handle deleted resources by removing from state
- ✅ Updates all attributes to detect configuration drift
- ✅ Never modifies the resource itself
- ✅ Handles network errors gracefully with warnings
- ✅ Converts API responses to Terraform types
- ✅ Logs operations for debugging
- ✅ Distinguishes between temporary and permanent errors
- ✅ Essential for accurate state management
What’s Next?
Now let’s implement the Update operation to modify existing resources!
Previous: ← Create Operation
Last updated: June 30, 2026