DEV Community

nao1515
nao1515

Posted on

Production-Ready AWS 3-Tier Architecture with Terraform (SSM & Secrets Manager)

Introduction

Building a "VPC-EC2-RDS" stack is a common task, but making it production-ready requires more than just resource creation. In this post, I will share a modular Terraform setup that implements:

  1. Modular Design: Reusable code for VPC, EC2, and RDS.
  2. Bastion-less Access: Using AWS Systems Manager (SSM) instead of SSH.
  3. Secret Management: Managing RDS passwords via AWS Secrets Manager.

Project Structure

We follow the standard environment/module separation to ensure scalability.

terraform/
├── envs/
│   └── dev/                # Environment-specific configuration
│       ├── main.tf         # Root module calling local modules
│       └── terraform.tfvars
└── modules/
    ├── vpc/                # Networking & Security Groups
    ├── ec2/                # IAM Roles & SSM-ready Instances
    └── rds/                # Private Database Instances
Enter fullscreen mode Exit fullscreen mode

Key Features

  • Networking (modules/vpc)

We define a VPC with both public and private subnets. The RDS instance stays in the private subnet, while the EC2 is also kept private for maximum security, relying on the NAT Gateway for outbound traffic.

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = { Name = "${var.project}-${var.env}-vpc" }
}

resource "aws_security_group" "web" {
  name   = "${var.project}-${var.env}-web-sg"
  vpc_id = aws_vpc.this.id

  ingress {
    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"]
  }
}

resource "aws_security_group" "db" {
  name   = "${var.project}-${var.env}-db-sg"
  vpc_id = aws_vpc.this.id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.web.id]
  }
}
Enter fullscreen mode Exit fullscreen mode
  • modules/ec2: Bastion-less Access via SSM

Build secure EC2 instances by closing all SSH ports and leveraging AWS Systems Manager (SSM) for remote access.

IAM Instance Profile for SSM Permissions
Instead of managing SSH keys, we attach an IAM role with the AmazonSSMManagedInstanceCore policy. This allows secure terminal access through the AWS Console or CLI.

Deployment in Private Subnets
The instances are placed in private subnets with no public IP addresses, significantly reducing the attack surface by making them inaccessible from the open internet.

resource "aws_instance" "this" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = var.security_group_ids
  iam_instance_profile   = aws_iam_instance_profile.ssm_profile.name

  metadata_options {
    http_endpoint = "enabled"
    http_tokens   = "required"
  }

  tags = { Name = "${var.project}-${var.env}-ec2" }
}

resource "aws_iam_role" "ssm_role" {
  name = "${var.project}-${var.env}-ssm-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{ Action = "sts:AssumeRole", Effect = "Allow", Principal = { Service = "ec2.amazonaws.com" } }]
  })
}

resource "aws_iam_role_policy_attachment" "ssm_managed" {
  role       = aws_iam_role.ssm_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
Enter fullscreen mode Exit fullscreen mode
  • modules/rds: Managed Database Layer

We deploy a production-grade PostgreSQL instance with a focus on maximum security and automated credential management.

Private Isolation from External Traffic
The database is placed in a dedicated private subnet with publicly_accessible = false. All direct ingress from the internet is strictly blocked, ensuring your data remains isolated.

Dynamic Password Injection via Variables
To avoid hardcoding sensitive information, the database password is parameterized. This allows for dynamic injection from higher-level environments or secret management services (like AWS Secrets Manager), ensuring a secure CI/CD pipeline.

resource "aws_db_subnet_group" "this" {
  name       = "${var.project}-${var.env}-rds-subnet-group"
  subnet_ids = var.subnet_ids
}

resource "aws_db_instance" "this" {
  identifier             = var.identifier
  engine                 = var.engine
  engine_version         = var.engine_version
  instance_class         = var.instance_class
  allocated_storage      = 20
  db_name                = var.database_name
  username               = var.master_username
  password               = var.master_password
  db_subnet_group_name   = aws_db_subnet_group.this.name
  vpc_security_group_ids = var.security_group_ids
  publicly_accessible    = false
  skip_final_snapshot    = true
}
Enter fullscreen mode Exit fullscreen mode
  • envs/dev: Environment-Specific Configuration & Secret Management

In this directory, we integrate the individual modules to build a complete "Development" environment while ensuring that sensitive information like passwords is handled securely.

Secure RDS Password Retrieval via AWS Secrets Manager
Instead of hardcoding database credentials, we dynamically fetch the RDS master password from AWS Secrets Manager at runtime.

data "aws_secretsmanager_secret" "rds_password" {
  name = "${var.project}/${var.env}/rds-password"
}

data "aws_secretsmanager_secret_version" "rds_password" {
  secret_id = data.aws_secretsmanager_secret.rds_password.id
}

module "vpc" {
  source   = "../../modules/vpc"
  vpc_cidr = var.vpc_cidr
  # ...
}

module "rds" {
  source          = "../../modules/rds"
  master_password = data.aws_secretsmanager_secret_version.rds_password.secret_string
  vpc_id          = module.vpc.vpc_id
  subnet_ids      = module.vpc.private_subnet_ids
  security_group_ids = [module.vpc.db_sg_id]
}

module "ec2" {
  source             = "../../modules/ec2"
  subnet_id          = module.vpc.private_subnet_ids[0]
  security_group_ids = [module.vpc.web_sg_id]
}
Enter fullscreen mode Exit fullscreen mode
  • Conclusion: Aiming for Production-Ready Terraform Architecture

In this walkthrough, we didn't just focus on turning resources into code. We prioritized the core pillars required in real-world operations: Maintainability, Security, and Reusability.

Key Benefits of This Architecture
Environment Portability
Need a production environment? Just create a new envs/prod/ directory. You can replicate this entire stack for production in minutes.

Moving Beyond Bastion Hosts
By leveraging AWS Systems Manager (SSM), we've eliminated the security risk of leaving SSH ports open to the world.

Proper Secret Management
With AWS Secrets Manager, sensitive passwords are decoupled from your codebase, ensuring they never leak into your Git history.

Infrastructure as Code (IaC) with Terraform is a powerful weapon that drastically reduces manual engineering hours and prevents human error. I encourage you to take this template and customize it to fit the specific needs of your own projects.

Top comments (0)