Posts Quizzes AI Interviewer Feedback
Login Register

AWS

Filtering by tag: AWS Clear
TECH

Creating an EC2 Instance in the Default VPC Using Terraform

Karen Jan 26, 2026

A good starting point for creating EC2 instances is to create them in the default VPC and subnet provided by AWS.

Deploying an Amazon EC2 instance within the default VPC subnet significantly reduces setup complexity while still providing a secure, scalable, and reliable infrastructure. The default VPC is automatically configured with essential components such as subnets in each Availability Zone, route tables, security groups, network ACLs, and an internet gateway. This allows to launch EC2 instances quickly without needing deep networking expertise, making it ideal for rapid production deployments. 

Additionally, using the default VPC subnet ensures built-in connectivity and high availability while following AWS best practices. Instances launched in a default subnet can access the internet immediately (when assigned a public IP). As workloads grow, these deployments in the default VPC can seamlessly integrate with load balancers, auto scaling groups, and managed services.

In this article we will go over the steps of setting up a minimal production-ready EC2 environment with Terraform which can be used for deploying your application. Before diving into the actual implementation, it helps to understand a few core AWS networking concepts.

Virtual Public Cloud (VPC)

A VPC is your own private network inside AWS. You can think of it as your company’s private “internet neighborhood,” isolated from others. Inside the VPC you define IP ranges, create subnets, and control all networking.

Subnet

A subnet is a smaller network segment inside your VPC.
Subnets can be:

  • Public: can reach the internet (through an Internet Gateway)

  • Private: cannot directly reach the internet

In the default VPC, all subnets are public because they have routes to the Internet Gateway.

Route Table

A route table contains rules that determine how network traffic is directed:

  • Internet-bound traffic is routed through the Internet Gateway

  • Internal traffic stays within the VPC

The default VPC includes a default route table that already has a 0.0.0.0/0 route to the internet gateway, making all its subnets public.

Internet Gateway (IGW)

An IGW allows resources inside your VPC to connect to the internet. Default VPC has an IGW attached.

Security Groups

Security groups are virtual firewalls attached to EC2 instances or other resources, defining what inbound and outbound traffic is allowed.

Terraform template

We select AWS as a provider, then fetch default vpc and subnet. Existing resources are referenced using the data keyword, while new resources are created using the resource keyword.

For the security group, we open port 80 and 22 for web traffic and ssh access repsectively, and outbound traffic is allowed to anywhere.

We then create a t2.micro EC2 instance, referencing correct subnet, security group and key_pair. The key pair is used for SSH access from your local machine.

To generate a key pair if one does not already exist, run:

ssh-keygen -t rsa -b 4096 -f ~/.ssh/my-key (passphrases may be skipped).

this command generates my-key and my-key.pub files in the ssh directory (you can give it any other name).

The public key is uploaded to EC2. You can verify this in the AWS console under:

Instance -> Details -> Key pair assigned at launch and it should list reference my-key

or by connecting to the instance and checking:

cat ~/.ssh/authorized_keys.

To ssh into you instance:

ssh -i ~\.ssh\my-key [user]@[public_ip_of_instance] (Bash)

or 

ssh -i $env:USERPROFILE\.ssh\my-key [user]@[public_ip_of_instance] (Powershell)

The corresponding terraform template is the following:

provider "aws" {
  region = "eu-west-2"  # Choose the AWS region to deploy into
}

# Fetch the region's default VPC
data "aws_vpc" "default" {
  default = true  # Tells AWS: use the default VPC
}

# Get all default subnets inside the default VPC
data "aws_subnets" "default" {
  filter {
    name   = "vpc-id"               # Filter subnets by VPC ID
    values = [data.aws_vpc.default.id]  # Use the default VPC's ID
  }
}

# Security group for the web instance
resource "aws_security_group" "web" {
  name        = "web-sg"        # Security group name
  description = "Allow HTTP and SSH"  # What this SG is for
  vpc_id      = data.aws_vpc.default.id  # Attach SG to default VPC

  ingress {
    description = "HTTP"           # Allow web traffic
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"            # HTTP uses TCP
    cidr_blocks = ["0.0.0.0/0"]    # Allow from anywhere
  }

  ingress {
    description = "SSH"            # Allow SSH access
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"            # SSH uses TCP
    cidr_blocks = ["0.0.0.0/0"]    # Allow SSH from anywhere (not ideal for prod)
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"             # -1 = all protocols
    cidr_blocks = ["0.0.0.0/0"]    # Allow all outbound traffic
  }
}

resource "aws_key_pair" "host_key" {
  key_name   = "my-key"
  public_key = file("~/.ssh/my-key.pub")
}


# EC2 instance running the web app
resource "aws_instance" "web" {
  ami                    = "ami-03a725ae7d906005d" # OS image (update for your region)
  instance_type          = "t2.micro"               # Instance size
  subnet_id              = data.aws_subnets.default.ids[0]  # Put instance in a default subnet
  vpc_security_group_ids = [aws_security_group.web.id] # Attach security group
  associate_public_ip_address = true  # Give the instance a public IP
  key_name = aws_key_pair.host_key.key_name

  tags = {
    Name = "web"  # Tag for identifying the instance
  }
}

IP Restriction

Opening SSH to all IPs (0.0.0.0/0) is not recommended, as it exposes the instance to brute-force attempts. A safer approach is to restrict SSH access to your own IP address. brute force attempts to connect to your instance, and even with key auth, there are risks involved. Let's replace cidr_blocks = ["0.0.0.0/0"] in

  ingress {
    description = "SSH"            # Allow SSH access
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"            # SSH uses TCP
    cidr_blocks = ["0.0.0.0/0"]    # Allow SSH from anywhere (not ideal for prod)
  }

