Terraform
Use configuration to move resources
As your Terraform configuration grows in complexity, updating resources becomes more risky: an update to one resource may cause unintended changes to other parts of your infrastructure. One way to address this is to refactor your existing Terraform code into separate modules. In addition to limiting the scope of potential changes, modules help abstract your resources, making your configuration easier to understand.
When you move existing resources from a parent to a child module, your Terraform resource IDs will change. Because of this, you must let Terraform know that you intend to move resources rather than replace them, or Terraform will destroy and recreate your resources with the new ID.
The moved
configuration block lets you track your resource moves in the
configuration itself. With the moved
configuration block, you can plan,
preview, and validate resource moves, enabling you to safely refactor your
configuration.
In this tutorial, you will create several AWS resources, and then divide them
into compute and security group modules. Then, you will use the moved
block to
refactor your configuration and update resource IDs, and review the
corresponding state changes before you apply the new configuration.
Prerequisites
You can complete this tutorial using the same workflow with either Terraform Community Edition or HCP Terraform. HCP Terraform is a platform that you can use to manage and execute your Terraform projects. It includes features like remote state and execution, structured plan output, workspace resource summaries, and more.
Select the HCP Terraform tab to complete this tutorial using HCP Terraform.
This tutorial assumes that you are familiar with the Terraform workflow. If you are new to Terraform, complete the Get Started tutorials first.
In order to complete this tutorial, you will need the following:
- Terraform v1.3+ installed locally.
- An AWS account with local credentials configured for use with Terraform.
Clone the example configuration
Clone the example repository for this tutorial.
$ git clone https://github.com/hashicorp-education/learn-terraform-move
Change to the repository directory.
$ cd learn-terraform-move
This configuration contains a VPC module, a security group module that allows
ingress HTTP traffic on port 8080
, and an EC2 instance resource that uses the
security group and VPC. Later in this tutorial, you will move these resources
into modules to make your configuration more manageable.
Initialize and apply the configuration
Initialize this configuration.
$ terraform init
Initializing modules...
- security_group in modules/security_group
Downloading registry.terraform.io/terraform-aws-modules/vpc/aws 3.14.4 for vpc...
- vpc in .terraform/modules/vpc
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v4.31.0...
- Installed hashicorp/aws v4.31.0 (signed by HashiCorp)
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
After Terraform initializes, apply the configuration and approve the run by
typing yes
at the prompt.
$ terraform apply
## ...
Plan: 24 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ public_ip = (known after apply)
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
##...
Apply complete! Resources: 24 added, 0 changed, 0 destroyed.
Outputs:
public_ip = "18.217.55.179"
Test your instance availability with the curl
command.
$ curl $(terraform output -raw public_ip):8080
Hello World
In the next section, you will refactor your configuration into modules.
Refactor your configuration
In this example, you have a small number of resources to manage. However, an operational environment may consist of many more resources and be difficult to manage in a single configuration file. By organizing these resources into modules, your configuration becomes flexible, reusable, and composable.
Create a new compute
subdirectory in the modules
directory.
$ mkdir -p modules/compute
Create a new variables.tf
file in the modules/compute
directory to capture
your module's input variables. You will use these variables to configure your
module.
modules/compute/variables.tf
variable "security_group" {
description = "The security groups assigned to this instance"
}
variable "public_subnets" {
description = "The public subnet IDs assigned to this instance for IP address assignment"
}
Then, create a new main.tf
file in the modules/compute
directory.
Cut and paste the AMI data source and the EC2 instance resource from main.tf
to modules/compute/main.tf
.
main.tf
- data "aws_ami" "ubuntu" {
- most_recent = true
-
- filter {
- name = "name"
- values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
- }
-
- filter {
- name = "virtualization-type"
- values = ["hvm"]
- }
-
- owners = ["099720109477"] # Canonical
- }
-
- resource "aws_instance" "example" {
- ami = data.aws_ami.ubuntu.id
- subnet_id = module.vpc.public_subnets[0]
- instance_type = "t2.micro"
- vpc_security_group_ids = [aws_security_group.sg_8080.id]
- associate_public_ip_address = true
-
- user_data = <<-EOF
- #!/bin/bash
- apt-get update
- apt-get install -y apache2
- sed -i -e 's/80/8080/' /etc/apache2/ports.conf
- echo "Hello World" > /var/www/html/index.html
- systemctl restart apache2
- EOF
- tags = {
- Name = "terraform-learn-move-ec2"
- }
- }
After you paste these blocks into modules/compute/main.tf
, update the
configuration to use the variables you defined in
modules/compute/variables.tf
.
modules/compute/main.tf
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
resource "aws_instance" "example" {
ami = data.aws_ami.ubuntu.id
subnet_id = var.public_subnets[0]
instance_type = "t2.micro"
vpc_security_group_ids = [var.security_group]
associate_public_ip_address = true
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y apache2
sed -i -e 's/80/8080/' /etc/apache2/ports.conf
echo "Hello World" > /var/www/html/index.html
systemctl restart apache2
EOF
tags = {
Name = "terraform-learn-move-ec2"
}
}
Finally, create a new outputs.tf
file with an output value for your instance's
IP address. You will use this output to pass as a variable into another module.
modules/compute/outputs.tf
output "public_ip" {
description = "The Public IP address used to access the instance"
value = aws_instance.example.public_ip
}
Now modules/compute
contains a modified version of the initial Terraform
configuration for your EC2 instance.
Update configuration to use modules
Replace the EC2 instance resource with the local module you just created, and the security group with the AWS security-group module from the Terraform Registry.
In your main.tf
file, remove the security group module, you will replace it
with a module from the Terraform public registry.
main.tf
- module "security_group" {
- source = "./modules/security_group"
- vpc_id = module.vpc.vpc_id
- }
Then, add new module
blocks to the end of your configuration.
main.tf
module "web_security_group" {
source = "terraform-aws-modules/security-group/aws"
version = "4.13.0"
vpc_id = module.vpc.vpc_id
use_name_prefix = false
name = "terraform-learn-move-sg"
description = "Security Group managed by Terraform"
ingress_with_cidr_blocks = [
{
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = "0.0.0.0/0"
}
]
egress_with_cidr_blocks = [
{
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = "0.0.0.0/0"
}
]
}
module "ec2_instance" {
source = "./modules/compute"
security_group = module.web_security_group.security_group_id
public_subnets = module.vpc.public_subnets
}
Save your changes.
Update the outputs.tf
file in the root directory to use the output value from
the compute module.
outputs.tf
output "public_ip" {
description = "The Public IP address used to access the instance"
value = module.ec2_instance.public_ip
}
Review planned changes
Next, reinitialize your configuration to install the new modules.
$ terraform init
Initializing modules...
- ec2_instance in modules/compute
Downloading registry.terraform.io/terraform-aws-modules/security-group/aws 4.13.0 for web_security_group...
- web_security_group in .terraform/modules/web_security_group
##...
Now that Terraform recognizes the new modules, generate a plan to observe the changes.
$ terraform plan
##...
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
- destroy
Terraform will perform the following actions:
# aws_instance.example will be destroyed
# (because aws_instance.example is not in configuration)
- resource "aws_instance" "example" {
##...
}
# module.ec2_instance.aws_instance.example will be created
+ resource "aws_instance" "example" {
##...
}
# module.security_group.aws_security_group.sg_8080 will be destroyed
# (because aws_security_group.sg_8080 is not in configuration)
- resource "aws_security_group" "sg_8080" {
##...
}
# module.security_group.aws_security_group_rule.egress_rule will be destroyed
# (because aws_security_group_rule.egress_rule is not in configuration)
- resource "aws_security_group_rule" "egress_rule" {
##...
}
# module.security_group.aws_security_group_rule.ingress_rule will be destroyed
# (because aws_security_group_rule.ingress_rule is not in configuration)
- resource "aws_security_group_rule" "ingress_rule" {
##...
}
# module.web_security_group.aws_security_group.this[0] will be created
+ resource "aws_security_group" "this" {
##...
}
# module.web_security_group.aws_security_group_rule.egress_with_cidr_blocks[0] will be created
+ resource "aws_security_group_rule" "egress_rules" {
##...
}
# module.web_security_group.aws_security_group_rule.ingress_with_cidr_blocks[0] will be created
+ resource "aws_security_group_rule" "ingress_rules" {
##...
}
Plan: 4 to add, 0 to change, 4 to destroy.
Changes to Outputs:
~ public_ip = "18.119.128.83" -> (known after apply)
Your module changes would delete and recreate your resources, which could cause
service interruptions. In the next section, you will use Terraform's moved
block to refactor your configuration to use modules without destroying the
existing resources.
Move your resources with the moved
configuration block
With the moved
configuration block, you can inform Terraform about all
resource address changes in your configuration. Terraform also validates those
changes to provide you with clearer operational output and you can safely review
plans before applying.
In your root main.tf
file, add moved
configuration blocks for the resources
you moved in the previous step.
main.tf
moved {
from = module.security_group.aws_security_group.sg_8080
to = module.web_security_group.aws_security_group.this[0]
}
moved {
from = module.security_group.aws_security_group_rule.ingress_rule
to = module.web_security_group.aws_security_group_rule.ingress_with_cidr_blocks[0]
}
moved {
from = module.security_group.aws_security_group_rule.egress_rule
to = module.web_security_group.aws_security_group_rule.egress_with_cidr_blocks[0]
}
moved {
from = aws_instance.example
to = module.ec2_instance.aws_instance.example
}
Re-apply your configuration to move your resources into your new moduless and
update the resource IDs. Confirm your changes with yes
after you verify that
Terraform will move your resources instead of replacing them.
$ terraform apply
##...
Terraform will perform the following actions:
# aws_instance.example has moved to module.ec2_instance.aws_instance.example
resource "aws_instance" "example" {
id = "i-04783269f41a67c2e"
tags = {
"Name" = "terraform-learn-move-ec2"
}
# (30 unchanged attributes hidden)
# (7 unchanged blocks hidden)
}
# module.web_security_group.aws_security_group.this[0] will be updated in-place
# (moved from module.security_group.aws_security_group.sg_8080)
~ resource "aws_security_group" "this" {
id = "sg-0f17c41383a0d2134"
name = "terraform-learn-move-sg"
~ tags = {
+ "Name" = "terraform-learn-move-sg"
}
~ tags_all = {
+ "Name" = "terraform-learn-move-sg"
# (1 unchanged element hidden)
}
# (7 unchanged attributes hidden)
+ timeouts {
+ create = "10m"
+ delete = "15m"
}
}
# module.web_security_group.aws_security_group_rule.egress_with_cidr_blocks[0] will be updated in-place
# (moved from module.security_group.aws_security_group_rule.egress_rule)
~ resource "aws_security_group_rule" "egress_with_cidr_blocks" {
~ description = "All protocols" -> "Egress Rule"
id = "sgrule-4237924237"
+ prefix_list_ids = []
# (7 unchanged attributes hidden)
}
# module.web_security_group.aws_security_group_rule.ingress_with_cidr_blocks[0] will be updated in-place
# (moved from module.security_group.aws_security_group_rule.ingress_rule)
~ resource "aws_security_group_rule" "ingress_with_cidr_blocks" {
~ description = "HTTP" -> "Ingress Rule"
id = "sgrule-201621573"
+ prefix_list_ids = []
# (7 unchanged attributes hidden)
}
##...
Plan: 0 to add, 3 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 0 added, 3 changed, 0 destroyed.
Outputs:
public_ip = "18.119.128.83"
Your instance ID changed, but the resource did not. The security group and its rules were updated in-pace when they moved because the new module includes default values for some attributes that the old module did not.
Rename and move a resource
You can also use the moved
configuration block to rename existing resources.
In the root of your configuration, open the main.tf
file.
Rename your vpc
module, and update the references to it in the rest of your
configuration.
main.tf
module "learn_vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.4"
##...
}
module "web_security_group" {
source = "terraform-aws-modules/security-group/aws"
version = "4.13.0"
vpc_id = module.learn_vpc.vpc_id
##...
}
module "ec2_instance" {
source = "./modules/compute"
security_group = module.web_security_group.security_group_id
public_subnets = module.learn_vpc.public_subnets
}
Add the moved
block for your VPC changes at the bottom of the file.
main.tf
moved {
from = module.vpc
to = module.learn_vpc
}
Run terraform init
to update your VPC module's name.
$ terraform init
Initializing modules...
Downloading registry.terraform.io/terraform-aws-modules/vpc/aws 3.14.4 for learn_vpc...
- learn_vpc in .terraform/modules/learn_vpc
Apply your changes. Confirm your changes with yes
after you verify there are
no resource changes.
$ terraform apply
##...
# module.vpc.aws_eip.nat[0] has moved to module.learn_vpc.aws_eip.nat[0]
resource "aws_eip" "nat" {
##...
Plan: 0 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
public_ip = "18.119.128.83"
Note
We strongly recommend you retain all moved
blocks in your
configuration as a record of your changes. Removing a moved
block plans to
delete that existing resource instead of moving it.
Clean up your resources
Remove the infrastructure you created in this tutorial. Respond to the
confirmation prompt with a yes
.
$ terraform destroy
##...
Plan: 0 to add, 0 to change, 24 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
##...
Destroy complete! Resources: 24 destroyed.
If you used HCP Terraform for this tutorial, after destroying your resources, delete the learn-terraform-move
workspace from your HCP Terraform organization.
Next steps
In this tutorial, you created an EC2 instance and supporting networking
resources. Then, you refactored your Terraform configuration into modules.
Finally, you used the moved
configuration block to safely update the resource
IDs.
For more information on modules and state, review the following resources:
- Review the Terraform
Documentation
on the
moved
configuration block. Learn to use themoved
configuration block with resources containing thefor_each
orcount
functions. - Follow the Build and Use a Local Module tutorial to review how to create a module.
- Follow the Manage Resources in Terraform State tutorial to learn other methods for updating your state file.
- Follow the Manage Resource Lifecycle tutorial and use the different lifecycle management options available in Terraform to prevent resource deletion.