Serverless Lambda code for stop/start RDS instances by schedule – Part 2

Table of Content

In the first part of articles we prepare Terraform configuration for deploy, run and log our Lambda function by schedule. Here I will show how to interact with AWS API through Node.js.

Structure of code

├── package
    ├── index.js
    ├── rds-scheduler.js

Implementation

The code is scaffolding which help me create schedulers for another type of instances on AWS:

// index.js
/**
 * Main function entrypoint for lambda.
 *
 * Stop and start AWS resources:
 *  - rds instances
 *  - rds aurora clusters
 *  - instance ec2
 *
 *  Suspend and resume AWS resources:
 *  - ec2 autoscaling groups
 *
 * Terminate spot instances (spot instance cannot be stopped by a user).
 *
 * @param event
 * @param context
 * @param callback
 */
exports.handler = (event, context, callback) => {
  const strategy = {};

  // Retrieve  variables from aws lambda ENVIRONMENT
  const scheduleAction = process.env.SCHEDULE_ACTION;
  const awsRegions = process.env.AWS_REGIONS.replace(" ", "").split(",");
  const resourceTags = JSON.parse(process.env.RESOURCES_TAGS);

  //strategy['AutoscalingScheduler'] = process.env.AUTOSCALING_SCHEDULE;
  //strategy['SpotScheduler'] = process.env.SPOT_SCHEDULE;
  //strategy['InstanceScheduler'] = process.env.EC2_SCHEDULE;
  strategy['rds-scheduler'] = process.env.RDS_SCHEDULE;

  console.log(scheduleAction);
  console.log(awsRegions);
  console.log(resourceTags);
  console.log(strategy);

  for (const serviceName in strategy) {
    let toSchedule = strategy[serviceName];
    if (toSchedule === 'true') {
      for (const awsRegion of awsRegions) {
        let Service = require(serviceName);
        let service = new Service(awsRegion);
        service.run(scheduleAction, resourceTags, callback);
      }
    }
  }
};
// rds-scheduler.js
const AWS = require('aws-sdk');

class RdsScheduler {

  constructor(awsRegion) {
    this.awsRegion = awsRegion;
    this.rds = new AWS.RDS();
  }

  async run(action, resourceTags, callback) {
    if (!resourceTags) {
      throw new Error('Resource tags must be specified otherwise you will shoutdown all instances');
    }

    this.rds.describeDBInstances(async (rdsErr, rdsData) => {
      for (let i = 0; i < rdsData.DBInstances.length; i++) {
        let dbInstance = rdsData.DBInstances[i];
        let rdsTagParams = {
          ResourceName: dbInstance.DBInstanceArn
        };

        try {
          let tagData = await this.rds.listTagsForResource(rdsTagParams).promise();
          let tags = tagData.TagList || [];

          if (this.allowDispatch(tags, resourceTags)) {
            let data = await this[action](dbInstance.DBInstanceIdentifier);
            callback(null, data);

            console.log(`${action[0].toUpperCase()}${action.slice(1)} RDS instance ${dbInstance.DBInstanceIdentifier}`);
          }
        } catch (e) {
          callback(e, null);
          console.exception(e);
        }
      }
    })
  }

  /**
   * Stop RDS instance by identifier
   *
   * @param instanceIdentifier
   * @returns {Promise<*>}
   */
  async stop(instanceIdentifier) {
    let shutdownParams = {
      DBInstanceIdentifier: instanceIdentifier
    };

    return this.rds.stopDBInstance(shutdownParams).promise();
  }

  /**
   * Start RDS instance by identifier
   *
   * @param instanceIdentifier
   * @returns {Promise<*>}
   */
  async start(instanceIdentifier) {
    let startParams = {
      DBInstanceIdentifier: instanceIdentifier
    };

    return await this.rds.startDBInstance(startParams).promise();
  }

  /**
   * Filter and compare tags which we get from DBInstance (instanceTags)
   * with tags with we pass from configuration (resourceTags).
   *
   * DBInstance must include all tags which were passed from configuration,
   * otherwise action won't dispatch.
   *
   * @param instanceTags
   * @param resourceTags
   */
  allowDispatch(instanceTags, resourceTags) {
    let toDispatch = true;

    instanceTags.forEach((tag) => {
      resourceTags.forEach((resourceTag) => {
        if (tag.Key === resourceTag.Key) {
          toDispatch = (tag.Value === resourceTag.Value) ? (toDispatch && true) : (toDispatch && false)
        }
      });
    });

    return toDispatch;
  }
}

module.exports = RdsScheduler;

Future improvements:

  • Implement autoscaling-scheduler, spot-scheduler, instance-scheduler;
  • Allow create snapshot for RDS instance before stopping. How it do you can find in this article;
  • Allow stop/start RDS aurora clusters (mark)

Further reading

Leave a Reply

Your email address will not be published. Required fields are marked *