Resource Import

8 minute read

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:

  1. Takes the import ID from user
  2. Sets it as the resource’s id attribute
  3. 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 ResourceWithImportState interface
  • ✅ Parse and validate import ID format
  • ✅ Support multiple import ID formats when helpful
  • ✅ Use ImportStatePassthroughID for 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!

Next: Resource Timeouts →


Previous: ← Delete Operation

Last updated: June 30, 2026

Updated: