Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HUG Lagos/Ibadan Terraform Challenge — Project 2

Build Reusable Infrastructure with Modules

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 image

Learning outcomes demonstrated

  • 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.tf with 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).

Architecture

                     ┌─────────────────────┐
                     │     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 in variables.tf, not a new resource block.
  • modules/security-group — a generic security group module driven by ingress_rules / egress_rules list 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 via user_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).

Project structure

.
├── 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

Prerequisites

  • 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"

Remote state bootstrap

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:

  1. First run locally, then migrate — comment out the backend "s3" block in backend.tf, run terraform init + terraform apply with local state to create the bucket, then uncomment the backend block and run terraform init -migrate-state to move state into S3.
  2. Bootstrap the bucket out-of-band — create the bucket once via the AWS CLI/console (matching var.bucket_name) before ever running terraform init against this configuration.

Usage

1. Initialize Terraform (downloads the aws, tls, local, and random providers, and pulls in the local child modules):

terraform init

2. Format and validate:

terraform fmt -recursive
terraform validate

3. Review the plan:

terraform plan

4. Apply:

terraform apply

Confirm 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_key

6. 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.

Variables

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

Outputs

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)

Notes

  • main.tf loads user_data.sh via file() inside modules/compute — keep the two in sync if you edit the HTML or install steps. user_data only 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/0 for hackathon convenience. In a real deployment, restrict this to your own IP via the ingress_rules passed into module.ec2_security_group in main.tf.
  • terraform.tfstate contains 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.

Cleanup

To avoid ongoing AWS charges once you're done:

terraform destroy

This removes every resource created above (note: the S3 state bucket may need force_destroy or manual emptying if versioning has accumulated objects).

Screenshots

image

Configured by

Ogechukwu Okoli LinkedIn Email

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages