'Terraform: How to create API Gateway endpoints and methods from a list of objects?
I want to create a terraform (v0.12+) module that outputs an AWS API Gateway with Lambda integration(s). I cannot quite understand how (or if it is even possible) to iterate over a list of maps to dynamically output resources.
The user should be able to instantiate the module like so:
module "api_gateway" {
source = "./apig"
endpoints = [
{
path = "example1"
method = "GET"
lambda = "some.lambda.reference"
},
{
path = "example1"
method = "POST"
lambda = "some.lambda.reference"
},
{
path = "example2"
method = "GET"
lambda = "another.lambda.reference"
}
]
}
From the endpoints interface, I want to output three resources:
- an
aws_api_gateway_resourcewherepath_part = endpoint[i].path - an
aws_api_gateway_methodwherehttp_method = endpoint[i].method - an
aws_api_gateway_integrationthat takes a reference toendpoint[i].lambda, etc
Terraform's for_each property doesn't seem robust enough to handle this. I know Terraform also supports for loops and for / in loops, but I have not been able to find any examples of using such expressions for resource declaration.
Solution 1:[1]
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_resourcefor each distinctpath. - One
aws_api_gateway_methodfor each distinctpathandmethodpair. - One
aws_api_gateway_integrationfor each distinctpathandmethodpair. - One
aws_api_gateway_integration_responsefor each distinctpath/method/status_codetriple. - One
aws_api_gateway_method_responsefor each distinctpath/method/status_codetriple.
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
localexpressions to describe that projection from input to the repetition collection. - Write
resourceblocks wherefor_eachrefers 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.
Solution 2:[2]
have a configuration file (json)
#configuration.json
{
"lambda1": {
"name": "my-name",
"path": "my-path",
"method": "GET"
},
"lambda2": {
"name": "my-name2",
"path": "my-path2",
"method": "GET"
},
}
and the following terraform
locals {
conf = jsondecode(file("${path.module}/configuration.json"))
name="name"
}
data "aws_caller_identity" "current" {}
resource "aws_lambda_permission" "apigw_lambda" {
for_each = local.conf
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = each.value.name
principal = "apigateway.amazonaws.com"
# More: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html
source_arn = "arn:aws:execute-api:${var.region}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.api.id}/*/${aws_api_gateway_method.methods[each.key].http_method}${aws_api_gateway_resource.worker-path[each.key].path}"
}
resource "aws_api_gateway_rest_api" "api" {
name = local.name
description = "an endpoints...."
endpoint_configuration {
types = ["REGIONAL"]
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_resource" "country-endpoint" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = local.country-code # https.exmaple.com/stage/uk
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_resource" "worker-path" {
for_each = local.conf
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_resource.country-endpoint.id
path_part = each.value.path # https.exmaple.com/stage/uk/path_from_json
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_method" "methods" {
for_each = local.conf
http_method = each.value.method
resource_id = aws_api_gateway_resource.worker-path[each.key].id
rest_api_id = aws_api_gateway_rest_api.api.id
authorization = "NONE"
}
resource "aws_api_gateway_integration" "lambda-api-integration-get-config" {
for_each = local.conf
# The ID of the REST API and the endpoint at which to integrate a Lambda function
resource_id = aws_api_gateway_resource.worker-path[each.key].id
rest_api_id = aws_api_gateway_rest_api.api.id
# The HTTP method to integrate with the Lambda function
http_method = aws_api_gateway_method.methods[each.key].http_method
# AWS is used for Lambda proxy integration when you want to use a Velocity template
type = "AWS_PROXY"
# The URI at which the API is invoked
uri = data.terraform_remote_state.workers.outputs.lambda_invoke[each.key]
integration_http_method = "POST"
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | helpper |
