Skip to content

RDS instances should be deployed in a VPC

Running an RDS instance inside a VPC gives you control over network isolation through subnets, route tables, network ACLs, and security groups. Without VPC placement, the instance sits on a flat, shared network where you cannot restrict traffic at the network layer and cannot use private IP addressing to keep database endpoints off the public internet.

AWS retired EC2-Classic in August 2022, so most modern accounts default to VPC. However, older accounts migrated from Classic may still contain legacy instances, and auditors flag any instance that lacks an explicit DB subnet group association.

Retrofit consideration

Moving a non-VPC RDS instance into a VPC requires creating a snapshot, restoring it into a DB subnet group within the target VPC, and updating application connection strings. This causes downtime.

Implementation

Choose the approach that matches how you manage Terraform.

If you use terraform-aws-modules/rds/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.

module "rds" {
  source  = "terraform-aws-modules/rds/aws"
  version = ">=7.0.0"

  allocated_storage      = 20
  db_name                = "myapp"
  db_subnet_group_name   = "example-db-subnet-group"
  engine                 = "mysql"
  engine_version         = "8.0.41"
  family                 = "mysql8.0"
  identifier             = "abc123"
  instance_class         = "db.t3.micro"
  major_engine_version   = "8.0"
  password_wo            = "change-me-in-production"
  skip_final_snapshot    = true
  username               = "dbadmin"
  vpc_security_group_ids = ["sg-12345678"]
}

Use AWS provider resources directly. See docs for the resources involved: aws_db_instance.

resource "aws_db_instance" "this" {
  allocated_storage               = 20
  enabled_cloudwatch_logs_exports = ["general", "slowquery"]
  engine                          = "mysql"
  identifier                      = "pofix-abc123"
  instance_class                  = "db.t3.micro"
  monitoring_interval             = 60
  monitoring_role_arn             = "arn:aws:iam::123456789012:role/example-role"
  password                        = "ChangeMe123!"
  skip_final_snapshot             = true
  username                        = "dbadmin"

  db_subnet_group_name = "example-db-subnet-group"
}

What this control checks

In Terraform, the aws_db_instance resource needs db_subnet_group_name set to a valid aws_db_subnet_group resource name. That subnet group must reference subnets in at least two Availability Zones within a VPC. If db_subnet_group_name is omitted entirely, the instance may land in the default VPC's default subnet group (which still passes), but explicit assignment is the reliable pattern. An instance fails this control when it has no VPC association at all, meaning DBSubnetGroup is null in the DescribeDBInstances API response. To pass, define a separate aws_db_subnet_group resource with subnet_ids pointing to private subnets, then reference its name output in the aws_db_instance block.

Common pitfalls

  • Default VPC deletion breaks implicit placement

    Omit db_subnet_group_name and you're implicitly relying on the default VPC. If that default VPC is later deleted, new RDS creates will fail with no obvious pointer back to this dependency. Always set db_subnet_group_name explicitly.

  • Single-AZ subnet groups cause deployment failures

    An aws_db_subnet_group with subnets in only one AZ will fail Multi-AZ deployments outright. Less obvious: AWS requires at least two AZs even for Single-AZ instances. Build subnet groups with subnets across a minimum of two AZs.

  • Restoring snapshots outside VPC

    When restoring an RDS snapshot via snapshot_identifier, db_subnet_group_name is still required. Skip it and the restore may land outside the intended VPC, in the default subnet group or worse.

  • Inline security group IDs without VPC context

    Setting vpc_security_group_ids without db_subnet_group_name produces confusing validation errors. The security groups must belong to the same VPC as the subnet group, so they're meaningless without one.

Audit evidence

An auditor expects AWS Config evaluation results confirming VPC placement, with all RDS instances showing a "COMPLIANT" status. Supporting evidence includes CLI output from aws rds describe-db-instances showing each instance's DBSubnetGroup.VpcId field populated with a valid VPC ID. Where Security Hub is in use, the finding history for this control shows continuous compliance over the audit period.

Framework-specific interpretation

Tool mappings

Use these identifiers to cross-reference this control across tools, reports, and evidence.

  • Compliance.tf Control: rds_db_instance_in_vpc

  • Powerpipe Control: aws_compliance.control.rds_db_instance_in_vpc

  • Prowler Check: rds_instance_inside_vpc

  • AWS Security Hub Control: RDS.18

Last reviewed: 2026-03-09