In this article, we will show how to create and manage 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!
2. Terraform installed
You'll need to have Terraform installed on your machine to write and execute Terraform code. You can download the appropriate version for your machine here. Extract the downloaded zip file to a directory on your machine and add the directory containing the Terraform binary to your system's PATH environment variable.
3. AWS CLI installed and 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. You can download the appropriate version for your system here.
Once you have installed the AWS CLI, you can configure it by running the following command in a terminal window:
aws configure
This will prompt you to enter your AWS access key ID, secret access key, default region, and default output format. Once logged in, you can obtain your access key ID and secret access key from the AWS Management Console by navigating to the security credential section or creating 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.
Learn more about AWS IAM best practices.
4. 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.
The ssh-keygen command generates two files: a private key file and a public key file. The private key file should be kept secure and never shared with anyone, while 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\confi
g 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.
Although it is not necessary (and may cause problems if one of your team uses the code in a different SDK), you can add a new profile section with a unique profile name, such as jack.roper
. The example below tells the AWS CLI to use the us-west-2
region for the jack.roper
profile.
[profile jack.roper]
region = us-west-2
In your Terraform configuration file, the provider block can be configured as follows to reference the named profile:
provider "aws" {
region = "us-west-2"
profile = "jack.roper"
}
Note you can also authenticate using environment variables:
$ export AWS_ACCESS_KEY_ID=
$ export AWS_SECRET_ACCESS_KEY=
How to create an EC2 instance using Terraform
Creating an EC2 instance using Terraform involves writing Terraform configuration to define the desired infrastructure. Below are two examples with a step-by-step guide to creating an EC2 instance using Terraform:
Example 1: How to 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 that 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 the 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"
profile = "jack.roper"
}
resource "aws_instance" "example_server" {
ami = "ami-04e914639d0cca79a"
instance_type = "t2.micro"
tags = {
Name = "JacksBlogExample"
}
}
Initialize the Terraform directory
From the directory containing 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.
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.example_server will be created
+ resource "aws_instance" "example_server" {
+ ami = "ami-04e914639d0cca79a"
+ arn = (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:
Read more about the terraform plan
command and terraform apply
command.
Verify the deployment
Terraform will notify you on the command line once the EC2 instance is completed:
Enter a value: yes
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
.
π‘ You might also like:
- Managing Multiple Terraform Environments Efficiently
- Terraform on AWS β Deploying AWS Resources
- How to Create AWS IAM Policy Using Terraform
- Don't miss out! Get more posts like this - subscribe to our newsletter
Example 2: How to create an EC2 instance with user_data
Adding user_data allows you to perform configuration tasks such as setting the hostname, mounting a file share, or installing a software package on the EC2 instance during creation.
In this example, we will add our generated SSH key to 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
We named our key jack1, so we can view the public portion of the key once created:
cat jack1.pub
We now copy this key into our configuration file under the user_data section as follows:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = "us-west-2"
profile = "jack.roper"
}
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 = "JacksBlogExample"
}
}
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.
How to create multiple EC2 instances with different configurations
To create multiple EC2 instances with Terraform, you can use loops, such as count or for_each, to dynamically provision instances.
For example, to simply create ten instances of our EC2 machine with the same configuration, we set the count variable:
resource "aws_instance" "example_server" {
ami = "ami-04e914639d0cca79a"
instance_type = "t2.micro"
count = 10
tags = {
Name = "JacksBlogExample"
}
}
To create multiple EC2 instances with different configurations, we create a new file:
dev.tfvars
configuration = [
{
"application_name" : "example_app_server-dev",
"ami" : "ami-04e914639d0cca79a",
"no_of_instances" : "10",
"instance_type" : "t2.medium",
},
{
"application_name" : "example_web_server-dev",
"ami" : "ami-04e914639d0cca79a",
"instance_type" : "t2.micro",
"no_of_instances" : "5"
},
]
In this example, we will create ten app servers of size t2.medium and five web servers of size t2.micro.
Our main.tf is modified as below:
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = "us-west-2"
profile = "jack.roper"
}
variable "configuration" {
description = "EC2 configuration"
default = [{}]
}
locals {
serverconfig = [
for srv in var.configuration : [
for i in range(1, srv.no_of_instances+1) : {
instance_name = "${srv.application_name}-${i}"
instance_type = srv.instance_type
ami = srv.ami
}
]
]
}
locals {
instances = flatten(local.serverconfig)
}
resource "aws_instance" "example_server" {
for_each = {for server in local.instances: server.instance_name => server}
ami = each.value.ami
instance_type = each.value.instance_type
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 = "${each.value.instance_name}"
}
}
output "instances" {
value = "${aws_instance.example_server}"
description = "EC2 details"
}
Alternatively, you could use a map(object) and leave the type, the instance name in the tags would then be set as each.key.instance_name
.
variable "configuration" {
default = {
instance1 = {
ami = "ami..."
instance_type = "t2.micro"
}
}
}
Check out also how to deploy an AWS ECS cluster with Terraform.
How to upload a local file to an Amazon EC2 instance using Terraform
To upload a local file to an Amazon EC2 instance using Terraform, you can use the file *provisioner inside your Amazon EC2 instance. Alternatively, you can also use *local-exec and run a scp command.
Depending on your EC2 configuration (Linux or Windows vms), you will need to define a connection block inside the file provisioner and then configure it accordingly.
Linux example
resource "aws_instance" "linux_instance" {
ami = "ami-id"
instance_type = "t2.micro"
key_name = "my-key"
subnet_id = "subnet-linux"
security_groups = ["ssh"]
provisioner "file" {
source = "local_file.txt"
destination = "/home/ec2-user/remote_file.txt"
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/my-key.pem")
host = self.public_ip
}
}
}
As you can see in the example above, we have defined a very simple Linux instance that uses the file provisioner. In the source, we specify the file we want to copy from the machine that runs Terraform, and in the destination, we specify the location in which the file will be copied.
In the connection block, because this is a Linux instance, we specify "ssh" as the type of connection. We then specify the user we will use for the ssh connection, the private key used for connecting to the instance, and also its IP.
By applying the code, the file will be copied to the home of the ec2-user.
Alternatively, if you want to use local-exec and run an scp command you can do the following:
resource "aws_instance" "linux_instance" {
ami = "ami-id"
instance_type = "t2.micro"
key_name = "my-key"
subnet_id = "subnet-linux"
security_groups = ["ssh"]
provisioner "local-exec" {
command = "scp -i ~/.ssh/my-key.pem local_file.txt ec2-user@${self.public_ip}:/home/ec2-user/remote_file.txt"
}
}
In the SCP command, we specify the SSH key to use and then where to connect, what to send, and where to send it.
Windows example
Things will look quite similar for Windows, but the connection will change to winrm, and you will use a user and a password to connect to your Windows instance. Make sure you use double "\" because "\" is a special character that is used for escaping, so you will need to escape it to use it:
resource "aws_instance" "windows_instance" {
ami = "ami-id"
instance_type = "t2.micro"
key_name = "my-key"
subnet_id = "subnet-windows"
security_groups = ["winrm-access"]
provisioner "file" {
source = "local_script.ps1"
destination = "C:\\Users\\Administrator\\remote_script.ps1"
connection {
type = "winrm"
user = "Administrator"
password = "YourInstancePassword"
host = self.public_ip
}
}
}
If we apply the code, the local_script.ps1 will be copied to C:\Users\Administrator\remote_script.ps1.
How to use Terraform modules to deploy an EC2 instance
The most straightforward way to deploy EC2 instances by using a Terraform module is to build your own module.
Let's build a very simple one:
main.tf
resource "aws_instance" "this" {
for_each = var.instances
ami = each.value.ami_id
instance_type = each.value.instance_type
subnet_id = each.value.subnet_id
tags = merge(
{
Name = each.key
},
each.value.tags
)
}
variables.tf
variable "instances" {
description = "Map of EC2 instances to create with their configurations"
type = map(object({
instance_type = string
subnet_id = string
ami_id = string
tags = map(string)
}))
}
outputs.tf
output "instances" {
description = "Map of instance names to their IDs and private ips"
value = { for k, v in aws_instance.this : k => {"id": v.id, "private_ip": v.private_ip } }
}
In the example above, we have defined a module that will create as many EC2 instances as we want based on the *instances *variable. We have also defined an output that maps the instance names to their IDs and private IPs.
Let's now build an example that uses this module:
provider "aws" {
region = "eu-west-1"
}
module "ec2_instances" {
source = "../"
instances = {
web-1 = {
instance_type = "t3.micro"
subnet_id = "subnet-12345678"
ami_id = "ami-0735c191cf914754d"
tags = {
env = "dev"
role = "web"
}
}
web-2 = {
instance_type = "t3.micro"
subnet_id = "subnet-87654321"
ami_id = "ami-0735c191cf914754d"
tags = {
env = "dev"
role = "web"
}
}
}
}
This example will create two EC2 instances with the instance type t3.micro in the eu-west-1 region. The configuration can be easily scaled up or down depending on your use case by adding or removing objects from the instances map(object).
Let's look at how easy it would be to scale down:
provider "aws" {
region = "eu-west-1"
}
module "ec2_instances" {
source = "../"
instances = {
web-1 = {
instance_type = "t3.micro"
subnet_id = "subnet-12345678"
ami_id = "ami-0735c191cf914754d"
tags = {
env = "dev"
role = "web"
}
}
}
}
If you want to explore how the public AWS module for EC2 works, check it out here.
How to automate Amazon EC2 instance starting and stopping with Terraform
There is no straightforward way to automate the starting and stopping process for an EC2 instance, but you can use a solution that combines Lambda and Eventbridge. The Lambda functions would take care of starting and stopping operations, while Eventbridge would trigger the functions based on a schedule.
Best practices for securing an EC2 instance created with Terraform
To secure an EC2 instance created with Terraform, you will need to actually create some other resources in conjunction with it to apply security measures:
- Network security - Create a security group with controlled egress traffic and ensure that ingress rules respect the least privilege principle.
- Access management - Leverage SSM for secure shell access, and build IAM role with the least privilege.
- Data protection - Encrypt your EBS volume with KMS, and enable key rotation.
- Observability - Install the AWS CloudWatch agent and enable detailed monitoring.
- OS hardening - Disable root login and password authentication, enable and configure your firewall, and install minimum required packages.
- Others - Use tags for tracking, and use the create before destroy option when making destructive changes to ensure that a new instance is created before the existing one is torn down.
Deploying Terraform resources with Spacelift
Terraform is really powerful, but to achieve an end-to-end secure Gitops approach, you need to use a product that can run your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a powerful CI/CD workflow and unlocking features such as:
- Policies (based on Open Policy Agent) - You can control how many approvals you need for runs, what kind of resources you can create, and what kind of parameters these resources can have, and you can also control the behavior when a pull request is open or merged.
- Multi-IaC workflows - Combine Terraform with Kubernetes, Ansible, and other infrastructure-as-code (IaC) tools such as OpenTofu, Pulumi, and CloudFormation, create dependencies among them, and share outputs
- Build self-service infrastructure - You can use Blueprints to build self-service infrastructure; simply complete a form to provision infrastructure based on Terraform and other supported tools.
- Integrations with any third-party tools - You can integrate with your favorite third-party tools and even build policies for them. For example, see how to Integrate security tools in your workflows using Custom Inputs.
Spacelift enables you to create private workers inside your infrastructure, which helps you execute Spacelift-related workflows on your end. For more information on configuring private workers, refer to the documentation.
You can check it for free by creating a trial account or booking a demo with one of our engineers.
Key points
You can create single or multiple instances of EC2 machines with the same or different configurations on AWS using Terraform. Terraform is an extremely powerful way to deploy, manage, and maintain your EC2 instances.
Written by Jack Roper
Top comments (0)