Commit 5b66e17b authored by Bertrand PINEL's avatar Bertrand PINEL
Browse files

Add terraform script to build AWS platform

parent abb810e5
......@@ -17,13 +17,17 @@ Usage
------------------------------------------------------------------------------
For generating a application, first use the standard command :
```
ember new test-ember-aws-ehipster --no-welcome
cd test-ember-aws-ehipster
ember install ember-aws-ehipster
```
Then generate a few entity through provided blueprints using the same syntax as the model blueprint :
```
ember g entity-factory blog title:string content:string order:number isVisible:boolean
ember g entity-factory post title:string content:string order:number visible:boolean
```
Contributing
------------------------------------------------------------------------------
......
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Ember EHipster API
Parameters:
LambdaFunctionName:
Type: String
Default: "EmberEHipsterFunction"
Description: "Name of the lambda function"
AppClientName:
Type: String
Default: "EmberEHipsterWebClient"
Description: "Cognito user pools app client name"
Resources:
ApiGatewayApi:
Type: AWS::Serverless::Api
DependsOn: CognitoUserPool
Properties:
StageName: Prod
DefinitionBody:
swagger: "2.0"
info:
version: "2017-02-24T04:09:00Z"
title: "EmberEHipsterAPI"
basePath: "/Prod"
schemes:
- "https"
paths:
"/{type}":
get:
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- name: "user"
in: "header"
required: false
type: "string"
- name: "id"
in: "query"
required: false
type: "string"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
security:
- EmberEHipster: []
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "## See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html\n\
## This template will pass through all parameters including path, querystring,\
\ header, stage variables, and context through to the integration endpoint\
\ via the body/payload\n#set($allParams = $input.params())\n{\n\"body-json\"\
\ : $input.json('$'),\n\"params\" : {\n#foreach($type in $allParams.keySet())\n\
\ #set($params = $allParams.get($type))\n\"$type\" : {\n #foreach($paramName\
\ in $params.keySet())\n \"$paramName\" : \"$util.escapeJavaScript($params.get($paramName))\"\
\n #if($foreach.hasNext),#end\n #end\n}\n #if($foreach.hasNext),#end\n\
#end\n},\n\"stage-variables\" : {\n#foreach($key in $stageVariables.keySet())\n\
\"$key\" : \"$util.escapeJavaScript($stageVariables.get($key))\"\n \
\ #if($foreach.hasNext),#end\n#end\n},\n\"context\" : {\n \"user\"\
\ : \"$context.authorizer.claims.sub\",\n\t\"email\" : \"$context.authorizer.claims.email\"\
,\n \"account-id\" : \"$context.identity.accountId\",\n \"api-id\"\
\ : \"$context.apiId\",\n \"api-key\" : \"$context.identity.apiKey\"\
,\n \"authorizer-principal-id\" : \"$context.authorizer.principalId\"\
,\n \"caller\" : \"$context.identity.caller\",\n \"cognito-authentication-provider\"\
\ : \"$context.identity.cognitoAuthenticationProvider\",\n \"cognito-authentication-type\"\
\ : \"$context.identity.cognitoAuthenticationType\",\n \"cognito-identity-id\"\
\ : \"$context.identity.cognitoIdentityId\",\n \"cognito-identity-pool-id\"\
\ : \"$context.identity.cognitoIdentityPoolId\",\n \"httpMethod\" :\
\ \"$context.httpMethod\",\n \"stage\" : \"$context.stage\",\n \"\
source-ip\" : \"$context.identity.sourceIp\",\n \"user\" : \"$context.identity.user\"\
,\n \"user-agent\" : \"$context.identity.userAgent\",\n \"user-arn\"\
\ : \"$context.identity.userArn\",\n \"request-id\" : \"$context.requestId\"\
,\n \"resource-id\" : \"$context.resourceId\",\n \"resource-path\"\
\ : \"$context.resourcePath\"\n }\n}\n"
uri: !Join [ "", [ "arn:aws:apigateway:", !Ref "AWS::Region", ":lambda:path/2015-03-31/functions/arn:aws:lambda:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":function:", !Ref "LambdaFunctionName", "/invocations" ] ]
passthroughBehavior: "when_no_templates"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws"
post:
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- name: "user"
in: "header"
required: false
type: "string"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
security:
- EmberEHipster: []
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "## See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html\n\
## This template will pass through all parameters including path, querystring,\
\ header, stage variables, and context through to the integration endpoint\
\ via the body/payload\n#set($allParams = $input.params())\n{\n\"body-json\"\
\ : $input.json('$'),\n\"params\" : {\n#foreach($type in $allParams.keySet())\n\
\ #set($params = $allParams.get($type))\n\"$type\" : {\n #foreach($paramName\
\ in $params.keySet())\n \"$paramName\" : \"$util.escapeJavaScript($params.get($paramName))\"\
\n #if($foreach.hasNext),#end\n #end\n}\n #if($foreach.hasNext),#end\n\
#end\n},\n\"stage-variables\" : {\n#foreach($key in $stageVariables.keySet())\n\
\"$key\" : \"$util.escapeJavaScript($stageVariables.get($key))\"\n \
\ #if($foreach.hasNext),#end\n#end\n},\n\"context\" : {\n \"user\"\
\ : \"$context.authorizer.claims.sub\",\n\t\"email\" : \"$context.authorizer.claims.email\"\
,\n \"account-id\" : \"$context.identity.accountId\",\n \"api-id\"\
\ : \"$context.apiId\",\n \"api-key\" : \"$context.identity.apiKey\"\
,\n \"authorizer-principal-id\" : \"$context.authorizer.principalId\"\
,\n \"caller\" : \"$context.identity.caller\",\n \"cognito-authentication-provider\"\
\ : \"$context.identity.cognitoAuthenticationProvider\",\n \"cognito-authentication-type\"\
\ : \"$context.identity.cognitoAuthenticationType\",\n \"cognito-identity-id\"\
\ : \"$context.identity.cognitoIdentityId\",\n \"cognito-identity-pool-id\"\
\ : \"$context.identity.cognitoIdentityPoolId\",\n \"httpMethod\" :\
\ \"$context.httpMethod\",\n \"stage\" : \"$context.stage\",\n \"\
source-ip\" : \"$context.identity.sourceIp\",\n \"user\" : \"$context.identity.user\"\
,\n \"user-agent\" : \"$context.identity.userAgent\",\n \"user-arn\"\
\ : \"$context.identity.userArn\",\n \"request-id\" : \"$context.requestId\"\
,\n \"resource-id\" : \"$context.resourceId\",\n \"resource-path\"\
\ : \"$context.resourcePath\"\n }\n}\n"
uri: !Join [ "", [ "arn:aws:apigateway:", !Ref "AWS::Region", ":lambda:path/2015-03-31/functions/arn:aws:lambda:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":function:", !Ref "LambdaFunctionName", "/invocations" ] ]
passthroughBehavior: "when_no_templates"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws"
delete:
consumes:
- "application/json"
produces:
- "application/json"
parameters:
- name: "user"
in: "header"
required: false
type: "string"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
security:
- EmberEHipster: []
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "## See http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html\n\
## This template will pass through all parameters including path, querystring,\
\ header, stage variables, and context through to the integration endpoint\
\ via the body/payload\n#set($allParams = $input.params())\n{\n\"body-json\"\
\ : $input.json('$'),\n\"params\" : {\n#foreach($type in $allParams.keySet())\n\
\ #set($params = $allParams.get($type))\n\"$type\" : {\n #foreach($paramName\
\ in $params.keySet())\n \"$paramName\" : \"$util.escapeJavaScript($params.get($paramName))\"\
\n #if($foreach.hasNext),#end\n #end\n}\n #if($foreach.hasNext),#end\n\
#end\n},\n\"stage-variables\" : {\n#foreach($key in $stageVariables.keySet())\n\
\"$key\" : \"$util.escapeJavaScript($stageVariables.get($key))\"\n \
\ #if($foreach.hasNext),#end\n#end\n},\n\"context\" : {\n \"user\"\
\ : \"$context.authorizer.claims.sub\",\n\t\"email\" : \"$context.authorizer.claims.email\"\
,\n \"account-id\" : \"$context.identity.accountId\",\n \"api-id\"\
\ : \"$context.apiId\",\n \"api-key\" : \"$context.identity.apiKey\"\
,\n \"authorizer-principal-id\" : \"$context.authorizer.principalId\"\
,\n \"caller\" : \"$context.identity.caller\",\n \"cognito-authentication-provider\"\
\ : \"$context.identity.cognitoAuthenticationProvider\",\n \"cognito-authentication-type\"\
\ : \"$context.identity.cognitoAuthenticationType\",\n \"cognito-identity-id\"\
\ : \"$context.identity.cognitoIdentityId\",\n \"cognito-identity-pool-id\"\
\ : \"$context.identity.cognitoIdentityPoolId\",\n \"httpMethod\" :\
\ \"$context.httpMethod\",\n \"stage\" : \"$context.stage\",\n \"\
source-ip\" : \"$context.identity.sourceIp\",\n \"user\" : \"$context.identity.user\"\
,\n \"user-agent\" : \"$context.identity.userAgent\",\n \"user-arn\"\
\ : \"$context.identity.userArn\",\n \"request-id\" : \"$context.requestId\"\
,\n \"resource-id\" : \"$context.resourceId\",\n \"resource-path\"\
\ : \"$context.resourcePath\"\n }\n}\n"
uri: !Join [ "", [ "arn:aws:apigateway:", !Ref "AWS::Region", ":lambda:path/2015-03-31/functions/arn:aws:lambda:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":function:", !Ref "LambdaFunctionName", "/invocations" ] ]
passthroughBehavior: "when_no_templates"
httpMethod: "POST"
contentHandling: "CONVERT_TO_TEXT"
type: "aws"
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
"200":
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS,POST,PUT,DELETE'"
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,user'"
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: "{\"statusCode\": 200}"
passthroughBehavior: "when_no_match"
type: "mock"
securityDefinitions:
EmberEHipster:
type: "apiKey"
name: "user"
in: "header"
x-amazon-apigateway-authtype: "cognito_user_pools"
x-amazon-apigateway-authorizer:
providerARNs:
- !GetAtt CognitoUserPool.Arn
type: "cognito_user_pools"
definitions:
Empty:
type: "object"
title: "Empty Schema"
LambdaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Ref LambdaFunctionName
Handler: index.handler
Runtime: nodejs4.3
Policies: AmazonDynamoDBFullAccess
Environment:
Variables:
TABLE_NAME: !Ref Table
Events:
GetDoc:
Type: Api
Properties:
RestApiId: !Ref ApiGatewayApi
Path: /{type}/{id}
Method: GET
GetDocs:
Type: Api
Properties:
RestApiId: !Ref ApiGatewayApi
Path: /{type}
Method: GET
NewDoc:
Type: Api
Properties:
RestApiId: !Ref ApiGatewayApi
Path: /{type}
Method: POST
DeleteDoc:
Type: Api
Properties:
RestApiId: !Ref ApiGatewayApi
Path: /{type}/{id}
Method: DELETE
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: EmberEHipster
AutoVerifiedAttributes:
- "email"
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
DependsOn: CognitoUserPool
Properties:
ClientName: !Ref AppClientName
UserPoolId: !Ref CognitoUserPool
GenerateSecret: false
CognitoIdentityPool:
Type: AWS::Cognito::IdentityPool
Properties:
AllowUnauthenticatedIdentities: true
CognitoIdentityProviders:
- ClientId: !Ref CognitoUserPoolClient
ProviderName: !GetAtt CognitoUserPool.ProviderName
CognitoIdentityPoolRoles:
Type: AWS::Cognito::IdentityPoolRoleAttachment
DependsOn: CognitoIdentityPool
Properties:
IdentityPoolId: !Ref CognitoIdentityPool
Roles:
authenticated: !GetAtt AuthenticatedRole.Arn
unauthenticated: !GetAtt UnauthenticatedRole.Arn
UnauthenticatedRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Federated: cognito-identity.amazonaws.com
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
cognito-identity.amazonaws.com:aud: !Ref CognitoIdentityPool
ForAnyValue:StringLike:
cognito-identity.amazonaws.com:amr: unauthenticated
Policies:
-
PolicyName: EmberEHipsterUnauthenticatedApi
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- mobileanalytics:PutEvents
- cognito-sync:*
Resource:
- "*"
AuthenticatedRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Federated: cognito-identity.amazonaws.com
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
cognito-identity.amazonaws.com:aud: !Ref CognitoIdentityPool
ForAnyValue:StringLike:
cognito-identity.amazonaws.com:amr: authenticated
Policies:
-
PolicyName: EmberEHipsterAuthenticatedApi
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- execute-api:Invoke
Resource: !Join [ "", [ "arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":", !Ref ApiGatewayApi, "/*" ] ]
Table:
Type: AWS::Serverless::SimpleTable
Outputs:
CognitoIdentityPoolId:
Description: Cognito Identity Pool ID
Value: !Ref CognitoIdentityPool
CognitoUserPoolsId:
Description: Cognito User Pools ID
Value: !Ref CognitoUserPool
CognitoUserPoolsClientId:
Description: Cognito User Pools App Client ID
Value: !Ref CognitoUserPoolClient
Api:
Description: API Gateway ID
Value: !Ref ApiGatewayApi
ApiUrl:
Description: URL of your API endpoint
Value: !Join
- ''
- - https://
- !Ref ApiGatewayApi
- '.execute-api.'
- !Ref 'AWS::Region'
- '.amazonaws.com/Prod'
'use strict';
const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();
const tableName = 'JsonApiTable';
const EPOCH = 1300000000000;
// Instagram inspired --> https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c
function generateRowId(subid) {
var ts = new Date().getTime() - EPOCH; // limit to recent
// 41 bits for time in milliseconds (gives us 41 years of IDs with a custom epoch
var randid = Math.floor(Math.random() * 512);
ts = (ts * 64); // bit-shift << 6
// Given shard (if any...)
ts = ts + subid;
// random value
return (ts * 512) + (randid % 512);
}
const createObject = (obj) => {
let objout = { "type": obj.ObjectType,
"id": obj.Id,
"attributes": {},
"relationships": {}
};
for (var attr in obj) {
if (attr !== 'ObjectType' && attr !== 'Id') {
if(!attr.endsWith('_id')) {
objout.attributes[attr] = obj[attr];
} else {
let relationDetails = attr.split('_');
let relationId = Number(obj[attr]);
objout.relationships[relationDetails[0]] = {
"links": {
"self": "/"+obj.ObjectType+"/"+obj.Id+"/relationships/"+relationDetails[0],
"related": "/"+obj.ObjectType+"/"+obj.Id+"/"+relationDetails[0]
},
"data": {"type": relationDetails[1], "id": relationId}
};
// TODO what's about if relation is not a belongsTo but a hasMany...
}
} else {
objout[(attr==='ObjectType')?'type':'id'] = obj[attr];
}
}
console.log('||JSONAPI|| Return object is '+JSON.stringify(objout));
return objout;
}
const createData = (data) => {
if (Array.isArray(data)) {
let outdata = [];
for (let i=0;i<data.length;i++){
outdata.push(createObject(data[i]));
}
return outdata;
} else {
return createObject(data);
}
}
const createRelationships = (data) => {
return {};
}
const createResponse = (statusCode, body) => {
console.log("||JSONAPI|| Body is "+JSON.stringify(body));
return {
'statusCode': statusCode,
'data': createData(body),
'relationships': createRelationships(body)
}
};
const getMethod = (event, context, callback) => {
let params = {
TableName: tableName,
},
type = event.params.path.type,
id = Number(event.params.path.id),
dbGet = {};
if (id) {
params.Key = {
'ObjectType': type,
'Id': id
};
dbGet = (params) => { return dynamo.get(params).promise() };
console.log('||JSONAPI|| Lambda GET single value with params: ', params);
} else {
params.KeyConditionExpression = 'ObjectType = :objectType';
params.ExpressionAttributeValues = { ':objectType': type };
dbGet = (params) => { return dynamo.query(params).promise() };
console.log('||JSONAPI|| Lambda GET multiple values with params: ', params);
}
dbGet(params).then( (data) => {
console.log('||JSONAPI|| Lambda GET data received: ', data);
if (id && !data.Item) {
callback(null, createResponse(404, "ITEM NOT FOUND"));
return;
} else if (id && data.Item) {
console.log(`||JSONAPI|| RETRIEVED ITEM SUCCESSFULLY WITH doc = ${data.Item}`);
callback(null, createResponse(200, data.Item));
} else {
console.log(`||JSONAPI|| RETRIEVED ITEMS SUCCESSFULLY WITH doc = ${data.Items}`);
callback(null, createResponse(200, data.Items));
}
}).catch( (err) => {
console.log(`||JSONAPI|| GET ITEM FAILED FOR Entry = ${params}, WITH ERROR: ${err}`);
callback(null, createResponse(500, err));
});
};
const putMethod = (event, context, callback) => {
const body=event['body-json'];
const attrs = body.data.attributes;
const relations = body.data.relationships;
// Without any body content, there is nothing to put...
if (!body || !attrs) {
callback(null, createResponse(500, 'No content found in body'));
return;
}
// Retrieving the type and generating a new id for created item
let type = event.params.path.type,
id = generateRowId(1);
// Final content contains at least these two fields + the atributes
let content = {
"ObjectType": type,
"Id": id
};
// Adding attributes as column in dynamoDb
for (var prop in attrs) {
content[prop] = attrs[prop];
}
// Dealing with relationships if any
if (relations){
for (var relName in relations) {
let relData = relations[relName]["data"];
let newCol;
if (!Array.isArray(relData)) {
newCol = relName+'_'+relData["type"]+'_id';
content[newCol] = relData["id"];
} else {
for (var i=0; i<relData.length;i++){
let currentData = relData[i];
newCol = relName+'_'+currentData["type"]+'_id';
content[newCol] = currentData["id"];
}
}
}
}
const entry = {
TableName: tableName,
Item: content
};
console.log('||JSONAPI|| Try saving entity of type '+type+' and content '+JSON.stringify(entry));
//let dbPut = (entry) => { return dynamo.put(entry).promise() };
dynamo.put(entry, function(err, data) {
if (err) {
console.log("||JSONAPI|| Error", err);
callback(null, createResponse(500, 'Error '+err));
} else {
body.data.id = id;
body['statusCode'] = 200;
console.log(`||JSONAPI|| PUT ITEM SUCCEEDED WITH data=`+JSON.stringify(body));
callback(null, body);
}
});
};
const deleteMethod = (event, context, callback) => {
let type = event.params.path.type,
id = Number(event.params.path.id),
params = {
'TableName': tableName,