'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_resource
wherepath_part = endpoint[i].path
- an
aws_api_gateway_method
wherehttp_method = endpoint[i].method
- an
aws_api_gateway_integration
that 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_resource
for each distinctpath
. - One
aws_api_gateway_method
for each distinctpath
andmethod
pair. - One
aws_api_gateway_integration
for each distinctpath
andmethod
pair. - One
aws_api_gateway_integration_response
for each distinctpath
/method
/status_code
triple. - One
aws_api_gateway_method_response
for each distinctpath
/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 wherefor_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
.
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 |