├── .gitignore ├── LICENSE.md ├── README.md ├── api ├── posts │ ├── postsByUser.js │ ├── postsCreate.js │ ├── postsList.js │ ├── serverless.yaml │ └── webpack.config.js └── users │ ├── serverless.yaml │ ├── token.js │ ├── usersCreate.js │ └── webpack.config.js ├── config.yaml ├── generalAuthorizer.js ├── package-lock.json ├── package.json └── serverless.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .serverless 2 | .webpack 3 | node_modules 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 GorillaStack Pty Ltd 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Split serverless stack - postsapi example 2 | 3 | This repository provides an example of using the [serverless framework](https://serverless.com) with split stacks 4 | i.e. splitting the CloudFormation resources across multiple stacks. 5 | 6 | It is the accompaniment for the [blog post found here](https://www.gorillastack.com/news/splitting-your-serverless-framework-api-on-aws) explaining how to split serverless stacks. 7 | 8 | ## Layout 9 | 10 | - `/serverless.yaml` - the base stack (contains API Gateway, authoriser, IAM role and shared path resources) 11 | - `/api/users/serverless.yaml` - the stack for users management 12 | - `/api/posts/serverless.yaml` - the stack for posts management 13 | 14 | ## Deploying 15 | 16 | You will need: 17 | - [nodejs 12](https://nodejs.org) or higher 18 | - an AWS account 19 | - [the awscli tool](https://aws.amazon.com/cli/) 20 | - serverless CLI tool (either installed globally i.e. `npm install -g serverless` or locally) 21 | 22 | _NOTE:_ It is not recommended to use this example in production - the storage of the JWT encryption secret is 23 | in plaintext, which is not secure. As an exercise, consider re-implementing using AWS Systems Manager Parameter Store to store the secret and then retrieving it upon startup :-) 24 | 25 | Steps: 26 | 1. First change the secret in `config.yaml` that is used to sign and verify the token so it is a random string. 27 | 2. Install the npm modules 28 | 29 | ```bash 30 | npm install 31 | ``` 32 | 33 | 3. Deploy the base stack 34 | 35 | ```bash 36 | # (optional) set the AWS_PROFILE variable as per the setup of your ~/.aws/credentials file. 37 | # This file is set with a default profile when you previously run `aws configure` and specified 38 | # your access key id and secret key 39 | export AWS_PROFILE=myprofile # or 'Set-Item env:AWS_PROFILE myprofile' in Powershell 40 | 41 | serverless deploy --stage dev 42 | 43 | # you can also run `npm bin`/serverless in bash/zsh to pick up the locally installed copy 44 | ``` 45 | 46 | 4. Change into the subdirectories and deploy the other stacks (using the same 47 | stage name): 48 | 49 | ```bash 50 | cd api/users 51 | serverless deploy --stage dev 52 | 53 | cd ../../ 54 | 55 | cd api/posts 56 | serverless deploy --stage dev 57 | ``` 58 | 59 | ## Usage 60 | 61 | (Not all the endpoints from the blog post are implemented, but a basic flow is possible.) 62 | 63 | Once you've deployed the API, you should get the endpoints printed to the console by serverless e.g.: 64 | 65 | ``` 66 | ....................................... 67 | Serverless: Stack update finished... 68 | Service Information 69 | service: postsapi-users 70 | stage: test 71 | region: us-east-1 72 | stack: postsapi-users-test 73 | api keys: 74 | None 75 | endpoints: 76 | POST - https://2398u2d9oa.execute-api.us-east-1.amazonaws.com/test/token 77 | POST - https://2398u2d9oa.execute-api.us-east-1.amazonaws.com/test/users 78 | functions: 79 | token: postsapi-users-test-token 80 | usersCreate: postsapi-users-test-usersCreate 81 | ``` 82 | 83 | Replace the endpoint in the following examples (all are using [curl](https://curl.haxx.se/docs/manpage.html), but [Postman](https://www.getpostman.com/) or a similar API testing tool is recommended). 84 | 85 | __Create User__ 86 | 87 | ``` 88 | > curl -XPOST -d '{ "name": "Chris", "password": "TeSTPassWOrd" }' https://2398u2d9oa.execute-api.us-east-1.amazonaws.com/test/users` 89 | 90 | {"userId":"d861a56e-802e-472b-8daf-3005bb2092f3","name":"Chris"} 91 | ``` 92 | 93 | __Get Token__ 94 | 95 | ``` 96 | > curl -XPOST -d '{ "userId": "d861a56e-802e-472b-8daf-3005bb2092f3", "password": "TeSTPassWOrd" }' https://2398u2d9oa.execute-api.us-east-1.amazonaws.com/test/token 97 | 98 | {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkODYxYTU2ZS04MDJlLTQ3MmItOGRhZi0zMDA1YmIyMDkyZjMiLCJpYXQiOjE1MzYyMTg3ODAsImV4cCI6MTUzNjgyMzU4MH0.j7exIrL-Y88KnhRn9WA7AWGMRPna8Ib3t42jEpSN7T4"} 99 | ``` 100 | 101 | __Write Post__ 102 | 103 | ``` 104 | > curl -XPOST -d '{ "text": "This is my extra interesting and short blog post." }' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkODYxYTU2ZS04MDJlLTQ3MmItOGRhZi0zMDA1YmIyMDkyZjMiLCJpYXQiOjE1MzYyMTg3ODAsImV4cCI6MTUzNjgyMzU4MH0.j7exIrL-Y88KnhRn9WA7AWGMRPna8Ib3t42jEpSN7T4' https://2398u2d9oa.execute-api.us-east-1.amazonaws.com/test/posts 105 | 106 | {"postId":"b47f126a-5a1c-4a3e-b1be-dcafaaaac7f6","userId":"d861a56e-802e-472b-8daf-3005bb2092f3","text":"This is my extra interesting and short blog post."} 107 | ``` 108 | 109 | __List Posts__ 110 | 111 | (try creating another user and adding posts as them first - the API should 112 | filter them out) 113 | 114 | ``` 115 | > curl -XGET https://2398u2d9oa.execute-api.ap-southeast-2.amazonaws.com/test/posts 116 | 117 | [{"postId":"b47f126a-5a1c-4a3e-b1be-dcafaaaac7f6","text":"This is my extra interesting and short blog post."}] 118 | ``` 119 | 120 | __List posts by user__ 121 | 122 | ``` 123 | > curl -XGET -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkODYxYTU2ZS04MDJlLTQ3MmItOGRhZi0zMDA1YmIyMDkyZjMiLCJpYXQiOjE1MzYyMTg3ODAsImV4cCI6MTUzNjgyMzU4MH0.j7exIrL-Y88KnhRn9WA7AWGMRPna8Ib3t42jEpSN7T4' https://2398u2d9oa.execute-api.us-east-1.amazonaws.com/test/users/me/posts 124 | 125 | [{"postId":"b47f126a-5a1c-4a3e-b1be-dcafaaaac7f6","userId":"d861a56e-802e-472b-8daf-3005bb2092f3","text":"This is my extra interesting and short blog post."}] 126 | ``` 127 | 128 | ## Writing code 129 | 130 | We've used the [serverless-webpack](https://github.com/serverless-heaven/serverless-webpack) plugin to package code 131 | before deploying it. This permits us to store our node modules in the root directory and not require a package.json 132 | per child-stack. An additional benefit is that only the node modules we require in the code will 133 | be deployed by the child stacks (no dev dependencies are deployed unless they are require'd through the lambda 134 | entry points!). 135 | 136 | We've also configured it externalise the node modules so they don't get 137 | included in the webpack bundle, but instead are copied 'as is'. It 138 | excludes the aws-sdk module, which is added automatically by AWS Lambda. 139 | 140 | NodeJS 12 is used, so its possible to use async/await along with destructuring and other fancy ES6 features. A side effect of using serverless-webpack too is that it is possible to use the babel-loader with webpack to bring in experimental features 141 | or newer JavaScript syntax. See the [serverless webpack website](https://github.com/serverless-heaven/serverless-webpack) 142 | for more details. 143 | 144 | -------------------------------------------------------------------------------- /api/posts/postsByUser.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | module.exports.handler = async event => { 4 | const dc = new AWS.DynamoDB.DocumentClient(); 5 | let nextToken; 6 | let items = []; 7 | do { 8 | const result = await dc.query({ 9 | TableName: process.env.POSTS_TABLE, 10 | ExclusiveStartKey: nextToken, 11 | IndexName: 'userId', 12 | KeyConditionExpression: 'userId = :userId', 13 | ExpressionAttributeValues: { 14 | ':userId': event.requestContext.authorizer.principalId, 15 | }, 16 | }).promise(); 17 | nextToken = result.LastEvaluatedKey; 18 | const retrieved = result.Items.map(item => ({ 19 | postId: item.postId, 20 | text: item.text, 21 | })); 22 | items = items.concat(retrieved); 23 | } while (nextToken); 24 | 25 | return { 26 | statusCode: 200, 27 | body: JSON.stringify(items), 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /api/posts/postsCreate.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid'); 2 | const AWS = require('aws-sdk'); 3 | 4 | const errorResponse = (statusCode, message) => ({ 5 | statusCode, 6 | body: JSON.stringify({ 7 | message, 8 | }) 9 | }); 10 | 11 | module.exports.handler = async event => { 12 | let body; 13 | try { 14 | body = JSON.parse(event.body) 15 | } catch (err) { return errorResponse(400, 'bad body'); } 16 | 17 | if (!body.text) { 18 | return errorResponse(400, 'expected text') 19 | } 20 | 21 | const item = { 22 | postId: uuid.v4(), 23 | userId: event.requestContext.authorizer.principalId, 24 | text: body.text, 25 | }; 26 | 27 | const dc = new AWS.DynamoDB.DocumentClient(); 28 | await dc.put({ 29 | Item: item, 30 | TableName: process.env.POSTS_TABLE, 31 | }).promise(); 32 | return { 33 | statusCode: 200, 34 | body: JSON.stringify(item), 35 | } 36 | }; -------------------------------------------------------------------------------- /api/posts/postsList.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | module.exports.handler = async event => { 4 | const dc = new AWS.DynamoDB.DocumentClient(); 5 | let nextToken; 6 | let items = []; 7 | do { 8 | const result = await dc.scan({ 9 | TableName: process.env.POSTS_TABLE, 10 | ExclusiveStartKey: nextToken, 11 | }).promise(); 12 | nextToken = result.LastEvaluatedKey; 13 | const retrieved = result.Items.map(item => ({ 14 | postId: item.postId, 15 | text: item.text, 16 | })); 17 | items = items.concat(retrieved); 18 | } while (nextToken); 19 | 20 | return { 21 | statusCode: 200, 22 | body: JSON.stringify(items), 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /api/posts/serverless.yaml: -------------------------------------------------------------------------------- 1 | service: postsapi-posts 2 | provider: 3 | name: aws 4 | runtime: nodejs12.x 5 | memorySize: 1024MB 6 | timeout: 10 7 | role: 8 | Fn::ImportValue: postsapi-${opt:stage}-IamRoleLambdaExecution 9 | apiGateway: 10 | restApiId: 11 | Fn::ImportValue: 'postsapi-${opt:stage}-RestApiId' 12 | restApiRootResourceId: 13 | Fn::ImportValue: 'postsapi-${opt:stage}-RootResourceId' 14 | restApiResources: 15 | users: { 'Fn::ImportValue': 'postsapi-${opt:stage}-ApiGatewayResourceUsers' } 16 | users/me: 17 | Fn::ImportValue: 'postsapi-${opt:stage}-ApiGatewayResourceUsersMe' 18 | environment: 19 | USERS_TABLE: 20 | Fn::ImportValue: postsapi-${opt:stage}-UsersTable 21 | POSTS_TABLE: 22 | Fn::ImportValue: postsapi-${opt:stage}-PostsTable 23 | custom: 24 | webpack: 25 | includeModules: 26 | forceExclude: 27 | - aws-sdk 28 | packagePath: ../../package.json 29 | authorizer: 30 | type: CUSTOM 31 | authorizerId: 32 | Fn::ImportValue: postsapi-${opt:stage}-ApiGatewayAuthorizerId 33 | plugins: 34 | - serverless-webpack 35 | package: 36 | include: 37 | - ../../node_modules 38 | functions: 39 | postsCreate: 40 | handler: postsCreate.handler 41 | events: 42 | - http: 43 | method: POST 44 | path: /posts 45 | integration: lambda-proxy 46 | authorizer: ${self:custom.authorizer} 47 | postsList: 48 | handler: postsList.handler 49 | events: 50 | - http: 51 | method: GET 52 | path: /posts 53 | integration: lambda-proxy 54 | # authorizer: ${self:custom.authorizer} 55 | postsByUser: 56 | handler: postsByUser.handler 57 | events: 58 | - http: 59 | method: GET 60 | path: /users/me/posts 61 | integration: lambda-proxy 62 | authorizer: ${self:custom.authorizer} 63 | -------------------------------------------------------------------------------- /api/posts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const slsw = require('serverless-webpack'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | target: 'node', 7 | entry: slsw.lib.entries, 8 | externals: [nodeExternals({ 9 | modulesDir: path.join(__dirname, '../../node_modules'), 10 | })] 11 | }; -------------------------------------------------------------------------------- /api/users/serverless.yaml: -------------------------------------------------------------------------------- 1 | service: postsapi-users 2 | provider: 3 | name: aws 4 | runtime: nodejs12.x 5 | memorySize: 1024MB 6 | timeout: 10 7 | role: 8 | Fn::ImportValue: postsapi-${opt:stage}-IamRoleLambdaExecution 9 | apiGateway: 10 | restApiId: 11 | Fn::ImportValue: 'postsapi-${opt:stage}-RestApiId' 12 | restApiRootResourceId: 13 | Fn::ImportValue: 'postsapi-${opt:stage}-RootResourceId' 14 | restApiResources: 15 | users: { 'Fn::ImportValue': 'postsapi-${opt:stage}-ApiGatewayResourceUsers' } 16 | users/me: 17 | Fn::ImportValue: 'postsapi-${opt:stage}-ApiGatewayResourceUsersMe' 18 | environment: 19 | # NOTE: change this before deploying 20 | SECRET: ${self:custom.config.encryption.secret} 21 | USERS_TABLE: 22 | Fn::ImportValue: postsapi-${opt:stage}-UsersTable 23 | custom: 24 | config: ${file(../../config.yaml)} 25 | webpack: 26 | includeModules: 27 | forceExclude: 28 | - aws-sdk 29 | packagePath: ../../package.json 30 | authorizer: 31 | type: CUSTOM 32 | authorizerId: 33 | Fn::ImportValue: postsapi-${opt:stage}-ApiGatewayAuthorizerId 34 | plugins: 35 | - serverless-webpack 36 | package: 37 | include: 38 | - ../../node_modules 39 | functions: 40 | token: 41 | handler: token.handler 42 | events: 43 | - http: 44 | method: POST 45 | path: /token 46 | integration: lambda-proxy 47 | usersCreate: 48 | handler: usersCreate.handler 49 | events: 50 | - http: 51 | method: POST 52 | path: /users 53 | integration: lambda-proxy 54 | # authorizer: ${self:custom.authorizer} 55 | -------------------------------------------------------------------------------- /api/users/token.js: -------------------------------------------------------------------------------- 1 | const jsonwebtoken = require('jsonwebtoken'); 2 | const AWS = require('aws-sdk'); 3 | const crypto = require('crypto'); 4 | 5 | const errorResponse = (statusCode, message) => ({ 6 | statusCode, 7 | body: JSON.stringify({ 8 | message, 9 | }) 10 | }) 11 | 12 | module.exports.handler = async event => { 13 | let body; 14 | try { 15 | body = JSON.parse(event.body); 16 | } catch (err) { 17 | return errorResponse(400, 'bad body'); 18 | } 19 | 20 | if (!body.userId || !body.password) { 21 | return errorResponse(400, 'missing userId or password'); 22 | } 23 | 24 | const dc = new AWS.DynamoDB.DocumentClient(); 25 | const result = await dc.get({ 26 | TableName: process.env.USERS_TABLE, 27 | Key: { 28 | userId: body.userId, 29 | }, 30 | }).promise(); 31 | if (!result.Item) { 32 | return errorResponse('unknown user'); 33 | } 34 | const { salt, password } = result.Item.secret; 35 | const verify = crypto.pbkdf2Sync(body.password, salt, 100000, 64, 'sha512'); 36 | if (verify.equals(password)) { 37 | const token = jsonwebtoken.sign({ sub: body.userId }, process.env.SECRET, { expiresIn: '7 days' }); 38 | return { 39 | statusCode: 200, 40 | body: JSON.stringify({ 41 | token 42 | }), 43 | } 44 | } else { 45 | return errorResponse(401, 'bad password'); 46 | } 47 | 48 | }; -------------------------------------------------------------------------------- /api/users/usersCreate.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const uuid = require('uuid'); 3 | const crypto = require('crypto'); 4 | 5 | module.exports.handler = async event => { 6 | const dynamo = new AWS.DynamoDB.DocumentClient(); 7 | const userId = uuid.v4(); 8 | const body = JSON.parse(event.body); 9 | 10 | const salt = crypto.randomBytes(16); 11 | const password = crypto.pbkdf2Sync(body.password, salt, 100000, 64, 'sha512') 12 | const item = Object.assign({}, { 13 | userId, 14 | name: body.name || 'Test User', 15 | secret: { 16 | password, 17 | salt, 18 | } 19 | }); 20 | const result = await dynamo.put({ 21 | Item: item, 22 | TableName: process.env.USERS_TABLE, 23 | }).promise(); 24 | delete item.secret; 25 | return { 26 | statusCode: 200, 27 | body: JSON.stringify(item), 28 | } 29 | }; -------------------------------------------------------------------------------- /api/users/webpack.config.js: -------------------------------------------------------------------------------- 1 | const slsw = require('serverless-webpack'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | target: 'node', 7 | entry: slsw.lib.entries, 8 | externals: [nodeExternals({ 9 | modulesDir: path.join(__dirname, '../../node_modules'), 10 | })] 11 | }; -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | encryption: 2 | # change this in production 3 | secret: BLAHBLAHBLAH -------------------------------------------------------------------------------- /generalAuthorizer.js: -------------------------------------------------------------------------------- 1 | const jsonwebtoken = require('jsonwebtoken'); 2 | 3 | const parseArn = arn => { 4 | const [left, right] = arn.split('/', 2); 5 | const [,, service, region, accountId, apiId] = left.split(':'); 6 | const [stage, method, resourcePath] = right.split('/'); 7 | return { 8 | service, region, accountId, apiId, stage, method, resourcePath, 9 | }; 10 | }; 11 | 12 | module.exports.handler = async event => { 13 | const token = event.authorizationToken.split(' ')[1]; 14 | 15 | try { 16 | const decodedToken = jsonwebtoken.verify(token, process.env.SECRET); 17 | const { service, region, accountId, apiId, stage } = parseArn(event.methodArn); 18 | const userId = decodedToken.sub; 19 | return { 20 | principalId: userId, 21 | policyDocument: { 22 | Version: '2012-10-17', 23 | Statement: [ 24 | { 25 | Effect: 'Allow', 26 | Resource: `arn:aws:${service}:${region}:${accountId}:${apiId}/${stage}/*`, 27 | Action: ['execute-api:Invoke'], 28 | }, 29 | ], 30 | } 31 | }; 32 | } catch (err) { 33 | console.error('error validating token', err); 34 | return { 35 | principalId: 'unknown', 36 | policyDocument: { 37 | Version: '2012-10-17', 38 | Statement: [ 39 | { 40 | Effect: 'Deny', 41 | Resource: '*', 42 | Action: [], 43 | }, 44 | ], 45 | } 46 | }; 47 | } 48 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "splitstack-postsapi", 3 | "version": "1.0.0", 4 | "description": "An example of split serverless stacks", 5 | "main": "index.js", 6 | "dependencies": { 7 | "jsonwebtoken": "^8.5.1", 8 | "uuid": "^3.4.0" 9 | }, 10 | "devDependencies": { 11 | "aws-sdk": "^2.747.0", 12 | "serverless": "^1.82.0", 13 | "serverless-webpack": "^5.3.4", 14 | "webpack": "^4.44.1", 15 | "webpack-node-externals": "^1.7.2" 16 | }, 17 | "scripts": {}, 18 | "author": "GorillaStack Pty Ltd", 19 | "license": "ISC", 20 | "repository": { 21 | "url": "https://github.com/GorillaStack/splitstack-postsapi.git" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /serverless.yaml: -------------------------------------------------------------------------------- 1 | service: postsapi 2 | provider: 3 | name: aws 4 | runtime: nodejs12.x 5 | iamRoleStatements: 6 | - 7 | Effect: "Allow" 8 | Action: [ logs:CreateLogStream ] 9 | Resource: 10 | Fn::Join: 11 | - "" 12 | - ["arn:aws:logs:", {Ref: "AWS::Region"}, ":", {Ref: "AWS::AccountId"}, ":log-group:/aws/lambda/postsapi-*:*"] 13 | - 14 | Effect: Allow 15 | Action: [ logs:PutLogEvents ] 16 | Resource: 17 | Fn::Join: 18 | - "" 19 | - ["arn:aws:logs:", {Ref: "AWS::Region"}, ":", {Ref: "AWS::AccountId"}, ":log-group:/aws/lambda/postsapi-*:*:*"] 20 | - 21 | Effect: Allow 22 | Action: [ dynamodb:* ] 23 | Resource: 24 | Fn::Join: 25 | - "" 26 | - ["arn:aws:dynamodb:", {Ref: "AWS::Region"}, ":", {Ref: "AWS::AccountId"}, ":table/postsapi-${opt:stage}-*"] 27 | functions: 28 | generalAuthorizer: 29 | handler: generalAuthorizer.handler 30 | environment: 31 | SECRET: ${self:custom.config.encryption.secret} 32 | custom: 33 | # NOTE: Change this if you plan to use/deploy this template! 34 | config: ${file(config.yaml)} 35 | resources: 36 | Resources: 37 | 38 | # Rest API 39 | ApiGatewayRestApi: 40 | Type: AWS::ApiGateway::RestApi 41 | Properties: 42 | Name: postsapi 43 | Description: Posts API Gateway 44 | 45 | # Rest API Paths 46 | ApiGatewayResourceUsers: 47 | Type: AWS::ApiGateway::Resource 48 | Properties: 49 | RestApiId: { Ref: "ApiGatewayRestApi" } 50 | ParentId: { Fn::GetAtt: "ApiGatewayRestApi.RootResourceId" } 51 | PathPart: users 52 | ApiGatewayResourceUsersMe: 53 | Type: AWS::ApiGateway::Resource 54 | Properties: 55 | RestApiId: { Ref: "ApiGatewayRestApi" } 56 | ParentId: { Ref: "ApiGatewayResourceUsers" } 57 | PathPart: "me" 58 | 59 | # Authorizer 60 | ApiGatewayAuthorizer: 61 | Type: AWS::ApiGateway::Authorizer 62 | Properties: 63 | AuthorizerResultTtlInSeconds: 300 64 | AuthorizerUri: 65 | Fn::Join: 66 | - '' 67 | - 68 | - 'arn:aws:apigateway:' 69 | - Ref: "AWS::Region" 70 | - ':lambda:path/2015-03-31/functions/' 71 | - Fn::GetAtt: "GeneralAuthorizerLambdaFunction.Arn" 72 | - "/invocations" 73 | IdentitySource: method.request.header.Authorization 74 | IdentityValidationExpression: "Bearer .+" 75 | RestApiId: { Ref: ApiGatewayRestApi } 76 | Type: TOKEN 77 | Name: postsapi-${opt:stage}-authorizer 78 | ApiGatewayAuthorizerPermission: 79 | Type: AWS::Lambda::Permission 80 | Properties: 81 | FunctionName: 82 | Fn::GetAtt: GeneralAuthorizerLambdaFunction.Arn 83 | Action: lambda:InvokeFunction 84 | Principal: 85 | Fn::Join: ["",["apigateway.", { Ref: "AWS::URLSuffix"}]] 86 | 87 | # Tables 88 | UsersTable: 89 | Type: AWS::DynamoDB::Table 90 | Properties: 91 | TableName: postsapi-${opt:stage}-users 92 | AttributeDefinitions: 93 | - 94 | AttributeName: userId 95 | AttributeType: S 96 | KeySchema: 97 | - 98 | AttributeName: userId 99 | KeyType: HASH 100 | ProvisionedThroughput: 101 | ReadCapacityUnits: 1 102 | WriteCapacityUnits: 1 103 | PostsTable: 104 | Type: AWS::DynamoDB::Table 105 | Properties: 106 | TableName: postsapi-${opt:stage}-posts 107 | AttributeDefinitions: 108 | - 109 | AttributeName: postId 110 | AttributeType: S 111 | - 112 | AttributeName: userId 113 | AttributeType: S 114 | KeySchema: 115 | - 116 | AttributeName: postId 117 | KeyType: HASH 118 | ProvisionedThroughput: 119 | ReadCapacityUnits: 1 120 | WriteCapacityUnits: 1 121 | GlobalSecondaryIndexes: 122 | - IndexName: userId 123 | KeySchema: 124 | - 125 | AttributeName: userId 126 | KeyType: HASH 127 | Projection: 128 | ProjectionType: ALL 129 | ProvisionedThroughput: 130 | ReadCapacityUnits: 1 131 | WriteCapacityUnits: 1 132 | 133 | Outputs: 134 | # RestApi resource ID (e.g. ei829oe) 135 | RestApiId: 136 | Value: 137 | Ref: ApiGatewayRestApi 138 | Export: 139 | Name: postsapi-${opt:stage}-RestApiId 140 | # RestApi Root Resource (the implicit '/' path) 141 | RootResourceId: 142 | Value: 143 | Fn::GetAtt: ApiGatewayRestApi.RootResourceId 144 | Export: 145 | Name: postsapi-${opt:stage}-RootResourceId 146 | # The IAM Role for Lambda execution 147 | IamRoleLambdaExecution: 148 | Value: 149 | Fn::GetAtt: IamRoleLambdaExecution.Arn 150 | Export: 151 | Name: postsapi-${opt:stage}-IamRoleLambdaExecution 152 | # The Authorizer (only if you are using an API Gateway authorizer) 153 | ApiGatewayAuthorizerId: 154 | Value: 155 | Ref: ApiGatewayAuthorizer 156 | Export: 157 | Name: postsapi-${opt:stage}-ApiGatewayAuthorizerId 158 | # Path Resources 159 | ApiGatewayResourceUsers: 160 | Value: 161 | Ref: ApiGatewayResourceUsers 162 | Export: 163 | Name: postsapi-${opt:stage}-ApiGatewayResourceUsers 164 | ApiGatewayResourceUsersMe: 165 | Value: 166 | Ref: ApiGatewayResourceUsersMe 167 | Export: 168 | Name: postsapi-${opt:stage}-ApiGatewayResourceUsersMe 169 | # Table Resources 170 | UsersTable: 171 | Value: 172 | Ref: UsersTable 173 | Export: 174 | Name: postsapi-${opt:stage}-UsersTable 175 | PostsTable: 176 | Value: 177 | Ref: PostsTable 178 | Export: 179 | Name: postsapi-${opt:stage}-PostsTable 180 | --------------------------------------------------------------------------------