Let's start off by writing out the declaration of that endpoints
variable, since the rest of the answer depends on it being defined this way:
variable "endpoints" {
type = set(object({
path = string
method = string
lambda = string
})
}
The above says that endpoints
is a set of objects, which means that the ordering of the items is not significant. The ordering is insignificant because we're going to create separate objects in API for each one anyway.
The next step is to figure out how to move from that given data structure into a structure that is a map where each key is unique and where each element maps to one instance of the resources you want to produce. To do that we must define what mapping we're intending, which I think here would be:
- One
aws_api_gateway_resource
for each distinct path
.
- One
aws_api_gateway_method
for each distinct path
and method
pair.
- One
aws_api_gateway_integration
for each distinct path
and method
pair.
- One
aws_api_gateway_integration_response
for each distinct path
/method
/status_code
triple.
- One
aws_api_gateway_method_response
for each distinct path
/method
/status_code
triple.
So it seems that we need three collections here: first is a set of all of the paths, second is a map from a path
+method
pair to the object that describes that method, and third is every combination of endpoints and status codes we want to model.
locals {
response_codes = toset({
status_code = 200
response_templates = {} # TODO: Fill this in
response_models = {} # TODO: Fill this in
response_parameters = {} # TODO: Fill this in
})
# endpoints is a set of all of the distinct paths in var.endpoints
endpoints = toset(var.endpoints.*.path)
# methods is a map from method+path identifier strings to endpoint definitions
methods = {
for e in var.endpoints : "${e.method} ${e.path}" => e
}
# responses is a map from method+path+status_code identifier strings
# to endpoint definitions
responses = {
for pair in setproduct(var.endpoints, local.response_codes) :
"${pair[0].method} ${pair[0].path} ${pair[1].status_code}" => {
method = pair[0].method
path = pair[0].path
method_key = "${pair[0].method} ${pair[0].path}" # key for local.methods
status_code = pair[1].status_code
response_templates = pair[1].response_templates
response_models = pair[1].response_models
response_parameters = pair[1].response_parameters
}
}
}
With these two derived collections defined, we can now write out the resource configurations:
resource "aws_api_gateway_rest_api" "example" {
name = "example"
}
resource "aws_api_gateway_resource" "example" {
for_each = local.endpoints
rest_api_id = aws_api_gateway_rest_api.example.id
parent_id = aws_api_gateway_rest_api.example.root_resource_id
path_part = each.value
}
resource "aws_api_gateway_method" "example" {
for_each = local.methods
rest_api_id = aws_api_gateway_resource.example[each.value.path].rest_api_id
resource_id = aws_api_gateway_resource.example[each.value.path].resource_id
http_method = each.value.method
}
resource "aws_api_gateway_integration" "example" {
for_each = local.methods
rest_api_id = aws_api_gateway_method.example[each.key].rest_api_id
resource_id = aws_api_gateway_method.example[each.key].resource_id
http_method = aws_api_gateway_method.example[each.key].http_method
type = "AWS_PROXY"
integration_http_method = "POST"
uri = each.value.lambda
}
resource "aws_api_gateway_integration_response" "example" {
for_each = var.responses
rest_api_id = aws_api_gateway_integration.example[each.value.method_key].rest_api_id
resource_id = aws_api_gateway_integration.example[each.value.method_key].resource_id
http_method = each.value.method
status_code = each.value.status_code
response_parameters = each.value.response_parameters
response_templates = each.value.response_templates
# NOTE: There are some other arguments for
# aws_api_gateway_integration_response that I've left out
# here. If you need them you'll need to adjust the above
# local value expressions to include them too.
}
resource "aws_api_gateway_response" "example" {
for_each = var.responses
rest_api_id = aws_api_gateway_integration_response.example[each.key].rest_api_id
resource_id = aws_api_gateway_integration_response.example[each.key].resource_id
http_method = each.value.method
status_code = each.value.status_code
response_models = each.value.response_models
}
You'll probably also need an aws_api_gateway_deployment
. For that, it's important to make sure it depends on all the API gateway resources we've defined above so that Terraform will wait until the API is fully configured before trying to deploy it:
resource "aws_api_gateway_deployment" "example" {
rest_api_id = aws_api_gateway_rest_api.example.id
# (whatever other settings are appropriate)
depends_on = [
aws_api_gateway_resource.example,
aws_api_gateway_method.example,
aws_api_gateway_integration.example,
aws_api_gateway_integration_response.example,
aws_api_gateway_method_response.example,
]
}
output "execution_arn" {
value = aws_api_gateway_rest_api.example.execution_arn
# Execution can't happen until the gateway is deployed, so
# this extra hint will ensure that the aws_lambda_permission
# granting access to this API will be created only once
# the API is fully deployed.
depends_on = [
aws_api_gateway_deployment.example,
]
}
API Gateway details aside, the general procedure for situations like this is:
- Define your input(s).
- Figure out how to get from your inputs to collections that have one element per instance you need for each resource.
- Write
local
expressions to describe that projection from input to the repetition collection.
- Write
resource
blocks where for_each
refers to the appropriate local value as its repetition value.
for
expressions, along with the flatten
and setproduct
functions, are our primary tool for projecting data from a structure that is convenient for the caller to provide in an input variable to the structure(s) we need for for_each
expressions.
API Gateway has a particularly complex data model though, and so expressing all of its possibilities within the Terraform language can require a lot more projection and other transformation than might be required for other services. Because OpenAPI already defines a flexible declarative language for defining REST APIs and API Gateway already natively supports it, it could be more straightforward and flexible to make your endpoints
variable take a standard OpenAPI definition and pass it directly to API Gateway, thus getting all the expressiveness of the OpenAPI schema format without having to implement all the details in Terraform yourself:
variable "endpoints" {
# arbitrary OpenAPI schema object to be validated by API Gateway
type = any
}
resource "aws_api_gateway_rest_api" "example" {
name = "example"
body = jsonencode(var.endpoints)
}
Even if you do still want your endpoints
variable to be a higher-level model, you could also consider using the Terraform language to construct an OpenAPI schema by deriving a data structure from var.endpoints
and finally passing it to jsonencode
.