Back to Resume
PrivateSeptember 2023· 2 weeks

Terraform for AWS

AWS Infrastructure as Code for a Startup's Web Platform

RoleInfrastructure Engineer

A startup needed its AWS environment built as code instead of clicked together by hand. I wrote the Terraform end to end: a private network, EC2 compute split between a general instance and a GPU instance, a load balancer in front, and a managed MongoDB-compatible database behind it. The build went through five iterations, starting from empty module stubs and ending as a reviewable, reproducible stack.

The problem

A startup was standing up its first real AWS environment. They needed a network with a public and private side, compute for both a general workload and a GPU workload, a database, and a way to route traffic in from the internet, all without anyone clicking through the AWS console by hand. Clicking infrastructure together in the console does not survive the first time someone needs to rebuild it, hand it off, or explain why a security group allows what it allows. It also makes it easy to leave the database or the compute reachable from the public internet by accident, because nothing forces the public and private sides apart. What they needed was infrastructure defined as code, so the network boundaries, the compute, and the database were all reproducible and reviewable from the start.

Constraints

  • ·Single region (eu-central-1), built and maintained by one engineer.
  • ·Two compute profiles: a general workload and a separate GPU-accelerated workload (g4ad.xlarge), meaning two instance types instead of one flexible box.
  • ·A MongoDB-compatible database, run as a managed service rather than self-hosted, to avoid owning replication and patching.
  • ·Iterative delivery: the stack was built up in stages across five versions rather than delivered whole on day one.

Architecture

Terraform modules (vpc, ec2, mongoDB, alb) compose one root module per version. The VPC module creates two public and two private subnets across two AZs, an internet gateway, a NAT gateway, and the security groups every other module depends on. EC2 instances (general, GPU, and a dedicated RPort remote-management instance) launch into the private subnets with no public IP. Amazon DocumentDB, a managed MongoDB-compatible engine, stands up across both private subnets with encrypted storage. An Application Load Balancer sits in the public subnets, with deletion protection on, and forwards HTTP traffic to the GPU instance's target group, the only target group with a live listener.

  • VPC module: two public and two private subnets across two AZs, internet gateway, NAT gateway, three security groups (app, database, ALB)
  • EC2 module: a general web instance, a GPU instance (g4ad.xlarge), and a dedicated RPort remote-management instance, all in the private subnet
  • MongoDB module: Amazon DocumentDB cluster and instance, encrypted storage, its own subnet group spanning both private subnets
  • ALB module: one load balancer in the public subnets, two target groups (GPU, general web), only the GPU target group has a wired listener
  • S3 module: a single bucket, present in v2 through v4, dropped from v5's root module

Five versions exist (v1 through v5), each a complete, standalone Terraform root. v5 is the fullest: it adds the live GPU EC2 instance, the RPort instance, the GPU target group's listener, and ALB deletion protection, and is the version this log describes.

How one request flows

  1. 01The VPC module stands up first: two public and two private subnets, an internet gateway, a NAT gateway, and the security groups other modules depend on.
  2. 02Compute launches into the private subnet: the GPU instance, the general instance, and the RPort management instance, all with no public IP.
  3. 03The database stands up alongside compute: Amazon DocumentDB creates its cluster and a subnet group spanning both private subnets, with encrypted storage on by default.
  4. 04The load balancer goes up in the public subnet, with its own security group open on port 80 and deletion protection turned on.
  5. 05A client request hits the ALB, which forwards it to the GPU instance's target group, the only target group with a live listener.
  6. 06The GPU instance talks to DocumentDB over the database security group, which only accepts connections from the application security group on port 27017.
  7. 07Outbound traffic from the private subnet, for updates or package installs, routes out through the NAT gateway rather than a direct route to the internet.

Engineering decisions

A managed database instead of self-hosted MongoDB

What. The database module runs Amazon DocumentDB, a managed MongoDB-compatible engine, rather than a MongoDB replica set on EC2.

