Resource Import
What is Resource Import?
Resource import allows users to bring existing infrastructure under Terraform management. Instead of destroying and recreating resources, users can import them into the state file.
Why import is important:
- ✅ Adopt Terraform for existing infrastructure
- ✅ Recover from state file loss
- ✅ Move resources between state files
- ✅ Fix state issues without destroying resources
Import workflow:
# User has existing database
# Write Terraform configuration
resource "mycloud_database" "imported" {
name = "existing-db"
# ... other config ...
}
# Import existing resource
terraform import mycloud_database.imported db-abc123xyz
# Terraform now manages the existing database
ImportState Interface
To support import, implement the ResourceWithImportState interface:
type DatabaseResource struct {
client *api.Client
}
// Ensure interface is implemented
var _ resource.Resource = &DatabaseResource{}
var _ resource.ResourceWithConfigure = &DatabaseResource{}
var _ resource.ResourceWithImportState = &DatabaseResource{} // Add this
func NewDatabaseResource() resource.Resource {
return &DatabaseResource{}
}
Basic Import Implementation
Simple ID Import
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Import by ID
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
What this does:
- Takes the import ID from user
- Sets it as the resource’s
idattribute - Triggers a Read operation to populate the rest
Usage:
terraform import mycloud_database.example db-abc123xyz
Custom Import Implementation
Parse Import ID
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Get import ID
id := req.ID
tflog.Info(ctx, "Importing database", map[string]any{
"id": id,
})
// Validate ID format
if !isValidDatabaseID(id) {
resp.Diagnostics.AddError(
"Invalid Import ID",
fmt.Sprintf(
"Expected database ID in format 'db-xxxxx', got: %s\n\n"+
"Usage: terraform import mycloud_database.example db-abc123",
id,
),
)
return
}
// Set ID attribute
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
// Read operation will be called automatically to populate other attributes
}
func isValidDatabaseID(id string) bool {
// Match pattern: db-<alphanumeric>
matched, _ := regexp.MatchString(`^db-[a-z0-9]+$`, id)
return matched
}
Composite Import ID
Import with multiple identifiers:
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Import format: region/database-id
// Example: us-east-1/db-abc123
parts := strings.Split(req.ID, "/")
if len(parts) != 2 {
resp.Diagnostics.AddError(
"Invalid Import ID Format",
fmt.Sprintf(
"Expected format: region/database-id\n"+
"Example: us-east-1/db-abc123\n"+
"Got: %s",
req.ID,
),
)
return
}
region := parts[0]
id := parts[1]
tflog.Info(ctx, "Importing database", map[string]any{
"region": region,
"id": id,
})
// Validate region
validRegions := []string{"us-east-1", "us-west-2", "eu-west-1"}
if !contains(validRegions, region) {
resp.Diagnostics.AddError(
"Invalid Region",
fmt.Sprintf("Region must be one of: %s. Got: %s", strings.Join(validRegions, ", "), region),
)
return
}
// Set attributes
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
}
Usage:
terraform import mycloud_database.example us-east-1/db-abc123
Import by Name
Import using a human-readable name instead of ID:
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
name := req.ID
tflog.Info(ctx, "Importing database by name", map[string]any{
"name": name,
})
// Look up database by name
db, err := r.client.GetDatabaseByName(ctx, name)
if err != nil {
if api.IsNotFoundError(err) {
resp.Diagnostics.AddError(
"Database Not Found",
fmt.Sprintf("No database found with name: %s", name),
)
return
}
resp.Diagnostics.AddError(
"Failed to Look Up Database",
fmt.Sprintf("Could not find database by name: %s", err.Error()),
)
return
}
// Set ID
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), db.ID)...)
tflog.Info(ctx, "Database found", map[string]any{
"id": db.ID,
})
}
Usage:
terraform import mycloud_database.example production-db
Populating State During Import
Option 1: Rely on Read
Most common pattern - set ID, then Read populates everything:
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Set ID
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
// Read will be called automatically to populate all other attributes
}
Option 2: Full Population
Populate all attributes immediately:
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id := req.ID
// Fetch resource
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
resp.Diagnostics.AddError(
"Failed to Import Database",
fmt.Sprintf("Could not fetch database: %s", err.Error()),
)
return
}
// Create state model
state := DatabaseResourceModel{
ID: types.StringValue(db.ID),
Name: types.StringValue(db.Name),
Engine: types.StringValue(db.Engine),
EngineVersion: types.StringValue(db.EngineVersion),
InstanceClass: types.StringValue(db.InstanceClass),
StorageGB: types.Int64Value(int64(db.StorageGB)),
Status: types.StringValue(db.Status),
Endpoint: types.StringValue(db.Endpoint),
ARN: types.StringValue(db.ARN),
CreatedAt: types.StringValue(db.CreatedAt),
UpdatedAt: types.StringValue(db.UpdatedAt),
}
// Convert tags
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...)
state.Tags = mapValue
}
// Set state
diags := resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}
Import Validation
Validate that resource can be imported:
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id := req.ID
// Fetch resource to validate it exists
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
if api.IsNotFoundError(err) {
resp.Diagnostics.AddError(
"Database Not Found",
fmt.Sprintf(
"Database with ID %s does not exist.\n\n"+
"Verify the ID is correct:\n"+
" terraform import mycloud_database.example <database-id>",
id,
),
)
return
}
resp.Diagnostics.AddError(
"Import Failed",
fmt.Sprintf("Could not fetch database: %s", err.Error()),
)
return
}
// Check if resource is in a state that can be imported
if db.Status == "deleting" {
resp.Diagnostics.AddError(
"Cannot Import Deleting Database",
fmt.Sprintf("Database %s is currently being deleted and cannot be imported.", id),
)
return
}
if db.Status == "failed" {
resp.Diagnostics.AddWarning(
"Database in Failed State",
fmt.Sprintf("Database %s is in a failed state. You can import it, but it may not be functional.", id),
)
}
// Set ID
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
tflog.Info(ctx, "Database imported successfully", map[string]any{
"id": id,
"status": db.Status,
})
}
Import with Required Attributes
Handle attributes that are required but not returned by API:
func (r *DatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
// Can't be imported - not returned by API
MarkdownDescription: "Database password. Cannot be imported - must be set after import.",
},
},
}
}
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Set ID
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
// Set unknown for password (required but not available)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("password"), types.StringUnknown())...)
resp.Diagnostics.AddWarning(
"Password Not Imported",
"The password attribute cannot be imported. "+
"You must set it in your configuration after import. "+
"Terraform will show a change for this attribute on the next plan.",
)
}
Multiple Import Formats
Support different import formats:
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
importID := req.ID
tflog.Debug(ctx, "Parsing import ID", map[string]any{
"import_id": importID,
})
var id, region string
// Try format 1: region/id
if strings.Contains(importID, "/") {
parts := strings.Split(importID, "/")
if len(parts) == 2 {
region = parts[0]
id = parts[1]
tflog.Debug(ctx, "Using region/id format")
}
}
// Try format 2: just ID (use default region)
if id == "" {
id = importID
region = "us-east-1" // Default region
tflog.Debug(ctx, "Using ID-only format, defaulting region", map[string]any{
"region": region,
})
}
// Validate
if !isValidDatabaseID(id) {
resp.Diagnostics.AddError(
"Invalid Import ID",
fmt.Sprintf(
"Invalid database ID format.\n\n"+
"Supported formats:\n"+
" 1. Database ID: db-abc123\n"+
" 2. Region and ID: us-east-1/db-abc123\n\n"+
"Got: %s",
importID,
),
)
return
}
// Set attributes
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
}
Import Multiple Resources
For resources that represent collections:
func (r *ReplicaSetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
primaryID := req.ID
tflog.Info(ctx, "Importing replica set", map[string]any{
"primary_id": primaryID,
})
// Fetch primary database
primary, err := r.client.GetDatabase(ctx, primaryID)
if err != nil {
resp.Diagnostics.AddError("Import Failed", err.Error())
return
}
// Fetch all replicas
replicas, err := r.client.ListReplicas(ctx, primaryID)
if err != nil {
resp.Diagnostics.AddError("Failed to List Replicas", err.Error())
return
}
tflog.Info(ctx, "Found replicas", map[string]any{
"count": len(replicas),
})
// Build state with primary and replicas
state := ReplicaSetResourceModel{
ID: types.StringValue(primary.ID),
PrimaryID: types.StringValue(primary.ID),
}
// Convert replicas
replicaModels := make([]ReplicaModel, len(replicas))
for i, r := range replicas {
replicaModels[i] = ReplicaModel{
ID: types.StringValue(r.ID),
Region: types.StringValue(r.Region),
InstanceClass: types.StringValue(r.InstanceClass),
Status: types.StringValue(r.Status),
}
}
listValue, diags := types.ListValueFrom(ctx, replicaObjectType, replicaModels)
resp.Diagnostics.Append(diags...)
state.Replicas = listValue
// Set state
resp.State.Set(ctx, &state)
}
Complete Example
func (r *DatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
importID := req.ID
tflog.Info(ctx, "Starting database import", map[string]any{
"import_id": importID,
})
// 1. Parse import ID
var id, region string
if strings.Contains(importID, "/") {
// Format: region/id
parts := strings.Split(importID, "/")
if len(parts) != 2 {
resp.Diagnostics.AddError(
"Invalid Import ID Format",
fmt.Sprintf(
"Expected format: region/database-id or just database-id\n"+
"Examples:\n"+
" - us-east-1/db-abc123\n"+
" - db-abc123\n"+
"Got: %s",
importID,
),
)
return
}
region = parts[0]
id = parts[1]
} else {
// Format: just ID
id = importID
region = "" // Will be fetched from API
}
// 2. Validate ID format
if !regexp.MustCompile(`^db-[a-z0-9]+$`).MatchString(id) {
resp.Diagnostics.AddError(
"Invalid Database ID",
fmt.Sprintf("Database ID must match pattern 'db-xxxxx'. Got: %s", id),
)
return
}
// 3. Fetch resource to validate it exists
tflog.Debug(ctx, "Fetching database for import validation")
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
if api.IsNotFoundError(err) {
resp.Diagnostics.AddError(
"Database Not Found",
fmt.Sprintf(
"No database found with ID: %s\n\n"+
"Verify the ID is correct and you have access to this database.",
id,
),
)
return
}
resp.Diagnostics.AddError(
"Import Failed",
fmt.Sprintf("Could not fetch database: %s", err.Error()),
)
return
}
// 4. Validate state
if db.Status == "deleting" {
resp.Diagnostics.AddError(
"Cannot Import",
"Database is currently being deleted and cannot be imported.",
)
return
}
// 5. Use API region if not provided
if region == "" {
region = db.Region
}
// 6. Set core attributes
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
// 7. Provide guidance
resp.Diagnostics.AddWarning(
"Import Successful - Review Configuration",
fmt.Sprintf(
"Database %s imported successfully.\n\n"+
"Next steps:\n"+
"1. Run 'terraform plan' to see any configuration differences\n"+
"2. Update your configuration to match the current state\n"+
"3. Some attributes (like passwords) cannot be imported and must be set manually",
id,
),
)
tflog.Info(ctx, "Database import completed", map[string]any{
"id": id,
"region": region,
"status": db.Status,
})
// Note: Read will be called automatically to populate remaining attributes
}
Best Practices
1. Validate Import ID
// ✅ Good: Validate format
if !isValidID(req.ID) {
resp.Diagnostics.AddError(
"Invalid ID",
fmt.Sprintf("Expected format: db-xxxxx, got: %s", req.ID),
)
return
}
2. Provide Clear Error Messages
// ✅ Good: Helpful error with examples
resp.Diagnostics.AddError(
"Invalid Import Format",
fmt.Sprintf(
"Expected format: region/database-id\n"+
"Example: terraform import mycloud_database.example us-east-1/db-abc123\n"+
"Got: %s",
req.ID,
),
)
3. Support Multiple Formats
// ✅ Good: Flexible import
if strings.Contains(id, "/") {
// Parse region/id format
} else {
// Use just ID with default region
}
4. Verify Resource Exists
// ✅ Good: Fetch to validate
db, err := r.client.GetDatabase(ctx, id)
if err != nil {
resp.Diagnostics.AddError("Not Found", ...)
return
}
5. Log Import Operations
// ✅ Good: Log for debugging
tflog.Info(ctx, "Importing database", map[string]any{
"id": id,
})
Key Takeaways
- ✅ Implement
ResourceWithImportStateinterface - ✅ Parse and validate import ID format
- ✅ Support multiple import ID formats when helpful
- ✅ Use
ImportStatePassthroughIDfor simple cases - ✅ Verify resource exists before completing import
- ✅ Validate resource is in importable state
- ✅ Provide clear error messages with examples
- ✅ Log import operations for debugging
- ✅ Read operation automatically populates remaining attributes
- ✅ Warn about attributes that can’t be imported
What’s Next?
Now let’s learn about configuring resource-specific timeouts!
Previous: ← Delete Operation
Last updated: June 30, 2026