DEV Community

Cover image for Secure Database Deployment - RDS + Secrets Manager

Secure Database Deployment - RDS + Secrets Manager

πŸ‘‹ 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
Enter fullscreen mode Exit fullscreen mode

How it happened:

  1. Developer hardcoded DB password in main.tf
  2. Committed to Git: git commit -m "Add database"
  3. Pushed to GitHub: git push origin main
  4. Security scanner found it 6 months later
  5. 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!
}
Enter fullscreen mode Exit fullscreen mode

❌ In Environment Variables:

export DB_PASSWORD="secret123"  # Still visible in shell history
Enter fullscreen mode Exit fullscreen mode

❌ In terraform.tfvars (committed to Git):

db_password = "MySecretPass"  # In version control!
Enter fullscreen mode Exit fullscreen mode

❌ In State File:

{
  "password": "MyPassword123",  // Plain text in S3!
}
Enter fullscreen mode Exit fullscreen mode

❌ Shared via Slack/Email:

"Hey, the DB password is MyPassword123"  // Now it's everywhere
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

With Secrets Manager:

Terraform β†’ Generates password β†’ Secrets Manager (encrypted) β†’ 
EC2 (IAM role) β†’ Retrieves password β†’ Connects to RDS βœ…
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Architecture Components:

  1. VPC - Isolated network
  2. Public Subnet - EC2 instance
  3. Private Subnets - RDS database (2 AZs required)
  4. Security Groups - Network firewall
  5. RDS MySQL - Managed database
  6. Secrets Manager - Encrypted credential storage
  7. 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
Enter fullscreen mode Exit fullscreen mode

πŸ”§ 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"
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

New Concept: Random Provider

resource "random_password" "db_password" {
  length  = 16
  special = true
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Security Architecture:

Internet β†’ EC2 (SSH from your IP, HTTP from anywhere)
           ↓
           RDS (MySQL port 3306 ONLY from EC2)
Enter fullscreen mode Exit fullscreen mode

Key Security Practices:

  1. RDS is NOT accessible from internet
  2. RDS only accepts connections from EC2 security group
  3. No direct database access from outside
  4. 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
  })
}
Enter fullscreen mode Exit fullscreen mode

What's Happening:

  1. Create Secret: Empty container for credentials
  2. Store Credentials: JSON with all connection info
  3. Encryption: Automatically encrypted at rest
  4. 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"
}
Enter fullscreen mode Exit fullscreen mode

πŸ—„οΈ 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
  }
}
Enter fullscreen mode Exit fullscreen mode

Breaking Down the Configuration:

1. DB Subnet Group:

subnet_ids = [aws_subnet.private_1.id, aws_subnet.private_2.id]
Enter fullscreen mode Exit fullscreen mode

Tells RDS which subnets to use (must be in different AZs).

2. Storage:

storage_type      = "gp3"
storage_encrypted = true
Enter fullscreen mode Exit fullscreen mode
  • gp3 - Latest generation SSD (better performance)
  • storage_encrypted - Encryption at rest

3. Backups:

backup_retention_period = 7
backup_window          = "03:00-04:00"
Enter fullscreen mode Exit fullscreen mode
  • Automatic daily backups
  • Retained for 7 days
  • Runs at 3 AM UTC

4. Maintenance:

maintenance_window = "mon:04:00-mon:05:00"
Enter fullscreen mode Exit fullscreen mode

AWS performs updates during this window.

5. CloudWatch Logs:

enabled_cloudwatch_logs_exports = ["error", "general", "slowquery"]
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Understanding IAM Components:

1. IAM Role:

  • Identity that EC2 can assume
  • No permanent credentials needed

2. Assume Role Policy:

{
  "Principal": {
    "Service": "ec2.amazonaws.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

Says "EC2 service can use this role"

3. IAM Policy:

{
  "Action": [
    "secretsmanager:GetSecretValue"
  ],
  "Resource": "arn:aws:secretsmanager:..."
}
Enter fullscreen mode Exit fullscreen mode

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
  ]
}
Enter fullscreen mode Exit fullscreen mode

User Data Magic:

  1. Install Tools:
yum install -y mysql jq aws-cli
Enter fullscreen mode Exit fullscreen mode
  • mysql - Database client
  • jq - JSON parser
  • aws-cli - AWS commands
  1. Fetch Credentials:
aws secretsmanager get-secret-value --secret-id $SECRET_ARN
Enter fullscreen mode Exit fullscreen mode

Uses IAM role (no keys needed!)

  1. Parse JSON:
DB_HOST=$(echo $SECRET_JSON | jq -r '.host')
Enter fullscreen mode Exit fullscreen mode

Extracts values from JSON.

  1. Create Helper Script:
cat > /home/ec2-user/connect-db.sh
Enter fullscreen mode Exit fullscreen mode

User can run ./connect-db.sh to connect to database.

Depends On:

depends_on = [
  aws_db_instance.main,
  aws_secretsmanager_secret_version.db_credentials
]
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Sensitive Outputs:

sensitive = true
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

