Provisioning an RDS Database with CloudFormation (part 2)
In part 1, we automated the provisioning of your Amazon EC2 instance using AWS CloudFormation. When you built the EC2 instance manually in the past, you were seeing inconsistencies between environments, had to manually test your infrastructure setup, manually deploy your team's web app, which happens all to infrequently. All of this has been error prone and time consuming. Bugs introduced into production code are more difficult and expensive to fix and your customers were ultimately the ones who suffered.
In addition to the EC2 instance, you also need a Postgresql database. In the past, it was running on the same instance as your webapp and sometimes web app traffic impacted the database and vice versa. In this post, part 2, you'll add an Amazon RDS Postgresql database to the CloudFormation template you built in part 1 so that both the EC2 instance and the database can be provisioned together as a set of resources. And since RDS is a managed service, you won’t have to do operating system patches or database patches — it’s all managed for you. And eventually, you want to introduce a continuous integration/continuous deployment (CI/CD) pipeline to automate the build, test, and deploy phases of your release process. Both part 1 and part 2 set you up to do that.
As a reminder, here's what we covered and where we're going:
- automate the provisioning of your Amazon EC2 instance using AWS CloudFormation (part 1),
- add an Amazon RDS Postgresql database to your stack with CloudFormation (this post, part 2), and
- create an AWS CodePipeline with CloudFormation (part 3).
Prerequisites
To work through the examples in this post, you’ll need:
- an AWS account (you can create your account here if you don’t already have one),
- the AWS CLI installed (you can find instructions for installing the AWS CLI here), and
- a key-pair to use for SSH (you can create a key-pair following these instructions).
Unfamiliar with CloudFormation or feeling a little rusty? Check out part 1 or my Intro to CloudFormation post before getting started.
Updating the CloudFormation Template
First we'll add the RDS database and then we'll add a security group to allow inbound traffic on port 5432. At the end of this post, you’ll delete the stack you’ve created and any snapshots so that you don’t incur any charges and then you can (quickly) customize and recreate the stack in the future.
Just want the code? Grab it here.
Let’s get started!
1. Add RDS Postgresql Database
First, we'll add an RDS database resource with the type AWS::RDS::DBInstance
to the CloudFormation template. We set the Engine
to the database engine we want to use, in this case postgres
.
# 05_rds.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Part 2 - Add a database with CloudFormation
Parameters:
AvailabilityZone:
Type: AWS::EC2::AvailabilityZone::Name
EnvironmentType:
Description: "Specify the Environment type of the stack."
Type: String
Default: dev
AllowedValues:
- dev
- test
- prod
AmiID:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Description: "The ID of the AMI."
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
KeyPairName:
Type: String
Description: The name of an existing Amazon EC2 key pair in this region to use to SSH into the Amazon EC2 instances.
DBInstanceIdentifier:
Type: String
Default: "webapp-db"
DBUsername:
NoEcho: "true"
Description: Username for Postgresql database access
Type: String
MinLength: "1"
MaxLength: "16"
AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
ConstraintDescription: Must begin with a letter and contain only alphanumeric characters.
Default: "postgres"
DBPassword:
NoEcho: "true"
Description: Password Postgresql database access
Type: String
MinLength: "8"
MaxLength: "41"
AllowedPattern: "[a-zA-Z0-9]*"
ConstraintDescription: Must contain only alphanumeric characters.
Mappings:
EnvironmentToInstanceType:
dev:
InstanceType: t2.nano
test:
InstanceType: t2.micro
prod:
InstanceType: t2.small
Resources:
WebAppInstance:
Type: AWS::EC2::Instance
Properties:
AvailabilityZone: !Ref AvailabilityZone
ImageId: !Ref AmiID
InstanceType:
!FindInMap [
EnvironmentToInstanceType,
!Ref EnvironmentType,
InstanceType,
]
KeyName: !Ref KeyPairName
SecurityGroupIds:
- !Ref WebAppSecurityGroup
WebAppSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Join ["-", [webapp-security-group, !Ref EnvironmentType]]
GroupDescription: "Allow HTTP/HTTPS and SSH inbound and outbound traffic"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
WebAppEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
InstanceId: !Ref WebAppInstance
Tags:
- Key: Name
Value: !Join ["-", [webapp-eip, !Ref EnvironmentType]]
WebAppDatabase:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: !Ref DBInstanceIdentifier
AllocatedStorage: "5"
DBInstanceClass: db.t3.micro
Engine: postgres
MasterUsername: !Ref DBUsername
MasterUserPassword: !Ref DBPassword
Tags:
- Key: Name
Value: !Join ["-", [webapp-rds, !Ref EnvironmentType]]
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Outputs:
WebsiteURL:
Value: !Sub http://${WebAppEIP}
Description: WebApp URL
WebServerPublicDNS:
Description: "Public DNS of EC2 instance"
Value: !GetAtt WebAppInstance.PublicDnsName
WebAppDatabaseEndpoint:
Description: "Connection endpoint for the database"
Value: !GetAtt WebAppDatabase.Endpoint.Address
In the template above, we've also added new parameters to customize the name of the database, the username, and password, as well as two new outputs to make our work easier: WebServerPublicDNS
and WebAppDatabasePublicDNS
. We'll use both of these outputs in step 2.
If you're creating your stack from scratch, you can run the create-stack
command:
$ aws cloudformation create-stack --stack-name rds-example --template-body file://05_rds.yaml \
--parameters ParameterKey=AvailabilityZone,ParameterValue=us-east-1a \
ParameterKey=EnvironmentType,ParameterValue=dev \
ParameterKey=KeyPairName,ParameterValue=jenna \
ParameterKey=DBPassword,ParameterValue=Abcd1234
Or, if you're updating the stack you created in part 1, you can use the update-stack
command instead.
Notice that we're only specifying the new DBPassword
parameter and relying on the default values for DBInstanceIdentifier
and DBUsername
that are specified in the template. If you'd like to customize these values, you can add them to the command in the same ParameterKey,ParameterValue
format shown above.
DeletionPolicy and UpdateReplacePolicy
You may have also noticed the DeletionPolicy
and UpdateReplacePolicy
properties set to Snapshot
. If and when you ever need to re-provision the database, you'll want to make sure you don't lose your precious data. Some template changes will require the resource to be recreated (as opposed to updated). When this happens, you'll want to be in control of how that happens and what happens to your data. These two properties give you that control. In the case of Snapshot
, CloudFormation will create a snapshot of the database when the stack is updated or deleted. You can read more about update and delete behaviors of stack resources here.
After creating or updating the stack, you'll now have your EC2 instance (and supporting resources from part 1) and an RDS database. Right now, no one can access that database instance from the outside world, so next we'll enable inbound traffic to the Postgresql port.
2. Enable Inbound Traffic on Port 5432
To allow inbound traffic on port 5432 so that our EC2 instance can talk to the RDS database, we'll add a security group with type AWS::EC2::SecurityGroup
.
# 06_rds.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Part 2 - Add a database with CloudFormation
Parameters:
AvailabilityZone:
Type: AWS::EC2::AvailabilityZone::Name
EnvironmentType:
Description: "Specify the Environment type of the stack."
Type: String
Default: dev
AllowedValues:
- dev
- test
- prod
AmiID:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Description: "The ID of the AMI."
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
KeyPairName:
Type: String
Description: The name of an existing Amazon EC2 key pair in this region to use to SSH into the Amazon EC2 instances.
DBInstanceIdentifier:
Type: String
Default: "webapp-db"
DBUsername:
NoEcho: "true"
Description: Username for Postgresql database access
Type: String
MinLength: "1"
MaxLength: "16"
AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
ConstraintDescription: Must begin with a letter and contain only alphanumeric characters.
Default: "postgres"
DBPassword:
NoEcho: "true"
Description: Password Postgresql database access
Type: String
MinLength: "8"
MaxLength: "41"
AllowedPattern: "[a-zA-Z0-9]*"
ConstraintDescription: Must contain only alphanumeric characters.
Mappings:
EnvironmentToInstanceType:
dev:
InstanceType: t2.nano
test:
InstanceType: t2.micro
prod:
InstanceType: t2.small
Resources:
WebAppInstance:
Type: AWS::EC2::Instance
Properties:
AvailabilityZone: !Ref AvailabilityZone
ImageId: !Ref AmiID
InstanceType:
!FindInMap [
EnvironmentToInstanceType,
!Ref EnvironmentType,
InstanceType,
]
KeyName: !Ref KeyPairName
SecurityGroupIds:
- !Ref WebAppSecurityGroup
WebAppSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Join ["-", [webapp-security-group, !Ref EnvironmentType]]
GroupDescription: "Allow HTTP/HTTPS and SSH inbound and outbound traffic"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
WebAppEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
InstanceId: !Ref WebAppInstance
Tags:
- Key: Name
Value: !Join ["-", [webapp-eip, !Ref EnvironmentType]]
WebAppDatabase:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: !Ref DBInstanceIdentifier
VPCSecurityGroups:
- !GetAtt DBEC2SecurityGroup.GroupId
AllocatedStorage: "5"
DBInstanceClass: db.t3.micro
Engine: postgres
MasterUsername: !Ref DBUsername
MasterUserPassword: !Ref DBPassword
Tags:
- Key: Name
Value: !Join ["-", [webapp-rds, !Ref EnvironmentType]]
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
DBEC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Join ["-", [webapp-db-security-group, !Ref EnvironmentType]]
GroupDescription: Allow postgres inbound traffic
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupName:
Ref: WebAppSecurityGroup
Tags:
- Key: Name
Value: !Join ["-", [webapp-db-security-group, !Ref EnvironmentType]]
Outputs:
WebsiteURL:
Value: !Sub http://${WebAppEIP}
Description: WebApp URL
WebServerPublicDNS:
Description: "Public DNS of EC2 instance"
Value: !GetAtt WebAppInstance.PublicDnsName
WebAppDatabaseEndpoint:
Description: "Connection endpoint for the database"
Value: !GetAtt WebAppDatabase.Endpoint.Address
In the template above, we've added the database security group and added it to the VPCSecurityGroups
property. We've also set SourceSecurityGroupName
to the WebAppSecurityGroup
we created earlier, to restrict the inbound traffic to that coming from the WebAppSecurityGroup
, or our EC2 instance.
To update your stack, you'll use the update-stack
command with the same parameters and values as before:
$ aws cloudformation update-stack --stack-name rds-example --template-body file://06_rds.yaml \
--parameters ParameterKey=AvailabilityZone,ParameterValue=us-east-1a \
ParameterKey=EnvironmentType,ParameterValue=dev \
ParameterKey=KeyPairName,ParameterValue=jenna \
ParameterKey=DBPassword,ParameterValue=Abcd1234
Now, you should have your EC2 instance (and supporting resources from part 1), an RDS database, and another security group exposing the database port. But how do we know the EC2 instance can talk to the database?
Testing the Security Group
To test your security group to make sure your EC2 instance can talk to the RDS database you provisioned in the last step, you can SSH into the instance and use the psql
client to connect to the database.
If you don't remember how to SSH into the instance, you can read more about that in part 1 or grab the WebServerPublicDNS
from the Outputs. Using the command below, replace the WebServerPublicDNS
and YOUR_KEY_PAIR_NAME
parts and SSH into the intstance:
$ ssh -i "YOUR_KEY_PAIR_NAME.pem" ec2-user@WebServerPublicDNS
Once you're in the EC2 instance, you'll need to enable the postgresql library from the Amazon Linux Extras repository and then install it. We could do this all in our CloudFormation template, but this is only for demonstration purposes, so we'll do it manually this one time. You can run these commands to do that:
sudo amazon-linux-extras list # Find the postgresql library and version to enable in the next command
sudo amazon-linux-extras enable postgresql13 -y
sudo yum clean metadata
sudo yum install postgresql
Now that psql
is available, you can test the connection to the database like this, replacing WebAppDatabaseEndpoint
with the corresponding value in the Outputs:
$ psql -h WebAppDatabaseEndpoint -U postgres -d postgres
Password for user postgres:
psql (13.2, server 12.5)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.
postgres=>
Congrats! You've successfully allowed your EC2 instance to talk to your RDS database.
If the connection isn't successful, check the CloudFormation console to make sure the RDS database and security group resources were created successfully. Additionally, you can check to make sure the database has the security group attached and the inbound rule opens up port 5432 (the default postgres port).
Wrapping Up
Delete Your Stack & Snapshots
Don’t forget to delete your stack so you don’t accrue charges. You can do that with the delete-stack
command:
$ aws cloudformation delete-stack --stack-name rds-example
If you left the DeletionPolicy
and UpdateReplacePolicy
properties set to snapshot and you no longer need those snapshots, then you can also delete those snapshots using the AWS Console so you don't accrue charges for those either.
Navigate to the RDS Management Console. From there, go to the Snapshots menu option. Select the snapshots created from your stack (hint: they will have a snapshot name that starts with your stack name) and select Delete snapshot from the Actions menu.
What You Learned
In this post, we updated the CloudFormation template from part 1 to provision an RDS database and enabled inbound traffic for the database. Now that both your EC2 instance and RDS database (and supporting resources) are all managed with code, you can setup and teardown the stack of resources together. This sets you up for part 3, where we'll create an AWS CodePipeline with CloudFormation (part 3) so we can build, test, and deploy our web app to the through each environment to production.
You can grab the final CloudFormation template we created here.
Like what you read? Follow me over on the Dev.to community or give me a follow on Twitter to stay updated!