Skip to content

Instantly share code, notes, and snippets.

@keeth
Last active January 13, 2021 14:34
Show Gist options
  • Save keeth/6bf8b67c82f9a085e03ecbb289a859d6 to your computer and use it in GitHub Desktop.
Save keeth/6bf8b67c82f9a085e03ecbb289a859d6 to your computer and use it in GitHub Desktop.
Apex + Terraform + AWS Lambda + API Gateway + JSON Encoded Errors + CORS
resource "aws_api_gateway_rest_api" "myApi" {
name = "myApi-${var.env}"
description = "My awesome API (${var.env} environment)"
}
resource "aws_api_gateway_deployment" "myApi" {
depends_on = [
"aws_api_gateway_integration.myApi_myEndpoint_post",
"aws_api_gateway_integration_response.myApi_myEndpoint_post",
"aws_api_gateway_integration_response.myApi_myEndpoint_post_400",
"aws_api_gateway_method_response.myApi_myEndpoint_post_200",
"aws_api_gateway_method_response.myApi_myEndpoint_post_400",
"aws_api_gateway_integration.myApi_myEndpoint_options"
]
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
stage_name = "${var.env}"
}
resource "aws_api_gateway_resource" "myApi_myEndpoint" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
parent_id = "${aws_api_gateway_rest_api.myApi.root_resource_id}"
path_part = "myEndpoint"
}
resource "aws_api_gateway_method" "myApi_myEndpoint_post" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "POST"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "myApi_myEndpoint_post" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "${aws_api_gateway_method.myApi_myEndpoint_post.http_method}"
type = "AWS"
integration_http_method = "POST" # Must be POST for invoking Lambda function
credentials = "${var.gateway_lambda_role_arn}"
# http://docs.aws.amazon.com/apigateway/api-reference/resource/integration/#uri
uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/${var.apex_function_myEndpoint}/invocations"
}
resource "aws_api_gateway_integration_response" "myApi_myEndpoint_post" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "${aws_api_gateway_method.myApi_myEndpoint_post.http_method}"
status_code = "${aws_api_gateway_method_response.myApi_myEndpoint_post_200.status_code}"
response_parameters_in_json = <<PARAMS
{
"method.response.header.Access-Control-Allow-Origin": "'*'"
}
PARAMS
}
resource "aws_api_gateway_integration_response" "myApi_myEndpoint_post_400" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "${aws_api_gateway_method.myApi_myEndpoint_post.http_method}"
status_code = "${aws_api_gateway_method_response.myApi_myEndpoint_post_400.status_code}"
selection_pattern = ".*message.*"
response_templates = {
"application/json" = <<EOT
#set ($errorMessageObj = $util.parseJson($input.path('$.errorMessage')))
{
"message" : "$errorMessageObj.message"
}
EOT
}
response_parameters_in_json = <<PARAMS
{
"method.response.header.Access-Control-Allow-Origin": "'*'"
}
PARAMS
}
resource "aws_api_gateway_method_response" "myApi_myEndpoint_post_200" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "${aws_api_gateway_method.myApi_myEndpoint_post.http_method}"
status_code = "200"
response_models = {
"application/json" = "Empty"
}
response_parameters_in_json = <<PARAMS
{
"method.response.header.Access-Control-Allow-Origin": true
}
PARAMS
}
resource "aws_api_gateway_method_response" "myApi_myEndpoint_post_400" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "${aws_api_gateway_method.myApi_myEndpoint_post.http_method}"
status_code = "400"
response_models = {
"application/json" = "Empty"
}
response_parameters_in_json = <<PARAMS
{
"method.response.header.Access-Control-Allow-Origin": true
}
PARAMS
}
# CORS
resource "aws_api_gateway_method" "myApi_myEndpoint_options" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "myApi_myEndpoint_options" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "${aws_api_gateway_method.myApi_myEndpoint_options.http_method}"
type = "MOCK"
request_templates = {
"application/json" = <<EOT
{"statusCode": 200}
EOT
}
}
resource "aws_api_gateway_integration_response" "myApi_myEndpoint_options_200" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "${aws_api_gateway_method.myApi_myEndpoint_options.http_method}"
status_code = "${aws_api_gateway_method_response.myApi_myEndpoint_options_200.status_code}"
response_parameters_in_json = <<PARAMS
{
"method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
"method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'",
"method.response.header.Access-Control-Allow-Origin": "'*'"
}
PARAMS
response_templates = {
"application/json" = <<EOT
{}
EOT
}
}
resource "aws_api_gateway_method_response" "myApi_myEndpoint_options_200" {
rest_api_id = "${aws_api_gateway_rest_api.myApi.id}"
resource_id = "${aws_api_gateway_resource.myApi_myEndpoint.id}"
http_method = "${aws_api_gateway_method.myApi_myEndpoint_options.http_method}"
status_code = "200"
response_models = {
"application/json" = "Empty"
}
response_parameters_in_json = <<PARAMS
{
"method.response.header.Access-Control-Allow-Headers": true,
"method.response.header.Access-Control-Allow-Methods": true,
"method.response.header.Access-Control-Allow-Origin": true
}
PARAMS
}
// Forked from https://github.com/apex/node-apex
import R from 'ramda';
function serializeError(error) {
if (!R.is(Object, error)) {
return error;
}
const copy = R.merge({}, error);
if (typeof error.message === 'string') {
copy.message = error.message;
}
return copy;
}
function wrapCb(cb) {
return function(error, result) {
if (!R.isNil(error)) {
error = JSON.stringify(serializeError(error));
}
return cb(error, result);
}
}
export default function λ(fn) {
return function(e, ctx, cb) {
cb = wrapCb(cb);
try {
var v = fn(e, ctx, cb);
if (v && typeof v.then == 'function') {
v.then(function (val) {
cb(null, val);
}).catch(cb);
return;
}
cb(null, v);
} catch (err) {
cb(err);
}
}
};
@keeth
Copy link
Author

keeth commented Sep 2, 2016

The endpoint in this example expects an application/json POST request and returns an application/json response. Error responses (400) are also JSON.

My lambda function wrapper serializes any exceptions to JSON e.g. {"message": "Something went wrong"}, which is picked up by the 400 integration response, so that meaningful error objects can be returned rather than just strings.

Requires Terraform v0.6.16

Note that in Terraform v0.7 the response_parameters_in_json is superseded by response_parameters and you can use a regular Terraform map literals rather than the PARAMS heredoc strings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment