Terraform
Manage infrastructure
In the previous tutorial, you created an EC2 instance on AWS with Terraform.
In this tutorial, you will learn how Terraform implements changes to your configuration. You will add input variables and output values to make your configuration more dynamic and flexible, and use a module to reference reusable collections of infrastructure.
Prerequisites
To follow this tutorial you will need:
- The Terraform CLI (1.2.0+) installed.
- The AWS CLI installed.
- An AWS account and associated
credentials
that allow you to create resources in the
us-west-2
region, including an EC2 instance, VPC, and security groups. - The configuration and infrastructure from the Create infrastructure tutorial.
After completing the previous tutorial, you will have a directory named
learn-terraform-get-started-aws
with the following configuration in a file
called main.tf
.
main.tf
provider "aws" {
region = "us-west-2"
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
}
owners = ["099720109477"] # Canonical
}
resource "aws_instance" "app_server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
tags = {
Name = "learn-terraform"
}
}
The configuration you created in the previous tutorial also includes a
terraform.tf
file that configures Terraform.
terraform.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.92"
}
}
required_version = ">= 1.2"
}
Variables and outputs
Input variables let you parametrize the behavior of your Terraform configuration. You can also define output values to expose data about the resources you create. Variables and outputs also allow you to integrate your Terraform workspaces with other automation tools by providing a consistent interface to configure and retrieve data about your workspace's infrastructure.
Input variables
Create a new file in your learn-terraform-aws-get-started
directory named
variables.tf
with the following configuration.
variables.tf
variable "instance_name" {
description = "Value of the EC2 instance's Name tag."
type = string
default = "learn-terraform"
}
variable "instance_type" {
description = "The EC2 instance's type."
type = string
default = "t2.micro"
}
These input variables allow you to update the EC2 instance's name and type
without modifying your configuration files each time. Both variables set a
default value for Terraform to use if you do not specify a value for them. We
recommend that you put your workspace's variable and output definitions in their
own respective files, variables.tf
and outputs.tf
, to make it easier for
users to maintain your Terraform configuration.
Update the instance configuration in main.tf
to refer to these variables
instead of hard-coding the argument values.
main.tf
resource "aws_instance" "app_server" {
ami = data.aws_ami.ubuntu.id
- instance_type = "t2.micro"
+ instance_type = var.instance_type
tags = {
- Name = "learn-terraform"
+ Name = var.instance_name
}
}
You can set values for your Terraform variables in a number of ways, including environment variables, command line arguments, and in files stored on disk.
Run a Terraform plan without applying it to see what would happen if you changed
your EC2 instance type from t2.micro
to t2.large
using a command line variable.
$ terraform plan -var instance_type=t2.large
data.aws_ami.ubuntu: Reading...
data.aws_ami.ubuntu: Read complete after 1s [id=ami-0026a04369a3093cc]
aws_instance.app_server: Refreshing state... [id=i-0c636e158c30e48f9]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# aws_instance.app_server will be updated in-place
~ resource "aws_instance" "app_server" {
id = "i-0c636e158c30e48f9"
~ instance_type = "t2.micro" -> "t2.large"
~ public_dns = "ec2-34-216-162-36.us-west-2.compute.amazonaws.com" -> (known after apply)
~ public_ip = "34.216.162.36" -> (known after apply)
tags = {
"Name" = "learn-terraform"
}
# (36 unchanged attributes hidden)
# (8 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply"
now.
If you were to apply this plan, Terraform would update your EC2 instance with
the new instance type. Terraform needs to replace the instance to implement this
change, so the AWS provider also indicates that the IP address and hostname will
change as well, but marks them as (known after apply)
because AWS will not
assign the new values until you apply the change to recreate the instance.
Output values
Output values allow you to access attributes from your Terraform configuration and consume their values with other automation tools or workflows.
Create a new file named outputs.tf
with the following configuration.
outputs.tf
output "instance_hostname" {
description = "Private DNS name of the EC2 instance."
value = aws_instance.app_server.private_dns
}
This output value exposes your EC2 instance's hostname from your Terraform workspace.
Apply your configuration. Since the default values of the two variables you
created are the same as the hard-coded values they replaced, Terraform will
detect that the only change is the output value you added. Respond to the
confirmation prompt with a yes
to add the output value to your workspace.
$ terraform apply
data.aws_ami.ubuntu: Reading...
data.aws_ami.ubuntu: Read complete after 1s [id=ami-0026a04369a3093cc]
aws_instance.app_server: Refreshing state... [id=i-0c636e158c30e48f9]
Changes to Outputs:
+ instance_hostname = "ip-172-31-35-26.us-west-2.compute.internal"
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
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:
instance_hostname = "ip-172-31-35-26.us-west-2.compute.internal"
Terraform prints out your output values when you run a plan or apply, and also stores them in your workspace's state file.
Review your output values using the terraform output
command.
$ terraform output
instance_hostname = "ip-172-31-35-26.us-west-2.compute.internal"
Modules
Modules are reusable sets of configuration. Use modules to consistently manage complex infrastructure deployments that include multiple resources and data sources. Like providers, you can source modules from the Terraform Registry. You can also create your own modules and share them within your organization.
Module blocks
Add a module block to your configuration in main.tf
to create a VPC and
related networking resources for your EC2 instance.
main.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.19.0"
name = "example-vpc"
cidr = "10.0.0.0/16"
azs = ["us-west-2a", "us-west-2b", "us-west-2c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24"]
enable_dns_hostnames = true
}
Since Terraform automatically resolves dependencies within your configuration, you can organize your configuration blocks in any order you like. As a best practice, we recommend that you organize your configuration so that it is easy for you and your team to maintain.
This configuration defines a VPC named example-vpc
with two public and two
private subnets. Review the AWS VPC module
page
in the Terraform Registry, which includes documentation and examples of how to
use the module to create networking resources including subnets, security groups, elastic IP addresses, and
a NAT gateway.
Move your EC2 instance into your new VPC by updating the resource block to match the one below.
main.tf
resource "aws_instance" "app_server" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
vpc_security_group_ids = [module.vpc.default_security_group_id]
subnet_id = module.vpc.private_subnets[0]
tags = {
Name = var.instance_name
}
}
This change will configure your EC2 instance to be in one of your new VPC's public subnets and use its default security group.
Whenever you add a new module to your configuration, you will need to install it by re-initializing the workspace.
Install the VPC module by running terraform init
.
$ terraform init
Initializing the backend...
Initializing modules...
Downloading registry.terraform.io/terraform-aws-modules/vpc/aws 5.19.0 for vpc...
- vpc in .terraform/modules/vpc
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.98.0
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.
When you initialize an existing workspace, Terraform detects and installs any
new providers and modules. Terraform tracks the current versions of the
providers used with your configuration in the .terraform.lock.hcl
file in your
workspace's directory.
Plan and apply changes
Apply your updated configuration to create your new VPC and move your EC2 instance into it. Because AWS does not support moving an existing EC2 instance to a new VPC, Terraform will plan to replace your existing instance rather than updating it in place. Approve Terraform's plan to add your VPC and replace your EC2 instance by responding yes
to the confirmation
prompt.
$ terraform apply
data.aws_ami.ubuntu: Reading...
data.aws_ami.ubuntu: Read complete after 1s [id=ami-0026a04369a3093cc]
aws_instance.app_server: Refreshing state... [id=i-0c636e158c30e48f9]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
-/+ destroy and then create replacement
Terraform will perform the following actions:
# aws_instance.app_server must be replaced
-/+ resource "aws_instance" "app_server" {
~ arn = "arn:aws:ec2:us-west-2:949008909725:instance/i-0c636e158c30e48f9" -> (known after apply)
~ associate_public_ip_address = true -> (known after apply)
~ availability_zone = "us-west-2b" -> (known after apply)
##...
}
}
Plan: 16 to add, 0 to change, 1 to destroy.
Changes to Outputs:
~ instance_hostname = "ip-172-31-35-26.us-west-2.compute.internal" -> (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
aws_instance.app_server: Destroying... [id=i-0fbb487cbdf2eb6ed]
aws_instance.app_server: Still destroying... [id=i-0fbb487cbdf2eb6ed, 00m10s elapsed]
aws_instance.app_server: Still destroying... [id=i-0fbb487cbdf2eb6ed, 00m20s elapsed]
aws_instance.app_server: Still destroying... [id=i-0fbb487cbdf2eb6ed, 00m30s elapsed]
aws_instance.app_server: Destruction complete after 31s
module.vpc.aws_vpc.this[0]: Creating...
module.vpc.aws_vpc.this[0]: Still creating... [00m10s elapsed]
module.vpc.aws_vpc.this[0]: Creation complete after 12s [id=vpc-01e157ec1af2d7314]
module.vpc.aws_subnet.private[0]: Creating...
module.vpc.aws_route_table.private[1]: Creating...
module.vpc.aws_subnet.private[1]: Creating...
module.vpc.aws_route_table.private[0]: Creating...
module.vpc.aws_default_route_table.default[0]: Creating...
module.vpc.aws_subnet.public[0]: Creating...
module.vpc.aws_internet_gateway.this[0]: Creating...
module.vpc.aws_default_security_group.this[0]: Creating...
## ...
module.vpc.aws_route_table_association.private[1]: Creation complete after 0s [id=rtbassoc-0ea0b41646a73659c]
module.vpc.aws_route_table_association.private[0]: Creation complete after 0s [id=rtbassoc-0d51502051aa2c2af]
module.vpc.aws_default_network_acl.this[0]: Creation complete after 2s [id=acl-03b6b08c6f6a81e4a]
module.vpc.aws_route.public_internet_gateway[0]: Creation complete after 1s [id=r-rtb-0cde73c077eadf7e61080289494]
module.vpc.aws_default_security_group.this[0]: Creation complete after 3s [id=sg-04f350f66843618db]
module.vpc.aws_subnet.public[0]: Creation complete after 3s [id=subnet-02a7d5d6b6e8742d6]
module.vpc.aws_route_table_association.public[0]: Creating...
module.vpc.aws_route_table_association.public[0]: Creation complete after 1s [id=rtbassoc-03db7fad8ba24eca0]
aws_instance.app_server: Still creating... [00m10s elapsed]
aws_instance.app_server: Creation complete after 13s [id=i-0226232d8b6e9eea6]
Apply complete! Resources: 16 added, 0 changed, 1 destroyed.
Outputs:
instance_hostname = "ip-10-0-1-75.us-west-2.compute.internal"
Terraform creates the VPC and related resources defined by the vpc
module you
added.
Notice that Terraform performed the required operations in order based on resource dependencies. First, it destroyed your old EC2 instance, then it created your VPC and most of the related resources before creating your new EC2 instance. When Terraform creates an execution plan, it constructs a dependency graph to determine the correct order of operations. When you apply your plan, Terraform creates, updates, and destroys your resources in dependency order, and in parallel when possible.
Print out a list of your workspace's resources with terraform state list
.
$ terraform state list
data.aws_ami.ubuntu
aws_instance.app_server
module.vpc.aws_default_network_acl.this[0]
module.vpc.aws_default_route_table.default[0]
module.vpc.aws_default_security_group.this[0]
module.vpc.aws_internet_gateway.this[0]
module.vpc.aws_route.public_internet_gateway[0]
module.vpc.aws_route_table.private[0]
module.vpc.aws_route_table.private[1]
module.vpc.aws_route_table.public[0]
module.vpc.aws_route_table_association.private[0]
module.vpc.aws_route_table_association.private[1]
module.vpc.aws_route_table_association.public[0]
module.vpc.aws_subnet.private[0]
module.vpc.aws_subnet.private[1]
module.vpc.aws_subnet.public[0]
module.vpc.aws_vpc.this[0]
Notice that the resources configured within the VPC module start with
module.vpc
. Like resource and data source addresses, module addresses must be
unique within your configuration. You can use the same module more than once by
specifying the same source
and version
arguments, while giving each module
block a unique name.
Continue on to the next tutorial to learn how to destroy infrastructure with Terraform.