Introduction:
In the world of DevOps, building secure, scalable, and automated infrastructure is a superpower. Today, we are going to provision a classic Two-Tier Architecture on AWS completely from scratch using Terraform. We won't just launch an EC2 instance, we are going to build a production-ready environment with custom VPC networking, private subnets for the database, secret management, and automated application bootstrapping.
->> We are Building:
- Web Tier: A Flask python application running on an ubuntu EC2 instance in a public subnet.
- Database Tier: A managed MySQL RDS instance hidden securely in private subnets.
- Security: Minimal privilege security Groups and AWS Secrets Manager for credential handling.
-
Automation: Zero manual configuration inside the server, everything is scripted via Terraform
user_data.
The Architecture
Before writing code, let's visualize what we are building.
- VPC: Our own isolated network
(10.0.0.0/16). - Public Subnet: For the Web Server (Internet accessible).
- Private Subnets: Two of them across different Availability Zones for the RDS database (No direct Internet access).
- Internet Gateway: To allow the web server to talk to the world.
Step 1: The Networking Foundation(VPC)
We need a VPC, one public subnet for the web server, and two private subnets for the RDS instance (AWS RDS requires at least two AZs for high availability).
Resource: aws_vpc, aws_subnets, aws_internet_gateway, aws_route_table.
# VPC Module
module "vpc" {
source = "./modules/vpc"
project_name = var.project_name
environment = var.environment
aws_region = var.aws_region
vpc_cidr = var.vpc_cidr
public_subnet_cidr = var.public_subnet_cidr
private_subnet_cidrs = var.private_subnet_cidrs
}
## Step 2: Security & Firewall Rules
Security Groups act as our virtual firewalls. We follow the Principle of Least Privilege.
- Web SG: Allows HTTP (80) from anywhere (0.0.0.0/0) and SSH (22) for management.
- Database SG: This is where the magic happens. We do not open port 3306 to the world. We only allow traffic from the Web Security Group.
# Security Groups Module
module "security_groups" {
source = "./modules/security_groups"
project_name = var.project_name
environment = var.environment
vpc_id = module.vpc.vpc_id
}
## Step 3: Managing Secrets
Never, ever hardcode database passwords in your main.tf files. We use the Terraform Random Provider to generate a password and store it immediately in AWS Secrets Manager.
# Secrets Module
module "secrets" {
source = "./modules/secrets"
project_name = var.project_name
environment = var.environment
db_username = var.db_username
}
Step 4: The Database (RDS MySQL)
We provision an AWS RDS instance running MySQL. We place it in the private subnets using a db_subnet_group so it's not accessible from the public internet.
The password? It's pulled dynamically from our Secrets module!
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
allocated_storage = var.allocated_storage
storage_type = "gp2"
engine = "mysql"
engine_version = var.engine_version
instance_class = var.instance_class
db_name = var.db_name
username = var.db_username
password = var.db_password
parameter_group_name = "default.mysql8.0"
skip_final_snapshot = true
vpc_security_group_ids = [var.db_security_group_id]
db_subnet_group_name = aws_db_subnet_group.main.name
publicly_accessible = false
tags = {
Name = "${var.project_name}-rds"
Environment = var.environment
}
}
Step 5: The Application Server (EC2 + User Data)
This is the coolest part. We don't want to SSH in and manually install Python, Flask, and Git. We use Terraform's user_data to script the entire setup process.
When the instance launches, it:
- Updates Ubuntu packages.
- Installs Python3 and Flask.
- Creates a simple Flask App connecting to our MySQL DB.
- Injects the Database Host and Credentials (passed from Terraform variables) directly into the Python code.
- Starts the web server as a systemd service.
# modules/ec2/templates/user_data.sh
#!/bin/bash
pip install flask mysql-connector-python
# Terraform Template Injection happening here:
DB_CONFIG = {
"host": "${db_host}",
"user": "${db_username}",
"password": "${db_password}",
"database": "${db_name}"
}
# ... application logic ...
Deployment Time!
With our modular structure in place (vpc, ec2, rds, secrets, security_groups), deploying the entire stack is as simple as:
- Initialize:
terraform init - Plan:
terraform plan - Apply:
terraform apply -auto-approve
Wait about 5-8 minutes (RDS takes a while to spin up), and Terraform will output your application URL.
Verification
Open the application_url in your browser. You should see the nice blue and white "Terraform RDS Demo" dashboard.
Try typing a message and clicking Save.
If it appears in "Recent Messages", congratulations! Your EC2 instance successfully talked to your RDS database in the private subnet!
Conclusion
In this project, we moved beyond simple resource creation and built a fully integrated environment.
Key Takeaways:
- Modular Design: Reusable code makes infrastructure manageable.
- Security First: Private subnets and strict Security Groups are essential.
- Automation: user_data saves hours of manual configuration.
- Secret Management: Never store secrets in plain text.
>> Connect With Me
If you enjoyed this post or want to follow my #30DaysOfAWSTerraformChallenge journey, feel free to connect with me here:
💼 LinkedIn: Amit Kushwaha
🐙 GitHub: Amit Kushwaha
📝 Hashnode / Amit Kushwaha
🐦 Twitter/X: Amit Kushwaha
Found this helpful? Drop a ❤️ and follow for more AWS and Terraform tutorials!
Questions? Drop them in the comments below! 👇
Thanks for reading! Happy Terraforming!







Top comments (0)