AWS EC2 Instance provising using Terraform

VAIBHAV HARIRAMANI
13 min readJun 10, 2024

--

In this blog, we will learn how to create an EC2 instance on the Amazon Web Services (AWS) cloud platform using Terraform.

We will detail the prerequisites, how to authenticate, how to set up your Terraform configuration files, and how to run through the Terraform lifecycle to initialize, plan, apply, and verify the deployment. We will also look at how to create multiple instances with different configuration values.

Prerequisites

To create an EC2 instance on AWS with Terraform, you’ll need to have the following prerequisites in place:

  1. AWS Account: You must have an AWS account to create and manage resources on the AWS cloud. If you don’t already have this, you can sign up for an account and use the free tier here. For the first 12 months, you can run a free EC2 instance of the following specifications:
  • 750 hours per month of Linux, RHEL, or SLES t2.micro or t3.micro instance dependent on region
  • 750 hours per month of Windows t2.micro or t3.micro instance dependent on region

If you continue to run your EC2 instance after the 12-month free tier allowance period is up you’ll start getting charged, so remember to clean up after the tutorial!

  1. Terraform installed: You’ll need to have Terraform installed on your machine to write and execute Terraform code.
  2. AWS CLI installed & IAM User with permissions: You’ll need to have the AWS Command Line Interface (CLI) installed on your machine to interact with EC2 instances and other AWS resources from the command line.
aws configure

This will prompt you to enter your AWS access key ID, secret access key, default region, and default output format. You can obtain your access key ID and secret access key from the AWS Management Console by navigating to the security credential section once logged in or create a new one from there if needed.

If you have not already done so, you should create an IAM user with the minimum required permissions necessary.

  1. SSH key pair: To access a Linux-based EC2 instance via SSH, you’ll need an SSH key pair.

Run the following command to generate a new SSH key pair:

ssh-keygen -t rsa -b 4096

This will create a new RSA key pair with a length of 4096 bits.

You will be prompted to enter a file name to save the key pair. The default location is in your user’s home directory under the .ssh directory. You can choose a different file name or directory if you prefer.

You will be prompted to enter a passphrase for the key pair. This is optional but recommended to add an extra layer of security.

# First create ssh keys in current directory using
ssh-keygen -f terraform_ec2_key
# This will create two files in current directory
# 1. terraform_ec2_key
# 2. terraform_ec2_key.pub

The ssh-keygen command will generate two files: a private key file and a public key file. The private key file should be kept secure and never shared with anyone. The public key file can be shared with Amazon EC2 instances to allow SSH access.

Finally, to use the key pair with an Amazon EC2 instance, you must add the public key to the instance when you configure it with Terraform.

Authentication with AWS

You can configure Terraform to authenticate with AWS using several methods. With AWS CLI installed, we can use the named profiles method, which is a recommended approach for authenticating Terraform to AWS because it allows you to manage multiple sets of AWS credentials and control access to resources using IAM roles and policies.

First, ensure the AWS CLI is installed and configured using the guidelines in the prerequisite section to add the AWS access key ID and secret access key using the aws configure command. By default, the AWS CLI-named profiles use the same access key ID and secret access key as your default profile, so you don’t need to specify them again for different profiles.

Create an AWS CLI named profile for Terraform in the ~/.aws/config file (Linux and macOS) or %UserProfile%\.aws\config file (Windows). You can override the default access key ID and secret access key for a named profile by setting the aws_access_key_id and aws_secret_access_key attributes if required.

In your Terraform configuration file, the provider block can be configured as follows to reference the named profile:

provider "aws" {
region = "us-west-2"

}

Create an EC2 instance using Terraform configuration files

Terraform configuration files contain the definitions of how to authenticate to AWS, and which resources you want Terraform to create and manage.

To create an EC2 instance on AWS in the simplest way using Terraform, create a file called main.tf and add the code from the example below.

Note the t2.micro instance type and the region of us-west-2 qualify under the AWS compute free tier so you will not be charged as long as your account is less than 12 months old. The AMI is of Amazon Linux 2 type.

main.tf

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}

provider "aws" {
region = "us-west-2"
}

resource "aws_instance" "example_server" {
ami = "ami-04e914639d0cca79a"
instance_type = "t2.micro"
tags = {
Name = "DemoBlogExample"
}
}

Initialize the Terraform directory

From the directory that contains the main.tf configuration file, run terraform init.

Terraform downloads the aws provider and installs it in a hidden subdirectory of your current working directory, named .terraform.

Run terraform plan and apply

Run terraform plan. Terraform will create an execution plan.

Once you are happy that there are no unexpected changes,

run terraform apply and enter yes to confirm the execution.

Verify the deployment

Terraform will notify you on the command line once the EC2 instance is completed:

