Now that we have seen all the networking steps required, let's put it all together.
But one last thing to mention first:
Public vs Elastic IPs
One thing we haven't talked about is public IP addresses. All the IP addresses I've mentioned so far are local to within the VPC, but you also need some sort of public IP address that people on the internet can visit to connect to your server.
You can create an EC2 instance with a public IP address. The key thing to note with public IPs though, is whenever you stop your instance, the public IP will change, so it’s not a good idea to point your domain name to your public IP. For that you need an Elastic IP. Request an Elastic IP from AWS, and then you can attach it to a particular EC2 instance (or to a load balancer, or a NAT gateway). I’ll show how to do this below.
Terraform walkthrough
With that explained, here is the complete Terraform code you need to get a server up and running on AWS. You can get the entire thing in one file from here:
Here's a visual summary of what we're going to do:
EC2 with public IP example
Boilerplate terraform initialization code:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-west-1"
# Optional, this will tag all resources we create
# so we can easily find them later to delete.
default_tags {
tags = {
Terraform = "true"
}
}
}
This is just boilerplate code you will need when using Terraform with AWS.
VPC
Now let’s create the VPC. It needs a CIDR block. The CIDR block can be anything, but I recommend using /16
as the suffix.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
# If you set the `Name` tag, AWS will use it
# for adding a name to your resource in the console view.
# This works for some resources but not others.
tags = {
Name = "terraform"
}
}
Subnet
Create the subnet and associate it with the VPC. Notice the CIDR block for the subnet is a subset of the CIDR block for the VPC.
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
AMI for EC2 instance
Now we want to create the EC2 instance and put it in the subnet. Every EC2 instance needs an AMI (Amazon Machine Image). You can get the ID for the AMI you want from AWS here: https://console.aws.amazon.com/ec2/
Or you can just look it up in Terraform like this:
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
EC2 instance
Now let's create an EC2 instance that uses that AMI.
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
# assign it a public ip so we can connect to it
associate_public_ip_address = true
# references security group created below
vpc_security_group_ids = [aws_security_group.sg.id]
lifecycle {
replace_triggered_by = [aws_security_group.sg]
}
# subnet to launch the instance in
subnet_id = aws_subnet.public.id
# simple server running on port 80 so we can verify
# that the instance is up and we can connect to it
user_data = <<-EOF
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p "80" &
EOF
}
Security group
We'll also create a security group that allows inbound HTTP traffic on port 80 from anywhere:
resource "aws_security_group" "sg" {
name = "terraform"
# We need to explicitly put the security group in this VPC
vpc_id = aws_vpc.main.id
# Inbound HTTP from anywhere
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
IGW
Create an internet gateway and associate it with the VPC
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
}
Route table
Create a new route table and route and add a route to the internet gateway
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
Associate our public subnet with this route table:
resource "aws_route_table_association" "public_subnet_asso" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
Finally, we need to output the public IP of the instance so we can connect to it.
output "public-ip" {
value = aws_instance.web.public_ip
}
# Optionally, output a URL for convenience
output "url" {
value = "http://${aws_instance.web.public_ip}"
}
Try it!
Put all that in a file called main.tf
. Run terraform init
and terraform apply
. I'm glossing over the details of how to use Terraform here, since there are other tutorials on that. After the changes apply, Terraform will print out the IP address and URL.
Hit the url that gets printed out using curl <url>
. You may need to give it a few minutes for the instance to boot up.
If you get an error:
If you get an error right away, that means everything works and you can hit your instance, but the server isn't up for some reason.
If there's a wait before you get the error, that means you weren't able to connect to your instance at all. This could be any number of things, such as the IP you're using is wrong, or your security group or NACL are not set up to allow traffic in.
EC2 instance with elastic IP
All of the above, plus:
Request an Elastic IP and associate it with your instance:
resource "aws_eip" "lb" {
instance = aws_instance.web.id
domain = "vpc"
}
# print the elastic IP
output "elastic-ip" {
value = aws_eip.lb.public_ip
}
The end
And that's it! That is my introductory guide to networking for AWS. To close out, please enjoy this drawing of Nicholas Cage.