@unly/serverless-plugin-dynamodb-backups
Version:
Serverless plugin - Automatically creates DynamoDB backups
201 lines (169 loc) • 5.81 kB
JavaScript
const AWS = require('aws-sdk');
const filter = require('lodash.filter');
const moment = require('moment');
const axios = require('axios');
const Dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10' });
const {
SLACK_WEBHOOK,
REGION,
BACKUP_RETENTION_DAYS,
BACKUP_REMOVAL_ENABLED,
TABLE_REGEX,
} = process.env;
const CONSOLE_ENDPOINT = `https://console.aws.amazon.com/dynamodb/home?region=${REGION}#backups:`;
/**
*
* @param message
*/
const log = (message) => {
console.log('@unly/serverless-plugin-dynamodb-backups:', message);
};
/**
*
* @param promises
* @returns {Promise<{success: Array, failure: Array} | never | void>}
*/
const resolveAllPromise = (promises) => Promise.all(promises).then((res) => {
const success = filter(res, 'created');
const failure = filter(res, ['created', false]);
return { success, failure };
}).catch((err) => log(err));
/**
*
* @param TableName
* @returns {Promise<PromiseResult<D, E>>}
*/
const createBackup = (TableName) => {
const BackupName = TableName + moment().format('_YYYY_MM_DD_HH-mm-ss');
const params = {
BackupName,
TableName,
};
return Dynamodb.createBackup(params).promise();
};
/**
*
* @returns {Promise<*>}
*/
const getTablesToBackup = async () => {
// Default return all of the tables associated with the current AWS account and endpoint.
const data = await Dynamodb.listTables({}).promise();
// If TABLE_REGEX environment variable is provide, return tables that match;
return process.env.TABLE_REGEX ? filter(data.TableNames, (val) => new RegExp(TABLE_REGEX).test(val)) : data.TableNames;
};
/**
*
* @param TableName
* @returns {Promise<DynamoDB.BackupSummaries | Array>}
*/
const listBackups = async (TableName) => {
let list;
const TimeRangeUpperBound = moment().subtract(BACKUP_RETENTION_DAYS, 'days').toISOString();
const params = {
TableName,
BackupType: process.env.BACKUP_TYPE || 'ALL',
TimeRangeUpperBound,
};
try {
list = await Dynamodb.listBackups(params).promise();
} catch (err) {
log(`Error could not list backups on table ${TableName} \n ${JSON.stringify(err)}`);
}
return list.BackupSummaries || [];
};
/**
*
* @param tables
* @returns {Promise<{success: Array, failure: Array} | never | void>}
*/
const removeStaleBackup = async (tables) => {
log(`Removing backups before the following date: ${moment().subtract(BACKUP_RETENTION_DAYS, 'days').format('LL')}`);
const backupSummaries = tables.map((tableName) => listBackups(tableName));
const resolveBackupSummaries = await Promise.all(backupSummaries);
const deleteBackupsPromise = resolveBackupSummaries.map((backupLogs) => backupLogs.map((backup) => {
const params = {
BackupArn: backup.BackupArn,
};
return Dynamodb.deleteBackup(params).promise()
.then(({ BackupDescription }) => ({
name: BackupDescription.BackupDetails.BackupName,
deleted: true,
})).catch((err) => ({
deleted: false,
error: err,
}));
}));
const results = deleteBackupsPromise.reduce((acc, el) => acc.concat(el), []);
return resolveAllPromise(results);
};
/**
*
* @param results
* @param action
* @returns {string}
*/
const formatMessage = (results, action = 'create') => {
let msg = '';
const success = results.success.map((el) => JSON.stringify(el));
const failure = results.failure.map((el) => JSON.stringify(el));
if (!success.length && !failure.length) {
return '@unly/serverless-plugin-dynamodb-backups: Tried running DynamoDB backup, but no tables were specified.\nPlease check your configuration.';
}
if (failure.length) {
msg += '\n@unly/serverless-plugin-dynamodb-backups:\n';
msg += `\nThe following tables failed:\n - ${failure.join('\n - ')}`;
msg += `\n\nTried to ${action} ${success.length + failure.length} backups. ${success.length} succeeded, and ${failure.length} failed.
See all backups${`<${CONSOLE_ENDPOINT}|here>`}.`;
}
return msg;
};
/**
*
* @param message
* @returns {Promise}
*/
const sendToSlack = (message) => axios.post(SLACK_WEBHOOK, { text: message });
/**
*
* @param tables
* @returns {Promise<{success: Array, failure: Array} | never | void>}
*/
const tablesToBackup = async (tables) => {
const promises = tables.map(async (tableName) => createBackup(tableName).then(({ BackupDetails }) => ({
name: BackupDetails.BackupName,
created: true,
status: BackupDetails.BackupStatus,
})).catch((err) => ({
created: false,
error: err,
})));
return resolveAllPromise(promises);
};
const shouldSendMessage = (shouldSend, messages, action = 'create') => {
if (shouldSend) {
const messagesAsSend = formatMessage(messages, action);
return sendToSlack(messagesAsSend);
}
return null;
};
const dynamodbAutoBackups = async (event, context) => {
let removeStaleBackupResults = null;
let isRemoveStaleBackupFailed = false;
let isTablesToBackupResultsFailed = false;
const isBackupRemovalEnabled = BACKUP_REMOVAL_ENABLED === 'true';
try {
const tables = await getTablesToBackup();
if (isBackupRemovalEnabled) {
removeStaleBackupResults = await removeStaleBackup(tables);
isRemoveStaleBackupFailed = !!SLACK_WEBHOOK && removeStaleBackupResults !== null && removeStaleBackupResults.failure.length;
}
const tablesToBackupResults = await tablesToBackup(tables);
isTablesToBackupResultsFailed = !!SLACK_WEBHOOK && tablesToBackupResults.failure.length;
await shouldSendMessage(isTablesToBackupResultsFailed, tablesToBackupResults);
await shouldSendMessage(isRemoveStaleBackupFailed, removeStaleBackupResults, 'remove');
log(removeStaleBackupResults);
} catch (err) {
log(`Error: ${JSON.stringify(err)}`);
}
};
module.exports = dynamodbAutoBackups;