aws_instance.example_server: Creating...
aws_instance.example_server: Still creating... [10s elapsed]
aws_instance.example_server: Still creating... [20s elapsed]
aws_instance.example_server: Still creating... [30s elapsed]
aws_instance.example_server: Creation complete after 38s [id=i-01e03444e238b394]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Browse to the EC2 section in the AWS portal us-west-2 region and view your EC2 instance.

To avoid being charged, you can destroy the EC2 instance with terraform destroy.

Adding user_data to EC2 instance

Adding user_data is a way to add perform some configuration on the EC2 instance during creation, such as setting the hostname, mounting a file share, or installing a software package.

In this example, we will add our generated SSH key so we can connect to the EC2 instance we created earlier.

If you need to generate an SSH keypair first run the following:

ssh-keygen -t rsa -b 4096

I named my key jack1, so I can view the public portion of the key once created:

cat <your_key_name>.pub

now to keep this pass the SSH key via variables in the variables.tf file and use it in your main.tf file. Here is how you can do it:

  1. Define the variable in variables.tf:
variable "ssh_key" {
description = "The SSH public key to be added to the instance."
type = string
}

Assign a default value in terraform.tfvars (or pass it via the command line or environment variables):

ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDT54B8Le3cQe6ufDHltjSfq/VU1beEy5B2uhVZOGWbOekBhItqEmY3FErYHJzlHRWKwiwuH43uLpSlo/mvhYm/sV2zDWU/Sqq5Th2m9pUYGg0daFUA/iK3wBfWIVJHe6KqIEmLjKyoN3i12nTACbpmSTb5qXEnp6DVvdgIh3Pa9ID/r+geEeS0YIEztmyVKa947bp64/+zKXznWxyYmQYDZkmbKi8JsMXLGTdemQp6QBIme6D3KTPkGIFyG2VECRBn1InruQHeG+kmKDIAzxBeOfGFmTSDyEA+cT4+DMyQtWwcMx1mc9UAmGVo6NEwY1Y/mBOLHwdjBCnJO4Eiis3eJYiA8n7+jIAJ66ANPVIfBYoQ6NoYi2+Ep3EvhDcTJbq2/WgsJTwFAd84F+42PNsltnkTIRsOdsJZtrhxh1dgV91Sk919d0oME0Gph4XHk9q1ddD1lXRPfsG9Ejq6i9GqTB+spk6PXWaC57Im++XL/w3FI/sNLCIVgtXZeeL/GktzDrhDI2s+81hYTcyaw5cfdEb4xULS0NxLVUklO907gQsw4zU0zHYJHwN/uhsEn2eIuqECTFrF5ZmoJyyRygz5ddUKO4qVmWCzqUD0FTQLmYlmG97TSIFmUzVMhH+ZWd2knqlBfSHBUq2tex7fYxRRT9jIGHIfTgAXtbiBkucjlQ== jackw@JAC10"
  1. Update main.tf to use the variable:

And copy this key into my configuration file under the user_data section as follows:

provider "aws" {
region = "us-west-2"
}
resource "aws_instance" "example_server" {
ami = "ami-04e914639d0cca79a"
instance_type = "t2.micro"
user_data = <<EOF
#!/bin/bash
echo "Copying the SSH Key to the server"
echo -e "${var.ssh_key}" >> /home/ubuntu/.ssh/authorized_keys
EOF
tags = {
Name = "DemoBlogExample"
}
}

or pass your ssh directly

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
} required_version = ">= 1.2.0"
}

provider "aws" {
region = "us-west-2"
}

resource "aws_instance" "example_server" {
ami = "ami-04e914639d0cca79a"
instance_type = "t2.micro"
user_data = <<EOF
#!/bin/bash
echo "Copying the SSH Key to the server"
echo -e "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDT54B8Le3cQe6ufDHltjSfq/VU1beEy5B2uhVZOGWbOekBhItqEmY3FErYHJzlHRWKwiwuH43uLpSlo/mvhYm/sV2zDWU/Sqq5Th2m9pUYGg0daFUA/iK3wBfWIVJHe6KqIEmLjKyoN3i12nTACbpmSTb5qXEnp6DVvdgIh3Pa9ID/r+geEeS0YIEztmyVKa947bp64/+zKXznWxyYmQYDZkmbKi8JsMXLGTdemQp6QBIme6D3KTPkGIFyG2VECRBn1InruQHeG+kmKDIAzxBeOfGFmTSDyEA+cT4+DMyQtWwcMx1mc9UAmGVo6NEwY1Y/mBOLHwdjBCnJO4Eiis3eJYiA8n7+jIAJ66ANPVIfBYoQ6NoYi2+Ep3EvhDcTJbq2/WgsJTwFAd84F+42PNsltnkTIRsOdsJZtrhxh1dgV91Sk919d0oME0Gph4XHk9q1ddD1lXRPfsG9Ejq6i9GqTB+spk6PXWaC57Im++XL/w3FI/sNLCIVgtXZeeL/GktzDrhDI2s+81hYTcyaw5cfdEb4xULS0NxLVUklO907gQsw4zU0zHYJHwN/uhsEn2eIuqECTFrF5ZmoJyyRygz5ddUKO4qVmWCzqUD0FTQLmYlmG97TSIFmUzVMhH+ZWd2knqlBfSHBUq2tex7fYxRRT9jIGHIfTgAXtbiBkucjlQ== jackw@JAC10" >> /home/ubuntu/.ssh/authorized_keys
EOF
tags = {
Name = "DemoBlogExample"
}
}

Alternatively, you could use a key_name variable and create a key_pair with the public key beforehand, instead of directly inserting the public key into the user data section.

Run terraform plan and apply

Run terraform plan. Terraform will create an execution plan.

Once you are happy that there are no unexpected changes,

run terraform apply and enter yes to confirm the execution.

Obtain Public IP or DNS of the EC2 Instance

After the instance is created, note down its Public IP or DNS name from the AWS Management Console or Terraform output.

Set Permissions for the Private Key File

Ensure the private key file (my-ec2-key) has the correct permissions:

chmod 400 /path/to/my-ec2-key

Step 6: SSH into the EC2 Instance

Use the SSH command to connect to your instance. Replace the placeholders with your actual user (ubuntu for Ubuntu instances or ec2-user for Amazon Linux instances), the path to your private key, and the instance's Public IP or DNS.

ssh -i /path/to/my-ec2-key ubuntu@your-instance-public-ip-or-dns

For example:

ssh -i /path/to/my-ec2-key ubuntu@ec2-203-0-113-25.compute-1.amazonaws.com

Step 7: Verify the Connection

If the connection is successful, you should see a welcome message and a command prompt from the EC2 instance. This indicates that the SSH key is correctly linked and working.

Note: if you get error AWS ssh access ‘port 22: Operation timed out’ issue

check the following

- On AWS console: is the Instance up and healthy?

- Is it in a public Subnet?

- Does it have a public ip?

- Does the VPC have an associated Internet Gateway?

- Does it have the Routing Table to the Internet Gateway? (Attached to the subnet?)

- Are the Network ACL rules default?

- Does the Security group allow ping? If yes, does the ping work?

- Does the Security group allow SSH inbound?

Creating a SSH access enabled EC2 instance using Terraform from scratch. This will essentially serve as a test environment for some future projects, thus it will be configured to be used as a module.

Setting up VPC:

A vpc consisting of a Subnet with an internet gateway attached to it, two ec2 instances, a security group that allows for ssh access and routing tables.

Setting up the working Directory:

Define the variables:
In this file, we define the variables that we are going to use, these will allow the child module to customize aspects of the configuration without changing the source code. This allows the module to be composable and reusable.

variables.tf
variable "key_pair_name" {
description = "key_pair_name"
type = string
}

variable "instance_type" {
description = "instance_type"
type = string
}

variable "instance_tag" {
description = "Tag given to each deployed Instance"
type = string
}

variable "counter" {
description = "Number of instances to launch"
type = number
}

variable "file_name" {
description = "Name of the key pair"
type = string
}

variable "cidr_block" {
description = "CIDR Block"
type = string
}

variable "availability_zone"{
description = "Availability Zones for the Subnet"
type = string
}

VPC:
The VPC is our virtual network and is where all the resources will be launched. This will be created in the vpc.tf file.

resource "aws_vpc" "test_env" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags{
Name = "test_env_vpc"
}
}

We are asking Terraform to set up an AWS VPC resource named “test_env”. The CIDR BLOCK is used to configure the expanse of our network. I am opting to not hard code the cidr block to allow the module to be flexible.

Subnets:
A subnet in our newly created VPC.
vpc_id attaches the subnet to the VPC and a variable is being used here to allow the user of the module to define which Availability Zone this will be deployed in.

resource "aws_subnet" "subnet" {
cidr_block = cidrsubnet(aws_vpc.test_env.cidr_block, 3, 1)
vpc_id = aws_vpc.test_env.id
availability_zone = var.availability_zone
}

Internet Gateway:

resource "aws_internet_gateway" "test_env_gw" {
vpc_id = aws_vpc.test_env.id
}

An internet gateway allows public traffic to come through. With this part of the VPC cam remain private and unexposed. This let’s us to keep part of the VPC private.

Security Groups:

