How I Built Highly Available Cloud Infrastructure For Hosting A Web Application
Using Infrastructure as Code (Terraform)
Date: 15/03/2025
Here I'll be walking you through how I setup the cloud infrastructure for hosting a web app on an EC2 instance in a way that ensures high availability for end users of the web app. I will be focusing on key components of the architecture in the main.tf file.
Some prerequisites required include:
- AWS Free Tier account setup
- HCP Terraform account setup
- Terraform installed on your computer
- GitHub account
- GitHub installed on your computer
Figure 1: Architectural diagram
Cloud Components (AWS Services)
| Component | Purpose |
|---|---|
| Virtual Private Cloud (VPC) | Isolated network environment for our resources |
| Public Subnets | Houses internet-facing components like load balancers |
| Private Subnets | Securely hosts EC2 instances away from direct internet access |
| Elastic Load Balancer (ELB) | Distributes traffic and provides failover capability - important for high availability |
| Network Address Translation (NAT) Gateway | Allows private instances to access internet while remaining secure |
| Elastic Compute Cloud (EC2) | Virtual server instances that host the web application |
| Amazon Machine Image (AMI) | Template containing software configuration used to create EC2 instances |
Terraform Project Structure (Key repository files)
To implement Infrastructure as Code (IaC) with Terraform my Terraform repository included the following scripts:
- main.tf - The main config file it contains resource definitions like VPCs, subnets, and EC2 instances and is where the core infrastructure is declared.
- terraform.tf (or versions.tf) - Contains Terraform settings including required providers, versions, and backend configuration for state storage.
- variables.tf - This script is where non-hardcoded variables can be stored, which can then be used throughout the configuration such as in the main.tf and the terraform.tf files.
- *.tfvars files - These contain variable values specific to the environment (e.g., terraform.auto.tfvars) that allow you to customize deployments for different environments without actually changing the core code.
- outputs.tf - This file contains the outputs from your infrastructure, IP addresses, DNS names, or resource IDs that could be used for reference in other systems.
Infrastructure Implementation - Terraform Code
1. VPC Configuration
The first component of consideration is the VPC in which the public and private subnets sit and their respective security groups.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.19.0" # updated to the latest on terraform registry - 13.03.2025
# allocates 65536 IP addresses to up to 65536 components in the VPC
cidr = var.vpc_cidr_block
azs = data.aws_availability_zones.available.names # 2 availability zones
# 2 private subnets, it allocates 256 IP addresses each
private_subnets = slice(var.private_subnet_cidr_blocks, 0, var.private_subnet_count)
# 2 public subnets, allocates 256 IP addresses each
public_subnets = slice(var.public_subnet_cidr_blocks, 0, var.public_subnet_count)
enable_nat_gateway = true # Enable NAT gateway for instances to enable security updates
enable_vpn_gateway = var.enable_vpn_gateway # Allows the VPN gateway to be toggled for secure connections
tags = var.resource_tags
}
- With this VPC module I've created an isolated network with both public and private subnets across two availability zones to guarantee high availability.
- The NAT gateway enables the private EC2 instances to access the internet for updates while remaining protected from direct external access.
- Note that number of variables are not hard coded here, instead they're defined in the variables.tf file which I've touched on below.
2. Adding Security Groups
The security groups define the permissions that will be assigned to specific components of the cloud infrastructure, in this case, the web app and the Elastic Load balancer
module "app_security_group" {
source = "terraform-aws-modules/security-group/aws//modules/web"
version = "5.3.0" # updated to the latest on terraform registry - 13.03.2025
name = "web-sg-${var.resource_tags["project"]}-${var.resource_tags["environment"]}"
description = "Security group for web-servers with HTTP ports open within VPC"
vpc_id = module.vpc.vpc_id
# Allow traffic only from public subnets
ingress_cidr_blocks = module.vpc.public_subnets_cidr_blocks
tags = var.resource_tags
}
module "lb_security_group" {
source = "terraform-aws-modules/security-group/aws//modules/web"
version = "5.3.0" # updated to the latest on terraform registry - 13.03.2025
name = "lb-sg-${var.resource_tags["project"]}-${var.resource_tags["environment"]}"
description = "Security group for load balancer with HTTP ports open within VPC"
# Security groups are VPC specific
vpc_id = module.vpc.vpc_id
# Allows all traffic from the internet
ingress_cidr_blocks = ["0.0.0.0/0"]
tags = var.resource_tags
}
- Security groups are VPC specific, and within a VPC they are component specific
- Their associated ingress Classless Inter-Domain Route (CIDR) Block determines the source(s) that can access the component
- Only traffic from the public subnet is permitted into the web app
- Traffic from the internet is permitted into the Elastic Load Balancer
3. Setting Up The Elastic Load Balancer
The Elastic Load balancer distributes traffic to EC2 instances as evenly as possible.
module "elb_http" {
source = "terraform-aws-modules/elb/aws"
version = "4.0.2" # updated to the latest on terraform registry - 13.03.2025
# Ensure load balancer name is unique
name = "lb-${random_string.lb_id.result}-${var.resource_tags["project"]}-${var.resource_tags["environment"]}"
# Ensures that the load balancer is internet facing
internal = false
security_groups = [module.lb_security_group.security_group_id]
subnets = module.vpc.public_subnets
number_of_instances = length(module.ec2_instances.instance_ids)
instances = module.ec2_instances.instance_ids
listener = [{
instance_port = "80"
instance_protocol = "HTTP"
lb_port = "80"
lb_protocol = "HTTP"
}]
# Check instance health by sending HTTP requests
health_check = {
target = "HTTP:80/index.html"
interval = 10 # number of seconds per check
healthy_threshold = 3 # number of consecutive successful checks for an instance to be considered healthy
unhealthy_threshold = 10 # number of consecutive unsuccessful checks for an instance to be considered unhealthy
timeout = 5 # number of seconds to wait before declaring the health check as failed
}
tags = var.resource_tags
}
- The ELB is prevents overloading of any one particular EC2 instance by checking the health of the Web app
- It ensures that the end user feels minimal disruption whilst interacting with the web app
- If one EC2 instance fails traffic can be redirected to another EC2 instance
4. Setting Up The Elastic Compute Cloud
The EC2 provides the compute capacity required to run the web app.
module "ec2_instances" {
source = "./modules/aws-instance"
depends_on = [module.vpc]
instance_count = var.instance_count
instance_type = var.ec2_instance_type
subnet_ids = module.vpc.private_subnets[*]
security_group_ids = [module.app_security_group.security_group_id]
tags = var.resource_tags
}
- Two EC2 are defined and each EC2 is kept in a separate private subnet
- The "depends_on" code is a meta-argument that informs Terraform to provision the EC2 only after the VPC has been setup.
- The app security group is assigned to the EC2s
Reflection
One of the key concerns that all customers and users have when using a Cloud Services Platform instead of having an On-Premises setup is security. This is why questions of security ought to be asked from the beginning as the architecture is being designed and also when the prototype architecture has been completed.
With this in mind I would propose the following changes to bolster the security of this web app:
| Area | Current | Suggested Improvement |
|---|---|---|
| Load Balancer Protocol | HTTP (Port 80) | Switch to HTTPS (TLS, Port 443) |
| ELB Security Group Ingress | Open to 0.0.0.0/0 | Restrict to trusted IPs, use WAF |
| Security Group Egress | Default allow all | Restrict egress traffic to specific needs |
| VPC Flow Logs | N/A | Enable for traffic monitoring & auditing |
| EC2 Access | N/A | Use AWS SSM Session Manager or CLI |
| Application Security Group Ingress | Allows public subnet CIDR blocks | Restrict to ELB Security Group only |
| Instance Hardening | N/A | Ensure latest patches, use IAM Roles |
| Identity & Access Management (IAM) & Multifactor Authentication (MFA) | N/A | Enforce MFA, apply least privilege IAM roles |
With these changes we minimise the susceptibility to cyber penetration, accidental and deliberate data leakage. It also improves traceability of activity on the cloud. It adds multiple layers of protections and it improves compliance with regulations like GDPR.
Author: Dolapo Ajayi BSc MSc