Intermediate

Terraform: Resource Lifecycle Management

The resource graph and dependencies, lifecycle management, importing and migrating resources.

Terraform version: 1.14.3
Provider: AWS (us-west-2)


Table of Contents

  1. Module 1 — Resource Graph and Dependencies
  2. Module 2 — Resource Lifecycle Management
  3. Module 3 — Importing Existing Resources
  4. Module 4 — Moving and Migrating Resources
  5. Diagrams
  6. Reference Tables

Module 1 — Resource Graph and Dependencies

Resource Lifecycle

Terraform manages the complete lifecycle of infrastructure, from initial creation to deletion. This lifecycle includes four fundamental operations:

OperationPlan SymbolDescription
Create+Initial resource creation
Read(none)Read current state (refresh)
Update~In-place modification of a mutable attribute
Replace-/+Destroy then recreate (immutable attribute)
Destroy-Permanent resource deletion

Less frequent operations also exist:

  • Import: Bring an existing resource (not created by Terraform) under Terraform management
  • Move: Change a resource’s address in state without destroying it
  • Forget: Remove a resource from state without physically destroying it
stateDiagram-v2
    [*] --> Unmanaged : Existing resource outside Terraform
    [*] --> Creation : terraform apply (new resource)
    Unmanaged --> Managed : terraform import
    Creation --> Managed : successful provisioning
    Managed --> Update : mutable attribute change (~)
    Update --> Managed : update in-place
    Managed --> Replace : immutable attribute change (-/+)
    Replace --> Managed : destroy + create
    Managed --> Deletion : terraform destroy
    Deletion --> [*]
    Managed --> Forgotten : terraform state rm / removed
    Forgotten --> [*]

Globomantics Scenario — Taco Wagon

Architecture deployed by the Taco Wagon team:

  • VPC with public subnets
  • EC2 instance secured by a Security Group
  • 2 S3 buckets: one for logs, one for static asset cache

Problems to solve:

  1. Security Group must be renamed per Globomantics naming standards (generates error without create_before_destroy)
  2. Log S3 bucket must be created before EC2 instance (depends_on)
  3. Log bucket must be protected against accidental deletion (prevent_destroy)
  4. Cache bucket must be recreated on each new application version (replace_triggered_by)
  5. EC2 tags are managed by an external policy — Terraform must not modify them (ignore_changes)

Terraform Resource Graph

During terraform plan, Terraform builds a directed acyclic graph (DAG) of your configuration:

  1. Each resource = a node
  2. References between resources = directed edges (dependencies)
  3. Resources without dependencies can be deployed in parallel
  4. Dependent resources are deployed after their dependencies
flowchart TD
    A[aws_vpc.main] --> B[aws_subnet.public_0]
    A --> C[aws_subnet.public_1]
    A --> D[aws_internet_gateway.main]
    B --> E[aws_route_table_association.public_0]
    C --> F[aws_route_table_association.public_1]
    D --> G[aws_route_table.public]
    G --> E
    G --> F
    A --> H[aws_security_group.main]
    B --> I[aws_instance.web]
    H --> I
    J[aws_s3_bucket.logging] --> I
    K[data.aws_ssm_parameter.amzn2_linux] --> I

    style A fill:#FF9900,color:#000
    style I fill:#1A73E8,color:#fff
    style J fill:#34A853,color:#fff

Visualizing the graph:

terraform graph | dot -Tsvg > graph.svg

Explicit Dependencies with depends_on

Terraform automatically determines dependencies via references in HCL code. However, there are cases where a dependency exists logically without a direct reference in the code.

Syntax:

resource "aws_instance" "web" {
  ami           = nonsensitive(data.aws_ssm_parameter.amzn2_linux.value)
  instance_type = var.instance_type
  subnet_id     = aws_subnet.public[0].id

  # Explicit dependency: log bucket must exist before EC2
  depends_on = [aws_s3_bucket.logging]
}

Usage rules:

  • Applies to resource blocks, module blocks, and output blocks
  • Takes a list of references to other objects
  • If a module dependency is declared, it includes all resources and outputs from that module
  • Use sparingly: direct references are always preferred as they give Terraform better understanding of the relationship

Module 2 — Resource Lifecycle Management

The lifecycle Block

The lifecycle block is supported by all resource types. It is placed inside a resource block and allows modifying Terraform’s default behavior.

resource "aws_example" "this" {
  # ... resource arguments ...

  lifecycle {
    create_before_destroy = bool
    prevent_destroy       = bool
    ignore_changes        = [list of attributes]
    replace_triggered_by  = [list of resources/attributes]
    # precondition { ... }   # covered in Validation & Testing course
    # postcondition { ... }  # covered in Validation & Testing course
    # action_trigger { ... } # covered in Terraform Actions course
  }
}

create_before_destroy

By default, Terraform destroys the existing resource before creating a new one (symbolized by -/+). This can be problematic when a resource cannot be deleted while another still depends on it.

Concrete example: Renaming a Security Group. AWS doesn’t allow deleting an SG while an EC2 instance is still attached. Solution: create the new SG first, migrate the EC2, then delete the old one.

resource "aws_security_group" "main" {
  name   = "${local.naming_prefix}sg"  # rename from "taco-wagon-sg"
  vpc_id = aws_vpc.main.id

  lifecycle {
    create_before_destroy = true  # create new SG before destroying old one
  }
}

Behavior without vs with create_before_destroy:

# Without create_before_destroy (default)
Plan: 1 to destroy, 1 to add  (-/+)
  - destroy aws_security_group.main  ← ERROR if EC2 still attached
  + create  aws_security_group.main

# With create_before_destroy
Plan: 1 to add, 1 to destroy  (+/-)
  + create  aws_security_group.main  ← new SG created first
  - destroy aws_security_group.main  ← old SG deleted after

Note: Propagation of create_before_destroy = true is automatic to dependent resources.


replace_triggered_by and terraform_data

Triggers recreation of a resource when another resource (or one of its attributes) is updated, even without a direct reference.

resource "aws_s3_bucket" "cache" {
  bucket_prefix = "${local.naming_prefix}cache-"

  lifecycle {
    # Recreate the cache bucket on each application version change
    replace_triggered_by = [terraform_data.application_version]
  }
}

resource "terraform_data" "application_version" {
  input = var.application_version  # versioned variable
}

Why terraform_data?

replace_triggered_by can only reference managed resources (not data sources, variables, or locals). The terraform_data resource (built-in provider) serves as a bridge.

# Force manual replacement
terraform plan -replace="aws_instance.web"
terraform apply -replace="aws_instance.web"

prevent_destroy

Protects a critical resource against accidental deletion or recreation.

resource "aws_s3_bucket" "logging" {
  bucket_prefix = "${local.naming_prefix}logging-"

  lifecycle {
    prevent_destroy = true  # blocks terraform destroy AND recreation
  }
}

Behavior:

  • The resource can still be updated in-place (~)
  • If a change requires recreation (-/+), Terraform raises an error
  • If terraform destroy is executed, Terraform raises an error
  • To bypass: set prevent_destroy = false — making the action deliberate

Test with -replace:

terraform plan -replace="aws_s3_bucket.logging"
# → Error: Instance cannot be destroyed
#   Resource aws_s3_bucket.logging has lifecycle.prevent_destroy
#   set, but the plan calls for this resource to be destroyed.

ignore_changes

Tells Terraform to ignore certain attributes in future plans. Useful when other tools (Ansible, organizational policies, scripts) manage certain attributes outside Terraform.

resource "aws_instance" "web" {
  ami           = nonsensitive(data.aws_ssm_parameter.amzn2_linux.value)
  instance_type = var.instance_type
  subnet_id     = aws_subnet.public[0].id

  tags = {
    Name        = "${local.naming_prefix}web"
    Environment = var.environment
  }

  lifecycle {
    # Tags managed by organizational policy — Terraform doesn't touch them
    ignore_changes = [tags]
  }
}

Use cases:

Ignored AttributeReason
tagsTags managed by organizational policy (e.g., AWS Tag Policies)
user_dataStartup scripts modified post-deployment
desired_countAuto-scaling modifies instance count
passwordRotating passwords managed by a secrets manager

Ignore all attributes:

lifecycle {
  ignore_changes = all  # Terraform plans no changes on this resource
}

Warning: ignore_changes = all should be used with extreme caution — Terraform becomes blind to all drift.


Module 3 — Importing Existing Resources

Import Options

When resources already exist in the environment (created via console, CLI, scripts) and need to be brought under Terraform management, two approaches are available:

flowchart TD
    A["Existing resources\nnot managed by Terraform"]
    A --> B["Option 1: terraform import\n(imperative)"]
    A --> C["Option 2: import block\n(declarative)"]
    
    B --> D["Updates state only\nDoes NOT generate HCL code\nMust write resource block manually"]
    C --> E["Declared in HCL code\nWith -generate-config-out\nAutomatically generates resource block"]
    
    style B fill:#FF9900,color:#000
    style C fill:#1E88E5,color:#fff

Imperative terraform import command:

# Syntax
terraform import <resource_address> <resource_id>

# Example: import an AWS VPC
terraform import aws_vpc.main vpc-0123456789abcdef0

Import process with the command:

  1. Terraform updates only the state (not HCL code)
  2. You must write the resource block manually in your configuration
  3. Match resource block attributes with the actual resource state
  4. Run terraform plan to verify the plan is empty (no changes planned)

Globomantics Scenario — Burrito Barn

The Burrito Barn team deployed infrastructure via Terraform, but a team member created two resources directly via the AWS console:

  • An additional public subnet
  • An EC2 instance (app server) in that subnet

These resources must be imported into the Terraform configuration without destroying them.


Resource Discovery with terraform query

Introduced in Terraform 1.14, terraform query is a native resource discovery mechanism.

Query files (extension .tfquery.hcl):

# updates.tfquery.hcl

list "burrito_barn" {
  type     = aws_instance
  provider = aws

  config {
    filter {
      name   = "tag:Team"
      values = ["BurritoBarn"]
    }
  }
}

Execution:

terraform query

Importing Resources with import Blocks

The declarative approach with import blocks is recommended because it is versioned in code.

Step 1 — Create the import blocks:

# imports.tf

import {
  id = "i-0123456789abcdef0"   # EC2 instance ID in AWS
  to = aws_instance.app_server
}

import {
  id = "subnet-0123456789abcdef0"  # Subnet ID in AWS
  to = aws_subnet.app_subnet
}

Step 2 — Automatically generate HCL code:

# Terraform generates resource blocks in update.tf
terraform plan -generate-config-out=update.tf

Step 3 — Validate and apply:

# Check the plan (should show "X to import, 0 to add, 0 to change")
terraform plan

# Apply the import
terraform apply

Module 4 — Moving and Migrating Resources

Moving Resources

As infrastructure evolves, it may be necessary to change a resource’s address in the configuration (refactoring, modularization, configuration split). Without explicit intervention, Terraform would interpret this change as a delete + create.

Two types of moves:

TypeDescription
Intra-configurationRenaming a label, moving to/from a module in the same configuration
Inter-configurationMigrating to a distinct Terraform configuration (different root module)

Resource address:

# Root module
resource_type.name_label
# Example: aws_s3_bucket.logging

# Child module
module.module_name.resource_type.name_label
# Example: module.vpc.aws_vpc.main

Globomantics Scenario — Sopes Saloon

