Add support for API Gateway

This commit is contained in:
Ryan Martin 2016-03-07 15:39:00 -05:00
parent 2d72709c84
commit e2f3a6efbe
6 changed files with 438 additions and 30 deletions

View file

@ -68,6 +68,77 @@ general manner - much the same way CloudFormation itself has permission to do al
## Included functions
### Create a full API in API Gateway
Pass in all of the components of the API endpoints, and the API will be created in API Gateway.
This will delete the entire API when the corresponding stack is deleted.
#### Parameters
##### name
The name of the API, seen in the list of APIs in AWS Console.
##### description
The description of what the API does.
##### endpoints
A JSON array of (see the example template below for a working example):
```javascript
{
"resource": {
"sub-resource": {
"GET": {
"authorizationType": "NONE",
"integration": {
"type": "MOCK",
"contentType": "text/html"
}
}
},
"POST": {
"authorizationType": "NONE",
"integration": {
"type": "AWS",
"integrationHttpMethod": "POST",
"uri": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:*:*:function:your-function-name/invocations",
"contentType": "application/json"
}
}
}
}
```
1. The properties of the JSON document are either a) the HTTP method or b) a sub-resource.
2. The resources cannot include a '/' - any resources nested in the path should also be nested
in the JSON document.
3. The parameters of each http method object are the same as putMethod callh ere:
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#putMethod-property -
except that httpMethod, resourceId, and restApiId will be added for you.
4. Each http method object can also optionally specify an "integration" property, which follows
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#putIntegration-property with
the same properties automatically filled in for you.
5. The "integration" property must include a "contentType" property that specifies the response
Content-Type of the endpoint.
#### Output
##### baseUrl
The base url of the API endpoints. Combine this with the relative paths defined in the config to
put together the full url for the API call.
##### restApiId
The id of the API that is created.
#### Reference Output Name
ApiGatewayCreateApiFunction
#### Example/Test Template
[apiGateway.createApi.template](test/aws/apiGateway.createApi.template)
### Insert items into DynamoDB
Pass in a list of items to be inserted into a DynamoDB table. This is useful to provide a template for the

127
aws/apiGateway.js Normal file
View file

