Skip to content

Commit 9828dd7

Browse files
authored
Merge pull request #16 from jameshy/refresh
2019 update
2 parents 39d31c6 + 8c1437e commit 9828dd7

27 files changed

+4708
-266
lines changed

.eslintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"indent": ["error", 4],
66
"semi": ["error", "never"],
77
"brace-style": ["error", "stroustrup"],
8-
'no-restricted-syntax': [
8+
"no-restricted-syntax": [
99
"error",
1010
"ForInStatement",
1111
"LabeledStatement",

.travis.yml

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
language: node_js
22
node_js:
3-
- '6.10'
4-
addons:
5-
postgresql: '9.4'
3+
- '12.14.0'
64
after_success: npm run coverage
75
before_deploy:
86
- npm run deploy

README.md

+62-44
Original file line numberDiff line numberDiff line change
@@ -3,85 +3,103 @@
33
[![Build Status](https://travis-ci.org/jameshy/pgdump-aws-lambda.svg?branch=master)](https://travis-ci.org/jameshy/pgdump-aws-lambda)
44
[![Coverage Status](https://coveralls.io/repos/github/jameshy/pgdump-aws-lambda/badge.svg?branch=master)](https://coveralls.io/github/jameshy/pgdump-aws-lambda?branch=master)
55

6-
# Overview
7-
86
An AWS Lambda function that runs pg_dump and streams the output to s3.
97

108
It can be configured to run periodically using CloudWatch events.
119

1210
## Quick start
1311

1412
1. Create an AWS lambda function:
15-
- Runtime: Node.js 6.10
16-
- Code entry type: Upload a .ZIP file
17-
([pgdump-aws-lambda.zip](https://github.com/jameshy/pgdump-aws-lambda/releases/download/v1.1.5/pgdump-aws-lambda.zip))
18-
- Configuration -> Advanced Settings
19-
- Timeout = 5 minutes
20-
- Select a VPC and security group (must be suitable for connecting to the target database server)
21-
2. Create a CloudWatch rule:
22-
- Event Source: Fixed rate of 1 hour
23-
- Targets: Lambda Function (the one created in step #1)
24-
- Configure input -> Constant (JSON text) and paste your config, e.g.:
13+
- Author from scratch
14+
- Runtime: Node.js 12.x
15+
2. Configuration -> Function code:
16+
- Code Entry Type: Upload a .zip file
17+
- Upload ([pgdump-aws-lambda.zip](https://github.com/jameshy/pgdump-aws-lambda/releases/latest))
18+
- Basic Settings -> Timeout: 15 minutes
19+
- Save
20+
3. Configuration -> Execution role
21+
- Edit the role and attach the policy "AmazonS3FullAccess"
22+
4. Test
23+
- Create new test event, e.g.:
2524
```json
2625
{
27-
"PGDATABASE": "oxandcart",
28-
"PGUSER": "staging",
29-
"PGPASSWORD": "uBXKFecSKu7hyNu4",
30-
"PGHOST": "database.com",
31-
"S3_BUCKET" : "my-db-backups",
26+
"PGDATABASE": "dbname",
27+
"PGUSER": "postgres",
28+
"PGPASSWORD": "password",
29+
"PGHOST": "host",
30+
"S3_BUCKET" : "db-backups",
3231
"ROOT": "hourly-backups"
3332
}
3433
```
34+
- *Test* and check the output
3535

36-
Note: you can test the lambda function using the "Test" button and providing config like above.
36+
5. Create a CloudWatch rule:
37+
- Event Source: Schedule -> Fixed rate of 1 hour
38+
- Targets: Lambda Function (the one created in step #1)
39+
- Configure input -> Constant (JSON text) and paste your config (as per step #3)
3740

38-
**AWS lambda has a 5 minute maximum execution time for lambda functions, so your backup must take less time that that.**
3941

40-
## File Naming
42+
#### File Naming
4143

4244
This function will store your backup with the following s3 key:
4345

4446
s3://${S3_BUCKET}${ROOT}/YYYY-MM-DD/[email protected]
4547

46-
## PostgreSQL version compatibility
48+
#### AWS Firewall
4749

48-
This script uses the pg_dump utility from PostgreSQL 9.6.2.
50+
- If you run the Lambda function outside a VPC, you must enable public access to your database instance, a non VPC Lambda function executes on the public internet.
51+
- If you run the Lambda function inside a VPC (not tested), you must allow access from the Lambda Security Group to your database instance. Also you must add a NAT gateway to your VPC so the Lambda can connect to S3.
4952

50-
It should be able to dump older versions of PostgreSQL. I will try to keep the included binaries in sync with the latest from postgresql.org, but PR or message me if there is a newer PostgreSQL binary available.
53+
#### Encryption
5154

52-
## Encryption
55+
You can add an encryption key to your event, e.g.
5356

54-
You can pass the config option 'ENCRYPTION_PASSWORD' and the backup will be encrypted using aes-256-ctr algorithm.
55-
56-
Example config:
5757
```json
5858
{
5959
"PGDATABASE": "dbname",
6060
"PGUSER": "postgres",
6161
"PGPASSWORD": "password",
62-
"PGHOST": "localhost",
63-
"S3_BUCKET" : "my-db-backups",
64-
"ENCRYPTION_PASSWORD": "my-secret-password"
62+
"PGHOST": "host",
63+
"S3_BUCKET" : "db-backups",
64+
"ROOT": "hourly-backups",
65+
"ENCRYPT_KEY": "c0d71d7ae094bdde1ef60db8503079ce615e71644133dc22e9686dc7216de8d0"
6566
}
6667
```
6768

68-
To decrypt these dumps, use the command:
69-
`openssl aes-256-ctr -d -in ./encrypted-db.backup -nosalt -out unencrypted.backup`
69+
The key should be exactly 64 hex characters (32 hex bytes).
70+
71+
When this key is present the function will do streaming encryption directly from pg_dump -> S3.
72+
73+
It uses the aes-256-cbc encryption algorithm with a random IV for each backup file.
74+
The IV is stored alongside the backup in a separate file with the .iv extension.
75+
76+
You can decrypt such a backup with the following bash command:
77+
78+
```bash
79+
openssl enc -aes-256-cbc -d \
80+
81+
82+
-K c0d71d7ae094bdde1ef60db8503079ce615e71644133dc22e9686dc7216de8d0 \
83+
84+
```
85+
86+
87+
## Developer
7088

71-
## Loading your own `pg_dump` binary
72-
1. Spin up an Amazon AMI image on EC2 (since the lambda function will run
73-
on Amazon AMI image, based off of CentOS, using it would have the
74-
best chance of being compatible)
75-
2. Install PostgreSQL using yum. You can install the latest version from the [official repository](https://yum.postgresql.org/repopackages.php#pg96).
76-
3. Add a new directory for your pg_dump binaries: `mkdir bin/postgres-9.5.2`
89+
#### Bundling a new `pg_dump` binary
90+
1. Launch an EC2 instance with the Amazon Linux 2 AMI
91+
2. Connect via SSH and (Install PostgreSQL using yum)[https://stackoverflow.com/questions/55798856/deploy-postgres11-to-elastic-beanstalk-requires-etc-redhat-release].
92+
3. Locally, create a new directory for your pg_dump binaries: `mkdir bin/postgres-11.6`
7793
3. Copy the binaries
78-
- `scp -i YOUR-ID.pem ec2-user@AWS_IP:/usr/bin/pg_dump ./bin/postgres-9.5.2/pg_dump`
79-
- `scp -i YOUR-ID.pem ec2-user@AWS_UP:/usr/lib64/libpq.so.5.8 ./bin/postgres-9.5.2/libpq.so.5`
80-
4. When calling the handler, pass the env variable PGDUMP_PATH=postgres-9.5.2 to use the binaries in the bin/postgres-9.5.2 directory.
94+
- `scp -i <aws PEM> ec2-user@<EC2 Instance IP>:/usr/bin/pg_dump ./bin/postgres-11.6/pg_dump`
95+
- `scp -i <aws PEM> ec2-user@<EC2 Instance IP>:/usr/lib64/{libcrypt.so.1,libnss3.so,libsmime3.so,libssl3.so,libsasl2.so.3,liblber-2.4.so.2,libldap_r-2.4.so.2} ./bin/postgres-11.6/`
96+
- `scp -i <aws PEM> ec2-user@<EC2 Instance IP>:/usr/pgsql-11/lib/libpq.so.5 ./bin/postgres-11.6/libpq.so.5`
97+
4. When calling the handler, pass the environment variable `PGDUMP_PATH=postgres-11.6` to use the binaries in the bin/postgres-11.6 directory.
98+
99+
#### Creating a new function zip
81100

82-
NOTE: `libpq.so.5.8` is found out by running `ll /usr/lib64/libpq.so.5`
83-
and looking at where the symlink goes to.
101+
`npm run deploy`
84102

85-
## Contributing
103+
#### Contributing
86104

87105
Please submit issues and PRs.

bin/makezip.sh

+21-29
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
#!/bin/bash
22
set -e
33

4-
SCRIPT=`readlink -f $0`
5-
SCRIPTPATH=`dirname $SCRIPT`
6-
PROJECTROOT=`readlink -f $SCRIPTPATH/..`
74
FILENAME="pgdump-aws-lambda.zip"
85

96
command_exists () {
@@ -12,43 +9,38 @@ command_exists () {
129

1310
if ! command_exists zip ; then
1411
echo "zip command not found, try: sudo apt-get install zip"
15-
exit 0
12+
exit 1
13+
fi
14+
if [ ! -f ./package.json ]; then
15+
echo "command must be run from the project root directory"
16+
exit 1
1617
fi
1718

1819

19-
cd $PROJECTROOT
20-
21-
echo "creating bundle.."
2220
# create a temp directory for our bundle
2321
BUNDLE_DIR=$(mktemp -d)
24-
# copy entire app into BUNDLE_DIR
25-
cp -r * $BUNDLE_DIR/
26-
27-
# prune things from BUNDLE_DIR
28-
echo "running npm prune.."
29-
cd $BUNDLE_DIR
30-
# prune dev-dependancies from node_modules
31-
npm prune --production >> /dev/null
32-
22+
# copy entire project into BUNDLE_DIR
23+
cp -R * $BUNDLE_DIR/
24+
25+
# remove unnecessary things
26+
pushd $BUNDLE_DIR > /dev/null
27+
echo "cleaning.."
28+
rm -rf node_modules/*
29+
npm install --production --no-progress > /dev/null
3330
rm -rf dist coverage test
3431

35-
36-
# create and empty the dist directory
37-
if [ ! -d $PROJECTROOT/dist ]; then
38-
mkdir $PROJECTROOT/dist
39-
fi
40-
rm -rf $PROJECTROOT/dist/*
41-
4232
# create zip of bundle/
43-
echo "creating zip.."
33+
echo "zipping.."
4434
zip -q -r $FILENAME *
45-
echo "zip -q -r $FILENAME *"
46-
mv $FILENAME $PROJECTROOT/dist/$FILENAME
35+
36+
# return to project dir
37+
popd > /dev/null
38+
39+
# copy the zip
40+
mkdir -p ./dist
41+
cp $BUNDLE_DIR/$FILENAME ./dist/$FILENAME
4742

4843
echo "successfully created dist/$FILENAME"
4944

5045
# remove bundle/
5146
rm -rf $BUNDLE_DIR
52-
53-
54-
cd $PROJECTROOT

bin/postgres-11.6/libcrypt.so.1

40.1 KB
Binary file not shown.

bin/postgres-11.6/liblber-2.4.so.2

60.4 KB
Binary file not shown.

bin/postgres-11.6/libldap_r-2.4.so.2

368 KB
Binary file not shown.

bin/postgres-11.6/libnss3.so

1.15 MB
Binary file not shown.

bin/postgres-11.6/libpq.so.5

297 KB
Binary file not shown.

bin/postgres-11.6/libsasl2.so.3

118 KB
Binary file not shown.

bin/postgres-11.6/libsmime3.so

156 KB
Binary file not shown.

bin/postgres-11.6/libssl3.so

350 KB
Binary file not shown.

bin/postgres-11.6/pg_dump

383 KB
Binary file not shown.

bin/postgres-9.6.2/libpq.so.5

-189 KB
Binary file not shown.

bin/postgres-9.6.2/pg_dump

-374 KB
Binary file not shown.

lib/config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@ const path = require('path')
22

33
module.exports = {
44
S3_REGION: 'eu-west-1',
5-
PGDUMP_PATH: path.join(__dirname, '../bin/postgres-9.6.2')
5+
PGDUMP_PATH: path.join(__dirname, '../bin/postgres-11.6'),
6+
// maximum time allowed to connect to postgres before a timeout occurs
7+
PGCONNECT_TIMEOUT: 15
68
}

lib/encryption.js

+25-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
const crypto = require('crypto')
22

3-
const algorithm = 'aes-256-ctr'
3+
4+
const ALGORITHM = 'aes-256-cbc'
45

56
module.exports = {
6-
encrypt(readableStream, password) {
7-
const cipher = crypto.createCipher(algorithm, password)
8-
return readableStream.pipe(cipher)
7+
encrypt(readableStream, key, iv) {
8+
this.validateKey(key)
9+
if (iv.length !== 16) {
10+
throw new Error(`encrypt iv must be exactly 16 bytes, but received ${iv.length}`)
11+
}
12+
const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(key, 'hex'), iv)
13+
readableStream.pipe(cipher)
14+
return cipher
15+
},
16+
decrypt(readableStream, key, iv) {
17+
this.validateKey(key)
18+
const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(key, 'hex'), iv)
19+
readableStream.pipe(decipher)
20+
return decipher
21+
},
22+
validateKey(key) {
23+
const bytes = Buffer.from(key, 'hex')
24+
if (bytes.length !== 32) {
25+
throw new Error('encrypt key must be a 32 byte hex string')
26+
}
27+
return true
928
},
10-
decrypt(readableStream, password) {
11-
const decipher = crypto.createDecipher(algorithm, password)
12-
return readableStream.pipe(decipher)
29+
generateIv() {
30+
return crypto.randomBytes(16)
1331
}
1432
}

lib/handler.js

+29-34
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,49 @@
11
const utils = require('./utils')
2+
const uploadS3 = require('./upload-s3')
3+
const pgdump = require('./pgdump')
24
const encryption = require('./encryption')
3-
const Promise = require('bluebird')
4-
// todo: make these const, (mockSpawn doesn't allow this, so remove mockSpawn)
5-
var uploadS3 = require('./upload-s3')
6-
var pgdump = require('./pgdump')
75

86
const DEFAULT_CONFIG = require('./config')
97

10-
function handler(event, context) {
11-
const config = Object.assign({}, DEFAULT_CONFIG, event)
12-
8+
async function backup(config) {
139
if (!config.PGDATABASE) {
1410
throw new Error('PGDATABASE not provided in the event data')
1511
}
1612
if (!config.S3_BUCKET) {
1713
throw new Error('S3_BUCKET not provided in the event data')
1814
}
1915

20-
// determine the path for the database dump
2116
const key = utils.generateBackupPath(
2217
config.PGDATABASE,
2318
config.ROOT
2419
)
2520

26-
const pgdumpProcess = pgdump(config)
27-
return pgdumpProcess
28-
.then(readableStream => {
29-
if (config.ENCRYPTION_PASSWORD) {
30-
console.log('encrypting dump')
31-
readableStream = encryption.encrypt(
32-
readableStream,
33-
config.ENCRYPTION_PASSWORD
34-
)
35-
}
36-
// stream to s3 uploader
37-
return uploadS3(readableStream, config, key)
38-
})
39-
.catch(e => {
40-
throw e
41-
})
21+
// spawn the pg_dump process
22+
let stream = await pgdump(config)
23+
if (config.ENCRYPT_KEY && encryption.validateKey(config.ENCRYPT_KEY)) {
24+
// if encryption is enabled, we generate an IV and store it in a separate file
25+
const iv = encryption.generateIv()
26+
const ivKey = key + '.iv'
27+
28+
await uploadS3(iv.toString('hex'), config, ivKey)
29+
stream = encryption.encrypt(stream, config.ENCRYPT_KEY, iv)
30+
}
31+
// stream the backup to S3
32+
return uploadS3(stream, config, key)
4233
}
4334

44-
module.exports = function (event, context, cb) {
45-
return Promise.try(() => handler(event, context))
46-
.then(result => {
47-
cb(null)
48-
return result
49-
})
50-
.catch(err => {
51-
cb(err)
52-
throw err
53-
})
35+
async function handler(event) {
36+
const config = { ...DEFAULT_CONFIG, ...event }
37+
try {
38+
return await backup(config)
39+
}
40+
catch (error) {
41+
// log the error and rethrow for Lambda
42+
if (process.env.NODE_ENV !== "test") {
43+
console.error(error)
44+
}
45+
throw error
46+
}
5447
}
48+
49+
module.exports = handler

0 commit comments

Comments
 (0)