Terraform
Create infrastructure
You can use Terraform to create and manage your infrastructure as code. In this tutorial, you will use Terraform to provision an EC2 instance on Amazon Web Services (AWS). EC2 instances are virtual machines running on AWS and a common component of many infrastructure projects. To provision your infrastructure, you will write configuration to define your provider and instance, set environment variables for your AWS credentials, initialize a new local workspace, and then apply your configuration to create your instance.
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 tutorials in this collection use resources that qualify under the AWS free tier. We are not responsible for any charges that you may incur. Remember to complete the Destroy infrastructure tutorial later in this collection to remove the infrastructure you create while following these tutorials.
Write configuration
Create a new directory for the Terraform configuration you will use in this tutorial.
$ mkdir learn-terraform-get-started-aws
Change into the directory.
$ cd learn-terraform-get-started-aws
Terraform configuration files are plain text files in HashiCorp's configuration
language, HCL, with file names ending with .tf
. When you perform
operations with the Terraform CLI, Terraform loads all of the configuration
files in the current working directory and automatically resolves dependencies
within your configuration. This allows you to organize your configuration into
multiple files and in any order you choose.
Terraform configuration is organized into a few types of blocks that let you configure Terraform itself, Terraform providers, and the resources and data sources that make up your infrastructure.
The terraform
block
The terraform {}
block configures Terraform itself, including which providers
to install, and which version of Terraform to use to provision your
infrastructure. Using a consistent file structure makes maintaining your
Terraform projects easier, so we recommend configuring your Terraform block in a
dedicated terraform.tf
file.
Create and open a new file named terraform.tf
with the following configuration
to define your Terraform block.
terraform.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.92"
}
}
required_version = ">= 1.2"
}
Terraform uses binary plugins called providers to manage your resources by
calling your cloud provider's APIs. Terraform providers are distributed and
versioned separately from Terraform. By decoupling providers from the Terraform
binary, Terraform can support any infrastructure vendor with an API. The
required_providers
block lets you set version constraints on the providers
your configuration uses. HashiCorp maintains the Terraform
Registry, from which you can source public
Terraform providers and modules.
Set source
and version
arguments for each provider in the required_providers
block.
The source
argument specifies a hostname (optional), namespace, and provider
name. In the example configuration, the aws
provider's source is
hashicorp/aws
, which is a shortened form of
registry.terraform.io/hashicorp/aws
, the address of the provider in the
Terraform Registry.
The version
argument sets a version constraint for your AWS provider. If you
do not specify a version constraint, Terraform defaults to the most recent
version of the provider. We recommend using version constraints to ensure that
Terraform does not install a version of the provider that you have not tested
with your configuration. The string ~> 5.92
means your configuration supports any version of the provider with a major version of 5
and a minor version greater than or equal to 92
.
The example configuration also defines the required version of Terraform,
itself. The string >= 1.2
means your configuration supports any version of
Terraform greater than or equal to 1.2
. When you installed Terraform, you
probably installed the latest version currently available.
You can check your current Terraform version by running the terraform -version
command.
$ terraform -version
Terraform v1.12.0
on darwin_arm64
Configuration blocks
Use your text editor to paste the configuration below into a new file named
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"
}
}
When you write a new Terraform configuration, we recommend defining your
provider blocks and other primary infrastructure in main.tf
as a best
practice. As you add to your configuration, you may choose to organize related
infrastructure into different files.
Review the blocks in your main.tf
file.
Providers
The provider
block configures options that apply to all resources managed by your provider, such as the region to create them in.
This provider
block configures the aws
provider. The label of the provider
block corresponds to the name of the provider in the required_providers
list
in your terraform
block.
main.tf
provider "aws" {
region = "us-west-2"
}
Note
If you would rather provision your resources in a different AWS region, update
the value of the region
argument to your preferred region.
You can use multiple provider blocks in your Terraform configuration to configure multiple providers or multiple instances of the same provider with different configurations, such as a different region. Terraform providers must authenticate with your cloud provider's API to manage your resources. Providers often support multiple authentication methods.
Terraform's AWS provider uses the same authentication methods as the AWS CLI. If you have not already done so, configure your AWS credentials as environment variables in your terminal.
To use your IAM credentials to authenticate the Terraform AWS provider, set the
AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
environment variables.
$ export AWS_ACCESS_KEY_ID=
$ export AWS_SECRET_ACCESS_KEY=
Tip
If you don't have access to IAM user credentials, use another authentication method described in the AWS provider documentation.
Use the AWS CLI to verify your credentials.
$ aws configure list
Name Value Type Location
---- ----- ---- --------
profile <not set> None None
access_key ****************ZJZK env
secret_key ****************St8S env
region <not set> None None
Data sources
You can use data
blocks to query your cloud provider for information about
other resources. This data source fetches data about the latest AWS AMI that
matches the filter, so you do not have to hardcode the AMI ID into your
configuration. Data sources help keep your configuration dynamic and avoid
hardcoded values that can become stale.
main.tf
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
}
Data sources have an ID, which you can use to reference the data attributes
within your configuration. Data source IDs are prefixed with data
, followed by
the block's type and name. In this example, the data.aws_ami.ubuntu
data
source loads an AMI for the most recent Ubuntu Noble Numbat release in the
region configured for your provider.
Resources
A resource
block defines components of your infrastructure. The example
configuration defines a resource block to create an AWS EC2 instance.
main.tf
resource "aws_instance" "app_server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
tags = {
Name = "learn-terraform"
}
}
Provider developers determine supported resources types and their arguments. The
first line of a resource
block declares a resource type and resource
name. In this example, the resource type is aws_instance
. The prefix of the
resource type corresponds to the name of the provider, and the rest of the
string is the provider-defined resource type. Together, the resource type and
resource name form a unique resource address for the resource in your
configuration. The resource address for your EC2 instance is
aws_instance.app_server
. You can refer to a resource in other parts of your
configuration by its resource address.
The arguments in your resource
block configure the resource and its behavior:
The
ami
argument specifies which machine image to use by referencing yourdata.aws_ami.ubuntu
data source'sid
attribute.The
instance_type
argument hardcodest2.micro
as the type, which qualifies for the AWS free tier.The
tags
argument sets the EC2 instance's name. You can also set other tags for your EC2 instance in the tags argument.
Format configuration
We recommend using consistent formatting to ensure readability. The terraform
fmt
command automatically reformats all configuration files in the current
directory according to HashiCorp's recommended style.
In your terminal, use Terraform to format your configuration files.
$ terraform fmt
main.tf
Terraform prints the names of the files it modified, if any. In this case, the
example configuration provided does not exactly match the recommended style, so
Terraform updated your main.tf
file.
Initialize your workspace
Before you can apply your configuration, you must initialize your Terraform
workspace with the terraform init
command. As part of initialization,
Terraform downloads and installs the providers defined in your configuration in
your current working directory.
Initialize your Terraform workspace.
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.92"...
- Installing hashicorp/aws v5.98.0...
- Installed hashicorp/aws v5.98.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
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.
Terraform downloaded the aws
provider and installed it in a hidden
.terraform
subdirectory of your current working directory. Terraform also
created a file named .terraform.lock.hcl
which specifies the exact provider
versions used with your workspace, ensuring consistency between runs.
Validate configuration
Make sure your configuration is syntactically valid and internally consistent by
using the terraform validate
command.
$ terraform validate
Success! The configuration is valid.
The example configuration provided above is valid, so Terraform returns a success message.
The validate
command helps you identify errors in your configuration. For
example, if you mistype a resource name or refer to an argument your resource
does not support, Terraform will report an error when you validate your
configuration.
Create infrastructure
Terraform makes changes to your infrastructure in two steps.
Terraform creates an execution plan for the changes it will make. Review this plan to ensure that Terraform will make the changes you expect.
Once you approve the execution plan, Terraform applies those changes using your workspace's providers.
This workflow ensures that you can detect and resolve any unexpected problems with your configuration before Terraform makes changes to your infrastructure.
Plan and apply your configuration now with the terraform apply
command.
Terraform will print out the execution plan and ask you to confirm the changes
before it applies them. Your configuration includes a single resource,
aws_instance.app_server
, so your plan will indicate that Terraform will create
your EC2 instance.
$ terraform apply
data.aws_ami.ubuntu: Reading...
data.aws_ami.ubuntu: Read complete after 1s [id=ami-0026a04369a3093cc]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_instance.app_server will be created
+ resource "aws_instance" "app_server" {
+ ami = "ami-0026a04369a3093cc"
+ arn = (known after apply)
+ associate_public_ip_address = (known after apply)
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
+ disable_api_stop = (known after apply)
+ disable_api_termination = (known after apply)
+ ebs_optimized = (known after apply)
+ enable_primary_ipv6 = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ host_resource_group_arn = (known after apply)
+ iam_instance_profile = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (known after apply)
+ instance_lifecycle = (known after apply)
+ instance_state = (known after apply)
+ instance_type = "t2.micro"
+ ipv6_address_count = (known after apply)
+ ipv6_addresses = (known after apply)
+ key_name = (known after apply)
+ monitoring = (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ placement_partition_number = (known after apply)
+ primary_network_interface_id = (known after apply)
+ private_dns = (known after apply)
+ private_ip = (known after apply)
+ public_dns = (known after apply)
+ public_ip = (known after apply)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ spot_instance_request_id = (known after apply)
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "learn-terraform"
}
+ tags_all = {
+ "Name" = "learn-terraform"
}
+ tenancy = (known after apply)
+ user_data = (known after apply)
+ user_data_base64 = (known after apply)
+ user_data_replace_on_change = false
+ vpc_security_group_ids = (known after apply)
+ capacity_reservation_specification (known after apply)
+ cpu_options (known after apply)
+ ebs_block_device (known after apply)
+ enclave_options (known after apply)
+ ephemeral_block_device (known after apply)
+ instance_market_options (known after apply)
+ maintenance_options (known after apply)
+ metadata_options (known after apply)
+ network_interface (known after apply)
+ private_dns_name_options (known after apply)
+ root_block_device (known after apply)
}
Plan: 1 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:
The output format is similar to the diff format generated by tools such as Git.
The +
next to resource "aws_instance" "app_server"
means that when you apply
this plan, Terraform will create the resource with aws_instance.app_server
as
its ID.
Terraform shows the attributes that will be set on your EC2 instance and
indicates that some values will be (known after apply)
. Terraform has not
created any infrastructure yet. If the plan showed unexpected changes, you could
cancel the operation before completing the apply step. In this case the plan is
acceptable, so type yes
at the confirmation prompt to proceed. Applying your
plan will take a few minutes.
Enter a value: yes
aws_instance.app_server: Creating...
aws_instance.app_server: Still creating... [10s elapsed]
aws_instance.app_server: Creation complete after 14s [id=i-0c636e158c30e48f9]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
You have now created infrastructure using Terraform. Visit the EC2 console and find your new EC2 instance.
Inspect state
When you applied your configuration, Terraform wrote data about your
infrastructure into a file called terraform.tfstate
. Terraform stores data
about your infrastructure in its state file, which it uses to manage resources
over their lifecycle.
List the resources and data sources in your Terraform workspace's state with the
terraform state list
command.
$ terraform state list
data.aws_ami.ubuntu
aws_instance.app_server
Even though the data source is not an actual resource, Terraform tracks it in your state file.
Print out your workspace's entire state using the terraform show
command.
$ terraform show
# data.aws_ami.ubuntu:
data "aws_ami" "ubuntu" {
architecture = "x86_64"
arn = "arn:aws:ec2:us-west-2::image/ami-0026a04369a3093cc"
block_device_mappings = [
{
device_name = "/dev/sda1"
ebs = {
"delete_on_termination" = "true"
"encrypted" = "false"
"iops" = "0"
"snapshot_id" = "snap-051c478203945e90f"
"throughput" = "0"
"volume_size" = "8"
"volume_type" = "gp3"
}
## ...
}
}
When you use Terraform to plan and apply changes to your workspace's infrastructure, Terraform compares the last known state in your state file, your current configuration, and data returned by your providers to create its execution plan.
Your state file can include sensitive information about your infrastructure, such as passwords or security keys, so you must store your state file securely and restrict access to only those who need to manage your infrastructure with Terraform. By default, Terraform creates your state file locally. As your infrastructure operations mature, storing your state remotely using HCP Terraform will let you collaborate with your team more easily and keep your state file secure.
Continue on to the next tutorial to learn how to modify your infrastructure.