You know that feeling ?, when you’re escaping a bad documentation, instead crawling around searching for a solution, and you find a snippet of code on Stack Overflow or Reddit, after you copy and paste it, it doesn’t work then your mind tells you “you need to change it a little bit”.
So you start changing the code to solve your problem, and guess what? A hell of a lot of new terminology and ideas enter your mind, and you start to get confused. Well, I Hamza Bou Issa am in that state of mind.
The last time I was left in “Deploying RDS with copilot using CloudFormation”, Yeah my approach to solving the problem is the same as before, typing a bunch of keywords and questions into Google, clicking on the first few links, if Stack Overflow then I detect responses with green mark and copy code, if it’s an article, I find snippets and copy the ones that have RDS or DB on them
I took the time to understand CloudFormation file structure and a few resource types
At first, I found this snippet
# Set AWS template version
AWSTemplateFormatVersion: "2010-09-09"
# Set Parameters
Parameters:
EngineVersion:
Description: PostgreSQL version.
Type: String
Default: "14.1"
SubnetIds:
Description: Subnets
Type: "List<AWS::EC2::Subnet::Id>"
VpcId:
Description: Insert your existing VPC id here
Type: String
Resources:
DBSubnetGroup:
Type: "AWS::RDS::DBSubnetGroup"
Properties:
DBSubnetGroupDescription: !Ref "AWS::StackName"
SubnetIds: !Ref SubnetIds
DatabaseSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: The Security Group for the database instance.
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
DBInstance:
Type: "AWS::RDS::DBInstance"
Properties:
AllocatedStorage: "30"
DBInstanceClass: db.t4g.medium
DBName: "postgres"
DBSubnetGroupName: !Ref DBSubnetGroup
Engine: postgres
EngineVersion: !Ref EngineVersion
MasterUsername: username
MasterUserPassword: password
StorageType: gp2
MonitoringInterval: 0
VPCSecurityGroups:
- !Ref DatabaseSecurityGroup
The following code will create 3 resources: DbInstance , DatabaseSecurityGroup , DBSubnetGroup , From my understanding the connection between those resources is a Database need to be created on private Subnets(DBSubnetGroup) on the other side for the database to accept connection it needs a security group( DatabaseSecurityGroup ) which should be on the same VPC as the Subnets
Now before I paste this code into environment/addons/rds.yml, I’m going to remove the parameters as we have an alternate method of passing the SubnetIds and VpcId.
CloudFormation gives the ability to import resources from previously created stacks with Fn::ImportValue
function. In this case, after I run copilot env deploy --name test
. Copilot create 2 CloudFormation stacks
The first stack is the interesting one, after we open on mycompany-app-test stack, we click on the Outputs panel, and it should show us the created resources with export names that can be imported on our RDS stack.
The two interesting export names are mycompany-app-test-PrivateSubnets , mycompany-app-test-VpcId , let’s refactor our rds.yml file and add them
# Set AWS template version
AWSTemplateFormatVersion: "2010-09-09"
# Set Parameters
Parameters:
App:
Type: String
Description: Your application's name.
Env:
Type: String
Description: The environment name your service, job, or workflow is being deployed
Name:
Type: String
Description: The name of the service, job, or workflow being deployed.
Resources:
DBSubnetGroup:
Type: "AWS::RDS::DBSubnetGroup"
Properties:
DBSubnetGroupDescription: !Ref "AWS::StackName"
SubnetIds:
!Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets'}]
DatabaseSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: The Security Group for the database instance.
VpcId:
Fn::ImportValue: !Sub '${App}-${Env}-VpcId'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: 0.0.0.0/0
DBInstance:
Type: "AWS::RDS::DBInstance"
Properties:
AllocatedStorage: "30"
DBInstanceClass: db.t4g.medium
DBName: "postgres"
DBSubnetGroupName: !Ref DBSubnetGroup
Engine: postgres
EngineVersion: "14.1"
MasterUsername: username
MasterUserPassword: password
StorageType: gp2
MonitoringInterval: 0
VPCSecurityGroups:
- !Ref DatabaseSecurityGroup
As you see, I removed the previous parameters and replace them with a few parameters App , Env , Name which is copilot required add-ons parameters. Also for SubnetIds I import PrivateSubnets and split it because it must be passed as separate values
After I run copilot env deploy --name test
a nested stack will get created
But how I’m going to test if the database is working or accepting connection while it’s not reachable on the public internet, well it seems I can create an ec2 instance on the public subnet and allow connection with the security group.
Here is the refactored code
# Set AWS template version
AWSTemplateFormatVersion: "2010-09-09"
# Set Parameters
Parameters:
App:
Type: String
Description: Your application's name.
Env:
Type: String
Description: The environment name your service, job, or workflow is being deployed to.
Resources:
DBSubnetGroup:
Type: "AWS::RDS::DBSubnetGroup"
Properties:
DBSubnetGroupDescription: !Ref "AWS::StackName"
SubnetIds:
!Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PrivateSubnets' }]
DatabaseSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: The Security Group for the database instance.
VpcId:
Fn::ImportValue:
!Sub '${App}-${Env}-VpcId'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: 0.0.0.0/0
DBInstance:
Type: "AWS::RDS::DBInstance"
Properties:
AllocatedStorage: "30"
DBInstanceClass: db.t4g.medium
DBName: "postgres"
DBSubnetGroupName: !Ref DBSubnetGroup
Engine: postgres
EngineVersion: "14.1"
MasterUsername: username
MasterUserPassword: password
StorageType: gp2
MonitoringInterval: 0
VPCSecurityGroups:
- !Ref DatabaseSecurityGroup
Tags:
- Key: Name
Value: !Sub 'copilot-${App}-${Env}'
NewKeyPair:
Type: 'AWS::EC2::KeyPair'
Properties:
KeyName: !Sub ${App}-${Env}-EC2-RDS-KEYPAIR
Tags:
- Key: Name
Value: !Sub 'copilot-${App}-${Env}'
EC2SecuityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: The Security Group for the ec2 instance.
VpcId:
Fn::ImportValue:
!Sub '${App}-${Env}-VpcId'
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub 'copilot-${App}-${Env}'
Ec2Instance:
Type: 'AWS::EC2::Instance'
Properties:
ImageId: ami-05e8e219ac7e82eba
InstanceType: t2.micro
KeyName: !Ref NewKeyPair
SubnetId: !Select ["0",!Split [',', { 'Fn::ImportValue': !Sub '${App}-${Env}-PublicSubnets' }]]
SecurityGroupIds:
- !Ref EC2SecuityGroup
UserData: |
#!/bin/bash
sudo apt update
sudo apt upgrade
sudo apt install postgresql postgresql-contrib
Tags:
- Key: Name
Value: !Sub 'copilot-${App}-${Env}'
Outputs:
ServerPublicDNS:
Description: "Public DNS of EC2 instance"
Value: !GetAtt Ec2Instance.PublicDnsName
DatabaseEndpoint:
Description: "Connection endpoint for the database"
Value: !GetAtt DBInstance.Endpoint.Address
A few things to notice, I added an Ec2Instance on a public subnet, a security group that allows only ssh port (22), meanwhile, the ImageId, InstanceType property are more tied to the region you’re deploying to, I am using eu-west-3(I’m not a French guy -_-). NewKeyPair is the ssh key to log into EC2, and finally the Outputs section for getting the EC2 instance and Database connection URL
I rerun copilot env deploy --name test
, wait for the stack to update, before connecting to ec2 an ssh file must be downloaded, the ssh key pair will be saved on AWS Parameter Store, here are the following steps to download it
aws ec2 describe-key-pairs --filters Name=key-name,Values=mycompany-app-test-E
The above command output.
key-05abb699beEXAMPLE
and to save the ssh key value
aws ssm get-parameter --name /ec2/keypair/key-05abb699beEXAMPLE --with-decrypt
Now, After I get the ssh file, try to connect to the server
ssh -i new-key-pair.pem ubuntu@ec2-host
I try to connect to RDS
psql -U username -d postgres -h database_host