By default AWS allows all outside traffic, but this is not enabled by default in Terraform.
This security group will allows connections via port 22 and traffic will be forwarded without restriction.
The ingress and egress blocks are used to control how traffic flows in and out of the VPC respectively.
All traffic is allowed to leave the VPC as defined in the egress block.

resource "aws_security_group" "security" {
name = "allow-all"
vpc_id = aws_vpc.test_env.id
ingress {
cidr_blocks = [
"0.0.0.0/0"
]
from_port = 22
to_port = 22
protocol = "tcp"
}
egress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
}
}

Launching the Instances:

The requirement is for instances being launched via this module to always have the latest Ubuntu AMI. Configuring our module to operate this way ensures that the instances will always be up to date.
To do this the data resource block will be utilized to pull the latest version of the AWS Ubuntu AMI.

data "aws_ami" "ubuntu_ami" {
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"]

}

Now for the instance itself. For this we have to set up an aws_instance resource.

resource "aws_instance" "test_env_ec2" {
count = var.counter
ami = data.aws_ami.ubuntu_ami.id
instance_type = var.instance_type
key_name = var.key_pair_name
security_groups = ["${aws_security_group.security.id}"]
associate_public_ip_address = true
subnet_id = aws_subnet.subnet.id
tags = {
Name = var.instance_tag[count.index]
}
}

All the values of the instances are being provided by the variables that were defined earlier.

The Internet Gateway:

For traffic to be routed to the VPC it needs an Internet Gateway

resource "aws_internet_gateway" "test_env_gw" {
vpc_id = aws_vpc.test_env.id
}

Route Table:

The Internet Gateway will be attached to this Route Table and a link between the subnet and the Route Table will expose the subnet to the internet allowing access.

resource "aws_subnet" "subnet" {
cidr_block = cidrsubnet(aws_vpc.test_env.cidr_block, 3, 1)
vpc_id = aws_vpc.test_env.id
availability_zone = var.availability_zone
}

Now it’s ready….or is it?

As opposed to creating the key pair for the Instance via the AWS Console this will be done via Terraform as well.

Key Pair Creation:

In the Terraform official documentation there is an AWS key pair resource that can be used to get this done.

resource "aws_key_pair" "tf_key" {
key_name = var.key_pair_name
public_key = tls_private_key.rsa.public_key_openssh
}

I am going to set the name of the key pair to the variable that was declared earlier in the module (var.key_pair_name)

When making a key pair you get a private and public key. The private key is to be downloaded and kept secure, whilst the public key is to be shared on the server.

To create the private key the tis_private_key resource will be utilized

# RSA key of size 4096 bits
resource "tls_private_key" "rsa-4096-example" {
algorithm = "RSA"
rsa_bits = 4096
}

Just like in the AWS Console this block of code provides all the configurations necessary to generate a private key pair. The name will be changed to rsa.

After this creation a public and private key will be generate
Update the public_key in the aws_key_pair resource block to “tls_private_key.rdssa.public_key_openssh”. This will allow us to get the public key

Storing the Key Pair:

Next the Private Key needs to be stored in the instance in order to ssh into the instance. To do this a folder must be created.
For this the local_file resource will be used.

The content will be private content of the key that will be created

resource "local_file" "tf_key" {
content = tls_private_key.rsa.private_key_pem
filename = var.file_name
}

The root module is complete! A child module can now used to call the resources made.

Running the configuration:

Terraform Plan (Creating Two EC2 Instances) Resources being added will vary based on variable values set in the Child module

All Good!

The resources created in the AWS Console

Key created via Terraform Configuration present in AWS Console

You can view this module on my GitHub

Conclusion

You can create instances of EC2 machines on AWS using Terraform with the same or different configuration. As you can see, Terraform is an extremely powerful way to deploy, manage and maintain your EC2 instances.

In upcoming articles, I’ll delve into exciting topics like Ansible, Helm charts, and beyond. Your feedback and questions are invaluable, so feel free to share as I continue this learning adventure. Stay curious, and let’s keep building amazing things! 🚀

Thank You for reading

Please give 👏🏻 Claps if you like the blog.

Made with ❤️by Vaibhav Hariramani

Don’t forget to tag us

if you find this blog beneficial for you don’t forget to share it with your friends and mention us as well. And Don’t forget to share us on Linkedin, instagram, facebook , twitter, Github

More Resources

To learn more about these Resources you can Refer to some of these articles written by Me:-

Do Checkout My other Blogs

Do find time check out my other articles and further readings in the reference section. Kindly remember to follow me so as to get notified of my publications.

Do Checkout My Youtube channel

Follow me

on Linkedin, instagram, facebook , twitter, Github

Happy coding ❤️ .

--

--

VAIBHAV HARIRAMANI

Hi there! I am Vaibhav Hariramani a Travel & Tech blogger who love to seek out new technologies and experience cool stuff.