├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── lambda └── index.js ├── package.json └── template.yaml /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | geturl.zip 3 | package-lock.json 4 | packaged-template.yaml 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/amazon-dax-lambda-nodejs-sample/issues), or [recently closed](https://github.com/aws-samples/amazon-dax-lambda-nodejs-sample/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/amazon-dax-lambda-nodejs-sample/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/amazon-dax-lambda-nodejs-sample/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Amazon DynamoDB Accelerator (DAX) Lambda Node.js Sample 2 | 3 | A sample application showing how to use Amazon DynamoDB Accelerator (DAX) with Lambda and CloudFormation. This is based on the blog post at TODO. 4 | 5 | ## Setup & Deployment 6 | Deploying the demo will require [npm](https://www.npmjs.com/), the [AWS CLI](https://aws.amazon.com/cli/), and an AWS account. The AWS credentials for that account should be [set up in the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html). 7 | 8 | First, download the dependencies using `npm`: 9 | 10 | npm install 11 | 12 | Then, create a zip file called geturl.zip containing the `lambda` and `node_modules` folders. On Mac/Linux/WSL, uses the `zip` command: 13 | 14 | zip -qur geturl node_modules lambda 15 | 16 | Otherwise, put the necessary folders in a zip file. 17 | 18 | CloudFormation needs the code & template to be stored in an S3 bucket (replace with something unique and remember it as you will need it when packaging and deploying): 19 | 20 | aws s3 mb s3:// 21 | 22 | Now we can create the CloudFormation package and deploy it: 23 | 24 | aws cloudformation package --template-file template.yaml --output-template-file packaged-template.yaml --s3-bucket 25 | aws cloudformation deploy --template-file packaged-template.yaml --capabilities CAPABILITY_NAMED_IAM --stack-name amazon-dax-lambda-nodejs-sample 26 | 27 | One the CloudFormation stack is created, determine the insternal endpoint name (macOS/Linux/WSL): 28 | 29 | endpointUrl=$(aws apigatewayv2 get-apis --query "Items[?Name == 'amazon-dax-lambda-nodejs-sample'].ApiEndpoint" --output text) 30 | 31 | To shorten a URL: 32 | 33 | curl -d 'https://www.amazon.com' "$endpointUrl" 34 | 35 | The output will be a "slug" that can be used to fetch the URL (in this case, grqpaeet): 36 | 37 | curl -v "$endpointUrl/grqpaeet" 38 | 39 | ## License Summary 40 | 41 | This sample code is made available under a modified MIT license. See the LICENSE file. 42 | -------------------------------------------------------------------------------- /lambda/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT No Attribution 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | software and associated documentation files (the "Software"), to deal in the Software 6 | without restriction, including without limitation the rights to use, copy, modify, 7 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | 18 | const AWS = require('aws-sdk'); 19 | const AmazonDaxClient = require('amazon-dax-client'); 20 | const crypto = require('crypto'); 21 | 22 | // Store this at file level so that it is preserved between Lambda executions 23 | var dynamodb; 24 | 25 | exports.handler = function(event, context, callback) { 26 | event.headers = event.headers || []; 27 | main(event, context, callback); 28 | }; 29 | 30 | function main(event, context, callback) { 31 | // Initialize the 'dynamodb' variable if it has not already been done. This 32 | // allows the initialization to be shared between Lambda runs to reduce 33 | // execution time. This will be re-run if Lambda has to recycle the container 34 | // or use a new instance. 35 | if(!dynamodb) { 36 | if(process.env.DAX_ENDPOINT) { 37 | console.log('Using DAX endpoint', process.env.DAX_ENDPOINT); 38 | dynamodb = new AmazonDaxClient({endpoints: [process.env.DAX_ENDPOINT]}); 39 | } else { 40 | // DDB_LOCAL can be set if using lambda-local with dynamodb-local or another local 41 | // testing envionment 42 | if(process.env.DDB_LOCAL) { 43 | console.log('Using DynamoDB local'); 44 | dynamodb = new AWS.DynamoDB({endpoint: 'http://localhost:8000', region: 'ddblocal'}); 45 | } else { 46 | console.log('Using DynamoDB'); 47 | dynamodb = new AWS.DynamoDB(); 48 | } 49 | } 50 | } 51 | 52 | let body = event.body; 53 | 54 | // Depending on the HTTP Method, save or return the URL 55 | if (event.requestContext.http.method == 'GET') { 56 | return getUrl(event.pathParameters.id, callback); 57 | } else if (event.requestContext.http.method == 'POST' && event.body) { 58 | 59 | // if base64 encoded event.body is sent in, decode it 60 | if (event.isBase64Encoded) { 61 | let buff = Buffer.from(body, 'base64'); 62 | body = buff.toString('utf-8'); 63 | } 64 | 65 | return setUrl(body, callback); 66 | } else { 67 | console.log ('HTTP method ', event.requestContext.http.method, ' is invalid.'); 68 | return done(400, JSON.stringify({error: 'Missing or invalid HTTP Method'}), 'application/json', callback); 69 | } 70 | } 71 | 72 | // Get URLs from the database and return 73 | function getUrl(id, callback) { 74 | const params = { 75 | TableName: process.env.DDB_TABLE, 76 | Key: { id: { S: id } } 77 | }; 78 | 79 | console.log('Fetching URL for', id); 80 | dynamodb.getItem(params, (err, data) => { 81 | if(err) { 82 | console.error('getItem error:', err); 83 | return done(500, JSON.stringify({error: 'Internal Server Error: ' + err}), 'application/json', callback); 84 | } 85 | 86 | if(data && data.Item && data.Item.target) { 87 | let url = data.Item.target.S; 88 | return done(301, url, 'text/plain', callback, {Location: url}); 89 | } else { 90 | return done(404, '404 Not Found', 'text/plain', callback); 91 | } 92 | }); 93 | } 94 | 95 | /** 96 | * Compute a unique ID for each URL. 97 | * 98 | * To do this, take the MD5 hash of the URL, extract the first 40 bits, and 99 | * then return that in base32 representation. 100 | * 101 | * If the salt is provided, prepend that to the URL first. This is used to 102 | * resolve hash collisions. 103 | * 104 | */ 105 | function computeId(url, salt) { 106 | if(salt) { 107 | url = salt + '$' + url 108 | } 109 | 110 | // For demonstration purposes MD5 is fine 111 | let md5 = crypto.createHash('md5'); 112 | 113 | // Compute the MD5, then use only the first 40 bits 114 | let h = md5.update(url).digest('hex').slice(0, 10); 115 | 116 | // Return results in base32 (hence 40 bits, 8*5) 117 | return parseInt(h, 16).toString(32); 118 | } 119 | 120 | // Save the URLs to the database 121 | function setUrl(url, callback, salt) { 122 | let id = computeId(url, salt); 123 | 124 | const params = { 125 | TableName: process.env.DDB_TABLE, 126 | Item: { 127 | id: { S: id }, 128 | target: { S: url } 129 | }, 130 | // Ensure that puts are idempotent 131 | ConditionExpression: "attribute_not_exists(id) OR target = :url", 132 | ExpressionAttributeValues: { 133 | ":url": {S: url} 134 | } 135 | }; 136 | 137 | dynamodb.putItem(params, (err, data) => { 138 | if (err) { 139 | if(err.code === 'ConditionalCheckFailedException') { 140 | console.warn('Collision on ' + id + ' for ' + url + '; retrying...'); 141 | // Retry with the attempted ID as the salt. 142 | // Eventually there will not be a collision. 143 | return setUrl(url, callback, id); 144 | } else { 145 | console.error('Dynamo error on save: ', err); 146 | return done(500, JSON.stringify({error: 'Internal Server Error: ' + err}), 'application/json', callback); 147 | } 148 | } else { 149 | return done(200, id, 'text/plain', callback); 150 | } 151 | }); 152 | } 153 | 154 | // We're done with this lambda, return to the client with given parameters 155 | function done(statusCode, body, contentType, callback, headers) { 156 | full_headers = { 157 | 'Content-Type': contentType 158 | } 159 | 160 | if(headers) { 161 | full_headers = Object.assign(full_headers, headers); 162 | } 163 | 164 | callback(null, { 165 | statusCode: statusCode, 166 | body: body, 167 | headers: full_headers, 168 | isBase64Encoded: false, 169 | }); 170 | } 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geturljs", 3 | "version": "1.0.1", 4 | "repository": "https://github.com/aws-samples/amazon-dax-lambda-nodejs-sample", 5 | "description": "Amazon DynamoDB Accelerator (DAX) Lambda Node.js Sample", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "test" 9 | }, 10 | "author": "author@example.com", 11 | "license": "MIT", 12 | "dependencies": { 13 | "amazon-dax-client": "^1.1.0", 14 | "aws-sdk": "^2.207.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: A sample application showing how to use Amazon DynamoDB Accelerator (DAX) with Lambda and CloudFormation. 3 | Transform: AWS::Serverless-2016-10-31 4 | Resources: 5 | siteFunction: 6 | Type: AWS::Serverless::Function 7 | Properties: 8 | CodeUri: geturl.zip 9 | Description: Resolve/Store URLs 10 | Environment: 11 | Variables: 12 | DAX_ENDPOINT: !GetAtt getUrlCluster.ClusterDiscoveryEndpoint 13 | DDB_TABLE: !Ref getUrlTable 14 | Events: 15 | getUrl: 16 | Type: HttpApi 17 | Properties: 18 | Method: get 19 | Path: /{id+} 20 | postUrl: 21 | Type: HttpApi 22 | Properties: 23 | Method: post 24 | Path: / 25 | Handler: lambda/index.handler 26 | Runtime: nodejs12.x 27 | Timeout: 10 28 | VpcConfig: 29 | SecurityGroupIds: 30 | - !GetAtt getUrlSecurityGroup.GroupId 31 | SubnetIds: 32 | - !Ref getUrlSubnet 33 | Role: !GetAtt getUrlRole.Arn 34 | 35 | getUrlTable: 36 | Type: AWS::DynamoDB::Table 37 | Properties: 38 | TableName: GetUrl-sample 39 | AttributeDefinitions: 40 | - 41 | AttributeName: id 42 | AttributeType: S 43 | KeySchema: 44 | - 45 | AttributeName: id 46 | KeyType: HASH 47 | BillingMode: PAY_PER_REQUEST 48 | 49 | getUrlCluster: 50 | Type: AWS::DAX::Cluster 51 | Properties: 52 | ClusterName: getUrl-sample 53 | Description: Cluster for GetUrl Sample 54 | IAMRoleARN: !GetAtt getUrlRole.Arn 55 | NodeType: dax.t2.small 56 | ReplicationFactor: 1 57 | SecurityGroupIds: 58 | - !GetAtt getUrlSecurityGroup.GroupId 59 | SubnetGroupName: !Ref getUrlSubnetGroup 60 | 61 | getUrlRole: 62 | Type: AWS::IAM::Role 63 | Properties: 64 | AssumeRolePolicyDocument: 65 | Statement: 66 | - Action: 67 | - sts:AssumeRole 68 | Effect: Allow 69 | Principal: 70 | Service: 71 | - dax.amazonaws.com 72 | - lambda.amazonaws.com 73 | Version: '2012-10-17' 74 | RoleName: getUrl-sample-Role 75 | Policies: 76 | - 77 | PolicyName: DAXAccess 78 | PolicyDocument: 79 | Version: '2012-10-17' 80 | Statement: 81 | - Effect: Allow 82 | Resource: '*' 83 | Action: 84 | - 'dax:PutItem' 85 | - 'dax:GetItem' 86 | - 'dynamodb:DescribeTable' 87 | - 'dynamodb:GetItem' 88 | - 'dynamodb:PutItem' 89 | ManagedPolicyArns: 90 | - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole 91 | 92 | getUrlSecurityGroup: 93 | Type: AWS::EC2::SecurityGroup 94 | Properties: 95 | GroupDescription: Security Group for GetUrl 96 | GroupName: getUrl-sample 97 | VpcId: !Ref getUrlVpc 98 | 99 | getUrlSecurityGroupIngress: 100 | Type: AWS::EC2::SecurityGroupIngress 101 | DependsOn: getUrlSecurityGroup 102 | Properties: 103 | GroupId: !GetAtt getUrlSecurityGroup.GroupId 104 | IpProtocol: tcp 105 | FromPort: 8111 106 | ToPort: 8111 107 | SourceSecurityGroupId: !GetAtt getUrlSecurityGroup.GroupId 108 | 109 | getUrlVpc: 110 | Type: AWS::EC2::VPC 111 | Properties: 112 | CidrBlock: 10.0.0.0/16 113 | EnableDnsHostnames: true 114 | EnableDnsSupport: true 115 | InstanceTenancy: default 116 | Tags: 117 | - Key: Name 118 | Value: getUrl-sample 119 | 120 | getUrlSubnet: 121 | Type: AWS::EC2::Subnet 122 | Properties: 123 | AvailabilityZone: 124 | Fn::Select: 125 | - 0 126 | - Fn::GetAZs: '' 127 | CidrBlock: 10.0.0.0/20 128 | Tags: 129 | - Key: Name 130 | Value: getUrl-sample 131 | VpcId: !Ref getUrlVpc 132 | 133 | getUrlSubnetGroup: 134 | Type: AWS::DAX::SubnetGroup 135 | Properties: 136 | Description: Subnet group for GetUrl Sample 137 | SubnetGroupName: getUrl-sample 138 | SubnetIds: 139 | - !Ref getUrlSubnet 140 | --------------------------------------------------------------------------------