architecture.png

<aside> ✅

Outcome: my portfolio site “app.bolarinwadaniel.online” is live over HTTPS on three private web servers in an Auto Scaling group across two Availability Zones, all serving one shared copy of the site from Amazon EFS. Datadog agents on every server alert Slack when CPU passes 75%.

</aside>

Project overview

The design goals were:

Component Name in my account Purpose
Region us-east-1 (N. Virginia) Hosts everything
Load balancer Application Load Balancer + target group Public HTTPS entry point (443) with an ACM certificate, health checks
Auto Scaling group webserver-auto (desired 3, limits 3 to 12) Keeps three healthy web servers running
Launch template project-temp, version 1 Boots each web server fully configured via user data
Web servers 3 x t3.small, private IPs only Run nginx and serve the site on port 80
Bastion Jumper-Server-01 (t3.small, Ubuntu 24.04) SSH entry point, uploads site files to EFS
NAT gateway Public subnet Outbound internet for the private web servers
Shared storage Amazon EFS, Regional, NFS 4.1 One copy of the site for every server
Monitoring Datadog Agent 7 + Slack Agents on all 4 servers, CPU alert at 75% to Slack
DNS Route 53 hosted zone, alias record to the ALB Points app.bolarinwadaniel.online at the load balancer
Network logging VPC Flow Logs on portfolio-vpc Records accepted and rejected traffic for troubleshooting and security review

Architecture

Visitors resolve my domain through Route 53 and reach the load balancer over HTTPS, the load balancer forwards to the web servers, and every web server reads the site from the same EFS file system.

flowchart TB
    V["Visitors"] -->|"DNS lookup"| R53["Route 53<br>app.bolarinwadaniel.online"]
    R53 -->|"HTTPS :443"| ALB
    A["Admin (Mac)"] -->|"SSH :22"| B
    subgraph VPC["VPC 192.168.0.0/16 · us-east-1 · Flow Logs enabled"]
        subgraph PUB["Public subnets"]
            ALB["Application Load Balancer<br>ACM certificate"]
            NAT["NAT gateway"]
            B["Jumper-Server-01 (bastion)<br>Datadog agent"]
        end
        subgraph ASG["Auto Scaling group webserver-auto"]
            subgraph AZA["us-east-1a · private subnet"]
                W1["Web server 1<br>nginx · Datadog agent"]
                W2["Web server 2<br>nginx · Datadog agent"]
            end
            subgraph AZB["us-east-1b · private subnet"]
                W3["Web server 3<br>nginx · Datadog agent"]
            end
        end
        EFS[("Amazon EFS<br>shared site files")]
    end
    ALB -->|"HTTP :80"| W1 & W2 & W3
    B -->|"SSH :22"| W1 & W2 & W3
    W1 & W2 & W3 -.->|"outbound"| NAT
    W1 & W2 & W3 -->|"NFS 2049"| EFS
    B -->|"writes site files"| EFS
    W1 & W2 & W3 & B -->|"metrics"| DD["Datadog"]
    DD -->|"CPU over 75%"| S["Slack · team devices"]

The bastion is the only SSH entry point and also writes the site files to EFS, so an upload there is live on every server at once.

How it was built

The build runs in seven stages, and the order matters: EFS must be reachable before any web server boots, or the servers come up serving an empty folder.

  1. Network. A custom VPC (192.168.0.0/16) with public subnets for the bastion, load balancer and NAT gateway, and private subnets for the web servers, across us-east-1a and us-east-1b. DNS resolution and DNS hostnames are both enabled, which EFS needs so its DNS name resolves. VPC Flow Logs record accepted and rejected traffic across the VPC.
  2. Security groups. public-sg for the bastion and private-sg for the web servers. The intent is that web servers accept SSH only from the bastion and HTTP only from the load balancer.
  3. EFS. A Regional file system with a mount target in each Availability Zone. Inbound NFS (port 2049) must be allowed from every server that mounts it.