State Capstone: Import blocks, moved blocks, and prevent_destroy
Scenario
An administrator "ClickOps'd" a critical Cloud Storage bucket directly in the GCP console. You need to bring it under Terraform management using the modern import { ... } block. Once imported, you must add prevent_destroy to protect it. Finally, you decide to rename the resource ID in your HCL code. To prevent Terraform from deleting the old bucket and creating a new one, you will orchestrate a seamless refactor using the moved { ... } block.
- (Out of band): Create a bucket via the `gcloud alpha storage mb` CLI or GCP Console manually.
- Write an import { id = ... ; to = ... } block mapping the GCP ID to a new HCL resource identifier.
- Write the corresponding google_storage_bucket block. Run `terraform plan` to execute the import phase.
- Add lifecycle { prevent_destroy = true } to the HCL block.
- Rename the HCL resource identifier (e.g. from `raw_data` to `ingestion`). Write a moved { from = ... ; to = ... } block to update the state file without touching the cloud resource.
Additional Context
Legacy CLI Import vs Block Import: Historically, you ran `terraform import google_storage_bucket.foo gs://my-bucket`. In Terraform 1.5+, you write an `import {}` block directly in the HCL file. This is highly preferred as it represents the import action "as code", integrates into Git PRs, and supports generating the missing HCL code automatically.
Moved Blocks: Refactoring HCL names (`resource "google_compute_instance" "OLD" {` -> `"NEW" {`) normally causes Terraform to plan a `[DELETE OLD]` and `[CREATE NEW]` action, which is catastrophic for databases or VMs. `moved {}` tells Terraform "I just renamed it locally, simply update the entry in the `terraform.tfstate` file, do not destroy the cloud resource."
Step 1: Out of band creation
export PROJECT_ID="my-gcp-project-123"
gcloud storage buckets create gs://clickops-bucket-555 --project=${PROJECT_ID} --location=us-central1Step 2: main.tf (The Import Phase)
provider "google" { project = "my-gcp-project-123" }
# Tell Terraform to map the existing cloud resource to a local HCL block
import {
# The GCP identifying string format changes per resource type. For buckets, it's just the name or project/name
id = "my-gcp-project-123/clickops-bucket-555"
# What we want to call it in Terraform
to = google_storage_bucket.raw_data
}
# The target block. Note that `terraform plan -generate-config-out=generated.tf`
# can actually write this block for you!
resource "google_storage_bucket" "raw_data" {
name = "clickops-bucket-555"
location = "US-CENTRAL1"
# Step 3: Prevent accidental terraform destroy
lifecycle {
prevent_destroy = true
}
}Step 4: Refactoring main.tf (The Move Phase)
We decide `raw_data` is a bad name. We rename it to `ingestion`.
provider "google" { project = "my-gcp-project-123" }
# Import block can be safely deleted once applied.
# The Moved Block updates the State File pointer seamlessly
moved {
from = google_storage_bucket.raw_data
to = google_storage_bucket.ingestion
}
# The renamed resource
resource "google_storage_bucket" "ingestion" {
name = "clickops-bucket-555"
location = "US-CENTRAL1"
lifecycle { prevent_destroy = true }
}Workflow Commands
terraform init
# 1. Apply the import
terraform apply
# Output: google_storage_bucket.raw_data: Importing...
# 2. Update your main.tf to reflect Step 4 (renaming and adding moved block)
# 3. Apply the move
terraform apply
# Output: Terraform will perform the following actions:
# # google_storage_bucket.raw_data has moved to google_storage_bucket.ingestion
# Plan: 0 to add, 0 to change, 0 to destroy.
# 4. Attempt destruction (Demonstrating prevent_destroy in action)
terraform destroy
# Output -> Error: Instance cannot be destroyed
# 5. Full Pipeline Cleanup
# - Remove `prevent_destroy = true` from main.tf
# - Apply the change `terraform apply`
# - Now destroy it `terraform destroy`
