Harish KM
Harish KM

Reputation: 1353

Terraform: Dynamically generate REST API endpoints in AWS API Gateway based on user input

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:

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

Answers (1)

Harish KM
Harish KM

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

Related Questions