@ -0,0 +1,127 @@
var Promise = require('bluebird'),
AWS = require('aws-sdk'),
base = require('lib/base'),
apiGateway = Promise.promisifyAll(new AWS.APIGateway());
// Exposes the SNS.subscribe API method
function CreateApi(event, context) {
base.Handler.call(this, event, context);
}
CreateApi.prototype = Object.create(base.Handler.prototype);
CreateApi.prototype.handleCreate = function() {
var p = this.event.ResourceProperties;
var rootObject = this;
return apiGateway.createRestApiAsync({
name: p.name,
description: p.description
})
.then(function(apiData) {
return rootObject.setReferenceData({ restApiId: apiData.id }) // Set this immediately, in case later calls fail
.then(function() {
return apiGateway.getResourcesAsync({
restApiId: apiData.id
})
.then(function(resourceData) {
return setupEndpoints(p.endpoints, resourceData.items[0].id, apiData.id)
.then(function(endpointsData) {
return apiGateway.createDeploymentAsync({
restApiId: apiData.id,
stageName: p.version
})
.then(function(deploymentData) {
// AWS.config.region is a bit of a hack, but I can't figure out how else to dynamically
// detect the region of the API - seems to be nothing in API Gateway or AWS Lambda context.
// Could possibly get it from the CloudFormation stack, but that seems wrong.
return {
baseUrl: "https://" + apiData.id + ".execute-api." + AWS.config.region + ".amazonaws.com/" + p.version,
restApiId: apiData.id
};
});
});
});
});
});
}
CreateApi.prototype.handleDelete = function(referenceData) {
return Promise.try(function() {
if (referenceData && referenceData.restApiId) {
// Can simply delete the entire API - don't need to delete each individual component
return apiGateway.deleteRestApiAsync({
restApiId: referenceData.restApiId
});
}
})
}
function setupEndpoints(config, parentResourceId, restApiId) {
return Promise.map(
Object.keys(config),
function(key) {
switch (key.toUpperCase()) {
case 'GET':
case 'HEAD':
case 'DELETE':
case 'OPTIONS':
case 'PATCH':
case 'POST':
case 'PUT':
var params = config[key];
params["httpMethod"] = key.toUpperCase()
params["resourceId"] = parentResourceId
params["restApiId"] = restApiId
params["apiKeyRequired"] = params["apiKeyRequired"] == "true" // Passing through CloudFormation, booleans become strings :(
var integration = params["integration"]
delete params.integration
return apiGateway.putMethodAsync(params)
.then(function() {
return Promise.try(function() {
if (integration) {
var contentType = integration["contentType"]
if (!contentType) {
throw "Integration config must include response contentType."
}
delete integration.contentType
integration["httpMethod"] = key.toUpperCase()
integration["resourceId"] = parentResourceId
integration["restApiId"] = restApiId
return apiGateway.putIntegrationAsync(integration)
.then(function(integrationData) {
var responseContentTypes = {}
responseContentTypes[contentType] = "Empty"
return apiGateway.putMethodResponseAsync({
httpMethod: key.toUpperCase(),
resourceId: parentResourceId,
restApiId: restApiId,
statusCode: '200',
responseModels: responseContentTypes
})
.then(function(methodResponseData) {
responseContentTypes[contentType] = ""
return apiGateway.putIntegrationResponseAsync({
httpMethod: key.toUpperCase(),
resourceId: parentResourceId,
restApiId: restApiId,
statusCode: '200',
responseTemplates: responseContentTypes
});
});
});
}
});
});
default:
return apiGateway.createResourceAsync({
parentId: parentResourceId,
pathPart: key,
restApiId: restApiId,
})
.then(function(resourceData) {
return setupEndpoints(config[key], resourceData.id, restApiId);
});
}
}
);
}
exports.createApi = function(event, context) {
handler = new CreateApi(event, context);
handler.handle();
}

View file

@ -1,29 +0,0 @@
var AWS = require('aws-sdk');
var Promise = require('bluebird');
var apiGateway = Promise.promisifyAll(new AWS.APIGateway()),
response = require('./lib/cfn-response');
exports.apiGatewayCreateRestApi = function(event, context) {
var p = event.ResourceProperties;
if (event.RequestType == 'Delete') {
// TODO: deleteRestApi here
response.send(event, context, response.SUCCESS);
} else {
apiGateway.createRestApiAsync({
name: p.name,
cloneFrom: p.cloneFrom,
description: p.description
})
.then(function(data) {
response.send(event, context, response.SUCCESS, { "data": data });
})
.catch(function(err) {
error(err, event, context);
});
}
}
function error(message, event, context) {
console.error(message);
response.send(event, context, response.FAILED, { Error: message });
}

View file

