AWS CloudFormation : Using a Jump host to access an RDS database in a private subnet

Sudip Sarkar
11 min readApr 13, 2023

--

Hello guys !! Today we are going to learn about aws cloudformation and using that we also setup the mentioned architecture ; means we will create a mysql db instance in a private subnet and that db instance will be only accessible via a EC2 instance which is in public subnet.

First let’s have some basic knowledge about IaC and cloudformation.

What is CloudFormation ?

CloudFormation is a powerful Infrastructure as Code tool that can help automate and manage your aws deployments. Now the question is what Infrastructure as Code or IaC tool is.

Infrastructure as Code (IaC) is the managing and provisioning of infrastructure through code instead of through manual processes. Automating infrastructure provisioning with IaC means that devops don’t need to manually provision and manage servers, operating systems, storage, and other infrastructure components each time they deploy an application. Some popular IaC tools are Terraform, Saltstack, AWS CloudFormation etc.

CloudFormation templates can be created with YAML in addition to JSON. Or you can use AWS CloudFormation Designer to visually create your templates and see the interdependencies of your resources. CloudFormation takes a declarative approach to configuration, meaning that you tell it what you want your environment to look like, and it finds its way there.

During this configuration process, CloudFormation automatically manages dependencies between your resources. Thus, you don’t have to specify the order in which resources are created, updated, or deleted. CloudFormation automatically determines the correct sequence of actions to create your environment, though you can use the DependsOn attribute, wait condition handlers, and nested stacks to specify the order of operations, if necessary.

Sometimes updating an infrastructure stack can cause anxiety because you’re not sure what changes might break the environment. Not to fear! CloudFormation Change Sets allow you to preview how your resources will be impacted before any changes are executed. Only after you execute your change set will your stack be edited. Even if you execute a change set that has errors in it, CloudFormation has Rollback Triggers that allow you to monitor your stack creation or update process and roll back your environment to a previous state.

There will be a large yaml file to implement the whole architecture. But I will break that into small parts so that you can understand step by step what is happening. Here one thing to remember that there is no additional charge for using AWS CloudFormation. You only pay for those resources which are being created by cloudformation.

1.

First create .yaml file and there write a script to create your custom vpc. In ‘Resources’ you will define the resources and its configuration you want to create. Inside that logical id/resource name is being declared and inside that ‘Type’ is key and value is aws resource type. In ‘Properties’, the resource’s properties are being declared.

AWSTemplateFormatVersion: 2010-09-09
Description: Deploy a VPC with public/private subnets

Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 12.12.0.0/24
EnableDnsHostnames: true
Tags:
- Key: Name
Value: sudip-test-vpc

Now log into your aws account and go to cloudFormation service.

Select ‘Create Stack’

Here select Template is ready as I will use my own template. Template source can be S3 URL or upload your template file. I will upload my template file, then next.

Give your stack name. As there is no parameters for now select next.

There is nothing to do here. So select next. Also in the next page nothing to do and select submit.

After submitting you will see that your vpc has been started to create. In events you will see the in_progress, create and failed status. In resources you will see what resources has been created by your yaml file.

2.

Now you need to edit the script for creating subnets, route table and internet gateway.

  PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 12.12.0.0/26
AvailabilityZone: ap-northeast-1a
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: 1a-sudip-test-subnet

PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 12.12.0.64/26
AvailabilityZone: ap-northeast-1c
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: 1c-sudip-test-subnet

PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 12.12.0.128/26
AvailabilityZone: ap-northeast-1d
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: 1d-sudip-test-private-subnet1

PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 12.12.0.192/26
AvailabilityZone: ap-northeast-1c
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: 1c-sudip-test-private-subnet2

TestInternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: sudip-test-igw

AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref TestInternetGateway

PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: sudip-test-routetable

PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway

PublicSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable

PublicSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable

PrivateRouteTable1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: sudip-test-private-routetable1

PrivateSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable1

PrivateRouteTable2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: sudip-test-private-routetable2

PrivateSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable2

Here with this script four subnets will be created- two public and two private subnets. Resources names for subnets are ‘PublicSubnet1’, ‘PublicSubnet2’, ‘PrivateSubnet1’ and ‘PrivateSubnet2’.

Resource for internet gateway has been declared which is named as ‘TestInternetGateway’. Then it is being attached with the custom vpc. Resource for igw attachment with vpc is named as ‘AttachGateway’.

Three routetable has been created where one is public routetable and another two are private routetables. Their resources names are ‘PublicRouteTable’, ‘PrivateRouteTable1’ and ‘PrivateRouteTable2’.

Then in public routetable our internet gateway will be attached in route via resource ‘PublicRoute’. In public routetable two public subnets are associated via resources ‘PublicSubnetRouteTableAssociation1’ and ‘PublicSubnetRouteTableAssociation2’. Also in private routetables, private subnet1 and private subnet2 will be associated via recources ‘PrivateSubnetRouteTableAssociation1’ and ‘PrivateSubnetRouteTableAssociation2’.

3.

Now go to cloudformation tab and replace your existing yaml file. In your stack, from stack actions select the option ‘Create change set for current stack’. Then upload your updated yaml file.

Then after next and submit you will land into a page where you can see what changes will be deployed. When the status is showing CREATE_COMPLETE, then select Execute change set from top right corner. After that a confirmation page will be shown where you need to check on ‘Roll back all stack resources’. It means that if the deployment got failed then it will rolled back the stack to the last known stable state. Then select Execute change set.

4.

Change the yaml file for creating security groups.

  InstanceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow http to client host
VpcId: !Ref VPC
GroupName: sudip-test-ec2-sec
SecurityGroupIngress:

- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0

- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: x.x.x.x/32
Tags:
- Key: Name
Value: sudip-test-ec2-sec

PrivateSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow http to client host
VpcId: !Ref VPC
GroupName: sudip-test-private-sg
SecurityGroupIngress:

- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref InstanceSecurityGroup
Tags:
- Key: Name
Value: sudip-test-private-sg

Here I have defined two resources. ‘InstanceSecurityGroup’ for creating public/instance security group where port 80 is allowed for all and port 22/ssh is allowed for only your system’s public ip. ‘PrivateSecurityGroup’ is for creating private security group which will be used for db instance. Most importantly in private security group port for mysql 3306 has been added only for public security group’s id.

After replacing the yaml file in cloudformation, execute the changes.

5.

Now you need to write script for dbsubnet and dbinstance.

 
Parameters:

DBInstanceName:
Description: Enter your DB instance name
Type: String
MinLength: 3
MaxLength: 30
Default: DBInstance

DBInstanceClass:
Description: choose instance for db
Type: String
AllowedValues:
- db.t3.small
- db.t2.small
- db.t3.medium
Default: db.t2.small

DBStorage:
Description: enter storage in gb for db
Type: String

DBMasterUserName:
Description: enter your db username
Type: String

DBMasterPass:
Description: Enter db password
Type: String

DBName:
Description: Enter database name
Type: String


Resources:

DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupName: sudip-12apr-dbsubnet
DBSubnetGroupDescription: description
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Tags:
- Key: Name
Value: sudip-test-dbsubnet

DBInstance:
Type: AWS::RDS::DBInstance
DeletionPolicy: Delete
Properties:
DBName: !Ref DBName
DBInstanceIdentifier: !Ref DBInstanceName
MasterUsername: !Ref DBMasterUserName
MasterUserPassword: !Ref DBMasterPass
Engine: MySQL
EngineVersion: '8.0.32'
DBInstanceClass: !Ref DBInstanceClass
StorageType: gp2
AllocatedStorage: !Ref DBStorage
DBSubnetGroupName: !Ref DBSubnetGroup
PubliclyAccessible: False
MultiAZ: False
VPCSecurityGroups:
- !Ref PrivateSecurityGroup

Here two resources has been added. ‘DBSubnetGroup’ for creating dbsubnet group and ‘DBInstance’ for creating db instance.

Besides ‘Resources’ I have used ‘Parameters’ here. If you you want user input instead of hard coded in script, then you can use parameters. Here you can see that there are so many field which I have added in parameters section like DBInstanceName, DBInstanceClass, DBMasterUserName, DBMasterPass etc.

While executing the changes you need to enter all the details related to db instance like instance name, instance class, database name, username, password, storage. So again replace your yaml file in cloudformation and execute the change set. It will take some time to create the db instance.

6.

Now add resources to create EC2 instance which is our jump host.

Parameters:

InstanceTypeParam: # give instance type from user input
Description: Select instance type
Type: String
AllowedValues:
- dev
- prod
- qa
Default: prod

Mappings:
EnvMap: #logical id/name of mapping
dev:
instanceType: "t2.micro"
name: "dev"
prod:
instanceType: "t2.large"
name: "prod"
qa:
instanceType: "t2.medium"
name: "qa"

Resources:
EC2Instance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !FindInMap [EnvMap, !Ref InstanceTypeParam, instanceType]
SecurityGroupIds:
- !Ref InstanceSecurityGroup
SubnetId: !Ref PublicSubnet2

Tags:
- Key: "Name"
Value: !FindInMap [EnvMap, !Ref InstanceTypeParam, name]
UserData:
Fn::Base64:
!Sub |
#!/bin/bash
apt-get update -y
apt-get install mysql-server -y

Here Mappings has been used. Mappings is a separate section of the template like Parameters, Conditions, and Resources. It lets you map one value to another value that will be used in a template.

For EC2 instance a bash script has been added by which mysql-server will be preinstalled in your instance.

While executing your changes you will find three options for instance type. ‘dev’ for t2.micro, ‘qa’ for t2.medium and ‘prod’ for t2.large . Choose any one and execute the changes. You will see a instance with your desired type has been launched.

7.

Now ssh into the instance and check mysql-server is installed or not.

Then login into your mysql server using your database endpoint and check your database — # sudo mysql -h <endpoint> -P 3306 -u admin -p

You can check that only using this instance you can access your database.

So, the whole yaml file is given below :

AWSTemplateFormatVersion: 2010-09-09
Description: Deploy a VPC with public/private subnets


Parameters:

InstanceTypeParam:
Description: Select instance type
Type: String
AllowedValues:
- dev
- prod
- qa
Default: prod

DBInstanceName:
Description: Enter your DB instance name
Type: String
MinLength: 3
MaxLength: 30
Default: DBInstance

DBInstanceClass:
Description: choose instance for db
Type: String
AllowedValues:
- db.t3.small
- db.t2.small
- db.t3.medium
Default: db.t2.small

DBStorage:
Description: enter storage in gb for db
Type: String

DBMasterUserName:
Description: enter your db username
Type: String

DBMasterPass:
Description: Enter db password
Type: String

DBName:
Description: Enter database name
Type: String


Mappings:
EnvMap: #logical id/name of mapping
dev:
instanceType: "t2.micro"
name: "dev"
prod:
instanceType: "t2.large"
name: "prod"
qa:
instanceType: "t2.medium"
name: "qa"

Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 12.12.0.0/24
EnableDnsHostnames: true
Tags:
- Key: Name
Value: sudip-test-vpc


PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 12.12.0.0/26
AvailabilityZone: ap-northeast-1a
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: 1a-sudip-test-public-subnet1

PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 12.12.0.64/26
AvailabilityZone: ap-northeast-1c
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: 1c-sudip-test-public-subnet2

PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 12.12.0.128/26
AvailabilityZone: ap-northeast-1d
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: 1d-sudip-test-private-subnet1

PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 12.12.0.192/26
AvailabilityZone: ap-northeast-1c
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: 1c-sudip-test-private-subnet2

TestInternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: sudip-test-igw

AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref TestInternetGateway

PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: sudip-test-routetable

PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref TestInternetGateway

PublicSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable

PublicSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable

PrivateRouteTable1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: sudip-test-private-routetable1

PrivateSubnetRouteTableAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable1

PrivateRouteTable2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: sudip-test-private-routetable2

PrivateSubnetRouteTableAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable2

InstanceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow http to client host
VpcId: !Ref VPC
GroupName: sudip-test-ec2-sec
SecurityGroupIngress:

- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0

- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 65.2.88.212/32
Tags:
- Key: Name
Value: sudip-test-ec2-sec

PrivateSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow http to client host
VpcId: !Ref VPC
GroupName: sudip-test-private-sg
SecurityGroupIngress:

- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !Ref InstanceSecurityGroup
Tags:
- Key: Name
Value: sudip-test-private-sg


DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupName: sudip-12apr-dbsubnet
DBSubnetGroupDescription: description
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Tags:
- Key: Name
Value: sudip-12apr-dbsubnet

DBInstance:
Type: AWS::RDS::DBInstance
DeletionPolicy: Delete
Properties:
DBName: !Ref DBName
DBInstanceIdentifier: !Ref DBInstanceName
MasterUsername: !Ref DBMasterUserName
MasterUserPassword: !Ref DBMasterPass
Engine: MySQL
EngineVersion: '8.0.32'
DBInstanceClass: !Ref DBInstanceClass
StorageType: gp2
AllocatedStorage: !Ref DBStorage
DBSubnetGroupName: !Ref DBSubnetGroup
PubliclyAccessible: False
MultiAZ: False
VPCSecurityGroups:
- !Ref PrivateSecurityGroup

EC2Instance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !FindInMap [EnvMap, !Ref InstanceTypeParam, instanceType]
ImageId: ami-0d979355d03fa2522
KeyName: mykey
SecurityGroupIds:
- !Ref InstanceSecurityGroup
SubnetId: !Ref PublicSubnet2

Tags:
- Key: "Name"
Value: !FindInMap [EnvMap, !Ref InstanceTypeParam, name]
UserData:
Fn::Base64:
!Sub |
#!/bin/bash
apt-get update -y
apt-get install mysql-server -y


Outputs:
VPC:
Description: VPC
Value: !Ref VPC
AZ1:
Description: Availability Zone 1
Value: !GetAtt
- PublicSubnet1
- AvailabilityZone

--

--