├── .nvmrc ├── examples └── serverless-example │ ├── .nvmrc │ ├── src │ ├── backups.js │ └── listBackups.js │ ├── .babelrc │ ├── webpack.config.js │ ├── README.md │ ├── package.json │ └── serverless.yml ├── .github └── FUNDING.yml ├── .eslintignore ├── .codeclimate.yml ├── .idea ├── markdown-navigator │ └── profiles_settings.xml ├── encodings.xml ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── jsLibraryMappings.xml ├── misc.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── serverless-plugin-db-backups.iml ├── vcs.xml └── markdown-navigator.xml ├── .babelrc ├── .travis.yml ├── helpers └── iamRole.js ├── .eslintrc.yml ├── LICENSE ├── test ├── __mocks__ │ └── serverlessMock.js └── serverless-plugin-db-backups.test.js ├── package.json ├── CHANGELOG.md ├── .gitignore ├── lib └── index.js ├── serverless-plugin-db-backups.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.16.1 2 | -------------------------------------------------------------------------------- /examples/serverless-example/.nvmrc: -------------------------------------------------------------------------------- 1 | v14.16.1 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: unlyEd 2 | github: [UnlyEd, Vadorequest] 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | /build/** 3 | /coverage/** 4 | local-settings.js 5 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | plugins: 3 | eslint: 4 | enabled: true 5 | channel: "eslint-4" 6 | -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "8.10" 8 | } 9 | } 10 | ] 11 | ] 12 | } -------------------------------------------------------------------------------- /examples/serverless-example/src/backups.js: -------------------------------------------------------------------------------- 1 | import dynamodbAutoBackups from '@unly/serverless-plugin-dynamodb-backups/lib'; 2 | 3 | 4 | export const handler = dynamodbAutoBackups; 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /examples/serverless-example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "8.10" 8 | } 9 | } 10 | ] 11 | ] 12 | } -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | - "12" 5 | - "10" 6 | cache: 7 | directories: 8 | - node_modules 9 | install: 10 | - yarn install 11 | before_script: 12 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 13 | - chmod +x ./cc-test-reporter 14 | - ./cc-test-reporter before-build 15 | script: 16 | - yarn run test:once 17 | after_script: 18 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 19 | -------------------------------------------------------------------------------- /.idea/serverless-plugin-db-backups.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /helpers/iamRole.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | Effect: 'Allow', 4 | Action: 5 | [ 6 | 'dynamodb:ListTables', 7 | 'dynamodb:ListBackups', 8 | 'dynamodb:DeleteBackup', 9 | ], 10 | Resource: '*', 11 | }, 12 | { 13 | Effect: 'Allow', 14 | Action: ['dynamodb:CreateBackup'], 15 | Resource: { 16 | 'Fn::Join': [ 17 | ':', [ 18 | 'arn:aws:dynamodb', 19 | { Ref: 'AWS::Region' }, 20 | { Ref: 'AWS::AccountId' }, 21 | 'table/*', 22 | ], 23 | ], 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /examples/serverless-example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const slsw = require('serverless-webpack'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | entry: slsw.lib.entries, 6 | target: 'node', 7 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 8 | externals: [nodeExternals()], 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | include: __dirname, 15 | use: [ 16 | { 17 | loader: 'babel-loader', 18 | }, 19 | ], 20 | }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 2017 3 | extends: [ 'airbnb-base' ] 4 | rules: 5 | semi: [ "error", "always"] 6 | quotes: [ "error", "single"] 7 | comma-spacing: [ "error", { before: false, after: true }] 8 | indent: [ 'error', 2 ] 9 | arrow-parens: [ "error", "always"] 10 | max-len: [ 'warn', 160 ] 11 | strict: off 12 | no-console: off 13 | no-unused-vars: off 14 | import/prefer-default-export: false 15 | import/no-extraneous-dependencies: ["error", {"peerDependencies": true}] # XXX https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-extraneous-dependencies.md 16 | env: 17 | jest: true 18 | node: true 19 | es6: true 20 | 21 | -------------------------------------------------------------------------------- /examples/serverless-example/README.md: -------------------------------------------------------------------------------- 1 | # serverless-plugin-dynamodb-backups-serverless-example 2 | 3 | ## AWS Credentials with serverless 4 | 5 | If you already have a proper Serverless environment configured, then you can skip this. 6 | 7 | See https://serverless.com/framework/docs/providers/aws/guide/credentials/ 8 | 9 | ## Usage 10 | 11 | 1. Clone this repository 12 | 1. `yarn install` 13 | 1. Update the `serverless.yml` and look out for `TODO`, that's where you're likely to have things to change to match your serverless configuration 14 | 1. `yarn run deploy` 15 | 1. `yarn run logs:backups` will display the logs when a backup is made 16 | 1. `yarn run invoke:listBackups` will display the list of backups that have been made 17 | 18 | > Don't forget to `sls remove` your stack once you've done playing with it, or it's gonna make backups indefinitely! 19 | -------------------------------------------------------------------------------- /examples/serverless-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-plugin-db-examples", 3 | "version": "1.0.0", 4 | "description": "Usage example of the serverless-plugin-dynamodb-backups plugin", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "sls offline start", 8 | "deploy": "sls deploy", 9 | "invoke:listBackups": "sls invoke -f listBackups -l", 10 | "logs:backups": "sls logs -f example-dynamodbAutoBackups -t" 11 | }, 12 | "author": "unlyEd", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "aws-sdk": "2.918.0", 16 | "@babel/core": "7.14.3", 17 | "babel-loader": "8.2.2", 18 | "@babel/preset-env": "7.14.4", 19 | "serverless": "2.43.1", 20 | "serverless-offline": "7.0.0", 21 | "serverless-webpack": "5.5.0", 22 | "webpack": "5.38.1", 23 | "webpack-node-externals": "3.0.0" 24 | }, 25 | "dependencies": { 26 | "@unly/serverless-plugin-dynamodb-backups": "1.2.0-alpha.1", 27 | "lodash.filter": "4.6.0", 28 | "moment": "2.29.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/serverless-example/src/listBackups.js: -------------------------------------------------------------------------------- 1 | const filter = require('lodash.filter'); 2 | const moment = require('moment'); 3 | const AWS = require('aws-sdk'); 4 | 5 | /** 6 | * @param TableName 7 | * @returns {Promise<*|Array>} 8 | */ 9 | const listBackups = async (TableName) => { 10 | const Dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10' }); 11 | const TimeRangeLowerBound = moment().subtract(2, 'days').toISOString(); 12 | const params = { 13 | TableName, 14 | BackupType: 'USER', 15 | TimeRangeLowerBound, 16 | }; 17 | const data = await Dynamodb.listBackups(params).promise(); 18 | 19 | return data.BackupSummaries || []; 20 | }; 21 | 22 | export const handler = async (event, context) => { 23 | const data = await listBackups(process.env.TABLE_NAME); 24 | 25 | const backupsFiltered = filter(data, ['BackupType', 'USER']); 26 | 27 | return { 28 | statusCode: 200, 29 | body: JSON.stringify({ backupsFiltered }), 30 | headers: { 31 | 'Access-Control-Allow-Origin': '*', 32 | 'Access-Control-Allow-Credentials': true, 33 | }, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 UnlyEd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/__mocks__/serverlessMock.js: -------------------------------------------------------------------------------- 1 | const Serverless = require('serverless'); 2 | 3 | const scenarios = { 4 | classic: { 5 | dynamodbAutoBackups: { 6 | backupRate: 'rate(5 minutes)', 7 | source: 8 | 'src/backups.handler', 9 | }, 10 | }, 11 | empty: { 12 | dynamodbAutoBackups: {} 13 | , 14 | }, 15 | missed: { 16 | dynamodbAutoBackups: { 17 | source: 'src/backups.handler', 18 | backupRemovalEnabled: 19 | true, 20 | } 21 | , 22 | }, 23 | all: { 24 | dynamodbAutoBackups: { 25 | backupRate: 'rate(5 minutes)', 26 | source: 27 | 'src/backups.handler', 28 | backupRemovalEnabled: 29 | true, 30 | backupRetentionDays: 31 | 15, 32 | } 33 | , 34 | }, 35 | disabled: { 36 | dynamodbAutoBackups: { 37 | backupRate: 'rate(5 minutes)', 38 | source: 39 | 'src/backups.handler', 40 | backupRemovalEnabled: 41 | true, 42 | backupRetentionDays: 43 | 15, 44 | active: false, 45 | }, 46 | }, 47 | }; 48 | 49 | const serverless = (scenarioName) => { 50 | const sls = new Serverless(); 51 | sls.service.custom = scenarios[scenarioName]; 52 | return sls; 53 | }; 54 | 55 | module.exports = serverless; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unly/serverless-plugin-dynamodb-backups", 3 | "version": "2.0.0-alpha1", 4 | "description": "Serverless plugin - Automatically creates DynamoDB backups", 5 | "main": "serverless-plugin-db-backups.js", 6 | "repository": "https://github.com/UnlyEd/serverless-plugin-db-backups.git", 7 | "bugs": { 8 | "url": "https://github.com/UnlyEd/serverless-plugin-db-backups/issues" 9 | }, 10 | "homepage": "https://github.com/UnlyEd/serverless-plugin-db-backups", 11 | "keywords": [ 12 | "serverless", 13 | "framework", 14 | "plugin", 15 | "backups", 16 | "aws", 17 | "dynamodb" 18 | ], 19 | "jest": { 20 | "verbose": true, 21 | "testEnvironment": "node", 22 | "collectCoverage": true, 23 | "coverageReporters": [ 24 | "lcov" 25 | ] 26 | }, 27 | "scripts": { 28 | "lint": "eslint . --cache --fix", 29 | "test:once": "jest", 30 | "test": "jest --watch", 31 | "coverage": "jest --coverage", 32 | "release:alpha": "npm publish --tag alpha --access=public", 33 | "release:latest": "npm publish --tag latest --access=public" 34 | }, 35 | "author": "unlyEd", 36 | "license": "MIT", 37 | "dependencies": { 38 | "axios": "0.21.1", 39 | "bluebird": "3.5.3", 40 | "chalk": "2.4.1", 41 | "lodash.assign": "4.2.0", 42 | "lodash.clone": "4.5.0", 43 | "lodash.filter": "4.6.0", 44 | "lodash.has": "4.5.2", 45 | "lodash.isequal": "4.5.0", 46 | "lodash.isplainobject": "4.0.6", 47 | "lodash.isstring": "4.0.1", 48 | "lodash.set": "4.3.2", 49 | "lodash.uniqwith": "4.5.0", 50 | "moment": "2.22.2", 51 | "semver": "5.6.0" 52 | }, 53 | "devDependencies": { 54 | "aws-sdk": "2.352.0", 55 | "@babel/cli": "7.14.3", 56 | "@babel/core": "7.14.3", 57 | "babel-preset-env": "1.7.0", 58 | "eslint": "4.19.1", 59 | "eslint-config-airbnb-base": "13.1.0", 60 | "eslint-plugin-import": "2.14", 61 | "jest": "27.0.3", 62 | "serverless": "2.43.1" 63 | }, 64 | "peerDependencies": { 65 | "aws-sdk": ">= 2.x.x" 66 | }, 67 | "files": [ 68 | "helpers", 69 | "serverless-plugin-db-backups", 70 | "lib", 71 | "README" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/tree/HEAD) 4 | 5 | [Full Changelog](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/compare/v1.1.1...HEAD) 6 | 7 | **Merged pull requests:** 8 | 9 | - V1.1.1 issue variables [\#4](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/pull/4) ([Vadorequest](https://github.com/Vadorequest)) 10 | 11 | ## [v1.1.1](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/tree/v1.1.1) (2018-12-14) 12 | [Full Changelog](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/compare/v1.1.0...v1.1.1) 13 | 14 | **Merged pull requests:** 15 | 16 | - V1.1.0 toogle active plugin option [\#3](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/pull/3) ([Fukoyamashisu](https://github.com/Fukoyamashisu)) 17 | 18 | ## [v1.1.0](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/tree/v1.1.0) (2018-12-11) 19 | [Full Changelog](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/compare/v1.0.1...v1.1.0) 20 | 21 | **Merged pull requests:** 22 | 23 | - Releasev1.0.1 [\#2](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/pull/2) ([Fukoyamashisu](https://github.com/Fukoyamashisu)) 24 | 25 | ## [v1.0.1](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/tree/v1.0.1) (2018-12-06) 26 | [Full Changelog](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/compare/v1.0.0...v1.0.1) 27 | 28 | **Merged pull requests:** 29 | 30 | - V1 review [\#1](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/pull/1) ([Vadorequest](https://github.com/Vadorequest)) 31 | 32 | ## [v1.0.0](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/tree/v1.0.0) (2018-12-05) 33 | [Full Changelog](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/compare/v1.0.0-alpha.7...v1.0.0) 34 | 35 | ## [v1.0.0-alpha.7](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/tree/v1.0.0-alpha.7) (2018-12-05) 36 | [Full Changelog](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/compare/v1.0.0-alpha.1...v1.0.0-alpha.7) 37 | 38 | ## [v1.0.0-alpha.1](https://github.com/UnlyEd/serverless-plugin-dynamodb-backups/tree/v1.0.0-alpha.1) (2018-12-05) 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/webstorm 2 | 3 | ### WebStorm ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | # .idea/modules.xml 32 | # .idea/*.iml 33 | # .idea/modules 34 | 35 | # CMake 36 | cmake-build-*/ 37 | 38 | # Mongo Explorer plugin 39 | .idea/**/mongoSettings.xml 40 | 41 | # File-based project format 42 | *.iws 43 | 44 | # IntelliJ 45 | out/ 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Cursive Clojure plugin 54 | .idea/replstate.xml 55 | 56 | # Crashlytics plugin (for Android Studio and IntelliJ) 57 | com_crashlytics_export_strings.xml 58 | crashlytics.properties 59 | crashlytics-build.properties 60 | fabric.properties 61 | 62 | # Editor-based Rest Client 63 | .idea/httpRequests 64 | 65 | ### WebStorm Patch ### 66 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 67 | 68 | # *.iml 69 | # modules.xml 70 | # .idea/misc.xml 71 | # *.ipr 72 | 73 | # Sonarlint plugin 74 | .idea/sonarlint 75 | 76 | 77 | # End of https://www.gitignore.io/api/webstorm 78 | 79 | ######################### CUSTOM/MANUAL ############################# 80 | 81 | # See https://help.github.com/ignore-files/ for more about ignoring files. 82 | 83 | # package directories 84 | node_modules 85 | jspm_packages 86 | 87 | # Forgotten tmp files 88 | yarn-error.log 89 | 90 | # Serverless directories 91 | .serverless 92 | .webpack 93 | .next 94 | dist 95 | .DS_Store 96 | .sls-simulate-registry 97 | coverage 98 | 99 | # Optional eslint cache 100 | .eslintcache 101 | 102 | local-settings.js 103 | -------------------------------------------------------------------------------- /examples/serverless-example/serverless.yml: -------------------------------------------------------------------------------- 1 | # For full config options, check the docs: 2 | # docs.serverless.com 3 | 4 | service: example-dynamodbAutoBackups 5 | 6 | frameworkVersion: "2" 7 | configValidationMode: error 8 | 9 | plugins: 10 | - '@unly/serverless-plugin-dynamodb-backups' # Must be first, even before "serverless-webpack", see https://github.com/UnlyEd/serverless-plugin-dynamodb-backups 11 | - serverless-webpack # Must be second, see https://github.com/99xt/serverless-dynamodb-local#using-with-serverless-offline-and-serverless-webpack-plugin 12 | - serverless-offline # See https://github.com/dherault/serverless-offline 13 | 14 | custom: 15 | dynamodbAutoBackups: # @unly/serverless-plugin-dynamodb-backups configuration (see README for more) 16 | backupRate: rate(1 minute) # Set to 1mn in the example to see the result quickly 17 | source: src/backups.handler 18 | name: ${self:custom.name} # Using the service name as a base name may be a good practice (but it doesn't work at the time, due to a bug on our side) 19 | backupRemovalEnabled: true 20 | backupRetentionDays: 1 # Created backups will be removed the next day 21 | name: example-dynamodbAutoBackups 22 | serverless-offline: 23 | port: 3000 24 | webpack: 25 | webpackConfig: ./webpack.config.js 26 | includeModules: true 27 | packager: yarn 28 | 29 | provider: 30 | name: aws 31 | runtime: nodejs14.x 32 | lambdaHashingVersion: 20201221 33 | stage: development # TODO You may want to change this 34 | region: eu-west-1 # TODO You may want to change this 35 | profile: sandbox # TODO You need to either remove this or use your own profile 36 | environment: 37 | TABLE_NAME: Book # We specify the table name so we can check its backups list using the dedicated endpoint /listBackups 38 | 39 | functions: 40 | listBackups: # Endpoint example to see the list of all backups that have been made 41 | handler: src/listBackups.handler 42 | events: 43 | - http: 44 | method: GET 45 | path: /listBackups 46 | 47 | resources: 48 | Resources: 49 | BookTable: # Create dynamodb table on aws for testing 50 | Type: AWS::DynamoDB::Table # see https://docs.aws.amazon.com/fr_fr/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html 51 | Properties: 52 | TableName: Book 53 | AttributeDefinitions: 54 | - AttributeName: id 55 | AttributeType: S 56 | KeySchema: 57 | - AttributeName: id 58 | KeyType: HASH 59 | ProvisionedThroughput: 60 | ReadCapacityUnits: 1 61 | WriteCapacityUnits: 1 62 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 103 | -------------------------------------------------------------------------------- /test/serverless-plugin-db-backups.test.js: -------------------------------------------------------------------------------- 1 | const serverless = require('./__mocks__/serverlessMock'); 2 | const DynamodbBackups = require('../serverless-plugin-db-backups'); 3 | 4 | describe('@unly/serverless-plugin-dynamodb-backups init', () => { 5 | global.console = { 6 | warn: jest.fn(), 7 | log: jest.fn(), 8 | error: jest.fn(), 9 | }; 10 | let slsPlugin; 11 | 12 | test('check if serverless version is >= 1.12', () => { 13 | slsPlugin = new DynamodbBackups(serverless('classic')); 14 | try { 15 | slsPlugin.serverless.version = '1.9.0'; 16 | slsPlugin.validate(); 17 | } catch (err) { 18 | expect(err.message).toEqual('Serverless version must be >= 1.12.0'); 19 | } 20 | }); 21 | 22 | test('custom config with no key source should throw error', () => { 23 | slsPlugin = new DynamodbBackups(serverless('empty')); 24 | try { 25 | slsPlugin.checkConfigPlugin(); 26 | } catch (err) { 27 | expect(err.message).toEqual('dynamodbAutoBackups source must be set !'); 28 | } 29 | }); 30 | 31 | test('custom config with no key backupRate should throw error', () => { 32 | slsPlugin = new DynamodbBackups(serverless('missed')); 33 | try { 34 | slsPlugin.checkConfigPlugin(); 35 | } catch (err) { 36 | expect(err.message).toEqual('dynamodbAutoBackups backupRate must be set !'); 37 | } 38 | }); 39 | 40 | test('custom config with key backupRemovalEnabled === true and no key backupRetentionDays should throw error', () => { 41 | try { 42 | slsPlugin.checkConfigPlugin(); 43 | } catch (err) { 44 | expect(err.message).toEqual('if backupRemovalEnabled, backupRetentionDays must be set !'); 45 | } 46 | }); 47 | 48 | test('should provide function dynamodbAutoBackups', () => { 49 | slsPlugin = new DynamodbBackups(serverless('all')); 50 | 51 | const afterPackageInit = slsPlugin.hooks['after:package:initialize']; 52 | 53 | afterPackageInit().then(() => { 54 | expect(slsPlugin.functionBackup).toHaveProperty('name'); 55 | expect(slsPlugin.functionBackup).toHaveProperty('events'); 56 | expect(slsPlugin.functionBackup).toHaveProperty('handler'); 57 | expect(slsPlugin.functionBackup).toHaveProperty('environment'); 58 | expect(slsPlugin.serverless.service.provider.iamRoleStatements.length).toEqual(2); 59 | expect(slsPlugin.serverless.service.functions.dynamodbAutoBackups).toMatchObject(slsPlugin.functionBackup); 60 | }); 61 | }); 62 | 63 | test('if iamRoleStatements in serverles should merge it', () => { 64 | slsPlugin = new DynamodbBackups(serverless('all')); 65 | slsPlugin.serverless.service.provider.iamRoleStatements = [ 66 | { 67 | Effect: 'Allow', 68 | Action: 69 | [ 70 | 'dynamodb:ListTables', 71 | 'dynamodb:ListBackups', 72 | 'dynamodb:DeleteBackup', 73 | ], 74 | Resource: '*', 75 | }, 76 | ]; 77 | 78 | const afterPackageInit = slsPlugin.hooks['after:package:initialize']; 79 | 80 | afterPackageInit().then(() => { 81 | expect(slsPlugin.functionBackup).toHaveProperty('name'); 82 | expect(slsPlugin.functionBackup).toHaveProperty('events'); 83 | expect(slsPlugin.functionBackup).toHaveProperty('handler'); 84 | expect(slsPlugin.functionBackup).toHaveProperty('environment'); 85 | expect(slsPlugin.serverless.service.provider.iamRoleStatements.length).toEqual(3); 86 | expect(global.console.log).toHaveBeenCalled(); 87 | }); 88 | }); 89 | 90 | test('if iamRoleStatements in serverles should merge it', () => { 91 | slsPlugin = new DynamodbBackups(serverless('all')); 92 | slsPlugin.serverless.service.provider.iamRoleStatements = [ 93 | { 94 | Effect: 'Allow', 95 | Action: 96 | [ 97 | 'dynamodb:ListTables', 98 | 'dynamodb:ListBackups', 99 | 'dynamodb:DeleteBackup', 100 | ], 101 | Resource: '*', 102 | }, 103 | ]; 104 | 105 | const afterPackageInit = slsPlugin.hooks['after:package:initialize']; 106 | 107 | afterPackageInit().then(() => { 108 | expect(slsPlugin.functionBackup).toHaveProperty('name'); 109 | expect(slsPlugin.functionBackup).toHaveProperty('events'); 110 | expect(slsPlugin.functionBackup).toHaveProperty('handler'); 111 | expect(slsPlugin.functionBackup).toHaveProperty('environment'); 112 | expect(slsPlugin.serverless.service.provider.iamRoleStatements.length).toEqual(3); 113 | expect(global.console.log).toHaveBeenCalled(); 114 | }); 115 | }); 116 | 117 | test('should disabled plugin and not provide function dynamodbAutoBackups', () => { 118 | slsPlugin = new DynamodbBackups(serverless('disabled')); 119 | 120 | const afterPackageInit = slsPlugin.hooks['after:package:initialize']; 121 | 122 | afterPackageInit().then(() => { 123 | expect(slsPlugin.serverless.service.functions.dynamodbAutoBackups).toBeUndefined(); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const filter = require('lodash.filter'); 3 | const moment = require('moment'); 4 | const axios = require('axios'); 5 | 6 | const Dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10' }); 7 | const { 8 | SLACK_WEBHOOK, 9 | REGION, 10 | BACKUP_RETENTION_DAYS, 11 | BACKUP_REMOVAL_ENABLED, 12 | TABLE_REGEX, 13 | } = process.env; 14 | const CONSOLE_ENDPOINT = `https://console.aws.amazon.com/dynamodb/home?region=${REGION}#backups:`; 15 | 16 | /** 17 | * 18 | * @param message 19 | */ 20 | const log = (message) => { 21 | console.log('@unly/serverless-plugin-dynamodb-backups:', message); 22 | }; 23 | 24 | /** 25 | * 26 | * @param TableName 27 | * @returns {Promise>} 28 | */ 29 | const createBackup = (TableName) => { 30 | const BackupName = TableName + moment().format('_YYYY_MM_DD_HH-mm-ss'); 31 | const params = { 32 | BackupName, 33 | TableName, 34 | }; 35 | return Dynamodb.createBackup(params).promise(); 36 | }; 37 | 38 | /** 39 | * 40 | * @returns {Promise<*>} 41 | */ 42 | const getTablesToBackup = async () => { 43 | // Default return all of the tables associated with the current AWS account and endpoint. 44 | const data = await Dynamodb.listTables({}).promise(); 45 | 46 | // If TABLE_REGEX environment variable is provide, return tables that match; 47 | return process.env.TABLE_REGEX ? filter(data.TableNames, (val) => new RegExp(TABLE_REGEX).test(val)) : data.TableNames; 48 | }; 49 | 50 | /** 51 | * 52 | * @param TableName 53 | * @returns {Promise} 54 | */ 55 | const listBackups = async (TableName) => { 56 | let list; 57 | const TimeRangeUpperBound = moment().subtract(BACKUP_RETENTION_DAYS, 'days').toISOString(); 58 | const params = { 59 | TableName, 60 | BackupType: process.env.BACKUP_TYPE || 'ALL', 61 | TimeRangeUpperBound, 62 | }; 63 | 64 | try { 65 | list = await Dynamodb.listBackups(params).promise(); 66 | } catch (err) { 67 | log(`Error could not list backups on table ${TableName} \n ${JSON.stringify(err)}`); 68 | } 69 | 70 | return list.BackupSummaries || []; 71 | }; 72 | 73 | /** 74 | * 75 | * @param tables 76 | * @returns {Promise<{success: Array, failure: Array} | never | void>} 77 | */ 78 | const removeStaleBackup = async (tables) => { 79 | log(`Removing backups before the following date: ${moment().subtract(BACKUP_RETENTION_DAYS, 'days').format('LL')}`); 80 | 81 | const backupSummaries = tables.map((tableName) => listBackups(tableName)); 82 | 83 | const resolveBackupSummaries = await Promise.all(backupSummaries); 84 | 85 | const deleteBackupsPromise = resolveBackupSummaries.map((backupLogs) => backupLogs.map((backup) => { 86 | const params = { 87 | BackupArn: backup.BackupArn, 88 | }; 89 | 90 | return Dynamodb.deleteBackup(params).promise() 91 | .then(({ BackupDescription }) => ({ 92 | name: BackupDescription.BackupDetails.BackupName, 93 | deleted: true, 94 | })).catch((err) => ({ 95 | deleted: false, 96 | error: err, 97 | })); 98 | })); 99 | 100 | const results = deleteBackupsPromise.reduce((acc, el) => acc.concat(el), []); 101 | 102 | return Promise.all(results).then((res) => { 103 | const success = filter(res, 'deleted'); 104 | const failure = filter(res, ['deleted', false]); 105 | 106 | return { success, failure }; 107 | }).catch((err) => log(err)); 108 | }; 109 | 110 | /** 111 | * 112 | * @param results 113 | * @param action 114 | * @returns {string} 115 | */ 116 | const formatMessage = (results, action = 'create') => { 117 | let msg = ''; 118 | const success = results.success.map((el) => JSON.stringify(el)); 119 | const failure = results.failure.map((el) => JSON.stringify(el)); 120 | 121 | if (!success.length && !failure.length) { 122 | return '@unly/serverless-plugin-dynamodb-backups: Tried running DynamoDB backup, but no tables were specified.\nPlease check your configuration.'; 123 | } 124 | 125 | if (failure.length) { 126 | msg += '\n@unly/serverless-plugin-dynamodb-backups:\n'; 127 | msg += `\nThe following tables failed:\n - ${failure.join('\n - ')}`; 128 | msg += `\n\nTried to ${action} ${success.length + failure.length} backups. ${success.length} succeeded, and ${failure.length} failed. 129 | See all backups${`<${CONSOLE_ENDPOINT}|here>`}.`; 130 | } 131 | 132 | return msg; 133 | }; 134 | 135 | /** 136 | * 137 | * @param message 138 | * @returns {Promise} 139 | */ 140 | const sendToSlack = (message) => axios.post(SLACK_WEBHOOK, { text: message }); 141 | 142 | /** 143 | * 144 | * @param tables 145 | * @returns {Promise<{success: Array, failure: Array} | never | void>} 146 | */ 147 | const tablesToBackup = async (tables) => { 148 | const promises = tables.map(async (tableName) => createBackup(tableName).then(({ BackupDetails }) => ({ 149 | name: BackupDetails.BackupName, 150 | created: true, 151 | status: BackupDetails.BackupStatus, 152 | })).catch((err) => ({ 153 | created: false, 154 | error: err, 155 | }))); 156 | return Promise.all(promises).then((res) => { 157 | const success = filter(res, 'created'); 158 | const failure = filter(res, ['created', false]); 159 | 160 | return { success, failure }; 161 | }).catch((err) => log(err)); 162 | }; 163 | 164 | const dynamodbAutoBackups = async (event, context) => { 165 | let removeStaleBackupResults = null; 166 | 167 | try { 168 | const tables = await getTablesToBackup(); 169 | 170 | if (BACKUP_REMOVAL_ENABLED === 'true') { 171 | try { 172 | removeStaleBackupResults = await removeStaleBackup(tables); 173 | console.log(removeStaleBackupResults); 174 | } catch (err) { 175 | log(`Error removing stale backups. Error: ${JSON.stringify(err)}`); 176 | } 177 | } 178 | 179 | const tablesToBackupResults = await tablesToBackup(tables); 180 | 181 | if (SLACK_WEBHOOK && tablesToBackupResults.failure.length) { 182 | const message = formatMessage(tablesToBackupResults); 183 | await sendToSlack(message); 184 | } else if (SLACK_WEBHOOK && removeStaleBackupResults !== null) { 185 | if (removeStaleBackupResults.failure.length) { 186 | const message = formatMessage(removeStaleBackupResults, 'remove'); 187 | await sendToSlack(message); 188 | } 189 | } 190 | 191 | log(tablesToBackupResults); 192 | 193 | if (BACKUP_REMOVAL_ENABLED === 'true' && removeStaleBackupResults !== null) { 194 | log(removeStaleBackupResults); 195 | } 196 | } catch (err) { 197 | log(err); 198 | } 199 | }; 200 | 201 | module.exports = dynamodbAutoBackups; 202 | -------------------------------------------------------------------------------- /serverless-plugin-db-backups.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isString = require('lodash.isstring'); 4 | const clone = require('lodash.clone'); 5 | const has = require('lodash.has'); 6 | const isPlainObject = require('lodash.isplainobject'); 7 | const set = require('lodash.set'); 8 | const assign = require('lodash.assign'); 9 | const uniqWith = require('lodash.uniqwith'); 10 | const isEqual = require('lodash.isequal'); 11 | const BbPromise = require('bluebird'); 12 | const SemVer = require('semver'); 13 | const chalk = require('chalk'); 14 | 15 | const hooks = [ 16 | 'after:package:initialize', 17 | 'before:deploy:deploy', 18 | 'before:invoke:local:invoke', 19 | 'before:offline:start:init', 20 | 'before:remove:remove', 21 | 'before:logs:logs', 22 | ]; 23 | 24 | // iamRole Helpers 25 | const iamRoleStatements = require('./helpers/iamRole'); 26 | 27 | /** 28 | * 29 | */ 30 | class DynamodbAutoBackup { 31 | constructor(serverless) { 32 | this.serverless = serverless; 33 | 34 | this.custom = this.serverless.service.custom; 35 | 36 | this.dynamodbAutoBackups = {}; 37 | 38 | this.isInstantiate = false; 39 | 40 | this.chainPromises = () => BbPromise.bind(this) 41 | .then(this.validate) 42 | .then(this.checkConfigPlugin) 43 | .then(this.populateEnv) 44 | .then(this.setCronEvent) 45 | .then(this.generateBackupFunction) 46 | .then(this.manageIamRole); 47 | 48 | this.hooks = hooks.reduce((initialValue, hook) => assign(initialValue, { [hook]: () => this.init() }), {}); 49 | } 50 | 51 | /** 52 | * Validate dynamodbAutoBackups options 53 | * @returns {Promise.resolve} 54 | */ 55 | checkConfigPlugin() { 56 | if (!this.dynamodbAutoBackups.source) { 57 | return BbPromise.reject(new this.serverless.classes.Error('dynamodbAutoBackups source must be set !')); 58 | } 59 | 60 | if (!this.dynamodbAutoBackups.backupRate) { 61 | return BbPromise.reject(new this.serverless.classes.Error('dynamodbAutoBackups backupRate must be set !')); 62 | } 63 | 64 | if (has(this.dynamodbAutoBackups, 'backupRemovalEnabled') && !has(this.dynamodbAutoBackups, 'backupRetentionDays')) { 65 | return BbPromise.reject( 66 | new this.serverless.classes.Error('if backupRemovalEnabled, backupRetentionDays must be set !'), 67 | ); 68 | } 69 | 70 | if (!has(this.dynamodbAutoBackups, 'slackWebhook') && this.dynamodbAutoBackups.active) { 71 | DynamodbAutoBackup.consoleLog('@unly/serverless-plugin-dynamodb-backups: -----------------------------------------------------------'); 72 | DynamodbAutoBackup.consoleLog(' Warning: slackWebhook is not provide, you will not be notified of errors !'); 73 | console.log(); 74 | } 75 | 76 | this.functionBackup = { 77 | name: this.dynamodbAutoBackups.name || 'dynamodbAutoBackups', 78 | handler: this.dynamodbAutoBackups.source, 79 | events: [], 80 | environment: {}, 81 | }; 82 | 83 | return BbPromise.resolve(); 84 | } 85 | 86 | /** 87 | * 88 | * check the compatibility of serverless version 89 | * @returns {Promise.resolve} 90 | */ 91 | validate() { 92 | // Check required serverless version 93 | if (SemVer.gt('1.12.0', this.serverless.getVersion())) { 94 | return BbPromise.reject(new this.serverless.classes.Error('Serverless version must be >= 1.12.0')); 95 | } 96 | 97 | return BbPromise.resolve(); 98 | } 99 | 100 | /** 101 | * add the schedule event 102 | * @returns {Promise.resolve} 103 | */ 104 | setCronEvent() { 105 | const events = []; 106 | 107 | if (isString(this.dynamodbAutoBackups.backupRate)) { 108 | const cron = { 109 | schedule: this.dynamodbAutoBackups.backupRate, 110 | }; 111 | events.push(cron); 112 | } 113 | 114 | console.log(this.functionBackup); 115 | this.functionBackup.events = events; 116 | 117 | return BbPromise.resolve(); 118 | } 119 | 120 | /** 121 | * Check config for variables and set them to dynamodbBackup function 122 | * @returns {Promise.resolve} 123 | */ 124 | populateEnv() { 125 | // Environment variables have to be a string in order to be processed properly 126 | if (has(this.dynamodbAutoBackups, 'backupRemovalEnabled') && has(this.dynamodbAutoBackups, 'backupRetentionDays')) { 127 | set(this.functionBackup, 'environment.BACKUP_REMOVAL_ENABLED', String(this.dynamodbAutoBackups.backupRemovalEnabled)); 128 | set(this.functionBackup, 'environment.BACKUP_RETENTION_DAYS', String(this.dynamodbAutoBackups.backupRetentionDays)); 129 | } 130 | if (has(this.dynamodbAutoBackups, 'slackWebhook')) { 131 | set(this.functionBackup, 'environment.SLACK_WEBHOOK', String(this.dynamodbAutoBackups.slackWebhook)); 132 | } 133 | if (has(this.dynamodbAutoBackups, 'backupType')) { 134 | set(this.functionBackup, 'environment.BACKUP_TYPE', String(this.dynamodbAutoBackups.backupType).toUpperCase()); 135 | } 136 | if (has(this.dynamodbAutoBackups, 'tableRegex')) { 137 | set(this.functionBackup, 'environment.TABLE_REGEX', String(this.dynamodbAutoBackups.tableRegex)); 138 | } 139 | return BbPromise.resolve(); 140 | } 141 | 142 | /** 143 | * Assign dynamodbBackup function to serverless service 144 | * @returns {Promise.resolve} 145 | */ 146 | generateBackupFunction() { 147 | const dynamodbAutoBackups = clone(this.functionBackup); 148 | dynamodbAutoBackups.events = uniqWith(this.functionBackup.events, isEqual); 149 | 150 | if (isPlainObject(dynamodbAutoBackups)) { 151 | assign(this.serverless.service.functions, { [this.dynamodbAutoBackups.name || 'dynamodbAutoBackups']: dynamodbAutoBackups }); 152 | } 153 | 154 | DynamodbAutoBackup.consoleLog('@unly/serverless-plugin-dynamodb-backups: -----------------------------------------------------------'); 155 | DynamodbAutoBackup.consoleLog(` function: ${this.functionBackup.name}`); 156 | console.log(); 157 | return BbPromise.resolve(); 158 | } 159 | 160 | /** 161 | * Provide iam access 162 | * @returns {Promise.resolve} 163 | */ 164 | manageIamRole() { 165 | if (this.serverless.service.provider.iamRoleStatements) { 166 | iamRoleStatements.map((role) => this.serverless.service.provider.iamRoleStatements.push(role)); 167 | } else { 168 | this.serverless.service.provider.iamRoleStatements = iamRoleStatements; 169 | } 170 | this.isInstantiate = true; 171 | return BbPromise.resolve(); 172 | } 173 | 174 | /** 175 | * check if an instance of the class is already running 176 | * @returns {function} 177 | */ 178 | isAlreadyInInstance() { 179 | if (this.isInstantiate) { 180 | return BbPromise.resolve(); 181 | } 182 | return this.isActivated(this.chainPromises); 183 | } 184 | 185 | /** 186 | * check if the plugin config is activated 187 | * Default to true 188 | * @returns {function} 189 | */ 190 | isActivated(cb) { 191 | DynamodbAutoBackup.consoleLog(`@unly/serverless-plugin-dynamodb-backups is ${this.dynamodbAutoBackups.active ? 'enabled' : 'disabled'}`); 192 | console.log(); 193 | 194 | if (!this.dynamodbAutoBackups.active) { 195 | return BbPromise.resolve(); 196 | } 197 | return cb(); 198 | } 199 | 200 | /** 201 | * check if dynamodbAutoBackups options is provide 202 | * @returns {function} 203 | */ 204 | init() { 205 | // if no dynamodbAutoBackups key at custom in serverless.yml or invalid format (throw error) 206 | if (!has(this.custom, 'dynamodbAutoBackups') || !isPlainObject(this.custom.dynamodbAutoBackups)) { 207 | return BbPromise.reject( 208 | new this.serverless.classes.Error('Invalid configuration, see https://www.npmjs.com/package/@unly/serverless-plugin-dynamodb-backups'), 209 | ); 210 | } 211 | 212 | assign(this.dynamodbAutoBackups, this.custom.dynamodbAutoBackups); 213 | 214 | // if dynamodbAutoBackups.active is not provide, default value to true 215 | if (!has(this.dynamodbAutoBackups, 'active')) { 216 | set(this.dynamodbAutoBackups, 'active', true); 217 | } 218 | 219 | return this.isAlreadyInInstance(); 220 | } 221 | 222 | /** 223 | * 224 | * @param message 225 | */ 226 | static consoleLog(message) { 227 | console.log(chalk.yellow.bold(message)); 228 | } 229 | } 230 | 231 | module.exports = DynamodbAutoBackup; 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Unly logo 2 | [![Build Status](https://travis-ci.com/UnlyEd/serverless-plugin-dynamodb-backups.svg?branch=master)](https://travis-ci.com/UnlyEd/serverless-plugin-dynamodb-backups) 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/980404446e29f74f95fd/maintainability)](https://codeclimate.com/github/UnlyEd/serverless-plugin-dynamodb-backups/maintainability) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/980404446e29f74f95fd/test_coverage)](https://codeclimate.com/github/UnlyEd/serverless-plugin-dynamodb-backups/test_coverage) 5 | [![Known Vulnerabilities](https://snyk.io/test/github/UnlyEd/serverless-plugin-dynamodb-backups/badge.svg?targetFile=package.json)](https://snyk.io/test/github/UnlyEd/serverless-plugin-dynamodb-backups?targetFile=package.json) 6 | 7 | # Serverless plugin DynamoDB backups 8 | 9 | ## Introduction 10 | 11 | > If you want to automate your AWS DynamoDB database backups, this plugin may be what you need. 12 | 13 | As we build various services on AWS using the "serverless" design, we need reusable backups services, both scalable and easy to implement. 14 | We therefore created this plugin, to make sure that each project can create its own DynamoDB automated backup solution. 15 | 16 | This is a plugin which simplifies **DynamoDB backups** creation automation for all the resources created in 17 | `serverless.yml` when using the [Serverless Framework](https://serverless.com) and AWS Cloud provider. 18 | 19 | 20 | This plugin officially supports Node.js **12.x**, **14.x** and **16.x**. 21 | 22 | This plugin officially supports Serverless Framework `>=2.0.0`. 23 | 24 | ## Benefits 25 | 26 | * Automated Backups on your configured resources (`serverless.yml`) 27 | * Report Error on slack channel _(see configuration)_ 28 | * Delete old Backups automatically (AKA "managed backups retention") _(see configuration)_ 29 | 30 | ## Installation 31 | 32 | Install the plugin using either Yarn or NPM. (we use Yarn) 33 | 34 | NPM: 35 | ```bash 36 | npm install @unly/serverless-plugin-dynamodb-backups 37 | ``` 38 | 39 | YARN: 40 | ```bash 41 | yarn add @unly/serverless-plugin-dynamodb-backups 42 | ``` 43 | 44 | ## Usage 45 | 46 | ### Step 1: Load the Plugin 47 | 48 | The plugin determines your environment during deployment and adds all environment variables to your Lambda function. 49 | All you need to do is to load the plugin: 50 | 51 | > Must be declared **before** `serverless-webpack`, despite what their officially doc says 52 | 53 | ```yaml 54 | plugins: 55 | - '@unly/serverless-plugin-dynamodb-backups' # Must be first, even before "serverless-webpack", see https://github.com/UnlyEd/serverless-plugin-dynamodb-backups 56 | - serverless-webpack # Must be second, see https://github.com/99xt/serverless-dynamodb-local#using-with-serverless-offline-and-serverless-webpack-plugin 57 | ``` 58 | 59 | ### Step 2: Create the backups handler function: 60 | 61 | Create a file, which will be called when performing a DynamoDB backup _(we named it `src/backups.js` in our `examples` folder)_: 62 | 63 | ```javascript 64 | import dynamodbAutoBackups from '@unly/serverless-plugin-dynamodb-backups/lib'; 65 | 66 | export const handler = dynamodbAutoBackups; 67 | ``` 68 | 69 | ### Step 3: Configure your `serverless.yml` 70 | 71 | Set the `dynamodbAutoBackups` object configuration as follows (list of all available options below): 72 | 73 | ```yaml 74 | custom: 75 | dynamodbAutoBackups: 76 | backupRate: rate(40 minutes) # Every 5 minutes, from the time it was deployed 77 | source: src/backups.js # Path to the handler function we created in step #2 78 | active: true 79 | ``` 80 | 81 | ## Configuration of `dynamodbAutoBackups` object: 82 | 83 | | Attributes | Type | Required | Default | Description | 84 | |----------------------|---------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 85 | | source | String | True | | Path to your handler function | 86 | | backupRate | String | True | | The schedule on which you want to backup your table. You can use either `rate` syntax (`rate(1 hour)`) or `cron` syntax (`cron(0 12 * * ? *)`). See [here](https://serverless.com/framework/docs/providers/aws/events/schedule/) for more details on configuration. | 87 | | name | String | False | auto | Automatically set, but you could provide your own name for this lambda | 88 | | slackWebhook | String | False | | A HTTPS endpoint for an [incoming webhook](https://api.slack.com/incoming-webhooks) to Slack. If provided, it will send error messages to a Slack channel. | 89 | | backupRemovalEnabled | Boolean | False | false | Enables cleanup of old backups. See the below option "backupRetentionDays" to specify the retention period. By default, backup removal is disabled. | 90 | | backupRetentionDays | Integer | False | | Specify the number of days to retain old backups. For example, setting the value to 2 will remove all backups that are older than 2 days. Required if `backupRemovalEnabled` is `true`. | 91 | | backupType | String | False | "ALL" | * `USER` - On-demand backup created by you. * `SYSTEM` - On-demand backup automatically created by DynamoDB. * `ALL` - All types of on-demand backups (USER and SYSTEM). | 92 | | active | Boolean | False | true | You can disable this plugin, useful to disable the plugin on a non-production environment, for instance | 93 | 94 | _Generated by https://www.tablesgenerator.com/markdown_tables_ 95 | 96 | --- 97 | 98 | ### Examples of configurations: 99 | 100 | #### 1. Creates backups every 40 minutes, delete all backups older than 15 days, send slack notifications if backups are not created. 101 | 102 | ```yaml 103 | custom: 104 | dynamodbAutoBackups: 105 | backupRate: rate(40 minutes) # Every 40 minutes, from the time it was deployed 106 | source: src/backups.js 107 | slackWebhook: https://hooks.slack.com/services/T4XHXX5C6/TT3XXXM0J/XXXXXSbhCXXXX77mFBr0ySAm 108 | backupRemovalEnabled: true # Enable backupRetentionDays 109 | backupRetentionDays: 15 # If backupRemovalEnabled is not provided, then backupRetentionDays is not used 110 | ``` 111 | 112 | #### 2. Creates some backups every friday at 2:00 am, delete all backups created by USER longer than 3 days, be warned if backups are not created. 113 | ```yaml 114 | custom: 115 | dynamodbAutoBackups: 116 | backupRate: cron(0 2 ? * FRI *) # Every friday at 2:00 am 117 | source: src/backups.js 118 | slackWebhook: https://hooks.slack.com/services/T4XHXX5C6/TT3XXXM0J/XXXXXSbhCXXXX77mFBr0ySAm 119 | backupRemovalEnabled: true # Enable backupRetentionDays 120 | backupRetentionDays: 3 # If backupRemovalEnabled is not provided, then backupRetentionDays is not used 121 | backupType: USER # Delete all backups created by a user, not the system backups 122 | ``` 123 | 124 | ### Try it out yourself 125 | 126 | To test this plugin, you can clone this repository. 127 | Go to `examples/serverless-example`, and follow the README. 128 | 129 | --- 130 | 131 | # Vulnerability disclosure 132 | 133 | [See our policy](https://github.com/UnlyEd/Unly). 134 | 135 | --- 136 | 137 | # Contributors and maintainers 138 | 139 | This project is being maintained by: 140 | - [Unly] Ambroise Dhenain ([Vadorequest](https://github.com/vadorequest)) **(active)** 141 | 142 | Thanks to our contributors: 143 | - Anthony Troupenat ([Fukoyamashisu](https://github.com/Fukoyamashisu)) 144 | 145 | --- 146 | 147 | # **[ABOUT UNLY]** Unly logo 148 | 149 | > [Unly](https://unly.org) is a socially responsible company, fighting inequality and facilitating access to higher education. 150 | > Unly is committed to making education more inclusive, through responsible funding for students. 151 | We provide technological solutions to help students find the necessary funding for their studies. 152 | 153 | We proudly participate in many TechForGood initiatives. To support and learn more about our actions to make education accessible, visit : 154 | - https://twitter.com/UnlyEd 155 | - https://www.facebook.com/UnlyEd/ 156 | - https://www.linkedin.com/company/unly 157 | - [Interested to work with us?](https://jobs.zenploy.io/unly/about) 158 | 159 | Tech tips and tricks from our CTO on our [Medium page](https://medium.com/unly-org/tech/home)! 160 | 161 | #TECHFORGOOD #EDUCATIONFORALL 162 | --------------------------------------------------------------------------------