Today we will create a simple t2.micro instance in AWS using Terraform. Currently, t2.micro instances are available in the AWS Free Tier. Everything here will be performed from a Windows environment. However, with a few adjustments, you will be able to follow with a different OS. That said, you will need the following:
- Terraform installed. The official documentation to install Terraform can be found here. Essentially, you will need the appropriate binary for your system and to update your PATH environment variable to include the local path of the Terraform binary in your computer.
- An AWS account. You can follow the instructions provided by AWS to create a new one.
- Visual Studio Code or any similar text editor to create the main.tf file. If you go with Visual Studio Code, I like to use the HashiCorp Terrafrom extension which offers some great features to develop with Terraform.
Setting up the environment
First, let us check if your Terraform installation is working. For that, you can open a new PowerShell instance and type: “terraform -version” which should return something like this:
If the command is not recognized by the terminal, check that the path for the Terraform binary for your system is included in the PATH environment variable of Windows. If you already did that and is still not working, perhaps you just need to launch a new instance of PowerShell.
Next thing is to create a new folder where Terraform will store all the necessary files. For this example, I’ve created ‘D:\aws_terraform\t2micro\’ directory:
Now, we must create the main.tf file. This file contains “the main set of configurations for your module”. In our case, this file will contain all the necessary things to instruct Terraform to create a new t2.micro instance in our AWS account:
Here, we will be opening the “D:\aws_terraform\t2micro” folder and the “main.tf” file we just created in VS Code. You can find the “Open folder” in the “File” tab of VS Code. Once there, we will open a new PowerShell terminal:
Creating the main.tf for a t2.micro instance in AWS
After we opened main.tf file in VS Code to edit it, the first thing to include is the “provider” block. This is the part that allows Terraform to “interact with cloud providers, SaaS providers, and other APIs.” Also in the Terraform documentation, you can find the AWS provider. To use it, we need to include the following code in the main.tf:
provider "aws" {
region = "us-west-2"
access_key = "my-access-key"
secret_key = "my-secret-key"
}
You can find or create your keys in the IAM section of your AWS account. Be sure to provide the required permissions to create the resources discussed here.
The next thing to create would be the resource block. Following the AWS documentation, we will use the following keys for the t2.micro resource block:
resource "aws_instance" "tfvm" {
ami = "ami-08c40ec9ead489470"
instance_type = "t2.micro"
vpc_security_group_ids = [ aws_security_group.webserver.id ]
user_data = <<-EOF
#!/bin/bash
echo "This worked!" > index.html
nohup busybox httpd -f -p 8080 &
EOF
tags = {
Name = "youritguy_demo"
}
}
Let us break down this resource block:
- ami: refers to Amazon Machine Images. Amazon, the community, or the user, can provide AMIs. In this case, we are using an AMI for Ubuntu Server 22.04 LTS that has the “Free tier eligible” tag:
- instance_type: This would be the type of the virtual machine we want to create, t2.micro in our case.
- The vpc_security_group_ids key refers to a security group and will allow us to connect to the t2.micro from the internet. The value of this key points to a resource that is the next thing we are declaring in the main.tf file.
- User_data is just a script that is going to run in the VM once it is created. It will create a html file with the text This worked!, then it will tell Ubuntu to create an HTTP server linked to the 8080 port.
- tags is a key to add a tag or keyword to identify the resource within AWS.
The next thing we need to include is the resource block for a AWS security group:
resource "aws_security_group" "webserver" {
name = "web-s01"
ingress = [{
cidr_blocks = [ "0.0.0.0/0" ]
description = "basic_http"
from_port = 8080
ipv6_cidr_blocks = []
prefix_list_ids = []
protocol = "tcp"
security_groups = []
self = false
to_port = 8080
}]
}
Note that the t2.micro resource block points to this security group id. In this block, we are interested in the options that will make the server accessible from the internet on port 8080: cidr_blocks, from_port, protocol, to_port. The other keys can be set to empty.
Finally, once Terraform creates the t2.micro instance it would be nice to see the IP address of it to check everything is working. For that, we use the output block:
output "instance_ips" {
value = aws_instance.tfvm.public_ip
}
With this the main.tf is completed.
Creating the t2.micro instance with Terraform
Now we save the file and switch to the terminal to run the Terraform commands to create the corresponding resources. Change the path to the folder we created at the beginning and run “terraform init” to initialize your working directory. Successful output will look like this:
Now we run “terraform plan” as the info for terraform init suggests. This command will output all the necessary changes that Terraform will do to create the resources declared in main.tf. In our case, it will create a VM and a security group, so in the last lines of the output you should see “Plan: 2 to add, 0 to change, 0 to destroy”:
PS D:\aws_terraform\t2micro> terraform plan
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.tfvm will be created
+ resource "aws_instance" "tfvm" {
+ ami = "ami-08c40ec9ead489470"
+ 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)
+ get_password_data = false
+ host_id = (known after apply)
+ host_resource_group_arn = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (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
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "youritguy_http_demo"
}
+ tags_all = {
+ "Name" = "youritguy_http_demo"
}
+ tenancy = (known after apply)
+ user_data = "4cc376e303883d4c4cc772dcd41546c994"
+ user_data_base64 = (known after apply)
+ user_data_replace_on_change = false
+ vpc_security_group_ids = (known after apply)
+ capacity_reservation_specification {
+ capacity_reservation_preference = (known after apply)
+ capacity_reservation_target {
+ capacity_reservation_id = (known after apply)
+ capacity_reservation_resource_group_arn = (known after apply)
}
}
+ ebs_block_device {
+ delete_on_termination = (known after apply)
+ device_name = (known after apply)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ snapshot_id = (known after apply)
+ tags = (known after apply)
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
+ enclave_options {
+ enabled = (known after apply)
}
+ ephemeral_block_device {
+ device_name = (known after apply)
+ no_device = (known after apply)
+ virtual_name = (known after apply)
}
+ maintenance_options {
+ auto_recovery = (known after apply)
}
+ metadata_options {
+ http_endpoint = (known after apply)
+ http_put_response_hop_limit = (known after apply)
+ http_tokens = (known after apply)
+ instance_metadata_tags = (known after apply)
}
+ network_interface {
+ delete_on_termination = (known after apply)
+ device_index = (known after apply)
+ network_card_index = (known after apply)
+ network_interface_id = (known after apply)
}
+ private_dns_name_options {
+ enable_resource_name_dns_a_record = (known after apply)
+ enable_resource_name_dns_aaaa_record = (known after apply)
+ hostname_type = (known after apply)
}
+ root_block_device {
+ delete_on_termination = (known after apply)
+ device_name = (known after apply)
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ tags = (known after apply)
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = (known after apply)
+ volume_type = (known after apply)
}
}
# aws_security_group.webserver will be created
+ resource "aws_security_group" "webserver" {
+ arn = (known after apply)
+ description = "Managed by Terraform"
+ egress = (known after apply)
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ "0.0.0.0/0",
]
+ description = "basic_http"
+ from_port = 8080
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = "tcp"
+ security_groups = []
+ self = false
+ to_port = 8080
},
]
+ name = "web-s01"
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags_all = (known after apply)
+ vpc_id = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_ips = (known after apply)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
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.
Once we are ok with the changes required, according to the plan made by Terraform, the next will be to run “terraform apply“. Without any options, this command will prompt the user for confirmation. If you want to avoid that, you can run “terraform apply -auto-approve” instead:
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_ips = (known after apply)
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_security_group.webserver: Creating...
aws_security_group.webserver: Creation complete after 3s [id=sg-006e9d4d3b0dbc479]
aws_instance.tfvm: Creating...
aws_instance.tfvm: Still creating... [10s elapsed]
aws_instance.tfvm: Still creating... [20s elapsed]
aws_instance.tfvm: Still creating... [30s elapsed]
aws_instance.tfvm: Creation complete after 33s [id=i-088574db81d49277c]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
instance_ips = "44.204.176.53"
We are done and everything looks good. Let us check to see if this IP actually responds on port 8080: