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

Figure 1: AWS High Availability Architecture - created using draw.io



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