This project refactors Project 1 (a monolithic Terraform
configuration that provisioned a VPC, subnets, security group, EC2 instance,
IAM user, and S3 bucket in flat .tf files) into a set of reusable
Terraform modules. The infrastructure it provisions is functionally the
same as Project 1 — a custom VPC with a public EC2 instance running Nginx —
but the code is now organized so each concern (networking, security,
compute, storage, IAM) can be reused, tested, and versioned independently.
Live server here: http:13.49.67.56

- Terraform Modules — every resource group is encapsulated in its own module with a clear input/output contract.
- Project Structure — a root module composes child modules instead of declaring resources directly.
- Reusability — modules take no hardcoded values; they're driven
entirely by variables, so the same modules could stand up a second
environment (e.g.
staging) by passing different inputs. - Variables — every module exposes typed
variables.tfwith descriptions and sensible defaults only where appropriate. - Outputs — every module exposes
outputs.tf, and the root module re-exports the values a consumer actually needs (public IP, private key, bucket name, IAM credentials).
┌─────────────────────┐
│ root module │
│ (main.tf) │
└──────────┬──────────┘
┌───────────┬───────────┼───────────┬───────────┬───────────┐
▼ ▼ ▼ ▼ ▼ ▼
modules/ modules/ modules/ modules/ modules/ modules/
vpc networking security- compute s3 iam
group
modules/vpc— the VPC itself.modules/networking— public subnet(s), Internet Gateway, route table, and route table associations. Subnets are defined via a map variable, so adding a third subnet is a one-line change invariables.tf, not a new resource block.modules/security-group— a generic security group module driven byingress_rules/egress_ruleslist variables (dynamic blocks), reused here for the EC2 security group but reusable for any future SG.modules/compute— generates the SSH key pair (tls_private_key+aws_key_pair) and launches the EC2 instance, bootstrapped viauser_data.sh.modules/s3— the S3 bucket used for Terraform remote state (versioned, with public access blocked).modules/iam— the developer IAM user, its access key, SSM parameters storing that key securely, and the scoped IAM policies (S3 object access, EC2 provisioning permissions).
.
├── backend.tf # Remote state backend (S3)
├── main.tf # Root module: wires child modules together
├── locals.tf # Common tags
├── output.tf # Root outputs, re-exported from modules
├── provider.tf # AWS provider configuration
├── variables.tf # Root input variables
├── version.tf # Terraform & provider version constraints
├── user_data.sh # Boot script: installs Nginx, serves HTML page
├── modules/
│ ├── vpc/ # VPC
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── networking/ # Subnets, IGW, route table, associations
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── security-group/ # Reusable security group (dynamic rules)
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/ # Key pair + EC2 instance
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── s3/ # State bucket (versioned, private)
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── iam/ # Developer IAM user, keys, policies
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── README.md
- Terraform >= 1.14.0
- An AWS account with programmatic access (Access Key ID + Secret Access Key)
- AWS CLI configured locally (
aws configure), or the equivalent environment variables set:export AWS_ACCESS_KEY_ID="..." export AWS_SECRET_ACCESS_KEY="..." export AWS_DEFAULT_REGION="eu-north-1"
backend.tf points Terraform at an S3 bucket for remote state, and that
same bucket is also created by modules/s3 in this configuration. On a
brand-new AWS account this is a classic chicken-and-egg problem: Terraform
needs the bucket to exist before it can init with the S3 backend, but the
bucket is itself managed by this code.
Two ways to handle it:
- First run locally, then migrate — comment out the
backend "s3"block inbackend.tf, runterraform init+terraform applywith local state to create the bucket, then uncomment the backend block and runterraform init -migrate-stateto move state into S3. - Bootstrap the bucket out-of-band — create the bucket once via the
AWS CLI/console (matching
var.bucket_name) before ever runningterraform initagainst this configuration.
1. Initialize Terraform (downloads the aws, tls, local, and
random providers, and pulls in the local child modules):
terraform init2. Format and validate:
terraform fmt -recursive
terraform validate3. Review the plan:
terraform plan4. Apply:
terraform applyConfirm with yes when prompted.
5. Retrieve your SSH key, IAM access/secret key, and the instance's public IP:
terraform output -raw private_key_pem > server-key.pem
chmod 400 server-key.pem
terraform output -raw instance_public_ip
terraform output -raw my_access_key_id
terraform output -raw my_secret_access_key6. SSH into the instance (optional — Nginx is already running via
user_data, so this is only needed for debugging):
ssh -i server-key.pem ec2-user@$(terraform output -raw instance_public_ip)7. View the web page:
Open http://<instance_public_ip> in a browser. You should see a styled
page displaying the project name and challenge details.
| Name | Description | Default |
|---|---|---|
region |
AWS region to deploy into | eu-north-1 |
project |
Project name, used in resource tags | Web-server-deployment |
team |
Team name, used in resource tags | Developer Team |
environment |
Environment tag | production |
managed_by |
Tag identifying the IaC tool | Terraform |
bucket_name |
Name of bucket for Terraform state | ogechukwu-web-server-bucket-hug2026 |
vpc_cidr |
CIDR block for the VPC | 10.0.0.0/16 |
public_subnets |
Map of public subnets (cidr/az per key) | two /24 subnets in the VPC |
ami_id |
AMI ID for the EC2 web server | ami-0ac1f955d6e62f3f1 |
instance_type |
EC2 instance type | t3.micro |
| Name | Description |
|---|---|
instance_public_ip |
Public IP address of the EC2 instance |
private_key_pem |
Generated SSH private key (sensitive) |
vpc_id |
ID of the created VPC |
public_subnet_ids |
IDs of the created public subnets |
s3_bucket_id |
Name of the S3 state bucket |
my_access_key_id |
IAM access key ID for the developer user |
my_secret_access_key |
IAM secret access key (sensitive) |
main.tfloadsuser_data.shviafile()insidemodules/compute— keep the two in sync if you edit the HTML or install steps.user_dataonly runs on first boot; if you change the script after the instance already exists, taint and recreate it for the change to take effect:terraform taint module.compute.aws_instance.this terraform apply
- The EC2 security group currently allows SSH (22) from
0.0.0.0/0for hackathon convenience. In a real deployment, restrict this to your own IP via theingress_rulespassed intomodule.ec2_security_groupinmain.tf. terraform.tfstatecontains sensitive values (including the private key and IAM secret key) in plaintext and should never be committed to version control — it's already excluded via.gitignore, and with the remote S3 backend configured it isn't stored locally at all once bootstrapped.
To avoid ongoing AWS charges once you're done:
terraform destroyThis removes every resource created above (note: the S3 state bucket may
need force_destroy or manual emptying if versioning has accumulated
objects).