├── .eslintrc.json ├── services ├── application-load-balancer-lambda │ ├── static-error-responses │ │ └── not-found.txt │ ├── README.md │ └── serverless.yml ├── echo-api │ ├── README.md │ ├── src │ │ └── handler.js │ └── serverless.yml ├── usage-custom-resources │ ├── README.md │ └── serverless.yml └── custom-resources │ ├── README.md │ ├── package.json │ ├── serverless.yml │ └── package-lock.json ├── README.md ├── .travis.yml ├── Gruntfile.js ├── package.json ├── LICENSE └── .gitignore /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "@silvermine/eslint-config/node", 4 | 5 | "rules": { 6 | "no-process-env": "off" 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /services/application-load-balancer-lambda/static-error-responses/not-found.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "b8815d20-534a-4466-92c5-3a2fd0cc74f7", 4 | "title": "Not found", 5 | "status": 404 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /services/echo-api/README.md: -------------------------------------------------------------------------------- 1 | # Echo API 2 | 3 | This is a simple example of an API that echos back the request event that it was sent. It 4 | uses [AWS' API Gateway][APIGW] and the [Serverless Framework][sls]. 5 | 6 | [APIGW]: https://aws.amazon.com/api-gateway/ 7 | [sls]: https://serverless.com/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Training Examples 2 | 3 | A collection of examples for referencing from https://serverless-training.com. 4 | 5 | See [the Serverless Training website][site] for articles that use these examples, as well 6 | as for the [Serving of Serverless Newsletter archives][site]. 7 | 8 | 9 | [site]: https://serverless-training.com 10 | -------------------------------------------------------------------------------- /services/usage-custom-resources/README.md: -------------------------------------------------------------------------------- 1 | # Example for Using CloudFormation Custom Resources 2 | 3 | When you deploy [the custom-resources service][crsvc], it creates a Lambda function that 4 | can be used as a service token in other CloudFormation stacks that need to create custom 5 | resources. 6 | 7 | This "service" demonstrates the use of one of the custom resources. You can copy this 8 | example and modify your template to use any of the other custom resources. 9 | 10 | [crsvc]: ../custom-resources/ 11 | -------------------------------------------------------------------------------- /services/custom-resources/README.md: -------------------------------------------------------------------------------- 1 | # CloudFormation Custom Resources 2 | 3 | This "service" deploys a set of CloudFormation custom resources that can be used by other 4 | examples in this repo. 5 | 6 | The custom resources themselves are implemented as an open-source project: 7 | https://github.com/silvermine/cloudformation-custom-resources 8 | 9 | Check out [my tutorial on setting up CloudFormation custom resources][tut] for more info. 10 | 11 | [tut]: https://serverless-training.com/articles/how-to-set-up-cloudformation-custom-resources/ 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" # Latest node version 4 | - "lts/*" # Latest LTS version 5 | - "10" 6 | - "8" 7 | - "8.10" 8 | - "6" 9 | 10 | before_install: if [[ `npm -v` != 6* ]]; then npm i -g npm@6.4.1; fi 11 | 12 | script: 13 | - node --version 14 | - npm --version 15 | - grunt standards 16 | # We don't have any tests defined for this project yet. 17 | # - npm test 18 | 19 | # For code coverage: 20 | after_success: 21 | cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 22 | -------------------------------------------------------------------------------- /services/custom-resources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-resources", 3 | "version": "0.0.0", 4 | "description": "CloudFormation custom resources", 5 | "main": "src/CustomResourceHandler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": { 10 | "name": "Jeremy Thomerson", 11 | "email": "jeremy@thomersonfamily.com", 12 | "url": "https://serverless-training.com" 13 | }, 14 | "license": "MIT", 15 | "dependencies": { 16 | "@silvermine/cloudformation-custom-resources": "1.0.0-alpha.1", 17 | "aws-sdk": "2.373.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | var config; 6 | 7 | config = { 8 | js: { 9 | all: [ 'Gruntfile.js', 'services/**/*.js', '!**/node_modules/**/*', '!**/coverage/**/*' ], 10 | }, 11 | }; 12 | 13 | grunt.initConfig({ 14 | 15 | pkg: grunt.file.readJSON('package.json'), 16 | 17 | eslint: { 18 | target: config.js.all, 19 | }, 20 | 21 | }); 22 | 23 | grunt.loadNpmTasks('grunt-eslint'); 24 | 25 | grunt.registerTask('standards', [ 'eslint' ]); 26 | grunt.registerTask('default', [ 'standards' ]); 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-training-examples", 3 | "version": "0.0.0", 4 | "description": "Examples for use on https://serverless-training.com", 5 | "main": "Gruntfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jthomerson/serverless-training-examples.git" 12 | }, 13 | "keywords": [ 14 | "serverless", 15 | "examples" 16 | ], 17 | "author": "Jeremy Thomerson", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/jthomerson/serverless-training-examples/issues" 21 | }, 22 | "homepage": "https://serverless-training.com", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "@silvermine/eslint-config": "2.0.0-preview.2", 26 | "coveralls": "3.0.2", 27 | "eslint": "5.10.0", 28 | "expect.js": "0.3.1", 29 | "grunt": "1.0.3", 30 | "grunt-eslint": "21.0.0", 31 | "istanbul": "0.4.5", 32 | "mocha": "5.2.0", 33 | "mocha-lcov-reporter": "1.3.0", 34 | "serverless": "1.34.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Jeremy Thomerson 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /services/echo-api/src/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.echo = async(evt) => { 4 | // By default we set up the response object for an APIGW response. 5 | let resp = { statusCode: 200, headers: { 'Content-Type': 'application/json' } }, 6 | addl = { integrationType: 'apigw', message: 'This is an API Gateway request' }; 7 | 8 | // If our function is getting invoked from an Application Load Balancer (ALB), then the 9 | // response format is slightly different. See [TODO: insert link to training site 10 | // article here] for more details. 11 | if (evt.requestContext && evt.requestContext.elb) { 12 | resp = { ...resp, ...{ statusDescription: '200 OK', isBase64Encoded: false } }; 13 | addl = { integrationType: 'alb', message: 'This is an Application Load Balancer request' }; 14 | // Similarly, if the ALB is configured to _send_ you multi-value headers for the 15 | // request, then you must also _respond_ with multi-value headers, so we make this 16 | // simple map function. 17 | if (evt.multiValueHeaders) { 18 | resp.multiValueHeaders = Object.keys(resp.headers).reduce((memo, k) => { 19 | memo[k] = [ resp.headers[k] ]; 20 | 21 | return memo; 22 | }, {}); 23 | } 24 | } 25 | 26 | resp.body = JSON.stringify({ 27 | ...addl, 28 | requestEvent: evt, 29 | }); 30 | 31 | return resp; 32 | }; 33 | -------------------------------------------------------------------------------- /services/echo-api/serverless.yml: -------------------------------------------------------------------------------- 1 | service: echo-api 2 | 3 | # Note: I write these examples using the latest version of Serverless. I pin the example 4 | # to that version so that I know it will work for you even if you find it a year later. 5 | # Likely, you can remove this line and use the example with any recent version of 6 | # Serverless. Give it a shot if you're using a different version. 7 | frameworkVersion: "=1.34.1" 8 | 9 | custom: 10 | defaultRegion: us-east-1 11 | region: ${opt:region, self:custom.defaultRegion} 12 | stage: ${opt:stage, env:USER} 13 | objectPrefix: '${self:service}-${self:custom.stage}' 14 | 15 | provider: 16 | name: aws 17 | runtime: nodejs8.10 18 | stackTags: # NOTE: STAGE is automatically added by SLS 19 | SLS_SVC_NAME: ${self:service} 20 | region: ${self:custom.region} 21 | stage: ${self:custom.stage} 22 | 23 | functions: 24 | echo: 25 | handler: src/handler.echo 26 | events: 27 | - http: 'ANY /' 28 | - http: 'ANY {proxy+}' 29 | 30 | resources: 31 | Outputs: 32 | # We export the ARN of the `echo` Lambda function so that it can be referenced by 33 | # the application-load-balancer-lambda service, which needs it in order to associate 34 | # the function to the ALB target group, and to add permissions to the function so 35 | # that the ALB service has the permissions it needs to invoke the function. 36 | EchoLambdaFunction: 37 | Value: { 'Fn::GetAtt': [ 'EchoLambdaFunction', 'Arn' ] } 38 | Export: { Name: '${self:custom.objectPrefix}-EchoLambdaFunction' } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/vim,node,macos,serverless 2 | 3 | ### macOS ### 4 | *.DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Thumbnails 9 | ._* 10 | 11 | # Files that might appear in the root of a volume 12 | .DocumentRevisions-V100 13 | .fseventsd 14 | .Spotlight-V100 15 | .TemporaryItems 16 | .Trashes 17 | .VolumeIcon.icns 18 | .com.apple.timemachine.donotpresent 19 | 20 | # Directories potentially created on remote AFP share 21 | .AppleDB 22 | .AppleDesktop 23 | Network Trash Folder 24 | Temporary Items 25 | .apdisk 26 | 27 | ### Node ### 28 | # Logs 29 | logs 30 | *.log 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # Runtime data 36 | pids 37 | *.pid 38 | *.seed 39 | *.pid.lock 40 | 41 | # Directory for instrumented libs generated by jscoverage/JSCover 42 | lib-cov 43 | 44 | # Coverage directory used by tools like istanbul 45 | coverage 46 | 47 | # nyc test coverage 48 | .nyc_output 49 | 50 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 51 | .grunt 52 | 53 | # Bower dependency directory (https://bower.io/) 54 | bower_components 55 | 56 | # node-waf configuration 57 | .lock-wscript 58 | 59 | # Compiled binary addons (http://nodejs.org/api/addons.html) 60 | build/Release 61 | 62 | # Dependency directories 63 | node_modules/ 64 | jspm_packages/ 65 | 66 | # Typescript v1 declaration files 67 | typings/ 68 | 69 | # Optional npm cache directory 70 | .npm 71 | 72 | # Optional eslint cache 73 | .eslintcache 74 | 75 | # Optional REPL history 76 | .node_repl_history 77 | 78 | # Output of 'npm pack' 79 | *.tgz 80 | 81 | # Yarn Integrity file 82 | .yarn-integrity 83 | 84 | # dotenv environment variables file 85 | .env 86 | 87 | 88 | ### Serverless ### 89 | # Ignore build directory 90 | .serverless 91 | 92 | ### Vim ### 93 | # swap 94 | .sw[a-p] 95 | .*.sw[a-p] 96 | # session 97 | Session.vim 98 | # temporary 99 | .netrwhist 100 | *~ 101 | # auto-generated tag files 102 | tags 103 | 104 | # End of https://www.gitignore.io/api/vim,node,macos,serverless 105 | -------------------------------------------------------------------------------- /services/application-load-balancer-lambda/README.md: -------------------------------------------------------------------------------- 1 | # Example Using Application Load Balancer With API Gateway 2 | 3 | NOTE: This service requires the use of CloudFormation custom resources because when this 4 | was written, CloudFormation had not yet been updated to support the Lambda/ALB 5 | integration. You can deploy [the custom-resources service][crsvc] so that your account has 6 | the necessary custom resources available. 7 | 8 | ## Purpose of Example 9 | 10 | This service creates an application load balancer that integrates with Lambda to provide a 11 | replacement for API Gateway. 12 | 13 | **See [my how-to set up Application Load Balancer with Lambda tutorial] for more details.** 14 | 15 | 16 | ## Please Note 17 | 18 | Here are a few important notes about using this stack. 19 | 20 | ### This Stack Costs Money When Idle 21 | 22 | **This stack will cost you money. Be sure to remove it when you are done testing.** Unlike 23 | most serverless stacks in this repo which will not cost you money when they are idle, this 24 | stack uses [AWS' Application Load Balancer][ALB], which has [pricing][ALBPricing] that 25 | includes per-hour charges (at time of writing, _$0.0225 per Application Load Balancer-hour 26 | (or partial hour)_ plus some other usage-related charges). **Thus, if you leave the stack 27 | set up in your account, you will start accruing charges (approx. $16 per month at this 28 | time)**. 29 | 30 | ### How-to Remove the Stack 31 | 32 | **To remove the stack, you must first empty the bucket that the Application Load Balancer 33 | uses to write its logs.** Here's how to do that: 34 | 35 | ``` 36 | # Fill in the region that you deployed the service to. 37 | # We're assuming you're using the default stage name, which is your username. If you're 38 | # not, then replace the `$(whoami)` with your stage name. 39 | aws s3 rm --recursive s3://alb-lambda-${REGION}-$(whoami)-logs/alb/ 40 | ``` 41 | 42 | ### Building the VPC Inside ALB Stack 43 | 44 | This example stack builds a VPC inside the same stack that the load balancer is built in. 45 | Normally, you wouldn't do that; you'd create a separate stack for your VPC. We're only 46 | combining them into a single stack here to simplify the example and get you up-and-running 47 | quicker. If you separate the VPC into its own stack, you can use CloudFormation stack 48 | exports to export the values from the VPC stack that you'll need to import using 49 | `Fn::ImportValue` in this stack (replacing places where the example uses `Ref`). 50 | 51 | 52 | [crsvc]: ../custom-resources/ 53 | [ALB]: https://aws.amazon.com/elasticloadbalancing/ 54 | [ALBPricing]: https://aws.amazon.com/elasticloadbalancing/pricing/ 55 | [tut]: https://serverless-training.com/articles/how-to-set-up-application-load-balancer-with-lambda/ 56 | -------------------------------------------------------------------------------- /services/usage-custom-resources/serverless.yml: -------------------------------------------------------------------------------- 1 | service: usage-custom-resources 2 | 3 | # Note: I write these examples using the latest version of Serverless. I pin the example 4 | # to that version so that I know it will work for you even if you find it a year later. 5 | # Likely, you can remove this line and use the example with any recent version of 6 | # Serverless. Give it a shot if you're using a different version. 7 | frameworkVersion: "=1.34.1" 8 | 9 | custom: 10 | defaultRegion: us-east-1 11 | region: ${opt:region, self:custom.defaultRegion} 12 | stage: ${opt:stage, env:USER} 13 | objectPrefix: '${self:service}-${self:custom.stage}' 14 | # NOTE: this caller reference value must be unique across the account, meaning it can 15 | # not be used in another stack. Thus, we put a default value here, but also allow it to 16 | # be overridden when the project is packaged or deployed (e.g. `sls deploy 17 | # --callerReference 1234`) so that multiple stacks could be deployed simultaneously. As 18 | # a user, you could also externalize this value and/or define a value per-stage. 19 | defaultAccessIdentityCallerReference: 9999 20 | accessIdentityCallerReference: ${opt:callerReference, self:custom.defaultAccessIdentityCallerReference} 21 | 22 | provider: 23 | name: aws 24 | runtime: nodejs8.10 25 | stackTags: # NOTE: STAGE is automatically added by SLS 26 | SLS_SVC_NAME: ${self:service} 27 | region: ${self:custom.region} 28 | stage: ${self:custom.stage} 29 | 30 | resources: 31 | Outputs: 32 | CloudFrontOriginAccessIdentityID: 33 | Description: The ID of our CloudFront Origin Access Identity - to be used by CloudFront and S3 for securing S3 website buckets. 34 | Value: { 'Ref': 'CloudFrontOriginAccessIdentity' } 35 | Export: 36 | Name: '${self:custom.objectPrefix}-CloudFrontOriginAccessIdentity-ID' 37 | CloudFrontOriginAccessIdentityS3CanonicalUserID: 38 | Description: The S3 canonical user ID of our CloudFront Origin Access Identity - to be used by CloudFront and S3 for securing S3 website buckets. 39 | Value: { 'Fn::GetAtt': [ 'CloudFrontOriginAccessIdentity', 'S3CanonicalUserId' ] } 40 | Export: 41 | Name: '${self:custom.objectPrefix}-CloudFrontOriginAccessIdentity-S3CanonicalUserID' 42 | Resources: 43 | CloudFrontOriginAccessIdentity: 44 | Type: 'Custom::CloudFrontOriginAccessIdentity' 45 | Properties: 46 | # The key to using custom resources is providing a "ServiceToken", which is 47 | # really an ARN that points to a Lambda function that should get invoked any 48 | # time CloudFormation needs to create, update, or delete your custom resource. 49 | # The properties you define here will get passed to the Lambda function so it 50 | # can take the appropriate action to create, update, or delete the resource. 51 | # In this case the service token points to the Lambda function that's deployed 52 | # as part of the `custom-resources` service (see `../custom-resources/`). 53 | # We're using `Fn::ImportValue` to pull in the ARN that is exported by the 54 | # CloudFormation stack for `custom-resources`. 55 | ServiceToken: { 'Fn::ImportValue': 'cloudformation-custom-resources-${self:custom.stage}-ServiceToken' } 56 | CallerReference: ${self:custom.accessIdentityCallerReference} 57 | Comment: '${self:custom.objectPrefix}' 58 | -------------------------------------------------------------------------------- /services/custom-resources/serverless.yml: -------------------------------------------------------------------------------- 1 | service: cloudformation-custom-resources 2 | 3 | # Note: I write these examples using the latest version of Serverless. I pin the example 4 | # to that version so that I know it will work for you even if you find it a year later. 5 | # Likely, you can remove this line and use the example with any recent version of 6 | # Serverless. Give it a shot if you're using a different version. 7 | frameworkVersion: "=1.34.1" 8 | 9 | custom: 10 | defaultRegion: us-east-1 11 | region: ${opt:region, self:custom.defaultRegion} 12 | stage: ${opt:stage, env:USER} 13 | objectPrefix: '${self:service}-${self:custom.stage}' 14 | 15 | package: 16 | exclude: 17 | - 'tests/**' 18 | 19 | provider: 20 | name: aws 21 | runtime: nodejs6.10 22 | stackTags: # NOTE: STAGE is automatically added by SLS 23 | SLS_SVC_NAME: ${self:service} 24 | region: ${self:custom.region} 25 | stage: ${self:custom.stage} 26 | environment: 27 | SLS_SVC_NAME: ${self:service} 28 | SLS_STAGE: ${self:custom.stage} 29 | iamRoleStatements: 30 | # Permissions needed for various services: 31 | # We know that APIGatewayDomainName requires this, and likely other resources do as 32 | # well. See 33 | # https://aws.amazon.com/blogs/security/introducing-an-easier-way-to-delegate-permissions-to-aws-services-service-linked-roles/ 34 | - 35 | Effect: 'Allow' 36 | Action: 37 | - 'iam:CreateServiceLinkedRole' 38 | Resource: 39 | - '*' 40 | # Permissions needed for SNSSQSSubscription: 41 | - 42 | Effect: 'Allow' 43 | Action: 44 | - 'sns:Subscribe' 45 | - 'sns:Unsubscribe' 46 | Resource: 47 | - '*' 48 | # Permissions needed for CloudFrontOriginAccessIdentity: 49 | - 50 | Effect: 'Allow' 51 | Action: 52 | - 'cloudfront:CreateCloudFrontOriginAccessIdentity' 53 | - 'cloudfront:DeleteCloudFrontOriginAccessIdentity' 54 | - 'cloudfront:GetCloudFrontOriginAccessIdentity' 55 | - 'cloudfront:GetCloudFrontOriginAccessIdentityConfig' 56 | - 'cloudfront:ListCloudFrontOriginAccessIdentities' 57 | - 'cloudfront:UpdateCloudFrontOriginAccessIdentity' 58 | Resource: 59 | - '*' 60 | # Permissions needed for DynamoDBGlobalTable: 61 | - 62 | Effect: 'Allow' 63 | Action: 64 | - 'dynamodb:DescribeTable' 65 | - 'dynamodb:DescribeGlobalTable' 66 | - 'dynamodb:CreateTable' 67 | - 'dynamodb:CreateGlobalTable' 68 | - 'dynamodb:UpdateTable' 69 | - 'dynamodb:UpdateGlobalTable' 70 | - 'dynamodb:DeleteTable' 71 | - 'dynamodb:ListTagsOfResource' 72 | - 'dynamodb:TagResource' 73 | Resource: 74 | - '*' 75 | # Permissions needed for SimpleEmailServiceDomainVerification: 76 | - 77 | Effect: 'Allow' 78 | Action: 79 | - 'ses:VerifyDomainIdentity' 80 | - 'ses:DeleteIdentity' 81 | Resource: 82 | - '*' 83 | # Permissions needed for SimpleEmailServiceRuleSetActivation: 84 | - 85 | Effect: 'Allow' 86 | Action: 87 | - 'ses:SetActiveReceiptRuleSet' 88 | Resource: 89 | - '*' 90 | # Permissions needed for APIGatewayDomainName: 91 | - 92 | Effect: 'Allow' 93 | Action: 94 | - 'apigateway:*' 95 | Resource: 96 | - '*' 97 | # Permissions needed for ELBTargetGroup: 98 | - 99 | Effect: 'Allow' 100 | Action: 101 | - 'elasticloadbalancing:CreateTargetGroup' 102 | - 'elasticloadbalancing:DeleteTargetGroup' 103 | - 'elasticloadbalancing:ModifyTargetGroup' 104 | - 'elasticloadbalancing:ModifyTargetGroupAttributes' 105 | Resource: 106 | - '*' 107 | # Permissions needed for ELBTargetGroupLambdaTarget: 108 | - 109 | Effect: 'Allow' 110 | Action: 111 | - 'elasticloadbalancing:RegisterTargets' 112 | - 'elasticloadbalancing:DeregisterTargets' 113 | Resource: 114 | - '*' 115 | 116 | functions: 117 | customResources: 118 | name: ${self:custom.objectPrefix} 119 | handler: node_modules/@silvermine/cloudformation-custom-resources/src/CustomResourceHandler.handler 120 | memorySize: 256 121 | timeout: 300 122 | 123 | resources: 124 | Outputs: 125 | CustomResourcesServiceToken: 126 | Description: The ARN of the custom resources Lambda function to use as a service token when using a custom resource. 127 | Value: { 'Fn::GetAtt': [ 'CustomResourcesLambdaFunction', 'Arn' ] } 128 | Export: 129 | Name: '${self:custom.objectPrefix}-ServiceToken' 130 | -------------------------------------------------------------------------------- /services/custom-resources/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-resources", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@silvermine/cloudformation-custom-resources": { 8 | "version": "1.0.0-alpha.1", 9 | "resolved": "https://registry.npmjs.org/@silvermine/cloudformation-custom-resources/-/cloudformation-custom-resources-1.0.0-alpha.1.tgz", 10 | "integrity": "sha512-Ap7zCF3HU/NvFjpm8WebmSe0f5bq9OBkjMKDZq2i2h0N2NO64nctOrG2+YRczf3PLUqKuJPDO2uTqa9qpWvtFg==", 11 | "requires": { 12 | "class.extend": "0.9.2", 13 | "q": "1.5.1", 14 | "silvermine-lambda-utils": "git+https://github.com/silvermine/lambda-utils.git#8929f5531db49f7364de7a5e3f9bb8dabee8896e", 15 | "underscore": "1.9.1" 16 | } 17 | }, 18 | "aws-sdk": { 19 | "version": "2.373.0", 20 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.373.0.tgz", 21 | "integrity": "sha512-NZYXwXGtFt9jxaKXc+PJsLPnpbD03t0MAZRxh93g36kbFMuRXtY8CDqHYNQ0ZcrgQpXbCQiz1fxT5/wu5Cu70g==", 22 | "requires": { 23 | "buffer": "4.9.1", 24 | "events": "1.1.1", 25 | "ieee754": "1.1.8", 26 | "jmespath": "0.15.0", 27 | "querystring": "0.2.0", 28 | "sax": "1.2.1", 29 | "url": "0.10.3", 30 | "uuid": "3.1.0", 31 | "xml2js": "0.4.19" 32 | } 33 | }, 34 | "base64-js": { 35 | "version": "1.3.0", 36 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", 37 | "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" 38 | }, 39 | "buffer": { 40 | "version": "4.9.1", 41 | "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 42 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 43 | "requires": { 44 | "base64-js": "^1.0.2", 45 | "ieee754": "^1.1.4", 46 | "isarray": "^1.0.0" 47 | } 48 | }, 49 | "class.extend": { 50 | "version": "0.9.2", 51 | "resolved": "https://registry.npmjs.org/class.extend/-/class.extend-0.9.2.tgz", 52 | "integrity": "sha1-4cvaNpfLMdPNqxwAHSaQdckDH3g=" 53 | }, 54 | "events": { 55 | "version": "1.1.1", 56 | "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", 57 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 58 | }, 59 | "ieee754": { 60 | "version": "1.1.8", 61 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", 62 | "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" 63 | }, 64 | "isarray": { 65 | "version": "1.0.0", 66 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 67 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 68 | }, 69 | "jmespath": { 70 | "version": "0.15.0", 71 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 72 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 73 | }, 74 | "punycode": { 75 | "version": "1.3.2", 76 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 77 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 78 | }, 79 | "q": { 80 | "version": "1.5.1", 81 | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", 82 | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" 83 | }, 84 | "querystring": { 85 | "version": "0.2.0", 86 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 87 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 88 | }, 89 | "sax": { 90 | "version": "1.2.1", 91 | "resolved": "http://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 92 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 93 | }, 94 | "silvermine-lambda-utils": { 95 | "version": "git+https://github.com/silvermine/lambda-utils.git#8929f5531db49f7364de7a5e3f9bb8dabee8896e", 96 | "from": "git+https://github.com/silvermine/lambda-utils.git#8929f5531db49f7364de7a5e3f9bb8dabee8896e" 97 | }, 98 | "underscore": { 99 | "version": "1.9.1", 100 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", 101 | "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" 102 | }, 103 | "url": { 104 | "version": "0.10.3", 105 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 106 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 107 | "requires": { 108 | "punycode": "1.3.2", 109 | "querystring": "0.2.0" 110 | } 111 | }, 112 | "uuid": { 113 | "version": "3.1.0", 114 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 115 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" 116 | }, 117 | "xml2js": { 118 | "version": "0.4.19", 119 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 120 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 121 | "requires": { 122 | "sax": ">=0.6.0", 123 | "xmlbuilder": "~9.0.1" 124 | } 125 | }, 126 | "xmlbuilder": { 127 | "version": "9.0.7", 128 | "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 129 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /services/application-load-balancer-lambda/serverless.yml: -------------------------------------------------------------------------------- 1 | service: alb-lambda 2 | 3 | # Note: I write these examples using the latest version of Serverless. I pin the example 4 | # to that version so that I know it will work for you even if you find it a year later. 5 | # Likely, you can remove this line and use the example with any recent version of 6 | # Serverless. Give it a shot if you're using a different version. 7 | frameworkVersion: "=1.34.1" 8 | 9 | custom: 10 | defaultRegion: us-east-1 11 | region: ${opt:region, self:custom.defaultRegion} 12 | stage: ${opt:stage, env:USER} 13 | objectPrefix: '${self:service}-${self:custom.stage}' 14 | withRegionObjectPrefix: '${self:service}-${self:custom.region}-${self:custom.stage}' 15 | 16 | provider: 17 | name: aws 18 | runtime: nodejs8.10 19 | stackTags: # NOTE: STAGE is automatically added by SLS 20 | SLS_SVC_NAME: ${self:service} 21 | region: ${self:custom.region} 22 | stage: ${self:custom.stage} 23 | 24 | resources: 25 | Outputs: 26 | LoadBalancerDNSName: 27 | Value: { 'Fn::GetAtt': [ LoadBalancer, 'DNSName' ] } 28 | Export: { Name: '${self:custom.objectPrefix}-LoadBalancerDNSName' } 29 | Resources: 30 | # These resources are pre-requisites for creating the Application Load Balancer: 31 | # First, a VPC that has a small subnet in two availability zones. Since we're not 32 | # actually deploying any resources into the VPC, the setup is very simple. 33 | VPC: 34 | Type: 'AWS::EC2::VPC' 35 | Properties: 36 | CidrBlock: 172.31.0.0/16 37 | EnableDnsHostnames: true 38 | InternetGateway: 39 | Type: 'AWS::EC2::InternetGateway' 40 | VPCGatewayAttachment: 41 | Type: 'AWS::EC2::VPCGatewayAttachment' 42 | Properties: 43 | VpcId: { Ref: VPC } 44 | InternetGatewayId: { Ref: InternetGateway } 45 | RouteTable: 46 | Type: 'AWS::EC2::RouteTable' 47 | Properties: 48 | VpcId: { Ref: VPC } 49 | InternetRoute: 50 | Type: 'AWS::EC2::Route' 51 | DependsOn: VPCGatewayAttachment 52 | Properties: 53 | DestinationCidrBlock: 0.0.0.0/0 54 | GatewayId: { Ref: InternetGateway } 55 | RouteTableId: { Ref: RouteTable } 56 | SubnetA: 57 | Type: 'AWS::EC2::Subnet' 58 | Properties: 59 | AvailabilityZone: '${self:custom.region}a' 60 | CidrBlock: 172.31.0.0/20 61 | MapPublicIpOnLaunch: false 62 | VpcId: { Ref: VPC } 63 | SubnetB: 64 | Type: 'AWS::EC2::Subnet' 65 | Properties: 66 | AvailabilityZone: '${self:custom.region}b' 67 | CidrBlock: 172.31.16.0/20 68 | MapPublicIpOnLaunch: false 69 | VpcId: { Ref: VPC } 70 | SubnetARouteTableAssociation: 71 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 72 | Properties: 73 | SubnetId: { Ref: SubnetA } 74 | RouteTableId: { Ref: RouteTable } 75 | SubnetBRouteTableAssociation: 76 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 77 | Properties: 78 | SubnetId: { Ref: SubnetB } 79 | RouteTableId: { Ref: RouteTable } 80 | SecurityGroup: 81 | Type: 'AWS::EC2::SecurityGroup' 82 | Properties: 83 | GroupName: 'http-https' 84 | GroupDescription: 'HTTPS/HTTPS inbound; Nothing outbound' 85 | VpcId: { Ref: VPC } 86 | SecurityGroupIngress: 87 | - 88 | IpProtocol: tcp 89 | FromPort: '80' 90 | ToPort: '80' 91 | CidrIp: 0.0.0.0/0 92 | - 93 | IpProtocol: tcp 94 | FromPort: '443' 95 | ToPort: '443' 96 | CidrIp: 0.0.0.0/0 97 | SecurityGroupEgress: 98 | - 99 | # We don't need any egress traffic, but if we don't specify a rule, AWS 100 | # will add the default "allow all" rule, so here we allow "egress" only 101 | # to localhost. The ELB does not seem to require an egress rule for it 102 | # to be able to connect to AWS resources like Lambda. 103 | IpProtocol: -1 104 | FromPort: '1' 105 | ToPort: '1' 106 | CidrIp: 127.0.0.1/32 107 | # And now we create a bucket for the ALB logs to go into: 108 | AWSServiceLoggingBucket: 109 | Type: 'AWS::S3::Bucket' 110 | Properties: 111 | BucketName: '${self:custom.withRegionObjectPrefix}-logs' 112 | AccessControl: 'LogDeliveryWrite' 113 | LifecycleConfiguration: 114 | Rules: 115 | - { Status: 'Enabled', ExpirationInDays: 45 } 116 | # And give access for the ALB to write to it: 117 | AWSServiceLoggingBucketPolicy: 118 | Type: 'AWS::S3::BucketPolicy' 119 | Properties: 120 | Bucket: { Ref: AWSServiceLoggingBucket } 121 | PolicyDocument: 122 | Statement: 123 | - 124 | Effect: 'Allow' 125 | Action: 's3:PutObject' 126 | Resource: { 'Fn::Join': [ '', [ 'arn:aws:s3:::', { Ref: AWSServiceLoggingBucket }, '/alb/*' ] ] } 127 | Principal: 128 | AWS: 129 | # Note that these are account IDs for AWS-owned accounts used 130 | # by Elastic Load Balancer. I've included all of the ones 131 | # currently listed in their documentation (except Gov and 132 | # China) so that you can deploy this service to any region you 133 | # want. However, I'd recommend only listing the one(s) you need 134 | # for the region(s) you use. 135 | # See https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-access-logs.html 136 | - 127311923021 137 | - 033677994240 138 | - 027434742980 139 | - 797873946194 140 | - 985666609251 141 | - 054676820928 142 | - 156460612806 143 | - 652711504416 144 | - 009996457667 145 | - 582318560864 146 | - 600734575887 147 | - 383597477331 148 | - 114774131450 149 | - 783225319266 150 | - 718504428378 151 | - 507241528517 152 | # Now we get to creating the actual Application Load Balancer and the associated 153 | # pieces. 154 | LoadBalancer: 155 | Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' 156 | Properties: 157 | Type: 'application' 158 | Name: '${self:custom.objectPrefix}' 159 | IpAddressType: 'ipv4' 160 | Scheme: 'internet-facing' 161 | LoadBalancerAttributes: 162 | - { Key: 'deletion_protection.enabled', Value: false } 163 | # TODO: add a note here about HTTP/2 164 | - { Key: 'routing.http2.enabled', Value: false } 165 | - { Key: 'access_logs.s3.enabled', Value: true } 166 | - { Key: 'access_logs.s3.bucket', Value: { Ref: AWSServiceLoggingBucket } } 167 | - { Key: 'access_logs.s3.prefix', Value: 'alb/${self:custom.objectPrefix}' } 168 | SecurityGroups: 169 | - { Ref: SecurityGroup } 170 | Subnets: 171 | - { Ref: SubnetA } 172 | - { Ref: SubnetB } 173 | # Each target group can have one Lambda function associated with it. Rules determine 174 | # which target group (and thus which function) is used for each request (based on 175 | # request domain and path) sent to the ALB. 176 | TargetGroup: 177 | Type: 'Custom::ELBTargetGroup' 178 | Properties: 179 | ServiceToken: { 'Fn::ImportValue': 'cloudformation-custom-resources-${self:custom.stage}-ServiceToken' } 180 | Name: '${self:service}-${self:custom.stage}-tg1' 181 | TargetGroupAttributes: [ { Key: 'lambda.multi_value_headers.enabled', Value: true } ] 182 | # You have to grant permission to Elastic Load Balancing to invoke your Lambda 183 | # function. 184 | InvokePermission: 185 | Type: 'AWS::Lambda::Permission' 186 | Properties: 187 | Action: 'lambda:InvokeFunction' 188 | FunctionName: { 'Fn::ImportValue': 'echo-api-${self:custom.stage}-EchoLambdaFunction' } 189 | Principal: 'elasticloadbalancing.amazonaws.com' 190 | SourceArn: { Ref: TargetGroup } 191 | # And finally you associate the Lambda function "target" with the target group, and 192 | # thus, the Application Load Balancer 193 | Target: 194 | Type: 'Custom::ELBTargetGroupLambdaTarget' 195 | # This is important because if you try to associate the function as a target 196 | # _before_ the permission exists on the function, an error will be thrown. 197 | DependsOn: InvokePermission 198 | Properties: 199 | ServiceToken: { 'Fn::ImportValue': 'cloudformation-custom-resources-${self:custom.stage}-ServiceToken' } 200 | TargetGroupArn: { Ref: TargetGroup } 201 | TargetFunctionArn: { 'Fn::ImportValue': 'echo-api-${self:custom.stage}-EchoLambdaFunction' } 202 | HTTPListener: 203 | Type: 'AWS::ElasticLoadBalancingV2::Listener' 204 | Properties: 205 | LoadBalancerArn: { Ref: LoadBalancer } 206 | Port: 80 207 | Protocol: 'HTTP' 208 | DefaultActions: 209 | - 210 | Type: 'fixed-response' 211 | Order: 1 212 | FixedResponseConfig: 213 | StatusCode: 404 214 | ContentType: 'application/json' 215 | MessageBody: '${file(./static-error-responses/not-found.txt)}' 216 | ListenerRule: 217 | Type: 'AWS::ElasticLoadBalancingV2::ListenerRule' 218 | Properties: 219 | ListenerArn: { Ref: HTTPListener } 220 | Priority: 1 221 | Actions: [ { Type: 'forward', Order: 1, TargetGroupArn: { Ref: TargetGroup } } ] 222 | Conditions: [ { Field: 'path-pattern', Values: [ '/echo/*' ] } ] 223 | --------------------------------------------------------------------------------