Why. A managed engine removes replication, patching, and failover from the engineer's own workload.

Tradeoff. Less low-level control over the database engine, accepted because a small team should not be tuning MongoDB internals by hand.

Splitting general and GPU compute

What. The general workload and the GPU workload run on separate EC2 instances instead of one instance sized for the heavier job.

Why. Keeps the GPU instance's cost isolated to the workload that needs it.

Tradeoff. Two instances to patch and monitor instead of one, accepted because the price gap between instance types is large enough to matter.

Public load balancer, private everything else

What. Only the ALB sits in a public subnet. Compute and the database sit in private subnets with no direct internet route.

Why. Keeps the attack surface off the public side by construction rather than by convention.

Tradeoff. None significant, this is the standard tradeoff of a bit more NAT/routing setup for a smaller public footprint.

A dedicated instance for remote management

What. RPort, the client's remote-access tool, runs as its own EC2 instance rather than being installed onto the general web instance.

Why. A problem with the web workload does not take down remote management, and a compromise of one does not automatically hand over the other.

Tradeoff. One more instance to run, accepted for the isolation.

Building the module set in stages

What. The Terraform started as empty module stubs for network, compute, and database (v1), then filled in real resources one module at a time across four more revisions.

Why. Each stage was a working, applyable state rather than one large change reviewed all at once.

Tradeoff. Slower to a first complete stack, in exchange for every stage being reviewable on its own.

What changed along the way

  • An S3 bucket module existed from the second version through the fourth, then was dropped from the final version's root module. It is not part of the delivered stack.
  • A second ALB target group for the general web instance was added in code but its listener was left commented out, so only the GPU path is actually reachable through the load balancer.
  • Security group and resource naming was inconsistent in the middle versions and only converged to one naming convention in the last revision.

Security and reliability

  • ·Private-by-default compute and database: EC2 instances and DocumentDB sit in private subnets with no direct internet route.
  • ·Scoped security groups: the database only accepts connections from the application security group on its port, and SSH into the application instances is only allowed from the load balancer's security group.
  • ·Encrypted storage on the DocumentDB cluster, on by default.
  • ·Deletion protection on the load balancer, so it cannot be torn down by accident.
  • ·This version does not yet configure IAM instance roles, TLS on the load balancer, or automated backup scheduling. Known gaps, not silent ones.

Metrics

5
Terraform iterations, each a working, applyable state
measured
4
modules: network, compute, database, load balancer
measured
private-by-default
only the load balancer sits in a public subnet
measured

What shipped

  • Delivered a reproducible Terraform build for the network, compute, database, and load balancer, replacing manual console setup.
  • Split general and GPU compute onto separate instances, each sized and secured for its own workload.
  • Provisioned an encrypted, managed MongoDB-compatible database reachable only from the application tier.
  • Isolated remote-management access onto its own instance, off the public-facing path.
  • Took the build from empty module stubs to a five-version, review-ready infrastructure stack.

Lessons learned

  • ·A managed database engine removes an entire category of operational work compared to self-hosting on EC2, worth the loss of low-level control for a small team.
  • ·Splitting compute by workload profile, general versus GPU, keeps cost and blast radius tied to the workload that actually needs the heavier instance.
  • ·A security group rule that mixes a scoped security-group source with a broad CIDR block on the same rule grants the broad one regardless of the narrow one alongside it, worth a second look in review.
  • ·Building infrastructure in stages, one module at a time, catches naming and wiring mistakes earlier than writing the whole stack in one pass.

Tech stack

Backend
AWS VPCpublic and private subnets across two AZsinternet gatewayNAT gatewaysecurity groupsAWS EC2 (general, GPU, RPort)AWS Application Load Balancer
Data & state
Amazon DocumentDB (MongoDB-compatible)encrypted storagededicated DB subnet group
Ops & quality
Terraform, modular by concern (network, compute, database, load balancer)five reviewed iterationsper-version root modules