AWS RDS (Relational Database Service) removes the heavy lifting of managing MySQL infrastructure – patching, backups, failover, and scaling are all handled by AWS. Defining your RDS instances with AWS CloudFormation takes this further by making your database infrastructure repeatable, version-controlled, and auditable.
This guide walks through creating an AWS RDS MySQL instance using CloudFormation. We cover the full stack – VPC networking, subnet groups, security groups, parameterized templates, Multi-AZ deployments, automated backups, monitoring with Enhanced Monitoring and Performance Insights, and RDS Proxy for connection pooling. Every resource is defined in a single CloudFormation template you can deploy, update, and tear down cleanly.
Prerequisites
Before starting, confirm the following are in place:
- An active AWS account with permissions to create RDS, VPC, IAM, and CloudFormation resources
- AWS CLI v2 installed and configured with valid credentials (
aws configure) - An existing VPC with at least two subnets in different Availability Zones (required for DB subnet groups)
- A MySQL client installed locally for testing connectivity (
mysqlCLI or MySQL Workbench) - Basic familiarity with CloudFormation YAML template syntax
Step 1: Design the RDS Architecture
An RDS MySQL deployment requires several networking and security components before the database instance itself. Here is what we need:
- DB Subnet Group – Tells RDS which subnets (and therefore which Availability Zones) it can place the instance in. You need subnets in at least two AZs for high availability
- Security Group – Controls which IP ranges or security groups can connect to the RDS instance on port 3306 (MySQL default)
- RDS Instance – The MySQL database engine, instance class, storage, and configuration
The architecture places the RDS instance in private subnets (not publicly accessible) with a security group allowing traffic only from your application servers or a specific CIDR range. This is the standard production pattern – never expose RDS directly to the internet.
Step 2: Create the CloudFormation Template
Create a file called rds-mysql.yaml with the complete CloudFormation template. This template defines the DB subnet group, security group, and the RDS MySQL instance as a single deployable stack.
vi rds-mysql.yaml
Add the following CloudFormation template:
AWSTemplateFormatVersion: '2010-09-09'
Description: RDS MySQL instance with subnet group, security group, and monitoring
Parameters:
VpcId:
Type: AWS::EC2::VPC::Id
Description: VPC where RDS will be deployed
SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: At least two subnets in different AZs for the DB subnet group
AllowedCidr:
Type: String
Default: 10.0.0.0/16
Description: CIDR range allowed to connect to MySQL (port 3306)
DBInstanceClass:
Type: String
Default: db.t3.micro
AllowedValues:
- db.t3.micro
- db.t3.small
- db.t3.medium
- db.r6g.large
- db.r6g.xlarge
Description: RDS instance type
DBAllocatedStorage:
Type: Number
Default: 20
MinValue: 20
MaxValue: 1000
Description: Storage size in GB (gp3)
DBName:
Type: String
Default: appdb
Description: Initial database name
AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*'
MaxLength: 64
MasterUsername:
Type: String
Default: admin
Description: Master username for the RDS instance
AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*'
MaxLength: 16
MasterUserPassword:
Type: String
NoEcho: true
MinLength: 8
MaxLength: 41
Description: Master password (8-41 characters)
MultiAZ:
Type: String
Default: 'false'
AllowedValues:
- 'true'
- 'false'
Description: Enable Multi-AZ deployment for high availability
BackupRetentionPeriod:
Type: Number
Default: 7
MinValue: 0
MaxValue: 35
Description: Number of days to retain automated backups (0 disables backups)
EngineVersion:
Type: String
Default: '8.0'
AllowedValues:
- '8.0'
- '8.4'
Description: MySQL engine version
Resources:
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for RDS MySQL
SubnetIds: !Ref SubnetIds
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-db-subnet-group'
DBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for RDS MySQL access
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
CidrIp: !Ref AllowedCidr
Description: MySQL access from allowed CIDR
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-rds-sg'
RDSMonitoringRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: monitoring.rds.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole
MySQLInstance:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: !Sub '${AWS::StackName}-mysql'
Engine: mysql
EngineVersion: !Ref EngineVersion
DBInstanceClass: !Ref DBInstanceClass
AllocatedStorage: !Ref DBAllocatedStorage
StorageType: gp3
DBName: !Ref DBName
MasterUsername: !Ref MasterUsername
MasterUserPassword: !Ref MasterUserPassword
DBSubnetGroupName: !Ref DBSubnetGroup
VPCSecurityGroups:
- !GetAtt DBSecurityGroup.GroupId
MultiAZ: !Ref MultiAZ
BackupRetentionPeriod: !Ref BackupRetentionPeriod
PreferredBackupWindow: '03:00-04:00'
PreferredMaintenanceWindow: 'sun:05:00-sun:06:00'
AutoMinorVersionUpgrade: true
PubliclyAccessible: false
StorageEncrypted: true
MonitoringInterval: 60
MonitoringRoleArn: !GetAtt RDSMonitoringRole.Arn
EnablePerformanceInsights: true
PerformanceInsightsRetentionPeriod: 7
DeletionProtection: false
CopyTagsToSnapshot: true
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-mysql'
- Key: Environment
Value: production
DeletionPolicy: Snapshot
Outputs:
RDSEndpoint:
Description: RDS MySQL endpoint address
Value: !GetAtt MySQLInstance.Endpoint.Address
Export:
Name: !Sub '${AWS::StackName}-endpoint'
RDSPort:
Description: RDS MySQL port
Value: !GetAtt MySQLInstance.Endpoint.Port
SecurityGroupId:
Description: RDS security group ID
Value: !GetAtt DBSecurityGroup.GroupId
DBSubnetGroupName:
Description: DB subnet group name
Value: !Ref DBSubnetGroup
This template defines everything in one file. The DeletionPolicy: Snapshot on the RDS instance ensures a final snapshot is taken if the stack is deleted – protecting against accidental data loss.
Step 3: Understand the Template Parameters
The template uses parameters to keep the configuration flexible across environments. Here is what each parameter controls:
| Parameter | Purpose | Default |
|---|---|---|
VpcId | Target VPC for the RDS deployment | None (required) |
SubnetIds | Subnets in 2+ AZs for the DB subnet group | None (required) |
AllowedCidr | CIDR range permitted to connect on port 3306 | 10.0.0.0/16 |
DBInstanceClass | Compute and memory capacity | db.t3.micro |
DBAllocatedStorage | Storage in GB (gp3 SSD) | 20 |
MasterUserPassword | Root password (NoEcho – hidden in console) | None (required) |
MultiAZ | Enable standby replica in another AZ | false |
BackupRetentionPeriod | Days to keep automated backups | 7 |
The NoEcho: true property on MasterUserPassword prevents the password from appearing in CloudFormation console output or event logs. For production deployments, store the password in AWS Secrets Manager and reference it with a dynamic reference instead of passing it as a plain parameter.
Step 4: Deploy the CloudFormation Stack
Before deploying, identify your VPC ID and subnet IDs. List your VPCs first:
aws ec2 describe-vpcs --query "Vpcs[*].[VpcId,Tags[?Key=='Name'].Value|[0],CidrBlock]" --output table
Then list subnets in your target VPC (replace the VPC ID with yours):
aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=vpc-0abc123def456" \
--query "Subnets[*].[SubnetId,AvailabilityZone,CidrBlock,Tags[?Key=='Name'].Value|[0]]" \
--output table
Deploy the stack with your actual VPC and subnet values. The CAPABILITY_NAMED_IAM capability is required because the template creates an IAM role for Enhanced Monitoring:
aws cloudformation create-stack \
--stack-name rds-mysql-prod \
--template-body file://rds-mysql.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=VpcId,ParameterValue=vpc-0abc123def456 \
ParameterKey=SubnetIds,ParameterValue="subnet-0aaa111,subnet-0bbb222" \
ParameterKey=AllowedCidr,ParameterValue=10.0.0.0/16 \
ParameterKey=DBInstanceClass,ParameterValue=db.t3.micro \
ParameterKey=DBAllocatedStorage,ParameterValue=20 \
ParameterKey=DBName,ParameterValue=appdb \
ParameterKey=MasterUsername,ParameterValue=admin \
ParameterKey=MasterUserPassword,ParameterValue=YourStr0ngP4ssword \
ParameterKey=MultiAZ,ParameterValue=false \
ParameterKey=BackupRetentionPeriod,ParameterValue=7 \
ParameterKey=EngineVersion,ParameterValue=8.0
RDS instance creation takes 10-15 minutes. Monitor the stack progress:
aws cloudformation wait stack-create-complete --stack-name rds-mysql-prod
Once the wait command returns, verify the stack was created successfully:
aws cloudformation describe-stacks \
--stack-name rds-mysql-prod \
--query "Stacks[0].StackStatus" \
--output text
The output should show the stack status as complete:
CREATE_COMPLETE
Retrieve the RDS endpoint from the stack outputs:
aws cloudformation describe-stacks \
--stack-name rds-mysql-prod \
--query "Stacks[0].Outputs[?OutputKey=='RDSEndpoint'].OutputValue" \
--output text
This returns the DNS endpoint for your RDS instance, which looks like:
rds-mysql-prod-mysql.c9abcdef12gh.us-east-1.rds.amazonaws.com
Step 5: Connect to the RDS MySQL Instance
Connect to the RDS instance using the MySQL client from a host within the allowed CIDR range (such as an EC2 instance in the same VPC). Replace the endpoint with the value from the previous step:
mysql -h rds-mysql-prod-mysql.c9abcdef12gh.us-east-1.rds.amazonaws.com \
-u admin -p \
--ssl-mode=REQUIRED
After entering the master password, run a quick verification query to confirm the connection and check the MySQL version:
SELECT VERSION();
SHOW DATABASES;
The output confirms MySQL is running and your initial database exists:
+-----------+
| VERSION() |
+-----------+
| 8.0.39 |
+-----------+
1 row in set (0.01 sec)
+--------------------+
| Database |
+--------------------+
| appdb |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.01 sec)
If the connection times out, check that your security group allows traffic from the source IP and that the source host is in a subnet that can route to the RDS subnets. RDS instances with PubliclyAccessible: false are only reachable from within the VPC.
Step 6: Enable Multi-AZ for High Availability
Multi-AZ creates a synchronous standby replica in a different Availability Zone. If the primary instance fails, RDS automatically fails over to the standby – typically within 60-120 seconds. No application code changes are needed because the DNS endpoint stays the same.
To enable Multi-AZ on an existing stack, update the MultiAZ parameter:
aws cloudformation update-stack \
--stack-name rds-mysql-prod \
--use-previous-template \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=VpcId,UsePreviousValue=true \
ParameterKey=SubnetIds,UsePreviousValue=true \
ParameterKey=AllowedCidr,UsePreviousValue=true \
ParameterKey=DBInstanceClass,UsePreviousValue=true \
ParameterKey=DBAllocatedStorage,UsePreviousValue=true \
ParameterKey=DBName,UsePreviousValue=true \
ParameterKey=MasterUsername,UsePreviousValue=true \
ParameterKey=MasterUserPassword,UsePreviousValue=true \
ParameterKey=MultiAZ,ParameterValue=true \
ParameterKey=BackupRetentionPeriod,UsePreviousValue=true \
ParameterKey=EngineVersion,UsePreviousValue=true
The modification takes 10-20 minutes and involves a brief I/O suspension while RDS provisions the standby. Verify Multi-AZ is active:
aws rds describe-db-instances \
--db-instance-identifier rds-mysql-prod-mysql \
--query "DBInstances[0].[MultiAZ,AvailabilityZone,SecondaryAvailabilityZone]" \
--output table
The output shows Multi-AZ enabled with the primary and standby in different zones:
---------------------------------------------
| DescribeDBInstances |
+-------+-------------+--------------------+
| True | us-east-1a | us-east-1b |
+-------+-------------+--------------------+
Multi-AZ roughly doubles the cost of the RDS instance. For non-production environments, keep it disabled to save costs.
Step 7: Configure Automated Backups
The template already configures automated backups with a 7-day retention period and a preferred backup window of 03:00-04:00 UTC. These are point-in-time recovery (PITR) backups – RDS continuously saves transaction logs, allowing you to restore to any second within the retention period.
Check the current backup configuration:
aws rds describe-db-instances \
--db-instance-identifier rds-mysql-prod-mysql \
--query "DBInstances[0].[BackupRetentionPeriod,PreferredBackupWindow,LatestRestorableTime]" \
--output table
To change the retention period (for example, to 14 days for production), update the stack parameter:
aws cloudformation update-stack \
--stack-name rds-mysql-prod \
--use-previous-template \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=VpcId,UsePreviousValue=true \
ParameterKey=SubnetIds,UsePreviousValue=true \
ParameterKey=AllowedCidr,UsePreviousValue=true \
ParameterKey=DBInstanceClass,UsePreviousValue=true \
ParameterKey=DBAllocatedStorage,UsePreviousValue=true \
ParameterKey=DBName,UsePreviousValue=true \
ParameterKey=MasterUsername,UsePreviousValue=true \
ParameterKey=MasterUserPassword,UsePreviousValue=true \
ParameterKey=MultiAZ,UsePreviousValue=true \
ParameterKey=BackupRetentionPeriod,ParameterValue=14 \
ParameterKey=EngineVersion,UsePreviousValue=true
To create a manual snapshot before a major change (such as a version upgrade), use:
aws rds create-db-snapshot \
--db-instance-identifier rds-mysql-prod-mysql \
--db-snapshot-identifier rds-mysql-prod-pre-upgrade-$(date +%Y%m%d)
Manual snapshots persist until you explicitly delete them and are not affected by the retention period. Always create a manual snapshot before engine upgrades or major configuration changes.
Step 8: Configure Monitoring
The CloudFormation template already enables two monitoring features – Enhanced Monitoring and Performance Insights. Here is what each provides and how to use them.
Enhanced Monitoring
Enhanced Monitoring collects OS-level metrics (CPU, memory, swap, disk I/O, network) from the RDS instance at 60-second intervals. These metrics go to CloudWatch Logs under the RDSOSMetrics log group. The template sets MonitoringInterval: 60 and creates the required IAM role automatically.
View Enhanced Monitoring metrics from the CLI:
aws logs get-log-events \
--log-group-name RDSOSMetrics \
--log-stream-name rds-mysql-prod-mysql \
--limit 1 \
--query "events[0].message" \
--output text | python3 -m json.tool | head -30
This returns detailed OS metrics including process lists, memory breakdowns, and disk utilization – data that standard CloudWatch RDS metrics don’t capture.
Performance Insights
Performance Insights identifies database performance bottlenecks by analyzing wait events, top SQL queries, and resource usage. The template enables it with a 7-day retention period (free tier). For production workloads that need historical analysis, increase retention to 731 days (2 years) in the template by changing PerformanceInsightsRetentionPeriod.
Query Performance Insights data to identify the top wait events over the last hour:
aws pi get-resource-metrics \
--service-type RDS \
--identifier db-ABCDEFGHIJKLMNOP \
--metric-queries '[{"Metric":"db.load.avg","GroupBy":{"Group":"db.wait_event"}}]' \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period-in-seconds 300
Replace the --identifier value with your DbiResourceId. Find it with:
aws rds describe-db-instances \
--db-instance-identifier rds-mysql-prod-mysql \
--query "DBInstances[0].DbiResourceId" \
--output text
Set up a CloudWatch alarm to alert when CPU exceeds 80% for 5 minutes. This catches runaway queries or undersized instances before they impact your application:
aws cloudwatch put-metric-alarm \
--alarm-name rds-mysql-prod-high-cpu \
--metric-name CPUUtilization \
--namespace AWS/RDS \
--dimensions Name=DBInstanceIdentifier,Value=rds-mysql-prod-mysql \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts
Replace the SNS topic ARN with your own. If you’re running Prometheus monitoring for MySQL, you can also scrape RDS metrics through the CloudWatch exporter for a unified dashboard.
Step 9: Update and Delete the Stack
One of the main benefits of CloudFormation is making controlled changes through stack updates instead of manual console clicks. To change the instance class (for example, scaling up from db.t3.micro to db.r6g.large):
aws cloudformation update-stack \
--stack-name rds-mysql-prod \
--use-previous-template \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=VpcId,UsePreviousValue=true \
ParameterKey=SubnetIds,UsePreviousValue=true \
ParameterKey=AllowedCidr,UsePreviousValue=true \
ParameterKey=DBInstanceClass,ParameterValue=db.r6g.large \
ParameterKey=DBAllocatedStorage,UsePreviousValue=true \
ParameterKey=DBName,UsePreviousValue=true \
ParameterKey=MasterUsername,UsePreviousValue=true \
ParameterKey=MasterUserPassword,UsePreviousValue=true \
ParameterKey=MultiAZ,UsePreviousValue=true \
ParameterKey=BackupRetentionPeriod,UsePreviousValue=true \
ParameterKey=EngineVersion,UsePreviousValue=true
Instance class changes require a reboot (brief downtime). If Multi-AZ is enabled, RDS performs the change on the standby first, then fails over – reducing downtime to under 60 seconds.
Check which resources CloudFormation would change before applying, using a change set:
aws cloudformation create-change-set \
--stack-name rds-mysql-prod \
--change-set-name scale-up-instance \
--use-previous-template \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=DBInstanceClass,ParameterValue=db.r6g.large \
ParameterKey=VpcId,UsePreviousValue=true \
ParameterKey=SubnetIds,UsePreviousValue=true \
ParameterKey=AllowedCidr,UsePreviousValue=true \
ParameterKey=DBAllocatedStorage,UsePreviousValue=true \
ParameterKey=DBName,UsePreviousValue=true \
ParameterKey=MasterUsername,UsePreviousValue=true \
ParameterKey=MasterUserPassword,UsePreviousValue=true \
ParameterKey=MultiAZ,UsePreviousValue=true \
ParameterKey=BackupRetentionPeriod,UsePreviousValue=true \
ParameterKey=EngineVersion,UsePreviousValue=true
Review what would change, then execute or delete the change set:
aws cloudformation describe-change-set \
--stack-name rds-mysql-prod \
--change-set-name scale-up-instance \
--query "Changes[*].ResourceChange.{Resource:LogicalResourceId,Action:Action,Replacement:Replacement}" \
--output table
To delete the entire stack when it is no longer needed:
aws cloudformation delete-stack --stack-name rds-mysql-prod
Because the template has DeletionPolicy: Snapshot, CloudFormation creates a final DB snapshot before deleting the RDS instance. This snapshot persists and can be used to restore the database later if needed. Verify the snapshot was created:
aws rds describe-db-snapshots \
--query "DBSnapshots[?DBInstanceIdentifier=='rds-mysql-prod-mysql'].[DBSnapshotIdentifier,Status,SnapshotCreateTime]" \
--output table
Step 10: Add RDS Proxy for Connection Pooling
RDS Proxy sits between your application and the database, maintaining a pool of persistent connections. This is important for applications that open and close database connections frequently – such as Lambda functions, microservices, or any application with bursty connection patterns. Without a proxy, each new connection incurs TCP handshake and MySQL authentication overhead.
RDS Proxy requires the database credentials stored in AWS Secrets Manager. Create the secret first:
aws secretsmanager create-secret \
--name rds-mysql-prod/credentials \
--secret-string '{"username":"admin","password":"YourStr0ngP4ssword"}'
Add the following resources to your CloudFormation template under the Resources section to create the RDS Proxy, its IAM role, and target group:
RDSProxyRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: rds.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: RDSProxySecretsAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:rds-mysql-prod/credentials*'
MySQLProxy:
Type: AWS::RDS::DBProxy
Properties:
DBProxyName: !Sub '${AWS::StackName}-proxy'
EngineFamily: MYSQL
RequireTLS: true
RoleArn: !GetAtt RDSProxyRole.Arn
Auth:
- AuthScheme: SECRETS
SecretArn: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:rds-mysql-prod/credentials'
IAMAuth: DISABLED
VpcSubnetIds: !Ref SubnetIds
VpcSecurityGroupIds:
- !GetAtt DBSecurityGroup.GroupId
MySQLProxyTargetGroup:
Type: AWS::RDS::DBProxyTargetGroup
Properties:
DBProxyName: !Ref MySQLProxy
TargetGroupName: default
DBInstanceIdentifiers:
- !Ref MySQLInstance
ConnectionPoolConfigurationInfo:
MaxConnectionsPercent: 90
MaxIdleConnectionsPercent: 50
ConnectionBorrowTimeout: 120
After adding the proxy resources and updating the stack, retrieve the proxy endpoint:
aws rds describe-db-proxies \
--db-proxy-name rds-mysql-prod-proxy \
--query "DBProxies[0].Endpoint" \
--output text
Connect through the proxy instead of directly to RDS. The proxy endpoint replaces the RDS endpoint in your application connection string:
mysql -h rds-mysql-prod-proxy.proxy-c9abcdef12gh.us-east-1.rds.amazonaws.com \
-u admin -p \
--ssl-mode=REQUIRED
The proxy handles connection multiplexing transparently. Your application connects to the proxy endpoint, and the proxy reuses existing database connections from its pool. This reduces connection overhead and protects the database from connection storms during traffic spikes.
Conclusion
We deployed a fully managed RDS MySQL instance using CloudFormation with proper networking, security groups, encrypted storage, automated backups, Multi-AZ failover, and monitoring through Enhanced Monitoring and Performance Insights. The entire stack is defined in a single template that can be version-controlled, reviewed in pull requests, and deployed consistently across environments.
For production hardening, store the master password in AWS Secrets Manager with automatic rotation, enable deletion protection (DeletionProtection: true), and set up CloudWatch alarms for storage space, replication lag, and connection counts. Use RDS Proxy to manage connection pooling for serverless or high-concurrency workloads, and consider read replicas if your application is read-heavy.