Reputation: 1353
I am building a Terraform module that deploys a REST API in AWS API Gateway. The users of this module will provide input like this:
api_resources = {
resource1 = {
api_endpoint = "/pets/{petID}"
http_method = "GET"
},
resource2 = {
api_endpoint = "/pets"
http_method = "GET"
},
resource3 = {
api_endpoint = "/toys"
http_method = "GET"
},
resource4 = {
api_endpoint = "/pets"
http_method = "POST"
}
}
In my module, this input will be deployed using the aws_api_gateway_resource
Terraform resource. It takes the following arguments:
resource "aws_api_gateway_resource" "resource" {
rest_api_id = # ID of the parent REST API resource.
parent_id = # ID of the immediate parent of this "part" of the API endpoint.
path_part = # The rightmost "part" of the endpoint URL.
}
Official documentation: Link.
Example: For the input /pets/{petID}
, the path_part
above will be {petID}
& the parent_id
will be the ID of the Terraform resource that created the pets
path_part
.
So something like this:
resource "aws_api_gateway_resource" "pets_resource" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
parent_id = aws_api_gateway_rest_api.rest_api.root_resource_id
path_part = "pets"
}
resource "aws_api_gateway_resource" "petID_resource" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
parent_id = aws_api_gateway_resource.pets_resource.id
path_part = "{petID}"
}
Note: The aws_api_gateway_rest_api
already exists elsewhere:
resource "aws_api_gateway_rest_api" "rest_api" {
name = "my-api"
}
In order to do all this dynamically based on user input, I have:
aws_api_gateway_resource
resource for each.Like this:
locals {
api_endpoints = toset([
for key, value in var.api_resources :
trimprefix(value.api_endpoint, "/")
])
}
resource "aws_api_gateway_resource" "resource" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
parent_id = aws_api_gateway_rest_api.rest_api.root_resource_id
for_each = local.api_endpoints # pets/{petID}, pets, toys
path_part = each.key
}
This works well for top-level resources /pets
& /toys
as seen in this Terraform plan:
Terraform will perform the following actions:
# aws_api_gateway_resource.resource["pets"] will be created
+ resource "aws_api_gateway_resource" "resource" {
+ id = (known after apply)
+ parent_id = "e79wlf30x5"
+ path = (known after apply)
+ path_part = "pets"
+ rest_api_id = "yrpm6dx4z8"
}
# aws_api_gateway_resource.resource["pets/{petID}"] will be created
+ resource "aws_api_gateway_resource" "resource" {
+ id = (known after apply)
+ parent_id = "e79wlf30x5"
+ path = (known after apply)
+ path_part = "pets/{petID}"
+ rest_api_id = "yrpm6dx4z8"
}
# aws_api_gateway_resource.resource["toys"] will be created
+ resource "aws_api_gateway_resource" "resource" {
+ id = (known after apply)
+ parent_id = "e79wlf30x5"
+ path = (known after apply)
+ path_part = "toys"
+ rest_api_id = "yrpm6dx4z8"
}
Plan: 3 to add, 0 to change, 0 to destroy.
How can I make it work for nested resources like /pets/{petID}
? Creation of the /pets/{petID}
resource in the above plan will fail! The challenge is setting the correct parent_id
for aws_api_gateway_resource
for nested resources. And this needs to work for any level of nesting.
Note: There exists a data source that can return the ID of any URL path like this:
data "aws_api_gateway_resource" "pets_resource" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
path = "/pets"
}
I just don't know how to put it all together!
Upvotes: 4
Views: 2317
Reputation: 1353
I ended up changing the input format to make things easier. The end result is as follows:
User input:
api_endpoints = {
"/" = { get = "lambda1" }
"/pets" = {
get = "lambda2"
post = "lambda1"
}
"/pets/{petID}" = { get = "lambda3" }
"/toys" = { get = "lambda3" }
}
lambda_functions = {
lambda1 = {
runtime = "nodejs14.x"
handler = "index.handler"
zip = "../lambda1.zip"
}
lambda2 = {
runtime = "nodejs14.x"
handler = "index.handler"
zip = "../lambda2.zip"
}
lambda3 = {
runtime = "python3.7"
handler = "index.handler"
zip = "../lambda3.zip"
}
}
And the code inside my Terraform module that works with this user input is as follows:
The REST API:
locals {
openAPI_spec = {
for endpoint, spec in var.api_endpoints : endpoint => {
for method, lambda in spec : method => {
x-amazon-apigateway-integration = {
type = "aws_proxy"
httpMethod = "POST"
uri = "arn:aws:apigateway:${data.aws_region.region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:${data.aws_region.region.name}:${data.aws_caller_identity.identity.account_id}:function:${lambda}/invocations"
}
}
}
}
}
resource "aws_api_gateway_rest_api" "rest_api" {
name = var.api_name
endpoint_configuration {
types = ["REGIONAL"]
}
body = jsonencode({
openapi = "3.0.1"
paths = local.openAPI_spec
})
}
The Lambda functions:
module "lambda_function" {
source = "terraform-aws-modules/lambda/aws"
for_each = var.lambda_functions
function_name = each.key
runtime = each.value.runtime
handler = each.value.handler
create_package = false
local_existing_package = each.value.zip
create_current_version_allowed_triggers = false
allowed_triggers = {
api-gateway = {
service = "apigateway"
source_arn = "${aws_api_gateway_rest_api.rest_api.execution_arn}/*/*/*"
}
}
}
More details in my GitHub repo.
Upvotes: 6