The Sopes Saloon team wants to:

  1. Adopt a common VPC module (rather than maintaining their own VPC code)
  2. Split the configuration into two root modules: one for networking, one for the application

All this without destroying or recreating deployed resources.


The moved Block

The moved block is the declarative solution for informing Terraform about resource movement.

# Basic syntax
moved {
  from = <old_address>
  to   = <new_address>
}

Example — Moving to a module:

# main.tf (sopes_saloon)

module "vpc" {
  source = "./modules/vpc"

  naming_prefix        = local.naming_prefix
  vpc_cidr_range       = var.vpc_cidr_range
  public_subnet_ranges = var.public_subnet_ranges
}

# moved blocks: tells Terraform these resources were moved to the module
moved {
  from = aws_vpc.main
  to   = module.vpc.aws_vpc.main
}

moved {
  from = aws_subnet.public
  to   = module.vpc.aws_subnet.public
}

moved {
  from = aws_internet_gateway.main
  to   = module.vpc.aws_internet_gateway.main
}

Workflow with moved:

flowchart LR
    A["1. Update resource blocks\n(rename / move to module)"]
    B["2. Add moved blocks"]
    C["3. terraform plan\n(verify: 0 to add,\n0 to destroy)"]
    D["4. terraform apply\n(update state)"]
    E["5. Optional:\nremove moved blocks\non next commit"]
    
    A --> B --> C --> D --> E
    
    style C fill:#43A047,color:#fff
    style D fill:#1E88E5,color:#fff

Migrating to a New Configuration

To move resources to another root module, combine removed blocks (in source configuration) and import blocks (in destination configuration).

Declarative approach (recommended):

In the destination configuration (sopes_saloon_app), create the import blocks:

# imports.tf (in sopes_saloon_app)

locals {
  instance_id           = "i-0abc123def456789"
  security_group_id     = "sg-0abc123def456789"
  ingress_rule_id       = "sgr-0abc123def456789"
  egress_rule_id        = "sgr-0abc123def456780"
}

import {
  id = local.instance_id
  to = aws_instance.web
}

import {
  id = local.security_group_id
  to = aws_security_group.main
}

The removed Block

The removed block is the declarative solution for removing resources from state without destroying them. Used in the source configuration during migration.

# removed.tf (in sopes_saloon — source configuration)

removed {
  from = aws_instance.web

  lifecycle {
    destroy = false  # Don't destroy the actual resource, just remove from state
  }
}

removed {
  from = aws_security_group.main

  lifecycle {
    destroy = false
  }
}

Reference Tables

Lifecycle Block Arguments

ArgumentDefaultDescription
create_before_destroyfalseCreates replacement before destroying original
prevent_destroyfalseBlocks any operation that would destroy the resource
ignore_changes[]List of attributes to ignore during plan
replace_triggered_by[]List of resources that trigger replacement when changed

Resource Operations Summary

OperationPlan SymbolWhen It OccursLifecycle Control
Create+New resource
Update in-place~Mutable attribute changed
Replace-/+Immutable attribute changedcreate_before_destroy
Destroy-terraform destroy or removed from configprevent_destroy
Import<=import block or terraform import
Movemoved block
Forget×removed { destroy = false }

Import Methods Comparison

MethodApproachGenerates HCL?Versioned in Code?
terraform importImperativeNoNo
import blockDeclarativeYes (with -generate-config-out)Yes

State Management Commands

# View state
terraform state list
terraform state show <resource_address>

# Imperative move (use declarative moved block instead)
terraform state mv <source> <destination>

# Remove from state without destroying
terraform state rm <resource_address>

# Force replacement
terraform plan -replace="<resource_address>"
terraform apply -replace="<resource_address>"

Search Terms

terraform · resource · lifecycle · management · infrastructure · ci/cd · devops · block · resources · globomantics · import · scenario · dependencies · graph · importing · migrating · moving

Interested in this course?

Contact us to book it or get a custom training plan for your team.