π Hey there, tech enthusiasts!
I'm Sarvar, a Cloud Architect with a passion for transforming complex technological challenges into elegant solutions. With extensive experience spanning Cloud Operations (AWS & Azure), Data Operations, Analytics, DevOps, and Generative AI, I've had the privilege of architecting solutions for global enterprises that drive real business impact. Through this article series, I'm excited to share practical insights, best practices, and hands-on experiences from my journey in the tech world. Whether you're a seasoned professional or just starting out, I aim to break down complex concepts into digestible pieces that you can apply in your projects.
Secure Database Deployment: RDS + Secrets Manager with Terraform
"A password in your code is like leaving your house key under the doormat everyone knows where to look."
π― Welcome Back!
Remember in Article 9 when you deployed IAM roles and security policies? You built secure access controls without access keys. Now let's protect your most sensitive asset your data.
Here's the reality: Web servers are stateless. They can crash and restart without losing anything. But what about:
- User accounts and profiles?
- Shopping cart data?
- Transaction history?
- Application state?
You need a database. But here's the catch databases hold your most sensitive data, and one mistake can expose everything.
That's where RDS and Secrets Manager come in.
By the end of this article, you'll:
- β Deploy RDS MySQL database with Terraform
- β Store credentials securely in AWS Secrets Manager
- β Never hardcode passwords in your code
- β Configure private subnets for databases
- β Set up IAM roles for secure access
- β Connect EC2 to RDS without access keys
Time Required: 50 minutes (25 min read + 25 min practice)
Cost: ~$15/month for db.t3.micro (free tier eligible for 12 months)
Difficulty: Intermediate
Let's build secure database infrastructure! π
π The Problem: Password Catastrophes
The Horror Story
It's Monday morning. Your security team sends this:
SECURITY INCIDENT REPORT
Severity: CRITICAL
Issue: Database credentials found in public GitHub repository
Exposure Time: 6 months
Impact: 500,000 user records potentially compromised
Estimated Cost: $2.5 million (fines + remediation)
Root Cause: Hardcoded password in Terraform code
How it happened:
- Developer hardcoded DB password in
main.tf - Committed to Git:
git commit -m "Add database" - Pushed to GitHub:
git push origin main - Security scanner found it 6 months later
- By then, password was in 47 commits across 3 branches
The damage:
- β Password exposed in version control history
- β Visible in Terraform state file (plain text)
- β Copied to multiple developer machines
- β Indexed by GitHub search
- β Potentially scraped by bots
- β Impossible to know who accessed it
Common Password Mistakes:
β Hardcoded in Code:
resource "aws_db_instance" "db" {
password = "MyPassword123!" # Everyone can see this!
}
β In Environment Variables:
export DB_PASSWORD="secret123" # Still visible in shell history
β In terraform.tfvars (committed to Git):
db_password = "MySecretPass" # In version control!
β In State File:
{
"password": "MyPassword123", // Plain text in S3!
}
β Shared via Slack/Email:
"Hey, the DB password is MyPassword123" // Now it's everywhere
Sound familiar? Let's fix this properly.
π What is AWS Secrets Manager? (Quick Theory)
Simple Definition
AWS Secrets Manager = Encrypted vault for sensitive data. It:
- Stores passwords, API keys, tokens securely
- Encrypts everything with AWS KMS
- Rotates credentials automatically
- Provides audit logs of who accessed what
- Integrates with IAM for access control
Think of it like this:
Without Secrets Manager:
Developer β Hardcodes password β Git β Everyone sees it β π₯ Breach
With Secrets Manager:
Terraform β Generates password β Secrets Manager (encrypted) β
EC2 (IAM role) β Retrieves password β Connects to RDS β
Why You Need This
Scenario 1: Security Breach
- Hardcoded password: Exposed forever in Git history
- Secrets Manager: Rotate password in 30 seconds, breach contained
- Result: Minimal damage
Scenario 2: Employee Leaves
- Hardcoded password: They know all your passwords
- Secrets Manager: Revoke IAM access, rotate secrets
- Result: Zero risk
Scenario 3: Compliance Audit
- Hardcoded password: Fail audit, face fines
- Secrets Manager: Pass audit, show encryption + rotation
- Result: Compliance achieved
What is RDS?
RDS (Relational Database Service) = Managed database service. AWS handles:
- Automated backups
- Software patching
- Hardware maintenance
- High availability
- Monitoring and alerts
You focus on: Your application and data, not database administration.
π Prerequisites
Before starting, make sure you have:
- β Completed Article 9: IAM Roles and Security
- β Terraform installed (v1.0+)
- β AWS CLI configured
- β Understanding of VPC and security groups
- β SSH key pair in AWS
ποΈ What We're Building
Internet
β
EC2 Instance (Public Subnet)
β (IAM Role)
βββ Secrets Manager (Get Credentials)
βββ RDS MySQL (Private Subnet)
Architecture Components:
- VPC - Isolated network
- Public Subnet - EC2 instance
- Private Subnets - RDS database (2 AZs required)
- Security Groups - Network firewall
- RDS MySQL - Managed database
- Secrets Manager - Encrypted credential storage
- IAM Role - Secure access without keys
π Project Structure
10-rds-secrets/
βββ main.tf # Infrastructure code
βββ variables.tf # Input variables
βββ outputs.tf # Output values
βββ terraform.tfvars # Variable values
π§ Step 1: Define Variables
Create variables.tf:
# AWS Region
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
# Project Name
variable "project_name" {
description = "Project name"
type = string
default = "terraform-db"
}
# Environment
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
# VPC CIDR
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
# Public Subnet CIDR
variable "public_subnet_cidr" {
description = "Public subnet CIDR"
type = string
default = "10.0.1.0/24"
}
# Private Subnet CIDRs
variable "private_subnet_1_cidr" {
description = "Private subnet 1 CIDR"
type = string
default = "10.0.11.0/24"
}
variable "private_subnet_2_cidr" {
description = "Private subnet 2 CIDR"
type = string
default = "10.0.12.0/24"
}
# Database Configuration
variable "db_name" {
description = "Database name"
type = string
default = "myappdb"
}
variable "db_username" {
description = "Database master username"
type = string
default = "admin"
}
variable "db_instance_class" {
description = "RDS instance class"
type = string
default = "db.t3.micro"
}
variable "db_allocated_storage" {
description = "Allocated storage in GB"
type = number
default = 20
}
# EC2 Configuration
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
variable "key_name" {
description = "SSH key pair name"
type = string
default = "my-key"
}
variable "my_ip" {
description = "Your IP for SSH access"
type = string
default = "0.0.0.0/0"
}
Key Points:
- No password variable! We'll generate it automatically
- Two private subnets required for RDS (multi-AZ)
-
db.t3.micro- Free tier eligible
π Step 2: Create VPC with Private Subnets
Add to main.tf:
# Terraform Configuration
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# Data Sources
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
# Random Password for Database
resource "random_password" "db_password" {
length = 16
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
Environment = var.environment
ManagedBy = "Terraform"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
Environment = var.environment
}
}
# Public Subnet
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidr
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-subnet"
Type = "Public"
}
}
# Private Subnets (for RDS)
resource "aws_subnet" "private_1" {
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_1_cidr
availability_zone = data.aws_availability_zones.available.names[0]
tags = {
Name = "${var.project_name}-private-subnet-1"
Type = "Private"
}
}
resource "aws_subnet" "private_2" {
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_2_cidr
availability_zone = data.aws_availability_zones.available.names[1]
tags = {
Name = "${var.project_name}-private-subnet-2"
Type = "Private"
}
}
# Public Route Table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-public-rt"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
New Concept: Random Provider
resource "random_password" "db_password" {
length = 16
special = true
}
Generates a secure random password automatically. Never hardcode passwords!
Why Two Private Subnets?
- RDS requires subnets in at least 2 availability zones
- Provides high availability
- Automatic failover if one AZ fails
π Step 3: Configure Security Groups
Add to main.tf:
# Security Group for EC2
resource "aws_security_group" "ec2" {
name = "${var.project_name}-ec2-sg"
description = "Security group for EC2 instance"
vpc_id = aws_vpc.main.id
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.my_ip]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-ec2-sg"
}
}
# Security Group for RDS
resource "aws_security_group" "rds" {
name = "${var.project_name}-rds-sg"
description = "Security group for RDS database"
vpc_id = aws_vpc.main.id
ingress {
description = "MySQL from EC2"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.ec2.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-rds-sg"
}
}
Security Architecture:
Internet β EC2 (SSH from your IP, HTTP from anywhere)
β
RDS (MySQL port 3306 ONLY from EC2)
Key Security Practices:
- RDS is NOT accessible from internet
- RDS only accepts connections from EC2 security group
- No direct database access from outside
- This is called "defense in depth"
π Step 4: Set Up AWS Secrets Manager
Add to main.tf:
# Secrets Manager Secret
resource "aws_secretsmanager_secret" "db_credentials" {
name = "${var.project_name}-db-credentials-${random_password.db_password.id}"
description = "Database credentials for ${var.project_name}"
tags = {
Name = "${var.project_name}-db-secret"
Environment = var.environment
}
}
# Store Database Credentials in Secrets Manager
resource "aws_secretsmanager_secret_version" "db_credentials" {
secret_id = aws_secretsmanager_secret.db_credentials.id
secret_string = jsonencode({
username = var.db_username
password = random_password.db_password.result
engine = "mysql"
host = aws_db_instance.main.address
port = aws_db_instance.main.port
dbname = var.db_name
})
}
What's Happening:
- Create Secret: Empty container for credentials
- Store Credentials: JSON with all connection info
- Encryption: Automatically encrypted at rest
- Access Control: IAM controls who can read it
Secret Structure:
{
"username": "admin",
"password": "randomly-generated-16-chars",
"engine": "mysql",
"host": "terraform-db.xxxxx.rds.amazonaws.com",
"port": 3306,
"dbname": "myappdb"
}
ποΈ Step 5: Deploy RDS MySQL Database
Add to main.tf:
# DB Subnet Group
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db-subnet-group"
subnet_ids = [aws_subnet.private_1.id, aws_subnet.private_2.id]
tags = {
Name = "${var.project_name}-db-subnet-group"
}
}
# RDS Instance
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
engine = "mysql"
engine_version = "8.0"
instance_class = var.db_instance_class
allocated_storage = var.db_allocated_storage
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = random_password.db_password.result
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = 7
backup_window = "03:00-04:00"
maintenance_window = "mon:04:00-mon:05:00"
skip_final_snapshot = true
final_snapshot_identifier = "${var.project_name}-final-snapshot"
enabled_cloudwatch_logs_exports = ["error", "general", "slowquery"]
tags = {
Name = "${var.project_name}-db"
Environment = var.environment
}
}
Breaking Down the Configuration:
1. DB Subnet Group:
subnet_ids = [aws_subnet.private_1.id, aws_subnet.private_2.id]
Tells RDS which subnets to use (must be in different AZs).
2. Storage:
storage_type = "gp3"
storage_encrypted = true
-
gp3- Latest generation SSD (better performance) -
storage_encrypted- Encryption at rest
3. Backups:
backup_retention_period = 7
backup_window = "03:00-04:00"
- Automatic daily backups
- Retained for 7 days
- Runs at 3 AM UTC
4. Maintenance:
maintenance_window = "mon:04:00-mon:05:00"
AWS performs updates during this window.
5. CloudWatch Logs:
enabled_cloudwatch_logs_exports = ["error", "general", "slowquery"]
Sends MySQL logs to CloudWatch for monitoring.
π€ Step 6: Create IAM Role for EC2
Add to main.tf:
# IAM Role for EC2 to access Secrets Manager
resource "aws_iam_role" "ec2_secrets_role" {
name = "${var.project_name}-ec2-secrets-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
tags = {
Name = "${var.project_name}-ec2-role"
}
}
# IAM Policy for Secrets Manager Access
resource "aws_iam_role_policy" "secrets_access" {
name = "${var.project_name}-secrets-policy"
role = aws_iam_role.ec2_secrets_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
Resource = aws_secretsmanager_secret.db_credentials.arn
}]
})
}
# Instance Profile
resource "aws_iam_instance_profile" "ec2_profile" {
name = "${var.project_name}-ec2-profile"
role = aws_iam_role.ec2_secrets_role.name
}
Understanding IAM Components:
1. IAM Role:
- Identity that EC2 can assume
- No permanent credentials needed
2. Assume Role Policy:
{
"Principal": {
"Service": "ec2.amazonaws.com"
}
}
Says "EC2 service can use this role"
3. IAM Policy:
{
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:..."
}
Grants permission to read ONLY this specific secret.
4. Instance Profile:
Attaches the role to EC2 instance.
Security Benefits:
- β No access keys on EC2
- β Credentials rotate automatically
- β Least privilege access
- β Auditable in CloudTrail
π₯οΈ Step 7: Deploy EC2 with Database Access
Add to main.tf:
# EC2 Instance
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
key_name = var.key_name
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.ec2.id]
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y mysql jq aws-cli
# Get database credentials from Secrets Manager
SECRET_ARN="${aws_secretsmanager_secret.db_credentials.arn}"
SECRET_JSON=$(aws secretsmanager get-secret-value --secret-id $SECRET_ARN --region ${var.aws_region} --query SecretString --output text)
DB_HOST=$(echo $SECRET_JSON | jq -r '.host')
DB_PORT=$(echo $SECRET_JSON | jq -r '.port')
DB_USER=$(echo $SECRET_JSON | jq -r '.username')
DB_PASS=$(echo $SECRET_JSON | jq -r '.password')
DB_NAME=$(echo $SECRET_JSON | jq -r '.dbname')
# Create connection script
cat > /home/ec2-user/connect-db.sh <<SCRIPT
#!/bin/bash
mysql -h $DB_HOST -P $DB_PORT -u $DB_USER -p$DB_PASS $DB_NAME
SCRIPT
chmod +x /home/ec2-user/connect-db.sh
chown ec2-user:ec2-user /home/ec2-user/connect-db.sh
# Create info file
cat > /home/ec2-user/db-info.txt <<INFO
Database Connection Information
================================
Host: $DB_HOST
Port: $DB_PORT
Database: $DB_NAME
Username: $DB_USER
To connect: ./connect-db.sh
INFO
chown ec2-user:ec2-user /home/ec2-user/db-info.txt
EOF
tags = {
Name = "${var.project_name}-app-server"
Environment = var.environment
}
depends_on = [
aws_db_instance.main,
aws_secretsmanager_secret_version.db_credentials
]
}
User Data Magic:
- Install Tools:
yum install -y mysql jq aws-cli
-
mysql- Database client -
jq- JSON parser -
aws-cli- AWS commands
- Fetch Credentials:
aws secretsmanager get-secret-value --secret-id $SECRET_ARN
Uses IAM role (no keys needed!)
- Parse JSON:
DB_HOST=$(echo $SECRET_JSON | jq -r '.host')
Extracts values from JSON.
- Create Helper Script:
cat > /home/ec2-user/connect-db.sh
User can run ./connect-db.sh to connect to database.
Depends On:
depends_on = [
aws_db_instance.main,
aws_secretsmanager_secret_version.db_credentials
]
Ensures database and secrets exist before EC2 starts.
π€ Step 8: Define Outputs
Create outputs.tf:
# VPC Outputs
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
# Subnet Outputs
output "public_subnet_id" {
description = "Public subnet ID"
value = aws_subnet.public.id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = [aws_subnet.private_1.id, aws_subnet.private_2.id]
}
# RDS Outputs
output "rds_endpoint" {
description = "RDS endpoint"
value = aws_db_instance.main.endpoint
}
output "rds_address" {
description = "RDS address"
value = aws_db_instance.main.address
}
output "rds_port" {
description = "RDS port"
value = aws_db_instance.main.port
}
output "database_name" {
description = "Database name"
value = aws_db_instance.main.db_name
}
# Secrets Manager Output
output "secret_arn" {
description = "Secrets Manager secret ARN"
value = aws_secretsmanager_secret.db_credentials.arn
}
output "secret_name" {
description = "Secrets Manager secret name"
value = aws_secretsmanager_secret.db_credentials.name
}
# EC2 Output
output "ec2_public_ip" {
description = "EC2 instance public IP"
value = aws_instance.app.public_ip
}
output "ec2_instance_id" {
description = "EC2 instance ID"
value = aws_instance.app.id
}
# Connection String (Sensitive)
output "connection_command" {
description = "MySQL connection command"
value = "mysql -h ${aws_db_instance.main.address} -P ${aws_db_instance.main.port} -u ${var.db_username} -p ${var.db_name}"
sensitive = true
}
Sensitive Outputs:
sensitive = true
Hides output value from terminal. Use terraform output connection_command to view.
βοΈ Step 9: Set Variable Values
Create terraform.tfvars:
aws_region = "us-east-1"
project_name = "terraform-db"
environment = "dev"
# Network Configuration
vpc_cidr = "10.0.0.0/16"
public_subnet_cidr = "10.0.1.0/24"
private_subnet_1_cidr = "10.0.11.0/24"
private_subnet_2_cidr = "10.0.12.0/24"
# Database Configuration
db_name = "myappdb"
db_username = "admin"
db_instance_class = "db.t3.micro"
db_allocated_storage = 20
# EC2 Configuration
instance_type = "t2.micro"
key_name = "my-key"
my_ip = "0.0.0.0/0"
π Step 10: Deploy the Infrastructure
1. Initialize Terraform
terraform init
2. Review the Plan
terraform plan
You should see:
Plan: 17 to add, 0 to change, 0 to destroy.
Resources being created:
- 1 VPC
- 1 Internet Gateway
- 3 Subnets (1 public, 2 private)
- 1 Route Table
- 2 Security Groups
- 1 Random Password
- 1 Secrets Manager Secret + Version
- 1 DB Subnet Group
- 1 RDS Instance
- 1 IAM Role + Policy + Instance Profile
- 1 EC2 Instance
3. Apply the Configuration
terraform apply
Type yes when prompted.
β° This will take 5-10 minutes because:
- RDS takes 5-7 minutes to provision
- Multi-AZ setup requires time
- Backups need to be configured
β Step 11: Test Database Connection
1. Get EC2 Public IP
terraform output ec2_public_ip
2. SSH to EC2 Instance
ssh -i my-key.pem ec2-user@<EC2_PUBLIC_IP>
3. Check Database Info
cat db-info.txt
Output:
Database Connection Information
================================
Host: terraform-db.xxxxx.us-east-1.rds.amazonaws.com
Port: 3306
Database: myappdb
Username: admin
To connect: ./connect-db.sh
4. Connect to Database
./connect-db.sh
You're now connected to MySQL!
5. Test Database
-- Show databases
SHOW DATABASES;
-- Create a table
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert data
INSERT INTO users (name, email) VALUES
('John Doe', 'john@example.com'),
('Jane Smith', 'jane@example.com');
-- Query data
SELECT * FROM users;
-- Exit
EXIT;
π Verify Secrets Manager
From Your Local Machine
# Get secret ARN
SECRET_ARN=$(terraform output -raw secret_arn)
# Retrieve secret
aws secretsmanager get-secret-value \
--secret-id $SECRET_ARN \
--query SecretString \
--output text | jq .
Output:
{
"username": "admin",
"password": "Xy9#mK2$pL5@qR8!",
"engine": "mysql",
"host": "terraform-db.xxxxx.rds.amazonaws.com",
"port": 3306,
"dbname": "myappdb"
}
Notice: Password is randomly generated and securely stored!
π§ͺ Advanced Testing
Test IAM Role Access
# SSH to EC2
ssh -i my-key.pem ec2-user@<EC2_IP>
# Try to get secret (should work)
aws secretsmanager get-secret-value \
--secret-id <SECRET_ARN> \
--region us-east-1
# Try to delete secret (should fail - no permission)
aws secretsmanager delete-secret \
--secret-id <SECRET_ARN> \
--region us-east-1
Test Database Backups
# List automated backups
aws rds describe-db-snapshots \
--db-instance-identifier terraform-db-db
# Create manual snapshot
aws rds create-db-snapshot \
--db-instance-identifier terraform-db-db \
--db-snapshot-identifier terraform-db-manual-snapshot
Monitor Database
# Check database status
aws rds describe-db-instances \
--db-instance-identifier terraform-db-db \
--query 'DBInstances[0].DBInstanceStatus'
# View CloudWatch logs
aws logs tail /aws/rds/instance/terraform-db-db/error --follow
π Troubleshooting
Issue 1: "Cannot connect to database"
Cause: RDS still provisioning or security group issue.
Solution:
# Check RDS status
aws rds describe-db-instances \
--db-instance-identifier terraform-db-db \
--query 'DBInstances[0].DBInstanceStatus'
# Should show "available"
# Check security group
aws ec2 describe-security-groups \
--group-ids <RDS_SG_ID>
Issue 2: "Access Denied" when getting secret
Cause: IAM role not attached or policy incorrect.
Solution:
# Check instance profile
aws ec2 describe-instances \
--instance-ids <INSTANCE_ID> \
--query 'Reservations[0].Instances[0].IamInstanceProfile'
# Should show the instance profile
# Check IAM role policies
aws iam list-role-policies --role-name terraform-db-ec2-secrets-role
Issue 3: RDS creation fails
Error:
Error: DB Subnet Group doesn't meet availability zone coverage requirement
Solution: Ensure private subnets are in different AZs:
availability_zone = data.aws_availability_zones.available.names[0] # First AZ
availability_zone = data.aws_availability_zones.available.names[1] # Second AZ
Issue 4: "mysql: command not found"
Cause: User data script hasn't completed.
Solution:
# Wait 2-3 minutes after instance launch
# Check user data logs
sudo cat /var/log/cloud-init-output.log
# Manually install if needed
sudo yum install -y mysql
π° Cost Breakdown
Monthly Costs (us-east-1):
| Resource | Quantity | Cost/Month | Total |
|---|---|---|---|
| db.t3.micro RDS | 1 | $12.41 | $12.41 |
| Storage (20 GB gp3) | 20 GB | $2.30 | $2.30 |
| Backup Storage (20 GB) | 20 GB | $0.95 | $0.95 |
| t2.micro EC2 | 1 | $8.50 | $8.50 |
| Secrets Manager | 1 secret | $0.40 | $0.40 |
| Total | ~$24.56 |
Free Tier Benefits:
- 750 hours/month of db.t3.micro (first 12 months)
- 750 hours/month of t2.micro
- 20 GB storage
- Actual cost with free tier: ~$1.35/month
Cost Optimization:
- Use free tier instances
- Delete when not in use
- Reduce backup retention (7 days β 1 day)
- Use smaller storage (20 GB β 10 GB minimum)
π Security Best Practices
β What We Did Right
-
No Hardcoded Passwords
- Generated automatically
- Stored encrypted
- Never in code
-
Private Subnets for Database
- No internet access
- Only accessible from EC2
-
Security Group Restrictions
- RDS only accepts connections from EC2
- Least privilege access
-
IAM Roles Instead of Keys
- No access keys on EC2
- Automatic credential rotation
- Auditable access
-
Encryption
- Storage encrypted at rest
- Secrets encrypted in Secrets Manager
- TLS for connections
-
Automated Backups
- 7-day retention
- Point-in-time recovery
- Disaster recovery ready
π Additional Security Recommendations
For Production:
- Enable Multi-AZ:
multi_az = true
- Use Parameter Groups:
resource "aws_db_parameter_group" "main" {
family = "mysql8.0"
parameter {
name = "slow_query_log"
value = "1"
}
}
- Enable Enhanced Monitoring:
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
- Use Secrets Rotation:
resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db_credentials.id
rotation_lambda_arn = aws_lambda_function.rotate.arn
rotation_rules {
automatically_after_days = 30
}
}
- Enable Deletion Protection:
deletion_protection = true
π§Ή Cleanup
Important: RDS deletion takes 5-10 minutes.
# Destroy everything
terraform destroy
# Type 'yes' when prompted
What gets deleted:
- β EC2 Instance
- β RDS Database
- β Secrets Manager Secret
- β IAM Roles and Policies
- β Security Groups
- β VPC and Subnets
Verify deletion:
# Check RDS
aws rds describe-db-instances
# Check Secrets
aws secretsmanager list-secrets
π Key Concepts Learned
1. Random Provider
resource "random_password" "db_password" {
length = 16
}
Generate secure random values.
2. AWS Secrets Manager
resource "aws_secretsmanager_secret_version" "db" {
secret_string = jsonencode({...})
}
Secure credential storage.
3. IAM Roles for EC2
resource "aws_iam_role" "ec2_role" {
assume_role_policy = jsonencode({...})
}
No access keys needed!
4. RDS Configuration
resource "aws_db_instance" "main" {
storage_encrypted = true
backup_retention_period = 7
}
Managed database with backups.
5. Sensitive Outputs
output "password" {
sensitive = true
}
Hide sensitive data from logs.
6. Depends On
depends_on = [aws_db_instance.main]
Control resource creation order.
π― Real-World Applications
This architecture is used for:
1. Web Applications
- WordPress with RDS
- E-commerce platforms
- SaaS applications
2. API Backends
- REST APIs with database
- GraphQL servers
- Microservices
3. Data Analytics
- ETL pipelines
- Reporting systems
- Data warehouses
4. Mobile Backends
- User authentication
- Content management
- Push notifications
π What's Next?
Congratulations! You've completed the Terraform By Sarvar foundation series! π
You now know how to:
- Build complete AWS infrastructure with Terraform
- Deploy VPCs, EC2, Lambda, IAM, and RDS
- Secure everything with IAM roles and Secrets Manager
- Follow production-ready best practices
Stay tuned for advanced topics:
- Terraform Modules
- Remote State Management
- Multi-Environment Setup
- CI/CD Integration
π Additional Resources
Official Documentation:
π Source Code
The complete source code and Terraform configuration used in this article can be found on GitHub:
simplynadaf
/
terraform-by-sarvar
Terraform Series
π Terraform By Sarvar
Complete Terraform tutorial series from basics to advanced concepts
π Series Overview
This repository contains ONLY Terraform code examples for the Terraform By Sarvar tutorial series.
β οΈ IMPORTANT: This repo contains only.tffiles and infrastructure code. Articles are published on dev.to, not stored here.
π Read the series on dev.to: https://dev.to/sarvar_04/series/36963
π― What You'll Learn
- β Infrastructure as Code (IaC) fundamentals
- β Terraform basics to advanced concepts
- β AWS resource provisioning
- β Best practices and real-world patterns
- β Production-ready configurations
π Series Progress
π Article Series
π Foundation (Articles 1-5) β Complete
- Introduction to Terraform & IaC - β Published
- Installation & Setup - β Published
- Your First AWS Resource - β Published
- Understanding Terraform State - β Published
- Variables and Outputs - β Published
π Real Infrastructure (Articles 6-10)
Getting Started
Follow these steps to run the project on your local machine:
- Clone the repository:
git clone https://github.com/simplynadaf/terraform-by-sarvar.git
- Navigate to the project directory:
cd terraform-by-sarvar/articles/10-rds-secrets
π Wrapping Up
Thank you for reading. I hope this article provided practical insights and a clearer understanding of the topic.
If you found this useful:
- β€οΈ Like if it added value
- π¦ Unicorn if youβre applying it today
- πΎ Save it for your next optimization session
- π Share it with your team
π‘ Whatβs Next
More deep dives are coming soon on:
- Cloud Operations
- GenAI & Agentic AI
- DevOps Automation
- Data & Platform Engineering
Follow along for weekly insights and hands-on guides.
π Portfolio & Work
You can explore my full body of work, certifications, architecture projects, and technical articles here:
π Visit My Website
π οΈ Services I Offer
If you're looking for hands-on guidance or collaboration, I provide:
- Cloud Architecture Consulting (AWS / Azure)
- DevSecOps & Automation Design
- FinOps Optimization Reviews
- Technical Writing (Cloud, DevOps, GenAI)
- Product & Architecture Reviews
- Mentorship & 1:1 Technical Guidance
π€ Letβs Connect
Iβd love to hear your thoughts. Feel free to drop a comment or connect with me on:
π LinkedIn
For collaborations, consulting, or technical discussions, reach out at:
Found this helpful? Share it with your team.
β Star the repo β’ π Follow the series β’ π¬ Ask questions
Made by Sarvar Nadaf
π https://sarvarnadaf.com
Top comments (0)