@ -52,6 +52,60 @@
}
}
},
"ApiGatewayCreateApiFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Ref": "RoleBasePolicy" }
],
"Policies": [
{
"PolicyName": "ApiGatewayWriter",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"apigateway:*"
],
"Resource": "*"
}
]
}
}
]
}
},
"ApiGatewayCreateApiFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": "com.gilt.public.backoffice",
"S3Key": "lambda_functions/cloudformation-helpers.zip"
},
"Description": "Used to create a full API in Api Gateway.",
"Handler": "aws/apiGateway.createApi",
"Role": {"Fn::GetAtt" : [ "ApiGatewayCreateApiFunctionRole", "Arn" ] },
"Runtime": "nodejs",
"Timeout": 30
},
"DependsOn": [
"ApiGatewayCreateApiFunctionRole"
]
},
"DynamoDBPutItemsFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
@ -221,6 +275,10 @@
}
},
"Outputs": {
"ApiGatewayCreateApiFunctionArn": {
"Description": "The ARN of the ApiGatewayCreateApiFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["ApiGatewayCreateApiFunction", "Arn"] }
},
"DynamoDBPutItemsFunctionArn": {
"Description": "The ARN of the DynamoDBPutItemsFunction, for use in other CloudFormation templates.",
"Value": { "Fn::GetAtt" : ["DynamoDBPutItemsFunction", "Arn"] }

View file

@ -2,7 +2,6 @@
"name": "cloudformation-helpers",
"version": "0.0.0",
"description": "A set of helper methods to fill in the gaps in existing CloudFormation support.",
"main": "cloudformation_helpers.js",
"repository": {
"type": "git",
"url": "https://github.com/gilt/cloudformation-helpers"

View file

@ -0,0 +1,182 @@
{
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": {
"CFHelperStackName": {
"Type": "String",
"Description": "The name of the stack where you installed the CloudFormation helper functions. See https://github.com/gilt/cloudformation-helpers."
}
},
"Resources": {
"CFHelperStack": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "https://s3.amazonaws.com/com.gilt.public.backoffice/cloudformation_templates/lookup_stack_outputs.template"
}
},
"CFHelper": {
"Type": "Custom::CFHelper",
"Properties": {
"ServiceToken": { "Fn::GetAtt" : ["CFHelperStack", "Outputs.LookupStackOutputsArn"] },
"StackName": { "Ref": "CFHelperStackName" }
},
"DependsOn": [
"CFHelperStack"
]
},
"TestFunctionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}
]
},
"Policies": [
{
"PolicyName": "LogWriter",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
}
]
}
},
"TestFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "index.handler",
"Role": { "Fn::GetAtt": [ "TestFunctionRole", "Arn" ] },
"Code": {
"ZipFile": {
"Fn::Join": [
"\n",
[
"exports.handler = function(event, context) {",
"console.log(JSON.stringify(event));",
"context.succeed('REQUEST RECEIVED:\\n' + JSON.stringify(event));",
"}"
]
]
}
},
"Runtime": "nodejs",
"Timeout": "30"
},
"DependsOn": [
"TestFunctionRole"
]
},
"ApiGatewayCreateApi": {
"Type": "Custom::ApiGatewayCreateApi",
"Properties": {
"ServiceToken": { "Fn::GetAtt" : ["CFHelper", "ApiGatewayCreateApiFunctionArn"] },
"name": { "Fn::Join": [ "-", ["test-api", { "Ref": "AWS::StackName" } ] ] },
"description": "Test API",
"endpoints": {
"foo": {
"{baz}": {
"PUT": {
"authorizationType": "NONE",
"apiKeyRequired": true,
"integration": {
"type": "MOCK",
"contentType": "text/plain"
}
}
},
"GET": {
"authorizationType": "NONE",
"integration": {
"type": "HTTP",
"integrationHttpMethod": "GET",
"uri": "http://www.example.com",
"contentType": "text/html"
}
}
},
"bar": {
"POST": {
"authorizationType": "NONE",
"integration": {
"type": "AWS",
"integrationHttpMethod": "POST",
"uri": {
"Fn::Join": [
"",
[
"arn:aws:apigateway:",
{ "Ref": "AWS::Region" },
":lambda:path/2015-03-31/functions/arn:aws:lambda:",
{ "Ref": "AWS::Region" },
":",
{ "Ref": "AWS::AccountId" },
":function:",
{ "Ref": "TestFunction" },
"/invocations"
]
]
},
"contentType": "application/json"
}
}
}
},
"version": "prod"
},
"DependsOn": [
"CFHelper",
"TestFunction"
]
},
"TestFunctionApiPermission": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": { "Fn::GetAtt": [ "TestFunction", "Arn" ] },
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:aws:execute-api:",
{ "Ref": "AWS::Region" },
":",
{ "Ref": "AWS::AccountId" },
":",
{ "Fn::GetAtt" : ["ApiGatewayCreateApi", "restApiId"] },
"/*/POST/bar"
]
]
}
},
"DependsOn": [
"TestFunction",
"ApiGatewayCreateApi"
]
}
},
"Outputs": {
"ApiEndpointRootUrl": {
"Description": "The root URL for the API's endpoints.",
"Value": { "Fn::GetAtt" : ["ApiGatewayCreateApi", "baseUrl"] }
}
}
}