2

I am trying to create the aws_api_gateway resources using terraform, to avoid the hassle of adding the same code again and again, am trying to create a loop which create multiple resources for me in the order i specify. But, I am facing difficulties as I am new to this and it is always complaining of cyclic dependency.

The cyclic dependency is caused in "parent_id" while creating resources

Any Help would be greatly appreciated.

Here's what I have coded:

locals {
  api_endpoints = [
    {
      path           = "v0"
      http_method    = "GET"
      integration_uri = "${uri_path}/v0"
    },
    {
      path           = "v0/expert/gen/btags"
      http_method    = "POST"
      integration_uri = "${uri_path}/v0/expert/gen/btags"
    },
    {
      path           = "v0/expert/generate/tags"
      http_method    = "POST"
      integration_uri = "${uri_path}/v0/expert/gen/tags"
    },
    {
      path           = "v0/expert/search"
      http_method    = "POST"
      integration_uri = "${uri_path}/v0/expert/search"
    },
  ]
  resources_hierarchy = {
      "v0" = aws_api_gateway_rest_api.gatewayh.root_resource_id
      "expert" = "v0"
      "gen" = "expert"
      "search" = "expert"
      "btags" = "gen"
      "tags" = "gen"
    }
}

resource "aws_api_gateway_resource" "api_resource" {
  for_each = local.resources_hierarchy

  rest_api_id = aws_api_gateway_rest_api.gatewayh.id
  parent_id   = each.value == aws_api_gateway_rest_api.gatewayh.root_resource_id ? each.value : aws_api_gateway_resource.api_resource[each.value].id
  path_part   = each.key
}



1
  • There's also this question (unanswered) that seems closely related to yours Commented Oct 16, 2023 at 21:10

5 Answers 5

2

The full Amazon API Gateway schema is challenging to construct dynamically in Terraform because it ends up requiring resources that depend on themselves. Dependencies in Terraform are between resource blocks as a whole rather than the individual instances of them, because Terraform must evaluate for_each or count before knowing which instances exist and those expressions can themselves have dependencies.

However, you can avoid that problem by using the OpenAPI-based method for describing your API, which follows a flat structure similar to what you've shown as your input. The API Gateway API internally transforms that into the corresponding individual resources, resource methods, integration methods, etc, so the result is the same but constructed in a way that doesn't involve as many resources and doesn't involve any dependencies between the objects.

For example:

locals {
  api_ops_by_path = tomap({
    for op in local.api_endpoints : op.path => op
  })
}

resource "aws_api_gateway_rest_api" "example" {
  # ...

  body = jsonencode({
    openapi = "3.0.1"
    info = {
      title   = "example"
      version = "1.0"
    }
    paths = {
      for path, ops in local.api_ops_by_path : path => {
        for op in ops : lower(op.http_method) => {
          x-amazon-apigateway-integration = {
            httpMethod = op.http_method
            uri        = op.integration_uri
            # ...
          }
        }
      }
    }
  })
}

The above uses for expressions to derive a data structure that follows the OpenAPI spec for describing methods associated with paths, and uses the x-amazon-apigateway-integration extension, specific to Amazon API Gateway, to describe what each operation integrates with. It then uses jsonencode to encode that resulting data structure into a form that API Gateway can parse and analyze.

When you define an API Gateway REST API with a body argument, you don't need to use any other resources to describe the schema; the information in body describes the same tree of resources in a different way. You will still need resources for the deployment, stages, etc, though, because they are independent of the actual API definition.

OpenAPI uses a flat structure -- all paths listed together as a single map, rather than resources nested inside one another -- and so that's a better fit both for the way your input data is structured and for describing this in a form that doesn't cause problems for Terraform's dependency graph.


References:

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks! this sounds promising, can you provide me some reference links which i can use to write this for my requirement
This helped but again stuck with one more cyclic dependency. Now i am able to create endpoints but i also need to attach custom lambda authorizer. @martin Can you help me with this
I imagine what you've created is now pretty different than what you originally shared when you asked this question, so I would suggest starting a new Stack Overflow question that describes what you've written so far and full details on what new problem you've encountered. I look for Terraform questions on here relatively often so I'll probably see your new question once you've posted it, but someone else might also answer first. 😀
Thanks a lot, i was able to figure that out myself. This answer helped!
1

I'm not sure why we need for a second loop in the Martin's answer, so for me it works with just

variable "paths" {
  type = list(object({
    path               = string
    http_method        = optional(string)
    integration_uri    = string
    integration_type   = optional(string)
    integration_method = optional(string)
    payload_version    = optional(string)
  }))
}

locals {
  api_path_map = tomap({
    for op in var.paths : op.path => op
  })
}


resource "aws_api_gateway_rest_api" "this" {
# ...
  paths = {
    for path, op in local.api_path_map : path => {
      lower(coalesce(op.http_method, "get")) = {
        x-amazon-apigateway-integration = {
          uri                  = op.integration_uri
          type                 = upper(coalesce(op.integration_type, "AWS_PROXY"))
          httpMethod           = upper(coalesce(op.integration_method, "POST"))
          payloadFormatVersion = upper(coalesce(op.payload_version, "2.0"))
        }
      }
    }
  }
}

And then call it with just

# ...
paths = [{
  path            = "/sku/{id}"
  integration_uri = module.lambda.lambda_arn
}]
# ...

Comments

0

That's impossible in Terraform.

In a Terraform state, objects depend on resources, not on instances of resources.

You're using aws_api_gateway_resource.api_resource[each.value].id as an input.

Had you used it as an input in another resource, the dependency would be created on aws_api_gateway_resource.api_resource, not on a particular instance of aws_api_gateway_resource.api_resource.

Since a resource cannot depend on itself, Terraform throws the error you're seeing.

Comments

0

while workaround this steps in my environment, the second iteration got failed.

 paths = {
      for path, ops in local.api_ops_by_path : path => {
        for op in ops : lower(op.http_method) => {
          x-amazon-apigateway-integration = {

Error message is given below

but the for op in ops : op.http_method is not working

error is : Can't access attributes on a primitive-typed value (string).

My guess is, looks the op is print only value of dict. not as key : value

1 Comment

Quassnoi - Is there any alternative solution ?
0

For anyone coming across this in 2025, there's no solution to this problem as terraform simply doesn't support it. See https://github.com/hashicorp/terraform/issues/26697

The only solution to this I can come up with is to use an "external" data resource (data "external" "generator") that points to a script you write, consumes your configuration data, and generates a terraform file you can then consume in your TF modules.

3 Comments

OpenAPI solution from @demmonico (stackoverflow.com/a/79492432/38753) worked fine for me.
@EightyEight His answer doesn't answer the question. If you need resources to refer to themselves, they can't. You have to flatten the resource creation, which can't be done in terraform. His answer excludes the part of the resource that is needed, and this problem is part of the reason why AWS added support for openapi to solve this.
I see, thank you for the clarification.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.