How to use API Gateway with CloudFront

The unique challenges when integrating API Gateway with CloudFront

Author's image
Tamás Sallai
4 mins

Background

CloudFront is a great tool for bringing all the different parts of your application under one domain. It does it by allowing different origins (backends) to be defined and then path patterns can be defined that routes to different origins.

But use it with API Gateway and you'll see some unique problems.

The basic case

When you have an API Gateway and a CloudFront Distribution, you need to define an origin first:

origin {
	domain_name = replace(aws_api_gateway_deployment.deployment.invoke_url, "/^https?://([^/]*).*/", "$1")
	origin_id   = "apigw"
	origin_path = "/stage"

	custom_origin_config {
		http_port              = 80
		https_port             = 443
		origin_protocol_policy = "https-only"
		origin_ssl_protocols   = ["TLSv1.2"]
	}
}

Then a cache behavior that targets that origin under a path:

ordered_cache_behavior {
	path_pattern     = "/api/*"
	allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
	cached_methods   = ["GET", "HEAD"]
	target_origin_id = "apigw"

	default_ttl = 0
	min_ttl     = 0
	max_ttl     = 0

	forwarded_values {
		query_string = true
		cookies {
			forward = "all"
		}
	}

	viewer_protocol_policy = "redirect-to-https"
}

If you then visit the <cloudfront domain>/api/ URL it will be served by the API Gateway.

Caching

APIs are usually not cacheable so it's a sensible default to disable proxy caching on the CloudFront side. This can be done by specifying all cache TTLs as 0:

ordered_cache_behavior {
	# ...

	default_ttl = 0
	min_ttl     = 0
	max_ttl     = 0
}

Cookies and query parameters

The same goes for cookies and query parameters. Usually, you want everything to be available for your API so it's best to forward everything:

ordered_cache_behavior {
	# ...

	forwarded_values {
		query_string = true
		cookies {
			forward = "all"
		}
	}
}

Domain name

This is where things get interesting. The origin's domain_name attribute requires only a domain, but the invoke_url defines the whole URL: https://<restApiId>.execute-api.<region>.amazonaws.com/<stageName>.

To extract the domain, you can use the replace function with a regular expression:

origin {
	domain_name = replace(aws_api_gateway_deployment.deployment.invoke_url, "/^https?://([^/]*).*/", "$1")

	# ...
}

Origin request URL

When CloudFront constructs the URL for the backend, you can specify three parts:

  • the domain_name
  • the origin_path
  • and the path_pattern at the cache behavior

CloudFront constructs the URL to the origin by replacing the distribution URL with the domain_name+origin_path, then it appends the path. In the above example if the client opened <distribution>.cloudfront.net/api/users, then the final URL is <restApiId>.execute-api.<region>.amazonaws.com/stage/api/users.

Depending on the path pattern and the API Gateway stage name, there are 3 cases.

API Gateway is on /

This is when you define the cache behavior as the default. In this case, you need to add the stage name as the origin_path:

origin {
	domain_name = replace(aws_api_gateway_deployment.deployment.invoke_url, "/^https?://([^/]*).*/", "$1")
	origin_id   = "apigw"
	origin_path = "/stage"

	# ...
}
default_cache_behavior {
	target_origin_id = "apigw"

	# ...
}

This is a simple case, as each path will be translated directly to under the stage. In the case of a Lambda function, event.path will be the full path.

The stage name is the same as the path pattern

In this case, the path_pattern is also the API Gateway stage name. This way, there is no need for origin_path as every forwarded path starts with the stage:

origin {
	domain_name = replace(aws_api_gateway_deployment.deployment.invoke_url, "/^https?://([^/]*).*/", "$1")
	origin_id   = "apigw"

	# ...
}
ordered_cache_behavior {
	path_pattern     = "/stage/*"
	target_origin_id = "apigw"

	# ...
}

The stage name is different than the path pattern

In this case, you need to set the origin_path to the stage name:

origin {
	domain_name = replace(aws_api_gateway_deployment.deployment.invoke_url, "/^https?://([^/]*).*/", "$1")
	origin_id   = "apigw"
	origin_path = "stage"

	# ...
}
ordered_cache_behavior {
	path_pattern     = "/api/*"
	target_origin_id = "apigw"

	# ...
}

This way both the stage name and the path_pattern will be appended to the final URL.

As a result, there is no way to remove the /api/ part of the URL sent to the origin without relying on Lambda@Edge. This can be a problem when your backend assumes all requests will come to / instead of /path_pattern/.

Update: With CloudFront Functions it is now possible to solve this without the complicated setup of Lambda@Edge.

Conclusion

Path translation can be a source of confusion when you try to integrate API Gateway with CloudFront. I hope this overview will help you with debugging.

September 24, 2019