with a variable-based configuration

variable "ssh_allowed_ips" {
  type = list(string)
}

ingress {
    description = "SSH"            # Allow SSH access
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"            # SSH uses TCP
    cidr_blocks = var.ssh_allowed_ips
  }

Then define ssh_allowed_ips variable in terraform.tfvars. terraform.tfvars file is located in the same directory as your main.tf.

ssh_allowed_ips = [
  "203.0.113.42/32"
]

Check your computer IP (can be checked with What Is My IP Address - See Your Public Address - IPv4 & IPv6) and adjust  ssh_allowed_ips to match your own public address.

This method, however, may not be ideal if your PC’s IP address changes frequently. In that case, configuring an IP range instead of a single IP, using a VPN, or leveraging AWS Systems Manager may be better options.

If everything was done correctly, you should have an up and running EC2 instance. You should also be able to SSH to the instance from your PC only.

 Notes

  • Route tables belong to a VPC but are associated with subnets
  • Internet Gateways are VPC-level resources
  • A public EC2 instance cannot reside in a private subnet
Read more
TECH

DNS and HTTPS Configurations

Karen Jan 24, 2026

In previous posts we explored how to create an EC2 instance using Terraform. However, to make the application production-ready, a few more important steps are still needed. We need to:

  • Create an Elastic IP and attach to the instance
  • Register a domain (Route 53 preferrably) and point to the Elastic IP
  • Create Route 53 records, such as yourdomain.com www.yourdomain.com 
  • Launch the instance with a predefined script for installations (optional)
  • Configure HTTPS
  • Update the nginx container and default.conf to handle HTTPS traffic

Elastic IP

Elastic IP can be thought of  as a static IP and it is better to point your domain to the elastic IP rather than to the public IP because when the instance restarts, public IP can change. Using an Elastic IP ensures that your application remains accessible at the same address even if the EC2 instance is stopped and started again.

In Terraform you can create and attach an elastic IP to your instance like this:

resource "aws_eip" "app_eip" {
  domain = "vpc"

  tags = {
    Name = "app-eip"
  }
}

resource "aws_eip_association" "app_eip_assoc" {
  instance_id   = aws_instance.web.id
  allocation_id = aws_eip.app_eip.id
}

Now when the Elastic IP is created and attached to the instance, it is time to get a domain. Assuming the domain is purchased using AWS Route 53, you can reference it in Terraform and point it to your Elastic IP. Managing DNS records through Terraform also allows your infrastructure and configuration to remain fully reproducible.

Domain Handling

variable "domain_name" {
  description = "Root domain name"
  type        = string
}

data "aws_route53_zone" "zone" {
  name = var.domain_name
  private_zone = false
}

resource "aws_route53_record" "root" {
  zone_id = data.aws_route53_zone.zone.zone_id
  name    = var.domain_name
  type    = "A"
  ttl     = 300
  records = [aws_eip.app_eip.public_ip]
}

resource "aws_route53_record" "www_root" {
  zone_id = data.aws_route53_zone.zone.zone_id
  name    = "www.${var.domain_name}"
  type    = "A"
  ttl     = 300
  records = [aws_eip.app_eip.public_ip]
}
var.domain_name is simply a variable referenced in the terraform.tfvars, for example 
domain_name = "yourdomain.com"
This approach keeps your configuration flexible across environments. If your domain is unlikely to change, you can also hardcode it directly inside the Terraform file instead of using terraform.tfvars.
 
HTPPS Configuration
 
Once the EC2 instance is up and running (you can refer to Creating an EC2 Instance in the Default VPC Using Terraform for the complete Terraform configuration) it is time to configure HTTPS. Securing your application with HTTPS is critical, as it encrypts traffic between the client and the server and is expected by modern browsers.
On the instance, run the following commands:
  • sudo yum install certbot python3-certbot-nginx
  • sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com

After the certificates are generated, verify that /etc/letsencrypt/live/ exists on the host and contains the expected certificate files. These certificates will later be mounted into the nginx container.

Now we need to modify nginx's default.conf and later the container to properly handle HTTPS traffic.

In default.conf we define two server blocks. Port 80 now redirects traffic to port 443, ensuring that all requests use HTTPS. Port 443 is configured to handle secure traffic using the certificates generated by Certbot.

Previously created ssl_certificate files are referenced. 

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    client_max_body_size 10M;
    server_name yourdomain.com www.yourdomain.com;

     # SSL certificate files
     ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    location /static/ {
        alias /app/static/;
        expires 1d;
        add_header Cache-Control "public";
    }

    location / {
        limit_req zone=mylimit burst=10 nodelay;
        proxy_pass http://web:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

This setup is similar to the one described in Gunicorn and Nginx Setup for Serving the Web App, with the key difference being the addition of redirecting, HTTPS support and certificate handling.

Updating the Nginx Container

Now it time to modify the nginx container. Since the SSL certificates are generated on the host, they must be mounted into the container so Nginx can access them.

In Gunicorn and Nginx Setup for Serving the Web App post's setup, the container only exposed port 80. We now expose port 443 as well and mount the /etc/letsencrypt directory as read-only. The updated Docker Compose configuration looks like this:

nginx:
    image: nginx:latest
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - web
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - ./src/nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/logs:/var/log/nginx
      - ./src/staticfiles:/app/static
    restart: always
    networks:
      - myproject-net

Assuming your web container is running on port 8000 and the nginx container is also successfully running, opening  yourdomain.com or www.yourdomain.com in the browser should now securely serve the web app over HTTPS.

Read more