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

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
------------------------------------------------------------------------------
......
This diff is collapsed.
'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,
'Key': {
'ObjectType': type,
'Id': id
},
'ReturnValues': 'ALL_OLD'
};
const body=event['body-json'];
let dbDelete = (params) => { return dynamo.delete(params).promise() };
dbDelete(params).then( (data) => {
if (!data.Attributes) {
callback(null, createResponse(404, "ITEM NOT FOUND FOR DELETION"));
return;
}
console.log(||JSONAPI|| `DELETED ITEM OF TYPE ${type} SUCCESSFULLY WITH id = ${id}`);
callback(null, body);
}).catch( (err) => {
console.log(`||JSONAPI|| DELETE ITEM OF TYPE ${type} FAILED FOR id = ${id}, WITH ERROR: ${err}`);
callback(null, createResponse(500, err));
});
};
exports.handler = (event, context, callback) => {
console.log("||JSONAPI|| ********************** Received Event *******************\n"+ JSON.stringify(event));
console.log("||JSONAPI|| httpMethod="+event.context.httpMethod);
switch ( event.context.httpMethod ) {
case 'GET':
getMethod(event,context,callback);
break;
case 'PUT':
case 'POST':
putMethod(event,context,callback);
break;
case 'DELETE':
deleteMethod(event,context,callback);
break;
default:
callback(null, createResponse(500, 'Unsupported Method: ' + context.httpMethod));
break;
}
};
Installing AWS infrastructure through Terraform scripts
==============================================================================
This file explains how to set up the expected infrastructure awaited to implement the JSON API.
The installation is based on Terraform (https://www.terraform.io/)
It expects that by AWS CLI and Terraform have previously been installed and set up correctly.
The AWS Credentials should also have been set up
How it works
------------------------------------------------------------------------------
AWS JSON API server relies on 3 main components, plus the needed IAM roles :
- A DynamoDB table manipuling all serialized objects
- A lambda function in charge of the translation from JSON API format to dynamoDB objects and managing the relationships and the optional parameters provided in the request
- An API Gateway configured to received the REST HTTP requests and proxying the Lambda
Three Terraform files are available, each one created the needed component and their relationships with each other.
Installation
------------------------------------------------------------------------------
The lambda code needs to be stored in a S3 bucket before running the Terraform scripts.
The script is available in the cloud/lambda directory of this project.
The following operations need to be performed (adapting the region you are using):
```
zip lambda-jsonapi.zip ../lambda/lambda-jsonapi.js
aws s3api create-bucket --bucket=lambda-jsonapi-bucket --region=us-east-1
aws s3 cp lambda-jsonapi-bucket.zip s3://lambda-jsonapi-bucket/v1.0.0/lambda-jsonapi-bucket.zip
```
One this is done, simply run the terraform script :
```
terraform apply
```
For a strange reason I don't understand, the first time you run the terraform command, the mapping for the integration response fails. You have to run the command again to have your complete setup.
\ No newline at end of file
resource "aws_api_gateway_rest_api" "jsonapi" {
name = "JsonApiRestGateway"
description = "HTTP request front API for JSON API rest persistance"
minimum_compression_size = 4096
}
# Path /{type}
resource "aws_api_gateway_resource" "typePath" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
parent_id = "${aws_api_gateway_rest_api.jsonapi.root_resource_id}"
path_part = "{type}"
}
# Path /{type}/{id}
resource "aws_api_gateway_resource" "typeIdPath" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
parent_id = "${aws_api_gateway_resource.typePath.id}"
path_part = "{id}"
}
# GET on path /{type}
resource "aws_api_gateway_method" "typePathGet" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typePath.id}"
http_method = "GET"
authorization = "NONE"
}
# POST on path /{type}
resource "aws_api_gateway_method" "typePathPost" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typePath.id}"
http_method = "POST"
authorization = "NONE"
}
# GET on path /{type}/{id}
resource "aws_api_gateway_method" "typeIdPathGet" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typeIdPath.id}"
http_method = "GET"
authorization = "NONE"
}
# DELETE on path /{type}/{id}
resource "aws_api_gateway_method" "typeIdPathDelete" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typeIdPath.id}"
http_method = "DELETE"
authorization = "NONE"
}
# PATCH on path /{type}/{id}
resource "aws_api_gateway_method" "typeIdPathPatch" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typeIdPath.id}"
http_method = "PATCH"
authorization = "NONE"
}
# Setup Integration Request for GET on {type}
resource "aws_api_gateway_integration" "lambdaJsonTypeGet" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_method.typePathGet.resource_id}"
http_method = "${aws_api_gateway_method.typePathGet.http_method}"
integration_http_method = "POST"
type = "AWS"
uri = "${aws_lambda_function.lambda-jsonapi.invoke_arn}"
# Transforms the incoming XML request to JSON
passthrough_behavior = "WHEN_NO_TEMPLATES"
request_templates {
"application/json" = "${file("template.vm")}"
}
}
# Setup Integration Request for POST on {type}
resource "aws_api_gateway_integration" "lambdaJsonTypePost" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_method.typePathPost.resource_id}"
http_method = "${aws_api_gateway_method.typePathPost.http_method}"
integration_http_method = "POST"
type = "AWS"
uri = "${aws_lambda_function.lambda-jsonapi.invoke_arn}"
# Transforms the incoming XML request to JSON
passthrough_behavior = "WHEN_NO_TEMPLATES"
request_templates {
"application/json" = "${file("template.vm")}"
}
}
# Setup Integration Request for GET on {type}/{id}
resource "aws_api_gateway_integration" "lambdaJsonTypeIdGet" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_method.typeIdPathGet.resource_id}"
http_method = "${aws_api_gateway_method.typeIdPathGet.http_method}"
integration_http_method = "POST"
type = "AWS"
uri = "${aws_lambda_function.lambda-jsonapi.invoke_arn}"
# Transforms the incoming XML request to JSON
passthrough_behavior = "WHEN_NO_TEMPLATES"
request_templates {
"application/json" = "${file("template.vm")}"
}
}
# Setup Integration Request for DELETE on {type}/{id}
resource "aws_api_gateway_integration" "lambdaJsonTypeIdDelete" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_method.typeIdPathDelete.resource_id}"
http_method = "${aws_api_gateway_method.typeIdPathDelete.http_method}"
integration_http_method = "POST"
type = "AWS"
uri = "${aws_lambda_function.lambda-jsonapi.invoke_arn}"
# Transforms the incoming XML request to JSON
passthrough_behavior = "WHEN_NO_TEMPLATES"
request_templates {
"application/json" = "${file("template.vm")}"
}
}
# Setup Integration Request for PATCH on {type}/{id}
resource "aws_api_gateway_integration" "lambdaJsonTypeIdPatch" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_method.typeIdPathPatch.resource_id}"
http_method = "${aws_api_gateway_method.typeIdPathPatch.http_method}"
integration_http_method = "POST"
type = "AWS"
uri = "${aws_lambda_function.lambda-jsonapi.invoke_arn}"
# Transforms the incoming XML request to JSON
passthrough_behavior = "WHEN_NO_TEMPLATES"
request_templates {
"application/json" = "${file("template.vm")}"
}
}
# Setup Integration Response for status code 200 and GET on {type}
resource "aws_api_gateway_method_response" "200TypeGet" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_method.typePathGet.resource_id}"
http_method = "${aws_api_gateway_method.typePathGet.http_method}"
status_code = "200"
}
resource "aws_api_gateway_integration_response" "200TypeGetIntegrationResponse" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typePath.id}"
http_method = "${aws_api_gateway_method.typePathGet.http_method}"
status_code = "${aws_api_gateway_method_response.200TypeGet.status_code}"
}
# Setup Integration Response for status code 200 and POST on {type}
resource "aws_api_gateway_method_response" "200TypePost" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typePath.id}"
http_method = "${aws_api_gateway_method.typePathPost.http_method}"
status_code = "200"
}
resource "aws_api_gateway_integration_response" "200TypePostIntegrationResponse" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typePath.id}"
http_method = "${aws_api_gateway_method.typePathPost.http_method}"
status_code = "${aws_api_gateway_method_response.200TypePost.status_code}"
response_templates = {
"application/json" = ""
}
}
# Setup Integration Response for status code 200 and GET on {type}/{id}
resource "aws_api_gateway_method_response" "200TypeIdGet" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_method.typeIdPathGet.resource_id}"
http_method = "${aws_api_gateway_method.typeIdPathGet.http_method}"
status_code = "200"
}
resource "aws_api_gateway_integration_response" "200TypeIdGetIntegrationResponse" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typeIdPath.id}"
http_method = "${aws_api_gateway_method.typeIdPathGet.http_method}"
status_code = "${aws_api_gateway_method_response.200TypeIdGet.status_code}"
response_templates = {
"application/json" = ""
}
}
# Setup Integration Response for status code 200 and DELETE on {type}/{id}
resource "aws_api_gateway_method_response" "200TypeIdDelete" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_method.typeIdPathDelete.resource_id}"
http_method = "${aws_api_gateway_method.typeIdPathDelete.http_method}"
status_code = "200"
}
resource "aws_api_gateway_integration_response" "200TypeIdDeleteIntegrationResponse" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typeIdPath.id}"
http_method = "${aws_api_gateway_method.typeIdPathDelete.http_method}"
status_code = "${aws_api_gateway_method_response.200TypeIdDelete.status_code}"
response_templates = {
"application/json" = ""
}
}
# Setup Integration Response for status code 200 and PATCH on {type}/{id}
resource "aws_api_gateway_method_response" "200TypeIdPatch" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_method.typeIdPathPatch.resource_id}"
http_method = "${aws_api_gateway_method.typeIdPathPatch.http_method}"
status_code = "200"
}
resource "aws_api_gateway_integration_response" "200TypeIdPatchIntegrationResponse" {
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
resource_id = "${aws_api_gateway_resource.typeIdPath.id}"
http_method = "${aws_api_gateway_method.typeIdPathPatch.http_method}"
status_code = "${aws_api_gateway_method_response.200TypeIdPatch.status_code}"
response_templates = {
"application/json" = ""
}
}
resource "aws_api_gateway_deployment" "jsonapiDeployment" {
depends_on = [
"aws_api_gateway_integration.lambdaJsonTypeGet",
"aws_api_gateway_integration.lambdaJsonTypePost",
"aws_api_gateway_integration.lambdaJsonTypeIdGet",
"aws_api_gateway_integration.lambdaJsonTypeIdPatch",
"aws_api_gateway_integration.lambdaJsonTypeIdDelete"
]
rest_api_id = "${aws_api_gateway_rest_api.jsonapi.id}"
stage_name = "staging"
}
output "base_url" {
value = "${aws_api_gateway_deployment.jsonapiDeployment.invoke_url}"
}
\ No newline at end of file
resource "aws_dynamodb_table" "JsonApiTable" {
name = "JsonApiTable"
read_capacity = 3
write_capacity = 3
hash_key = "ObjectType"
range_key = "Id"
attribute {
name = "ObjectType"
type = "S"
}
attribute {
name = "Id"
type = "N"
}
}
resource "aws_iam_role" "lambda_jsonapi" {
name = "lambda_jsonapi"
path = "/"
assume_role_policy = <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
POLICY
}
resource "aws_iam_role_policy_attachment" "lambda_jsonapi_AmazonDynamoDBFullAccess" {
role = "${aws_iam_role.lambda_jsonapi.name}"
policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess"
}
resource "aws_iam_role_policy_attachment" "lambda_jsonapi_CloudWatchFullAccess" {
role = "${aws_iam_role.lambda_jsonapi.name}"
policy_arn = "arn:aws:iam::aws:policy/CloudWatchFullAccess"
}
variable "region" {
default = "us-east-1"
}
provider "aws" {
region = "${var.region}"
}
resource "aws_lambda_function" "lambda-jsonapi" {
function_name = "JSONAPILambda"
# The bucket name as created earlier with "aws s3api create-bucket"
s3_bucket = "lambda-jsonapi-bucket"
s3_key = "v1.0.0/lambda-jsonapi.zip"
# "main" is the filename within the zip file (main.js) and "handler"
# is the name of the property under which the handler function was
# exported in that file.
handler = "lambda-jsonapi.handler"
runtime = "nodejs8.10"
role = "${aws_iam_role.lambda_jsonapi.arn}"
}
# Allow API Gateway to access the defined lambda function
resource "aws_lambda_permission" "apigw" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = "${aws_lambda_function.lambda-jsonapi.arn}"
principal = "apigateway.amazonaws.com"
# The /*/* portion grants access from any method on any resource
# within the API Gateway "REST API".
source_arn = "${aws_api_gateway_deployment.jsonapiDeployment.execution_arn}/*/*"
}
\ No newline at end of file
#set($allParams = $input.params())
{
"params" : {
#foreach($type in $allParams.keySet())
#set($params = $allParams.get($type))
"$type" : {
#foreach($paramName in $params.keySet())
"$paramName" : "$util.escapeJavaScript($params.get($paramName))"
#if($foreach.hasNext),#end
#end
}
#if($foreach.hasNext),#end
#end
},
"context" : {
"httpMethod" : "$context.httpMethod",
"account-id" : "$context.identity.accountId",
"api-id" : "$context.apiId",
"api-key" : "$context.identity.apiKey",
"authorizer-principal-id" : "$context.authorizer.principalId",
"caller" : "$context.identity.caller",
"cognito-authentication-provider" : "$context.identity.cognitoAuthenticationProvider",
"cognito-authentication-type" : "$context.identity.cognitoAuthenticationType",
"cognito-identity-id" : "$context.identity.cognitoIdentityId",
"cognito-identity-pool-id" : "$context.identity.cognitoIdentityPoolId",
"stage" : "$context.stage",
"source-ip" : "$context.identity.sourceIp",
"user" : "$context.identity.user",
"user-agent" : "$context.identity.userAgent",
"user-arn" : "$context.identity.userArn",
"request-id" : "$context.requestId",
"resource-id" : "$context.resourceId",
"resource-path" : "$context.resourcePath"
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment