Terraform version: 1.14.3
Provider: AWS (us-west-2)
Table of Contents
- Module 1 — Resource Graph and Dependencies
- Module 2 — Resource Lifecycle Management
- Module 3 — Importing Existing Resources
- Module 4 — Moving and Migrating Resources
- Diagrams
- 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:
| Operation | Plan Symbol | Description |
|---|---|---|
| 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:
- Security Group must be renamed per Globomantics naming standards (generates error without
create_before_destroy) - Log S3 bucket must be created before EC2 instance (
depends_on) - Log bucket must be protected against accidental deletion (
prevent_destroy) - Cache bucket must be recreated on each new application version (
replace_triggered_by) - 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:
- Each resource = a node
- References between resources = directed edges (dependencies)
- Resources without dependencies can be deployed in parallel
- 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 = trueis 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 destroyis 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 Attribute | Reason |
|---|---|
tags | Tags managed by organizational policy (e.g., AWS Tag Policies) |
user_data | Startup scripts modified post-deployment |
desired_count | Auto-scaling modifies instance count |
password | Rotating passwords managed by a secrets manager |
Ignore all attributes:
lifecycle {
ignore_changes = all # Terraform plans no changes on this resource
}
Warning:
ignore_changes = allshould 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:
- Terraform updates only the state (not HCL code)
- You must write the resource block manually in your configuration
- Match resource block attributes with the actual resource state
- Run
terraform planto 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:
| Type | Description |
|---|---|
| Intra-configuration | Renaming a label, moving to/from a module in the same configuration |
| Inter-configuration | Migrating 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:
- Adopt a common VPC module (rather than maintaining their own VPC code)
- 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
| Argument | Default | Description |
|---|---|---|
create_before_destroy | false | Creates replacement before destroying original |
prevent_destroy | false | Blocks 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
| Operation | Plan Symbol | When It Occurs | Lifecycle Control |
|---|---|---|---|
| Create | + | New resource | — |
| Update in-place | ~ | Mutable attribute changed | — |
| Replace | -/+ | Immutable attribute changed | create_before_destroy |
| Destroy | - | terraform destroy or removed from config | prevent_destroy |
| Import | <= | import block or terraform import | — |
| Move | → | moved block | — |
| Forget | × | removed { destroy = false } | — |
Import Methods Comparison
| Method | Approach | Generates HCL? | Versioned in Code? |
|---|---|---|---|
terraform import | Imperative | No | No |
import block | Declarative | Yes (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