πŸš€ Step 10: Deploy the Infrastructure

1. Initialize Terraform

terraform init
Enter fullscreen mode Exit fullscreen mode

2. Review the Plan

terraform plan
Enter fullscreen mode Exit fullscreen mode

You should see:

Plan: 17 to add, 0 to change, 0 to destroy.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

2. SSH to EC2 Instance

ssh -i my-key.pem ec2-user@<EC2_PUBLIC_IP>
Enter fullscreen mode Exit fullscreen mode

3. Check Database Info

cat db-info.txt
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

4. Connect to Database

./connect-db.sh
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

πŸ” 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 .
Enter fullscreen mode Exit fullscreen mode

Output:

{
  "username": "admin",
  "password": "Xy9#mK2$pL5@qR8!",
  "engine": "mysql",
  "host": "terraform-db.xxxxx.rds.amazonaws.com",
  "port": 3306,
  "dbname": "myappdb"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ› 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Issue 3: RDS creation fails

Error:

Error: DB Subnet Group doesn't meet availability zone coverage requirement
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ’° 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:

  1. Use free tier instances
  2. Delete when not in use
  3. Reduce backup retention (7 days β†’ 1 day)
  4. Use smaller storage (20 GB β†’ 10 GB minimum)

πŸ” Security Best Practices

βœ… What We Did Right

  1. No Hardcoded Passwords

    • Generated automatically
    • Stored encrypted
    • Never in code
  2. Private Subnets for Database

    • No internet access
    • Only accessible from EC2
  3. Security Group Restrictions

    • RDS only accepts connections from EC2
    • Least privilege access
  4. IAM Roles Instead of Keys

    • No access keys on EC2
    • Automatic credential rotation
    • Auditable access
  5. Encryption

    • Storage encrypted at rest
    • Secrets encrypted in Secrets Manager
    • TLS for connections
  6. Automated Backups

    • 7-day retention
    • Point-in-time recovery
    • Disaster recovery ready

πŸ”’ Additional Security Recommendations

For Production:

  1. Enable Multi-AZ:
multi_az = true
Enter fullscreen mode Exit fullscreen mode
  1. Use Parameter Groups:
resource "aws_db_parameter_group" "main" {
  family = "mysql8.0"

  parameter {
    name  = "slow_query_log"
    value = "1"
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Enable Enhanced Monitoring:
monitoring_interval = 60
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
Enter fullscreen mode Exit fullscreen mode
  1. 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
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Enable Deletion Protection:
deletion_protection = true
Enter fullscreen mode Exit fullscreen mode

🧹 Cleanup

Important: RDS deletion takes 5-10 minutes.

# Destroy everything
terraform destroy

# Type 'yes' when prompted
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ“š Key Concepts Learned

1. Random Provider

resource "random_password" "db_password" {
  length = 16
}
Enter fullscreen mode Exit fullscreen mode

Generate secure random values.

2. AWS Secrets Manager

resource "aws_secretsmanager_secret_version" "db" {
  secret_string = jsonencode({...})
}
Enter fullscreen mode Exit fullscreen mode

Secure credential storage.

3. IAM Roles for EC2

resource "aws_iam_role" "ec2_role" {
  assume_role_policy = jsonencode({...})
}
Enter fullscreen mode Exit fullscreen mode

No access keys needed!

4. RDS Configuration

resource "aws_db_instance" "main" {
  storage_encrypted = true
  backup_retention_period = 7
}
Enter fullscreen mode Exit fullscreen mode

Managed database with backups.

5. Sensitive Outputs

output "password" {
  sensitive = true
}
Enter fullscreen mode Exit fullscreen mode

Hide sensitive data from logs.

6. Depends On

depends_on = [aws_db_instance.main]
Enter fullscreen mode Exit fullscreen mode

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:

πŸš€ Terraform By Sarvar

Complete Terraform tutorial series from basics to advanced concepts

Terraform AWS License dev.to

πŸ“– Read the Series β€’ 🌐 Portfolio β€’ πŸ’Ό LinkedIn


πŸ“š Series Overview

This repository contains ONLY Terraform code examples for the Terraform By Sarvar tutorial series.

⚠️ IMPORTANT: This repo contains only .tf files 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

Progress Status

πŸ“– Article Series

πŸ“˜ Foundation (Articles 1-5) βœ… Complete

  1. Introduction to Terraform & IaC - βœ… Published
  2. Installation & Setup - βœ… Published
  3. Your First AWS Resource - βœ… Published
  4. Understanding Terraform State - βœ… Published
  5. Variables and Outputs - βœ… Published

πŸ“— Real Infrastructure (Articles 6-10)

  1. Building a VPC from Scratch…

Getting Started

Follow these steps to run the project on your local machine:

  1. Clone the repository:
git clone https://github.com/simplynadaf/terraform-by-sarvar.git
Enter fullscreen mode Exit fullscreen mode
  1. Navigate to the project directory:
cd terraform-by-sarvar/articles/10-rds-secrets
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ 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:

πŸ“§ simplynadaf@gmail.com


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)