diff --git a/README.md b/README.md index 27b257f..f35625b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Description -`failure-lambda` is a small Node module for injecting failure into AWS Lambda (https://aws.amazon.com/lambda). It offers a simple failure injection wrapper for your Lambda handler where you then can choose to inject failure by setting the `failureMode` to `latency`, `exception`, `statuscode` or `diskspace`. You control your failure injection using SSM Parameter Store. +`failure-lambda` is a small Node module for injecting failure into AWS Lambda (https://aws.amazon.com/lambda). It offers a simple failure injection wrapper for your Lambda handler where you then can choose to inject failure by setting the `failureMode` to `latency`, `exception`, `blacklist`, `diskspace` or `statuscode`. You control your failure injection using SSM Parameter Store. ## How to install @@ -22,10 +22,10 @@ exports.handler = failureLambda(async (event, context) => { ``` 4. Create a parameter in SSM Parameter Store. ```json -{"isEnabled": false, "failureMode": "latency", "rate": 1, "minLatency": 100, "maxLatency": 400, "exceptionMsg": "Exception message!", "statusCode": 404, "diskSpace": 100} +{"isEnabled": false, "failureMode": "latency", "rate": 1, "minLatency": 100, "maxLatency": 400, "exceptionMsg": "Exception message!", "statusCode": 404, "diskSpace": 100, "blacklist": ["s3.*.amazonaws.com", "dynamodb.*.amazonaws.com"]} ``` ```bash -aws ssm put-parameter --region eu-north-1 --name failureLambdaConfig --type String --overwrite --value "{\"isEnabled\": false, \"failureMode\": \"latency\", \"rate\": 1, \"minLatency\": 100, \"maxLatency\": 400, \"exceptionMsg\": \"Exception message!\", \"statusCode\": 404, \"diskSpace\": 100}" +aws ssm put-parameter --region eu-west-1 --name failureLambdaConfig --type String --overwrite --value "{\"isEnabled\": false, \"failureMode\": \"latency\", \"rate\": 1, \"minLatency\": 100, \"maxLatency\": 400, \"exceptionMsg\": \"Exception message!\", \"statusCode\": 404, \"diskSpace\": 100, \"blacklist\": [\"s3.*.amazonaws.com\", \"dynamodb.*.amazonaws.com\"]}" ``` 5. Add an environment variable to your Lambda function with the key FAILURE_INJECTION_PARAM and the value set to the name of your parameter in SSM Parameter Store. 6. Try it out! @@ -36,12 +36,13 @@ Edit the values of your parameter in SSM Parameter Store to use the failure inje * `isEnabled: true` means that failure is injected into your Lambda function. * `isEnabled: false` means that the failure injection module is disabled and no failure is injected. -* `failureMode` selects which failure you want to inject. The options are `latency`, `exception` or `statuscode` as explained below. +* `failureMode` selects which failure you want to inject. The options are `latency`, `exception`, `blacklist`, `diskspace` or `statuscode` as explained below. * `rate` controls the rate of failure. 1 means that failure is injected on all invocations and 0.5 that failure is injected on about half of all invocations. * `minLatency` and `maxLatency` is the span of latency in milliseconds injected into your function when `failureMode` is set to `latency`. * `exceptionMsg` is the message thrown with the exception created when `failureMode` is set to `exception`. * `statusCode` is the status code returned by your function when `failureMode` is set to `statuscode`. * `diskSpace` is size in MB of the file created in tmp when `failureMode` is set to `diskspace`. +* `blacklist` is an array of regular expressions, if a connection is made to a host matching one of the regular expressions it will be blocked. ## Example @@ -57,9 +58,14 @@ Inspired by Yan Cui's articles on latency injection for AWS Lambda (https://hack ## Changelog +### 2020-02-17 v0.2.0 + +* Added blacklist failure. +* Updated example application to retrive file from S3 based on URL from DynamoDB. + ### 2020-02-13 v0.1.1 -* Fixed issue with exception injection not throwing the exception. Thanks to [Jason Barto](https://github.com/jpbarto)! +* Fixed issue with exception injection not throwing the exception. ### 2019-12-30 v0.1.0 @@ -70,6 +76,8 @@ Inspired by Yan Cui's articles on latency injection for AWS Lambda (https://hack * Initial release -## Authors +## Contributors **Gunnar Grosch** - [GitHub](https://github.com/gunnargrosch) | [Twitter](https://twitter.com/gunnargrosch) | [LinkedIn](https://www.linkedin.com/in/gunnargrosch/) + +**Jason Barto** - [GitHub](https://github.com/jpbarto) | [Twitter](https://twitter.com/Jason_Barto) | [LinkedIn](https://www.linkedin.com/in/jasonbarto) diff --git a/example/index.js b/example/index.js index 93da08c..c16bcaf 100644 --- a/example/index.js +++ b/example/index.js @@ -1,17 +1,40 @@ 'use strict' const failureLambda = require('failure-lambda') const fs = require('fs') +const AWS = require('aws-sdk') +const s3 = new AWS.S3() +const dynamoDb = new AWS.DynamoDB.DocumentClient() let response exports.handler = failureLambda(async (event, context) => { try { - fs.writeFile('/tmp/example-' + Date.now() + '.tmp', 'Contents', (err) => { + let fileName = Date.now() + '.tmp' + let contents = 'Hello failureLambda!' + fs.writeFile('/tmp/' + fileName, contents, (err) => { + if (err) throw err + }) + let s3Params = { + Bucket: process.env.FAILURE_INJECTION_BUCKET, + Key: fileName, + Body: contents + } + s3.upload(s3Params, (err) => { + if (err) throw err + }) + let ddbParams = { + TableName: process.env.FAILURE_INJECTION_TABLE, + Item: { + id: Date.now(), + contents: contents + } + } + dynamoDb.put(ddbParams, (err) => { if (err) throw err }) response = { statusCode: 200, body: JSON.stringify({ - message: 'Hello failureLambda!' + message: contents }) } } catch (err) { diff --git a/example/serverless.yml b/example/serverless.yml index c13f238..170f10c 100644 --- a/example/serverless.yml +++ b/example/serverless.yml @@ -9,7 +9,42 @@ provider: Action: - ssm:GetParameters - ssm:GetParameter - Resource: "arn:aws:ssm:${opt:region, self:provider.region}:*:parameter/${self:service}-${opt:stage, self:provider.stage}-failureLambdaExample" + Resource: + Fn::Join: + - '' + - - 'arn:aws:ssm:${opt:region, self:provider.region}:*:parameter/' + - Ref: failureLambdaParameter + - Effect: Allow + Action: + - s3:ListBucket + Resource: + Fn::Join: + - '' + - - 'arn:aws:s3:::' + - Ref: failureLambdaBucket + - Effect: Allow + Action: + - s3:GetObject + - s3:PutObject + Resource: + Fn::Join: + - '' + - - 'arn:aws:s3:::' + - Ref: failureLambdaBucket + - '/*' + - Effect: Allow + Action: + - dynamodb:Query + - dynamodb:Scan + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:UpdateItem + - dynamodb:DeleteItem + Resource: + Fn::Join: + - '' + - - 'arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/' + - Ref: failureLambdaTable functions: failureLambdaExample: handler: index.handler @@ -17,6 +52,10 @@ functions: environment: FAILURE_INJECTION_PARAM: Ref: failureLambdaParameter + FAILURE_INJECTION_BUCKET: + Ref: failureLambdaBucket + FAILURE_INJECTION_TABLE: + Ref: failureLambdaTable events: - http: path: failureLambdaExample/ @@ -27,9 +66,26 @@ resources: failureLambdaParameter: Type: 'AWS::SSM::Parameter' Properties: - Name: ${self:service}-${opt:stage, self:provider.stage}-failureLambdaExample Type: String - Value: '{"isEnabled": false, "failureMode": "latency", "rate": 1, "minLatency": 100, "maxLatency": 400, "exceptionMsg": "Exception message!", "statusCode": 404, "diskSpace": 100}' + Value: '{"isEnabled": false, "failureMode": "latency", "rate": 1, "minLatency": 100, "maxLatency": 400, "exceptionMsg": "Exception message!", "statusCode": 404, "diskSpace": 100, "blacklist": ["s3.*.amazonaws.com", "dynamodb.*.amazonaws.com"]}' + failureLambdaBucket: + Type: 'AWS::S3::Bucket' + Properties: + VersioningConfiguration: + Status: Suspended + failureLambdaTable: + Type: 'AWS::DynamoDB::Table' + DeletionPolicy: Delete + Properties: + AttributeDefinitions: + - + AttributeName: id + AttributeType: N + KeySchema: + - + AttributeName: id + KeyType: HASH + BillingMode: PAY_PER_REQUEST package: exclude: - .vscode diff --git a/lib/failure.js b/lib/failure.js index 8902d6a..301ab84 100644 --- a/lib/failure.js +++ b/lib/failure.js @@ -2,6 +2,7 @@ const aws = require('aws-sdk') const ssm = new aws.SSM() const childProcess = require('child_process') +const Mitm = require('mitm') async function getConfig() { try { @@ -36,6 +37,27 @@ var injectFailure = function (fn) { } else if (config.failureMode === 'diskspace') { console.log('Injecting disk space: ' + config.diskSpace + ' MB') childProcess.spawnSync('dd', ['if=/dev/zero', 'of=/tmp/diskspace-failure-' + Date.now() + '.tmp', 'count=1000', 'bs=' + config.diskSpace * 1000]) + } else if (config.failureMode === 'blacklist') { + console.log('Injecting dependency failure through a network blackhole for blacklisted sites: ' + config.blacklist) + let mitm = Mitm() + let blRegexs = [] + config.blacklist.forEach(function (regexStr) { + blRegexs.push(new RegExp(regexStr)) + }) + mitm.on('connect', function (socket, opts) { + let block = false + blRegexs.forEach(function (blRegex) { + if (blRegex.test(opts.host)) { + console.log('Intercepted network connection to ' + opts.host) + block = true + } + }) + if (block) { + socket.end() + } else { + socket.bypass() + } + }) } } return fn.apply(this, arguments) diff --git a/package-lock.json b/package-lock.json index 86f9c3c..599f3dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "failure-lambda", - "version": "0.0.1", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1014,6 +1014,22 @@ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, + "mitm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mitm/-/mitm-1.7.0.tgz", + "integrity": "sha512-O6EjnhSf7dDq+pveB5WlRbaz34cZgQuRfey7RonqsD2QB//eVXniYnH5u8QMB6bG7BUIJofL7UPaopE3iYkKlA==", + "requires": { + "semver": ">= 5 < 6", + "underscore": ">= 1.1.6 < 1.6" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", @@ -1589,6 +1605,11 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "underscore": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", + "integrity": "sha1-EzXF5PXm0zu7SwBrqMhqAPVW3gg=" + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", diff --git a/package.json b/package.json index fbb467d..7c97be9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "failure-lambda", - "version": "0.1.1", + "version": "0.2.0", "description": "Module for failure injection into AWS Lambda", "main": "./lib/failure.js", "scripts": { @@ -33,5 +33,7 @@ "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1" }, - "dependencies": {} + "dependencies": { + "mitm": "^1.7.0" + } }