Terraform and CORS-Enabled AWS API Gateway

Brian Ponath
3 min readJan 16, 2018

In this article I’m going to show how to use Terraform to set up an AWS API Gateway endpoint with CORS enabled.

I’ve broken it up in sections to make it easier to explain what each part is doing. You can simply concatenate all the sections into a single Terraform file.

In the below section, I am declaring resources for the REST API, a resource on the API, the OPTIONS method, the OPTIONS method integration, and integration response. The OPTIONS method is required in order to enable CORS. The AWS documentation does a pretty good job of explaining CORS and all the nuances with integrations and integration responses on so I’ll just include a link to it here.

resource "aws_api_gateway_rest_api" "cors_api" {
name = "MyAPI"
description = "An API for demonstrating CORS-enabled methods."
}
resource "aws_api_gateway_resource" "cors_resource" {
path_part = "Employee"
parent_id = "${aws_api_gateway_rest_api.cors_api.root_resource_id}"
rest_api_id = "${aws_api_gateway_rest_api.cors_api.id}"
}
resource "aws_api_gateway_method" "options_method" {
rest_api_id = "${aws_api_gateway_rest_api.cors_api.id}"
resource_id = "${aws_api_gateway_resource.cors_resource.id}"
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_method_response" "options_200" {
rest_api_id = "${aws_api_gateway_rest_api.cors_api.id}"
resource_id = "${aws_api_gateway_resource.cors_resource.id}"
http_method = "${aws_api_gateway_method.options_method.http_method}"
status_code = "200"
response_models {
"application/json" = "Empty"
}
response_parameters {
"method.response.header.Access-Control-Allow-Headers" = true,
"method.response.header.Access-Control-Allow-Methods" = true,
"method.response.header.Access-Control-Allow-Origin" = true
}
depends_on = ["aws_api_gateway_method.options_method"]
}
resource "aws_api_gateway_integration" "options_integration" {
rest_api_id = "${aws_api_gateway_rest_api.cors_api.id}"
resource_id = "${aws_api_gateway_resource.cors_resource.id}"
http_method = "${aws_api_gateway_method.options_method.http_method}"
type = "MOCK"
depends_on = ["aws_api_gateway_method.options_method"]
}
resource "aws_api_gateway_integration_response" "options_integration_response" {
rest_api_id = "${aws_api_gateway_rest_api.cors_api.id}"
resource_id = "${aws_api_gateway_resource.cors_resource.id}"
http_method = "${aws_api_gateway_method.options_method.http_method}"
status_code = "${aws_api_gateway_method_response.options_200.status_code}"
response_parameters = {
"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" = "'GET,OPTIONS,POST,PUT'",
"method.response.header.Access-Control-Allow-Origin" = "'*'"
}
depends_on = ["aws_api_gateway_method_response.options_200"]
}

Take note of the double and single quotation marks in the response_parameters property. This tripped me up since neither the Terraform nor the AWS documentation was very clear about this. It was only through trial-and-error and comparing what got deployed in the console manually versus what Terraform deployed that I was able to deduce the correct punctuation.

Also note that I’m using depends_on properties in several of the resources. I found that these reduced errors and made for more consistent deployments.

Now that we have declared resources for the API, the resource, and the requiste OPTIONS method with its dependencies, we can move on to declaring the method we’ll be calling from our website or other application, in this case, a POST method. This section also includes a Lambda function resource which gets integrated with the POST method.

resource "aws_api_gateway_method" "cors_method" {
rest_api_id = "${aws_api_gateway_rest_api.cors_api.id}"
resource_id = "${aws_api_gateway_resource.cors_resource.id}"
http_method = "POST"
authorization = "NONE"
}
resource "aws_api_gateway_method_response" "cors_method_response_200" {
rest_api_id = "${aws_api_gateway_rest_api.cors_api.id}"
resource_id = "${aws_api_gateway_resource.cors_resource.id}"
http_method = "${aws_api_gateway_method.cors_method.http_method}"
status_code = "200"
response_parameters = {
"method.response.header.Access-Control-Allow-Origin" = true
}
depends_on = ["aws_api_gateway_method.cors_method"]
}
resource "aws_api_gateway_integration" "integration" {
rest_api_id = "${aws_api_gateway_rest_api.cors_api.id}"
resource_id = "${aws_api_gateway_resource.cors_resource.id}"
http_method = "${aws_api_gateway_method.cors_method.http_method}"
integration_http_method = "POST"
type = "AWS_PROXY"
uri = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${aws_lambda_function.lambda.arn}/invocations"
depends_on = ["aws_api_gateway_method.cors_method", "aws_lambda_function.lambda"]
}
resource "aws_api_gateway_deployment" "deployment" {
rest_api_id = "${aws_api_gateway_rest_api.cors_api.id}"
stage_name = "Dev"
depends_on = ["aws_api_gateway_integration.integration"]
}
resource "aws_lambda_permission" "apigw_lambda" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = "${aws_lambda_function.lambda.arn}"
principal = "apigateway.amazonaws.com"
source_arn = "arn:aws:execute-api:us-east-1:123456789012:${aws_api_gateway_rest_api.cors_api.id}/*/${aws_api_gateway_method.cors_method.http_method}/Employee"
}
resource "aws_lambda_function" "lambda" {
filename = "lambda_code.zip"
function_name = "API_GATEWAY_PREPROCESS"
role = "arn:aws:iam::1234567899012:role/service-role/lambdaRole"
handler = "my_function.lambda_handler"
runtime = "python2.7"
timeout = 60
source_code_hash = "${base64sha256(file("/home/user/lambda_code.zip"))}"
}

One note about your lambda function: because Lambda functions that are integrated with API methods use the proxy integration, the integration response on your API method won’t be able to have the Access-Control-Allow-Origin added as a Header Mapping. This header will have to be included with your response from your Lambda function as shown in the snippet below.

def lambda_handler(event, context):
responseMsg = {
'statusCode' : '200',
'body': 'Hello world',
'headers' : {
'Access-Control-Allow-Origin' : '*'
}
return responseMsg

Hopefully this helps someone out there trying to figure out how to build a CORS-enabled resource in API Gateway via Terraform.

--

--