Low hanging fruit… Cutting your RDS instance costs in half

Melvyn Mathews
6 min readJun 21, 2017

--

Here at CognitoIQ, the Cloud Engineering team is responsible for taking care of and building upon our AWS infrastructure, handle deployments, support developers, maintain Jenkins and most importantly, automate as much as humanly possible! In this article we’ll be discussing how to easily automate the stopping and starting of your RDS instances.

It was a dark, chilly morning here at Cognito when our CTO initiated a cost-cutting-crusade to try and cut our bill to a third of the cost!

“A third of the cost?!” we said.

How would we ever manage to save so much money while maintaining an ever-growing infrastructure? We needed some quick wins if we ever had the chance of saving so much money. The search for the Low hanging fruit began.

We applied aggressive resizing to our infrastructure and got rid of as much dead-weight as we could while maintaining system stability. Some of these changes included:

  • Converting from ELB’s to ALB’s. (Read more about this topic from our very own Leigh Hayward here)
  • Converting our developer environments from multi-AZ to single AZ.
  • Resizing our existing instances to use an EC2 class more appropriate to the work that were carrying out.
  • Resizing RDS.
  • Implementing Lambda functions to handle cleaning up old EBS volumes, RDS backups, notify us when instances are spinning among many more. However, in this article we’ll focus on implementing an automated lambda function to stop and start your RDS instances with ease!

It’s all too easy to forget how much something costs, especially when it’s your boss paying for it. During January 2017 we hit our peak RDS cost at just under $10k, a significant charge just for RDS! Something had to change. We started off by implementing a function to scale all of our instances down to the smallest possible size they could be while we didn’t use them. This helped to make quite a nice dent in our RDS bill. Even though we were making significant savings throughout the night compared to before, we were still spending nearly $2k per month on instances we just weren’t using throughout the night time.

5 months go by and then AWS makes the following announcement:

AWS RDS Supports Stopping and Starting of Database Instances

Great! We can now make further costs by changing our existing scale up/down function to stop/start the instances instead. This should allow us to save a further ~$1-2k.

Getting down to business

We’ll be using the following to implement this specific solution, although feel free to swap and change to anything you’re more comfortable with.

  • Python 3.6 with Boto3 (make sure to use a version newer than 01/06/17), but you can also use Node.js, C# and Java.
  • CloudWatch rules.
  • IAM.

The function we’ll be creating will take two inputs:

  • Instances: The RDS instances that we want to make a change to.
  • Action: Do we start or stop these instances

Once we have our input and action set by the user, we can build a program similar to below to perform our stop/start jobs.

Step 1.

Firstly, we’ll need to create a role to allow Lambda to describe and stop/start our RDS instances.

From IAM, select Policies and then select Create policy. This will take you to the Create Policy page, select Create Your Own Policy.

Give the policy an appropriate name, description and then copy in the following code:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1497951619924",
"Action": [
"rds:DescribeDBInstances",
"rds:StartDBInstance",
"rds:StopDBInstance"
],
"Effect": "Allow",
"Resource": "*"
}
]
}

Taking a look at the policy you should hopefully be able to work out that “rds:DescribeDBInstances”, “rds:StartDBInstance”, “rds:StopDBInstance” means that we can DescribeDBInstances, Start and Stop a DB instance. We’ll need these permissions for our Lambda function.

Validate your policy and then hit Create Policy.

After creating a policy, you’ll need to create a Role. From IAM, select Roles and then click Create a new role. Select AWS Lambda from the AWS Service Role list.

Find and attach your custom policy when on the Attach Policy page, press Next and then insert a role name, a description and press Create Role.

Now it’s time to build our Lambda function.

Go to Lambda > Create a Lambda function > Python 3.6 Runtime > Blank Function > Skip Triggers

Add a name and description.

Now it’s time to select the role that you made earlier.

Next > Create Function.

After creating the function, create a folder on your local machine and create an empty folder with a lambda_function.py inside of it. As of writing this article, the boto3 library supported by Lambda was older than 01/06/17, if you believe you will have the same issue you will have to package up the boto3 library alongside your lambda function. Execute the following line (on *nix systems cli) to install boto3 to local directory from within your lambda_function folder.

pip install boto3 -t .

Inside your lambda_function.py file, copy and paste the following:

import boto3
RDS = boto3.client('rds')
def lambda_handler(event, context):

# Check that our inputs are valid
try:
instances = event.get(’instances’)
action = event.get(’action’)
except Exception as e:
return "Exception! Failed with: {0}".format(e)

if (not (action == "stop" or action == "start")) or (not isinstance(instances, list)):
return "instances must be a list of strings, action must be \"Start\" or \"Stop\""

# Filter through our databases, only get the instances that are featured in our instances list
dbs = set([])
rds_instances = RDS.describe_db_instances()
for rds_instance in rds_instances[’DBInstances’]:
for instance in instances:
if instance in rds_instance[’DBInstanceIdentifier’]:
dbs.add(rds_instance[’DBInstanceIdentifier’])

# Apply our action
for db in dbs:
try:
if action == "start":
response = RDS.start_db_instance(DBInstanceIdentifier=db)
else:
response = RDS.stop_db_instance(DBInstanceIdentifier=db)

print("{0} status: {1}".format(db, response[’DBInstanceStatus’]))
except Exception as e:
print("Exception: {0}".format(e))

return "Completed!"

If this is your first time looking at Python, it may be worth taking a quick look into the basics of Python. If not, then you should be able to compare this code to our state-machine/flow-chart seen earlier in this article.

Once you’ve copied in the code, you’ll need to zip your lambda_function folder, you can do this with the zip command (*nix system) from within the lambda_function folder:

zip -r ../lambda_function.zip *

Go back to the Lambda console on AWS, select Upload a .ZIP file and upload the newly created lambda_function.zip file.

We now have a function that can take an input and stop or start RDS instances. To be able to automate this we can take advantage of CloudWatch rules.

Go to CloudWatch > Rules > Create Rule

Select Schedule > Cron expression, in the example below I’ve set my expression to 0 20 * * ? *, this will trigger this event every day at 8pm. You can change this to whichever will suit you best. To read up about this, check out: http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html

In the Targets section, select your Lambda function and then Configure Input and then select Constant (JSON text). Type in {“instances”: [“<instance-1>”, “<instance-2>”], “action”: “stop”}. In my example below, I’m setting “my-rds-instance” along with “my-rds-instance-2” to stop.

Press Configure Details > Add a name and description > Create

Now, do the same procedure again to set up a CloudWatch rule to start your RDS instances.

Voila! You now have the ability to start and stop your RDS instances as you wish.

--

--

Melvyn Mathews

CEO @ Developyn Ltd, Consulting Full Stack Developer and Solutions Architect @ IKEA