Add support for API Gateway
This commit is contained in:
parent
2d72709c84
commit
e2f3a6efbe
6 changed files with 438 additions and 30 deletions
71
README.md
71
README.md
|
@ -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
127
aws/apiGateway.js
Normal 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();
|
||||
}
|
|
@ -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 });
|
||||
}
|
|
@ -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"] }
|
||||
|
|
|
@ -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"
|
||||
|
|
182
test/aws/apiGateway.createApi.template
Normal file
182
test/aws/apiGateway.createApi.template
Normal 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"] }
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue