Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cycle error for replacement of aws_api_gateway_deployment with lifecycle create_before_destroy set to true and API Gateway resources in depends_on section #11344

Closed
martyna-autumn opened this issue Dec 18, 2019 · 57 comments
Labels
service/apigateway Issues and PRs that pertain to the apigateway service.

Comments

@martyna-autumn
Copy link

martyna-autumn commented Dec 18, 2019

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request
  • Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment

Terraform Version

Terraform v0.12.18
+ provider.aws v2.42.0

Affected Resource(s)

  • aws_api_gateway_deployment

Terraform Configuration Files

I'm not copying all API Gateway resources' configuration as it's pretty standard but happy to share configuration of whole API Gateway if requested

resource "aws_api_gateway_deployment" "deployment" {
  depends_on = [
    aws_api_gateway_rest_api.api,
    aws_api_gateway_resource.api_email_health,
    aws_api_gateway_method.api_email_health_get,
    aws_api_gateway_integration.api_email_health_get_integration,
    aws_api_gateway_method.api_email_health_options,
    aws_api_gateway_integration.api_email_health_options_integration,
    aws_api_gateway_integration_response.api_email_health_options_integration_response,
    aws_api_gateway_method_response.api_email_health_options_response,
    aws_api_gateway_resource.api_email_templates,
    aws_api_gateway_method.api_email_templates_get,
    aws_api_gateway_integration.api_email_templates_get_integration,
    aws_api_gateway_method.api_email_templates_options,
    aws_api_gateway_integration.api_email_templates_options_integration,
    aws_api_gateway_integration_response.api_email_templates_options_integration_response,
    aws_api_gateway_method_response.api_email_templates_options_response,
    aws_api_gateway_resource.api_email_emails,
    aws_api_gateway_method.api_email_emails_post,
    aws_api_gateway_integration.api_email_emails_post_integration,
    aws_api_gateway_method.api_email_emails_options,
    aws_api_gateway_integration.api_email_emails_options_integration,
    aws_api_gateway_integration_response.api_email_emails_options_integration_response,
    aws_api_gateway_method_response.api_email_emails_options_response,
    aws_api_gateway_resource.api_email
  ]

  rest_api_id = aws_api_gateway_rest_api.api.id

  stage_description = "Deployed at ${timestamp()}"

  stage_name = var.aws_spotlight_environment

  lifecycle {
    create_before_destroy = true
  }
}

Expected Behavior

As resource aws_api_gateway_deployment is configured as depends_on all API Gateway resources/methods/integrations/responses, it shouldn't be created before all resources in API Gateway are provisioned so outcome should be (and was this way till recently): old API Gateway resources are destroyed, new are created, new deployment created, old deployment destroyed
We force replacement of aws_api_gateway_deployment so current API Gateway state is always deployed to main stage

This was behaviour in Terraform 0.11.x

Actual Behavior

Cycle Error

Error: Cycle: aws_api_gateway_integration.api_email_health_get_integration (destroy), aws_api_gateway_integration.api_email_health_options_integration (destroy), aws_api_gateway_integration_response.api_email_health_options_integration_response (destroy),
aws_api_gateway_method_response.api_email_health_options_response (destroy), aws_api_gateway_method.api_email_health_options (destroy), aws_api_gateway_resource.api_email_health (destroy), aws_api_gateway_deployment.deployment, aws_api_gateway_deployment.deployment (destroy deposed 359e79c1),
aws_api_gateway_method.api_email_health_get (destroy)

Removal off create_before_destroy = true in lifecycle of resource aws_api_gateway_deployment helps but causes it to fail anyway on different error:

Error: error deleting API Gateway Deployment (bdq86u): BadRequestException: Active stages pointing to this deployment must be moved or deleted

If I remove depends_on section instead, I have situations that deployment happens before all API methods are properly configured. Example:

Error: Error creating API Gateway Deployment: BadRequestException: No integration defined for method

I tried adding separate resource for stage aws_api_gateway_stage but problem persists

Steps to Reproduce

  1. Create API Gateway with aws_api_gateway_deployment which depends on API Gateway resources and is recreated with every terraform apply
  2. Run terraform apply
  3. Change one or more API Gateway resources which forces them to be destroyed and recreated (ie change API Gateway resource path)
  4. Run terraform apply
@ghost ghost added the service/apigateway Issues and PRs that pertain to the apigateway service. label Dec 18, 2019
@github-actions github-actions bot added the needs-triage Waiting for first response or review from a maintainer. label Dec 18, 2019
@sparvia
Copy link

sparvia commented Dec 20, 2019

We also ran into this problem, and solved it by removing create_before_destroy from the deployment, and manually running terraform taint on the stage resource to force it to be recreated, which got rid of the other error you mention.

@Glen-Moonpig
Copy link
Contributor

We also ran into this problem, and solved it by removing create_before_destroy from the deployment, and manually running terraform taint on the stage resource to force it to be recreated, which got rid of the other error you mention.

If you taint the resource, does that mean that the deployment will be destroyed before a new one created and so the API be unavailable for the period of time in between destroy and create?

@martyna-autumn
Copy link
Author

martyna-autumn commented Jan 2, 2020

We also ran into this problem, and solved it by removing create_before_destroy from the deployment, and manually running terraform taint on the stage resource to force it to be recreated, which got rid of the other error you mention.

Isn't it manual wrangling to solve problem? We use CD software to deploy our TF code so we would prefer avoid such workarounds. Plus our stage is active as its attached to Custom Domain Name so we can't have it destroyed or have not existing deployment.

Currently we use null resource with some sleep command and deployment resource explicitly set to depends on that null resource as form of workaround. Deployment resource itself isn't set to depend on any API Gateway resources but delay gives time to all of required resources (methods, integrations and so on) to be provisioned before deployment is created (example below uses PowerShell as language for command because that's what we use in our company mostly)

resource "null_resource" "wait_for_all_resources" {
  triggers = {
    timestamp = timestamp()
  }
  provisioner "local-exec" {
    command     = "Start-Sleep -Seconds 60"
    interpreter = ["PowerShell", "-Command"]
  }
}

@edbighead
Copy link

Having same issue with

Terraform v0.12.19
+ provider.aws v2.45.0

@lordz-md
Copy link

Having same issue with

Terraform v0.12.19
+ provider.aws v2.45.0

Same with
Terraform v0.12.19

  • provider.aws v2.46.0

@lordz-md
Copy link

Does anyone know if there is any work on this issue?

@katherinel
Copy link

Same issue here, with terraform 0.11

@kromol
Copy link

kromol commented Jun 2, 2020

I am experiencing the same issue with terraform 0.12 and with new triggers argument.
Removing create_before_destroy solves the problem, even though it's not ideal solution.

@nakamasato
Copy link

I also encountered the same issue. I tried two possible compromise solutions.

  1. Wait for a while until all the dependent resources are created

    I tried the following solution and I could change method and resource at least. The drawback is that this will trigger deployment every time you apply even if you don't have any change in the dependent resources.

    resource "aws_api_gateway_deployment" "deployment" {
    - depends_on = [
    -   module.method.lambda-integration
    - ]
    
      rest_api_id = aws_api_gateway_rest_api.api.id
    
      triggers = {
    -   redeployment = sha1(join(",", list(
    -     jsonencode(module.method.lambda-integration), # I was using lambda integration as a trigger of deployment.
    -   )))
    +   redeployment = timestamp()
      }
    
      provisioner "local-exec" {
        command = "sleep 30"
      }
    
      lifecycle {
        create_before_destroy = true
      }
    }
  2. Pass variable for trigger
    In this way, we can control when to recreate deployment, but you need to separate the resource update and deployment trigger. If you put them in one apply, creating and destroying deployment will start before completing to update the dependent resources.

    resource "aws_api_gateway_deployment" "deployment" {
      rest_api_id = aws_api_gateway_rest_api.api.id
    
      triggers = {
        redeployment = var.release-date
      }
    
      lifecycle {
        create_before_destroy = true
      }
    }
    

@hdryx
Copy link

hdryx commented Jun 19, 2020

So the bug is still there ? There is no fix ? We have to do workarounds ?

I'm using 0.12.26 and having the same issue.

@vladcar
Copy link

vladcar commented Jun 23, 2020

I also had this issue, the following solution worked well for me.
I'm using random_uuid resource to produce a value that is passed to triggers block in aws_api_gateway_deployment resource.
The random_uuid is re-generated when keepers values change, which can be set to anything e.g jsonencode(aws_api_gateway_method.method) and jsonencode(aws_api_gateway_integration.integration). It is important to make sure that aws_api_gateway_deployment is created after everything, I achieved it by extracting it into a module and using mandatory variable.

variable "required_resources" {
  type        = list(string)
  description = "Change in these values trigger redeployment"
}

resource "aws_api_gateway_deployment" "deployment" {
  rest_api_id = var.rest_api_id
  stage_name  = var.stage

  # hack to force redeployment every time this hash changes
  triggers = {
    redeployment = sha1(join(",", var.required_resources)
  }

  # false by default, just for clarity
  lifecycle {
    create_before_destroy = false
  }
}

The above resource is placed in its own module.

resource "random_uuid" "deployment_trigger" {
  depends_on = [aws_api_gateway_integration.integration, aws_api_gateway_method.method]
  keepers = {
    # Generate a new id every time something happens to these resources
    method      = jsonencode(aws_api_gateway_method.method)
    integration = jsonencode(aws_api_gateway_integration.integration)
    path        = var.resource_path
  }
}

# some other gateway stuff...

module "deployment" {
  source      = "../modules/api-gateway-deployment"
  rest_api_id = aws_api_gateway_rest_api.api.id
  stage       = var.stage

  required_resources = [
    random_uuid.deployment_trigger.id,
    random_uuid.deployemnt_trigger_for_another_method.id,
# add random uuid for each method/integration
  ]
}

I placed stuff required for adding new method into its own module as well so I don't have to write "random_uuid" "deployment_trigger" multiple times. This seems to be working fine for consecutive deployments and changes to api gateway integration/method.

I published modules I use, they are very basic and might not work for all projects but code can be adapted for your needs.
https://github.com/vladcar/terraform-aws-serverless-common-api-gateway-method
https://github.com/vladcar/terraform-aws-serverless-common-api-gateway-deployment

@walidmansia
Copy link

hello everybody i did find a solution,
terraform handel resources in singleton mode, it means on resource with a specific name should exist only one time in a tf state, in the case of apigateway deployment, a deployment cant be modified, its a partucularity of aws, and it is quite normal it is like a tag.
my solution is to remove the resource from the tfstate after each apply
terraform state rm aws_api_gateway_deployment.gw_deploy_dev
and now i can see the history of terrform deployments on my Api
i hope it will help you,
corona virus is a mess but thanks to the time that i had i could made a reverse engineering of the apigw,
but in the end i think that Terraform should add new type of ressource based of the design pattern Prototype

@andrewp-sf
Copy link

This is not a valid solution. One - you're doing manual work around configuration. Two - when you remove this from state, it means deployment will be created on next apply automatically (even if you don't need/want to).
What I see here is a way of tainting/abandoning deployment on destroy. Can't we have some parameter that simply removes deployment from state instead of running API call to delete deployment?

@shederman
Copy link

Guys, this bug means that Terraform CANNOT work with API Gateway in Production. Is there ANY view to when this CRITICAL defect in the AWS Provider will be fixed? Alternatively we will have to move away from Terraform.

@walidmansia
Copy link

i dont agree, you can split your infra in two module, one special for the deployment.
each time you ask for deployment its a new one, like the design pattern PROTOTYPE

@shederman
Copy link

Completely wiping out the value of declarative Infrastructure as Code. If we should manually do a whole bunch of extra work to the top level every single time some minor change in a bottom level happens, what's the point of Terraform?

@shederman
Copy link

Since it seems this code has zero value, I will post the code that is not working. We have made numerous changes to try and get this working, and not one has worked. This particular variation builds the API Gateway just fine, but any slight change (e.g. to what parameters we validate) results in "Error: error deleting API Gateway Deployment (ufn1gl): BadRequestException: Active stages pointing to this deployment must be moved or deleted"

The only way to make this work in tooling (fully automated) is to entirely destroy the entire API gateway and recreate it, resulting in a completely new URL. I would not be happy with that solution in a Development environment; in a Production one it's a joke.

The defects related to our issue are:

locals {
  private_config_map = { type = "PRIVATE", vpc_endpoint_ids = var.vpc_endpoint_ids }
  regional_config_map = { type = "REGIONAL", vpc_endpoint_ids = null }
}

/* ---------------------------
 * API GATEWAY
 * --------------------------- */
resource "aws_api_gateway_rest_api" "main" {
  name            = var.name

  dynamic "endpoint_configuration" {
    for_each = var.private == true ? list(local.private_config_map) : list(local.regional_config_map)

    content {
      types             = [endpoint_configuration.value["type"]]
      vpc_endpoint_ids  = endpoint_configuration.value["vpc_endpoint_ids"]
    }
  }

  api_key_source  = "HEADER"
  body            = var.body
  tags            = var.tags

  lifecycle {
    ignore_changes = [
      policy
    ]
  }
}

/* ---------------------------
 * SETTINGS
 * --------------------------- */
resource "aws_api_gateway_method_settings" "main" {
  rest_api_id = aws_api_gateway_rest_api.main.id
  stage_name  = aws_api_gateway_deployment.deploy.stage_name
  method_path = "*/*"

  settings {
    metrics_enabled    = true
    logging_level      = "INFO"
    data_trace_enabled = true
  }
}

/* ---------------------------
 * MAIN STAGE
 * --------------------------- */
resource "aws_api_gateway_stage" "main" {
  stage_name            = "main"
  description           = "Main Stage for deploying functionality"
  rest_api_id           = aws_api_gateway_rest_api.main.id
  deployment_id         = aws_api_gateway_deployment.deploy.id
  xray_tracing_enabled  = var.xray_tracing_enabled

  variables             = var.variables

  access_log_settings {
    destination_arn = var.cloudwatch_log_arn
    format          = "\"{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"caller\":\"$context.identity.caller\",\"user\":\"$context.identity.user\",\"requestTime\":$context.requestTimeEpoch,\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":$context.status,\"protocol\":\"$context.protocol\",\"path\":\"$context.path\",\"stage\":\"$context.stage\",\"xrayTraceId\":\"$context.xrayTraceId\",\"userAgent\":\"$context.identity.userAgent\",\"responseLength\":$context.responseLength}\""
  }

  lifecycle {
    ignore_changes = [
      deployment_id
    ]
  }

  tags = var.tags

  depends_on = [aws_api_gateway_deployment.deploy]
}

/* ---------------------------
 * DEPLOYMENT
 * --------------------------- */
resource "aws_api_gateway_deployment" "deploy" {
  rest_api_id = aws_api_gateway_rest_api.main.id
  stage_name  = "deploy"
  stage_description = "Deployed at ${timestamp()}"

  triggers = {
    redeployment = sha1(join(",", list(
      jsonencode(var.body)
    )))
  }

  lifecycle {
    create_before_destroy = true
  }
}

@shederman
Copy link

@mitchellh Are you aware of this issue? Essentially, Terraform does not support AWS API Gateway. How exactly do we get developer focus on this, seems like an issue like this just gets closed and reopened and closed and reopened in cycle forever.

@Glen-Moonpig
Copy link
Contributor

Glen-Moonpig commented Sep 29, 2020

@mitchellh Are you aware of this issue? Essentially, Terraform does not support AWS API Gateway. How exactly do we get developer focus on this, seems like an issue like this just gets closed and reopened and closed and reopened in cycle forever.

Terraform does support API Gateway. I think the solution is not to be found in Terraform, but in the AWS Terraform Provider. Rather than have all of the elements of the API Gateway as separate Terraform resources, they should be blocks on the aws_api_gateway_rest_api resource, then when any part of the gateway resource changes the provider would know to create a new deployment. I would do the work but I do not code in go, but could design the solution.

resource "aws_api_gateway_rest_api" "my_gateway" {
  name        = "my-gateway"

  endpoint_configuration {
    types = ["REGIONAL"]
  }

  resource {
    path_part = "products"
    method {
        http_method = "GET"
        authorization = "NONE"
        integration = {
            integration_http_method = "POST"
            type                  = "AWS_PROXY"
            uri                     = aws_lambda_alias.example.invoke_arn
        }
    }
    resource {
        path_part = "toys"
        method {
            http_method = "GET"
            authorization = "NONE"
            integration = {
                integration_http_method = "POST"
                type                  = "AWS_PROXY"
                uri                     = aws_lambda_alias.example.invoke_arn
            }
        }
    }
  }
}

@shederman
Copy link

@Glen-Moonpig your solution sounds interesting. The one piece I would dispute is that Terraform supports API Gateway. Terraform is supposed to be a tool to manage infrastructure as code - this is a production focused tool. If Terraform cannot create and manage components like API Gateway without causing production outages not required in normal operation of the component, then I would argue quite vehemently that it is not in fact supported.

Especially since this has been unresolved in one shape or form for over a year.

@Glen-Moonpig
Copy link
Contributor

@Glen-Moonpig your solution sounds interesting. The one piece I would dispute is that Terraform supports API Gateway. Terraform is supposed to be a tool to manage infrastructure as code - this is a production focused tool. If Terraform cannot create and manage components like API Gateway without causing production outages not required in normal operation of the component, then I would argue quite vehemently that it is not in fact supported.

Especially since this has been unresolved in one shape or form for over a year.

I am using Terraform to deploy and maintain API Gateways in numerous projects. I have not had any production outages. There are very simple ways to handle this particular scenario. You can just break your changes down into multiple applys and they will go through fine.
Terraform 0.13.3/0.14 might resolve the cycle issue as there are various changes around cycles and plans.

@shederman
Copy link

We're using Terraform Cloud for this, which does not appear to support multiple apply's as you state; and even if I try this manually, the multiple applies always result in the same underlying error. I believe it may be because we are using OpenAPI Import instead of manually specifying each resource independently; but that's a key feature of API Gateway.

@riley-clarkson
Copy link

@shederman My team is having the same issue (posting from my personal github however); most of these workarounds are not ideal and some won't work if you have both a deployment and a stage resource.

We currently workaround by

  • adding a timestamp() trigger to the deployment resource
  • tainting our stage (add a taint command to our deploy script essentially) and destroy/recreate both deployment and stage every apply

This should not be necessary though. I hope that this is indeed resolved in .13

@shederman
Copy link

@riley-clarkson Do you get any service interruptions like that? We have mission-critical services running on API Gateway and the idea of destroying stages on every deploy is not a popular one I can tell you!

@riley-clarkson
Copy link

@shederman We do have service interruptions, which is okay for us, but still not ideal (and will not be possible for some projects/teams). We tried most of the workarounds in this thread before resorting to tainting the stage every deployment. Would like to see this fixed

@shederman
Copy link

Yeah, that clearly shows that this is not Production ready for mission critical systems

@shederman
Copy link

Does anyone know how long it will be until the Hashicorp bot autocloses the issue (as happened to the previous few)?

@breathingdust
Copy link
Member

Hi all! 👋 Just a quick note to let you know this is on our radar and we will be taking a look in the near future to arrive at a resolution.

@shederman
Copy link

@bflad Thanks so much fo this, we will start testing right away to see if it alleviates our issues. A initial run through looks very promising.

@rbowater
Copy link

In my testing, if you trigger deployments off changes in id as per the example, that means that the resource will be destroyed and recreated to reflect the ID change (e.g. you change the method from a POST to a PUT). This leads to the following situation:

aws_api_gateway_integration.api_integration: Creation complete after 1s [id=agi-mn2mqickwg-fdd064-PUT]
aws_api_gateway_deployment.deployment: Creating...
aws_api_gateway_deployment.deployment: Creation complete after 1s [id=b5r1ui]
aws_api_gateway_deployment.deployment: Destroying... [id=s0ir4f]
aws_api_gateway_deployment.deployment: Destruction complete after 1s
aws_api_gateway_integration.api_integration: Destroying... [id=agi-mn2mqickwg-fdd064-POST]

Essentially, the API gets deployed before the old integration is destroyed, which means your API deployment will contain both the old integration and the new one at the same time. This might not be desired, so unless I've missed something it's worth taking care when triggering deployments off resources that are getting destroyed and recreated as opposed to just being modified in place.

bflad added a commit that referenced this issue Jan 28, 2021
… discourage stage_name and further encourage create_before_destroy

Reference: #11344

Adds new end-to-end example of an OpenAPI REST API and also encourages the usage of OpenAPI specifications for configuring the REST API. Support for the other API Gateway resources is not going anywhere, but the dependency management aspect of deployments can be more difficult in that model and it is much easier to discover the API Gateway resources over the OpenAPI support.

In the future, it may be worth considering deprecating the `stage_name` and friends arguments since having a Terraform resource manage two remote resources is an anti-pattern and not well supported.

Output from example:

```console
$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_acm_certificate.example will be created
  + resource "aws_acm_certificate" "example" {
      + arn                       = (known after apply)
      + certificate_body          = (known after apply)
      + domain_name               = (known after apply)
      + domain_validation_options = (known after apply)
      + id                        = (known after apply)
      + private_key               = (sensitive value)
      + status                    = (known after apply)
      + subject_alternative_names = (known after apply)
      + validation_emails         = (known after apply)
      + validation_method         = (known after apply)
    }

  # aws_api_gateway_base_path_mapping.example will be created
  + resource "aws_api_gateway_base_path_mapping" "example" {
      + api_id      = (known after apply)
      + domain_name = (known after apply)
      + id          = (known after apply)
      + stage_name  = "example"
    }

  # aws_api_gateway_deployment.example will be created
  + resource "aws_api_gateway_deployment" "example" {
      + created_date  = (known after apply)
      + execution_arn = (known after apply)
      + id            = (known after apply)
      + invoke_url    = (known after apply)
      + rest_api_id   = (known after apply)
      + triggers      = {
          + "redeployment" = "e042aae1faf8de8d7c7c98c063a986025f058c69"
        }
    }

  # aws_api_gateway_domain_name.example will be created
  + resource "aws_api_gateway_domain_name" "example" {
      + arn                      = (known after apply)
      + certificate_upload_date  = (known after apply)
      + cloudfront_domain_name   = (known after apply)
      + cloudfront_zone_id       = (known after apply)
      + domain_name              = (known after apply)
      + id                       = (known after apply)
      + regional_certificate_arn = (known after apply)
      + regional_domain_name     = (known after apply)
      + regional_zone_id         = (known after apply)
      + security_policy          = (known after apply)

      + endpoint_configuration {
          + types = [
              + "REGIONAL",
            ]
        }
    }

  # aws_api_gateway_method_settings.example will be created
  + resource "aws_api_gateway_method_settings" "example" {
      + id          = (known after apply)
      + method_path = "*/*"
      + rest_api_id = (known after apply)
      + stage_name  = "example"

      + settings {
          + cache_data_encrypted                       = (known after apply)
          + cache_ttl_in_seconds                       = (known after apply)
          + caching_enabled                            = (known after apply)
          + data_trace_enabled                         = (known after apply)
          + logging_level                              = (known after apply)
          + metrics_enabled                            = true
          + require_authorization_for_cache_control    = (known after apply)
          + throttling_burst_limit                     = -1
          + throttling_rate_limit                      = -1
          + unauthorized_cache_control_header_strategy = (known after apply)
        }
    }

  # aws_api_gateway_rest_api.example will be created
  + resource "aws_api_gateway_rest_api" "example" {
      + api_key_source               = (known after apply)
      + arn                          = (known after apply)
      + binary_media_types           = (known after apply)
      + body                         = jsonencode(
            {
              + info    = {
                  + title   = "api-gateway-rest-api-openapi-example"
                  + version = "1.0"
                }
              + openapi = "3.0.1"
              + paths   = {
                  + /path1 = {
                      + get = {
                          + x-amazon-apigateway-integration = {
                              + httpMethod           = "GET"
                              + payloadFormatVersion = "1.0"
                              + type                 = "HTTP_PROXY"
                              + uri                  = "https://ip-ranges.amazonaws.com/ip-ranges.json"
                            }
                        }
                    }
                }
            }
        )
      + created_date                 = (known after apply)
      + description                  = (known after apply)
      + disable_execute_api_endpoint = (known after apply)
      + execution_arn                = (known after apply)
      + id                           = (known after apply)
      + minimum_compression_size     = -1
      + name                         = "api-gateway-rest-api-openapi-example"
      + policy                       = (known after apply)
      + root_resource_id             = (known after apply)

      + endpoint_configuration {
          + types            = [
              + "REGIONAL",
            ]
          + vpc_endpoint_ids = (known after apply)
        }
    }

  # aws_api_gateway_stage.example will be created
  + resource "aws_api_gateway_stage" "example" {
      + arn           = (known after apply)
      + deployment_id = (known after apply)
      + execution_arn = (known after apply)
      + id            = (known after apply)
      + invoke_url    = (known after apply)
      + rest_api_id   = (known after apply)
      + stage_name    = "example"
    }

  # tls_private_key.example will be created
  + resource "tls_private_key" "example" {
      + algorithm                  = "RSA"
      + ecdsa_curve                = "P224"
      + id                         = (known after apply)
      + private_key_pem            = (sensitive value)
      + public_key_fingerprint_md5 = (known after apply)
      + public_key_openssh         = (known after apply)
      + public_key_pem             = (known after apply)
      + rsa_bits                   = 2048
    }

  # tls_self_signed_cert.example will be created
  + resource "tls_self_signed_cert" "example" {
      + allowed_uses          = [
          + "key_encipherment",
          + "digital_signature",
          + "server_auth",
        ]
      + cert_pem              = (known after apply)
      + dns_names             = [
          + "example.com",
        ]
      + early_renewal_hours   = 0
      + id                    = (known after apply)
      + key_algorithm         = "RSA"
      + private_key_pem       = (sensitive value)
      + ready_for_renewal     = true
      + validity_end_time     = (known after apply)
      + validity_period_hours = 12
      + validity_start_time   = (known after apply)

      + subject {
          + common_name  = "example.com"
          + organization = "ACME Examples, Inc"
        }
    }

Plan: 9 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + domain_url       = (known after apply)
  + stage_invoke_url = (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

tls_private_key.example: Creating...
tls_private_key.example: Creation complete after 0s [id=c1129fc488709c4293493669e43d40b60144999d]
tls_self_signed_cert.example: Creating...
tls_self_signed_cert.example: Creation complete after 0s [id=199729227385231255426302845367097804347]
aws_api_gateway_rest_api.example: Creating...
aws_acm_certificate.example: Creating...
aws_api_gateway_rest_api.example: Creation complete after 2s [id=halquax36h]
aws_api_gateway_deployment.example: Creating...
aws_acm_certificate.example: Creation complete after 3s [id=arn:aws:acm:us-west-2:123456789012:certificate/35cc4fc5-072f-4543-99d1-a1336ac05a41]
aws_api_gateway_domain_name.example: Creating...
aws_api_gateway_deployment.example: Creation complete after 1s [id=tj62g3]
aws_api_gateway_stage.example: Creating...
aws_api_gateway_stage.example: Creation complete after 1s [id=ags-halquax36h-example]
aws_api_gateway_method_settings.example: Creating...
aws_api_gateway_method_settings.example: Creation complete after 1s [id=halquax36h-example-*/*]
aws_api_gateway_domain_name.example: Creation complete after 3s [id=example.com]
aws_api_gateway_base_path_mapping.example: Creating...
aws_api_gateway_base_path_mapping.example: Creation complete after 1s [id=example.com/]

Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

Outputs:

domain_url = "curl -H 'Host: example.com' https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path1 # may take a minute to become available on initial deploy"
stage_invoke_url = "curl https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path1"

$ curl -s https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path1 | jq '.createDate'
"2021-01-21-00-44-18"

$ curl -H 'Host: example.com' -s https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path1 | jq '.createDate'
"2021-01-21-00-44-18"

$ terraform apply -var 'rest_api_path=/path2'
tls_private_key.example: Refreshing state... [id=c1129fc488709c4293493669e43d40b60144999d]
tls_self_signed_cert.example: Refreshing state... [id=199729227385231255426302845367097804347]
aws_api_gateway_rest_api.example: Refreshing state... [id=halquax36h]
aws_acm_certificate.example: Refreshing state... [id=arn:aws:acm:us-west-2:123456789012:certificate/35cc4fc5-072f-4543-99d1-a1336ac05a41]
aws_api_gateway_deployment.example: Refreshing state... [id=tj62g3]
aws_api_gateway_domain_name.example: Refreshing state... [id=example.com]
aws_api_gateway_stage.example: Refreshing state... [id=ags-halquax36h-example]
aws_api_gateway_base_path_mapping.example: Refreshing state... [id=example.com/]
aws_api_gateway_method_settings.example: Refreshing state... [id=halquax36h-example-*/*]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place
+/- create replacement and then destroy

Terraform will perform the following actions:

  # aws_api_gateway_deployment.example must be replaced
+/- resource "aws_api_gateway_deployment" "example" {
      ~ created_date  = "2021-01-22T02:59:46Z" -> (known after apply)
      ~ execution_arn = "arn:aws:execute-api:us-west-2:123456789012:halquax36h/" -> (known after apply)
      ~ id            = "tj62g3" -> (known after apply)
      ~ invoke_url    = "https://halquax36h.execute-api.us-west-2.amazonaws.com/" -> (known after apply)
      ~ triggers      = { # forces replacement
          ~ "redeployment" = "e042aae1faf8de8d7c7c98c063a986025f058c69" -> "e6742b53b5eed7039e6fec056113bb049954d64b"
        }
        # (1 unchanged attribute hidden)
    }

  # aws_api_gateway_rest_api.example will be updated in-place
  ~ resource "aws_api_gateway_rest_api" "example" {
      ~ body                         = jsonencode(
          ~ {
              ~ paths   = {
                  - /path1 = {
                      - get = {
                          - x-amazon-apigateway-integration = {
                              - httpMethod           = "GET"
                              - payloadFormatVersion = "1.0"
                              - type                 = "HTTP_PROXY"
                              - uri                  = "https://ip-ranges.amazonaws.com/ip-ranges.json"
                            }
                        }
                    } -> null
                  + /path2 = {
                      + get = {
                          + x-amazon-apigateway-integration = {
                              + httpMethod           = "GET"
                              + payloadFormatVersion = "1.0"
                              + type                 = "HTTP_PROXY"
                              + uri                  = "https://ip-ranges.amazonaws.com/ip-ranges.json"
                            }
                        }
                    }
                }
                # (2 unchanged elements hidden)
            }
        )
        id                           = "halquax36h"
        name                         = "api-gateway-rest-api-openapi-example"
        tags                         = {}
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # aws_api_gateway_stage.example will be updated in-place
  ~ resource "aws_api_gateway_stage" "example" {
      ~ deployment_id         = "tj62g3" -> (known after apply)
        id                    = "ags-halquax36h-example"
        tags                  = {}
        # (8 unchanged attributes hidden)
    }

Plan: 1 to add, 2 to change, 1 to destroy.

Changes to Outputs:
  ~ domain_url       = "curl -H 'Host: example.com' https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path1 # may take a minute to become available on initial deploy" -> "curl -H 'Host: example.com' https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path2 # may take a minute to become available on initial deploy"
  ~ stage_invoke_url = "curl https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path1" -> "curl https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path2"

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_api_gateway_rest_api.example: Modifying... [id=halquax36h]
aws_api_gateway_rest_api.example: Modifications complete after 1s [id=halquax36h]
aws_api_gateway_deployment.example: Creating...
aws_api_gateway_deployment.example: Creation complete after 1s [id=9vc6zm]
aws_api_gateway_stage.example: Modifying... [id=ags-halquax36h-example]
aws_api_gateway_stage.example: Modifications complete after 1s [id=ags-halquax36h-example]
aws_api_gateway_deployment.example: Destroying... [id=tj62g3]
aws_api_gateway_deployment.example: Destruction complete after 0s

Apply complete! Resources: 1 added, 2 changed, 1 destroyed.

Outputs:

domain_url = "curl -H 'Host: example.com' https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path2 # may take a minute to become available on initial deploy"
stage_invoke_url = "curl https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path2"

$ curl -s https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path2 | jq '.createDate'
"2021-01-21-00-44-18"

$ curl -H 'Host: example.com' -s https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path2 | jq '.createDate'
"2021-01-21-00-44-18"
```
bflad added a commit that referenced this issue Jan 29, 2021
… discourage stage_name and further encourage create_before_destroy (#17230)

* docs/service/apigateway: aws_api_gateway_deployment usage overhaul to discourage stage_name and further encourage create_before_destroy

Reference: #11344

Adds new end-to-end example of an OpenAPI REST API and also encourages the usage of OpenAPI specifications for configuring the REST API. Support for the other API Gateway resources is not going anywhere, but the dependency management aspect of deployments can be more difficult in that model and it is much easier to discover the API Gateway resources over the OpenAPI support.

In the future, it may be worth considering deprecating the `stage_name` and friends arguments since having a Terraform resource manage two remote resources is an anti-pattern and not well supported.

Output from example:

```console
$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_acm_certificate.example will be created
  + resource "aws_acm_certificate" "example" {
      + arn                       = (known after apply)
      + certificate_body          = (known after apply)
      + domain_name               = (known after apply)
      + domain_validation_options = (known after apply)
      + id                        = (known after apply)
      + private_key               = (sensitive value)
      + status                    = (known after apply)
      + subject_alternative_names = (known after apply)
      + validation_emails         = (known after apply)
      + validation_method         = (known after apply)
    }

  # aws_api_gateway_base_path_mapping.example will be created
  + resource "aws_api_gateway_base_path_mapping" "example" {
      + api_id      = (known after apply)
      + domain_name = (known after apply)
      + id          = (known after apply)
      + stage_name  = "example"
    }

  # aws_api_gateway_deployment.example will be created
  + resource "aws_api_gateway_deployment" "example" {
      + created_date  = (known after apply)
      + execution_arn = (known after apply)
      + id            = (known after apply)
      + invoke_url    = (known after apply)
      + rest_api_id   = (known after apply)
      + triggers      = {
          + "redeployment" = "e042aae1faf8de8d7c7c98c063a986025f058c69"
        }
    }

  # aws_api_gateway_domain_name.example will be created
  + resource "aws_api_gateway_domain_name" "example" {
      + arn                      = (known after apply)
      + certificate_upload_date  = (known after apply)
      + cloudfront_domain_name   = (known after apply)
      + cloudfront_zone_id       = (known after apply)
      + domain_name              = (known after apply)
      + id                       = (known after apply)
      + regional_certificate_arn = (known after apply)
      + regional_domain_name     = (known after apply)
      + regional_zone_id         = (known after apply)
      + security_policy          = (known after apply)

      + endpoint_configuration {
          + types = [
              + "REGIONAL",
            ]
        }
    }

  # aws_api_gateway_method_settings.example will be created
  + resource "aws_api_gateway_method_settings" "example" {
      + id          = (known after apply)
      + method_path = "*/*"
      + rest_api_id = (known after apply)
      + stage_name  = "example"

      + settings {
          + cache_data_encrypted                       = (known after apply)
          + cache_ttl_in_seconds                       = (known after apply)
          + caching_enabled                            = (known after apply)
          + data_trace_enabled                         = (known after apply)
          + logging_level                              = (known after apply)
          + metrics_enabled                            = true
          + require_authorization_for_cache_control    = (known after apply)
          + throttling_burst_limit                     = -1
          + throttling_rate_limit                      = -1
          + unauthorized_cache_control_header_strategy = (known after apply)
        }
    }

  # aws_api_gateway_rest_api.example will be created
  + resource "aws_api_gateway_rest_api" "example" {
      + api_key_source               = (known after apply)
      + arn                          = (known after apply)
      + binary_media_types           = (known after apply)
      + body                         = jsonencode(
            {
              + info    = {
                  + title   = "api-gateway-rest-api-openapi-example"
                  + version = "1.0"
                }
              + openapi = "3.0.1"
              + paths   = {
                  + /path1 = {
                      + get = {
                          + x-amazon-apigateway-integration = {
                              + httpMethod           = "GET"
                              + payloadFormatVersion = "1.0"
                              + type                 = "HTTP_PROXY"
                              + uri                  = "https://ip-ranges.amazonaws.com/ip-ranges.json"
                            }
                        }
                    }
                }
            }
        )
      + created_date                 = (known after apply)
      + description                  = (known after apply)
      + disable_execute_api_endpoint = (known after apply)
      + execution_arn                = (known after apply)
      + id                           = (known after apply)
      + minimum_compression_size     = -1
      + name                         = "api-gateway-rest-api-openapi-example"
      + policy                       = (known after apply)
      + root_resource_id             = (known after apply)

      + endpoint_configuration {
          + types            = [
              + "REGIONAL",
            ]
          + vpc_endpoint_ids = (known after apply)
        }
    }

  # aws_api_gateway_stage.example will be created
  + resource "aws_api_gateway_stage" "example" {
      + arn           = (known after apply)
      + deployment_id = (known after apply)
      + execution_arn = (known after apply)
      + id            = (known after apply)
      + invoke_url    = (known after apply)
      + rest_api_id   = (known after apply)
      + stage_name    = "example"
    }

  # tls_private_key.example will be created
  + resource "tls_private_key" "example" {
      + algorithm                  = "RSA"
      + ecdsa_curve                = "P224"
      + id                         = (known after apply)
      + private_key_pem            = (sensitive value)
      + public_key_fingerprint_md5 = (known after apply)
      + public_key_openssh         = (known after apply)
      + public_key_pem             = (known after apply)
      + rsa_bits                   = 2048
    }

  # tls_self_signed_cert.example will be created
  + resource "tls_self_signed_cert" "example" {
      + allowed_uses          = [
          + "key_encipherment",
          + "digital_signature",
          + "server_auth",
        ]
      + cert_pem              = (known after apply)
      + dns_names             = [
          + "example.com",
        ]
      + early_renewal_hours   = 0
      + id                    = (known after apply)
      + key_algorithm         = "RSA"
      + private_key_pem       = (sensitive value)
      + ready_for_renewal     = true
      + validity_end_time     = (known after apply)
      + validity_period_hours = 12
      + validity_start_time   = (known after apply)

      + subject {
          + common_name  = "example.com"
          + organization = "ACME Examples, Inc"
        }
    }

Plan: 9 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + domain_url       = (known after apply)
  + stage_invoke_url = (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

tls_private_key.example: Creating...
tls_private_key.example: Creation complete after 0s [id=c1129fc488709c4293493669e43d40b60144999d]
tls_self_signed_cert.example: Creating...
tls_self_signed_cert.example: Creation complete after 0s [id=199729227385231255426302845367097804347]
aws_api_gateway_rest_api.example: Creating...
aws_acm_certificate.example: Creating...
aws_api_gateway_rest_api.example: Creation complete after 2s [id=halquax36h]
aws_api_gateway_deployment.example: Creating...
aws_acm_certificate.example: Creation complete after 3s [id=arn:aws:acm:us-west-2:123456789012:certificate/35cc4fc5-072f-4543-99d1-a1336ac05a41]
aws_api_gateway_domain_name.example: Creating...
aws_api_gateway_deployment.example: Creation complete after 1s [id=tj62g3]
aws_api_gateway_stage.example: Creating...
aws_api_gateway_stage.example: Creation complete after 1s [id=ags-halquax36h-example]
aws_api_gateway_method_settings.example: Creating...
aws_api_gateway_method_settings.example: Creation complete after 1s [id=halquax36h-example-*/*]
aws_api_gateway_domain_name.example: Creation complete after 3s [id=example.com]
aws_api_gateway_base_path_mapping.example: Creating...
aws_api_gateway_base_path_mapping.example: Creation complete after 1s [id=example.com/]

Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

Outputs:

domain_url = "curl -H 'Host: example.com' https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path1 # may take a minute to become available on initial deploy"
stage_invoke_url = "curl https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path1"

$ curl -s https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path1 | jq '.createDate'
"2021-01-21-00-44-18"

$ curl -H 'Host: example.com' -s https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path1 | jq '.createDate'
"2021-01-21-00-44-18"

$ terraform apply -var 'rest_api_path=/path2'
tls_private_key.example: Refreshing state... [id=c1129fc488709c4293493669e43d40b60144999d]
tls_self_signed_cert.example: Refreshing state... [id=199729227385231255426302845367097804347]
aws_api_gateway_rest_api.example: Refreshing state... [id=halquax36h]
aws_acm_certificate.example: Refreshing state... [id=arn:aws:acm:us-west-2:123456789012:certificate/35cc4fc5-072f-4543-99d1-a1336ac05a41]
aws_api_gateway_deployment.example: Refreshing state... [id=tj62g3]
aws_api_gateway_domain_name.example: Refreshing state... [id=example.com]
aws_api_gateway_stage.example: Refreshing state... [id=ags-halquax36h-example]
aws_api_gateway_base_path_mapping.example: Refreshing state... [id=example.com/]
aws_api_gateway_method_settings.example: Refreshing state... [id=halquax36h-example-*/*]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place
+/- create replacement and then destroy

Terraform will perform the following actions:

  # aws_api_gateway_deployment.example must be replaced
+/- resource "aws_api_gateway_deployment" "example" {
      ~ created_date  = "2021-01-22T02:59:46Z" -> (known after apply)
      ~ execution_arn = "arn:aws:execute-api:us-west-2:123456789012:halquax36h/" -> (known after apply)
      ~ id            = "tj62g3" -> (known after apply)
      ~ invoke_url    = "https://halquax36h.execute-api.us-west-2.amazonaws.com/" -> (known after apply)
      ~ triggers      = { # forces replacement
          ~ "redeployment" = "e042aae1faf8de8d7c7c98c063a986025f058c69" -> "e6742b53b5eed7039e6fec056113bb049954d64b"
        }
        # (1 unchanged attribute hidden)
    }

  # aws_api_gateway_rest_api.example will be updated in-place
  ~ resource "aws_api_gateway_rest_api" "example" {
      ~ body                         = jsonencode(
          ~ {
              ~ paths   = {
                  - /path1 = {
                      - get = {
                          - x-amazon-apigateway-integration = {
                              - httpMethod           = "GET"
                              - payloadFormatVersion = "1.0"
                              - type                 = "HTTP_PROXY"
                              - uri                  = "https://ip-ranges.amazonaws.com/ip-ranges.json"
                            }
                        }
                    } -> null
                  + /path2 = {
                      + get = {
                          + x-amazon-apigateway-integration = {
                              + httpMethod           = "GET"
                              + payloadFormatVersion = "1.0"
                              + type                 = "HTTP_PROXY"
                              + uri                  = "https://ip-ranges.amazonaws.com/ip-ranges.json"
                            }
                        }
                    }
                }
                # (2 unchanged elements hidden)
            }
        )
        id                           = "halquax36h"
        name                         = "api-gateway-rest-api-openapi-example"
        tags                         = {}
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # aws_api_gateway_stage.example will be updated in-place
  ~ resource "aws_api_gateway_stage" "example" {
      ~ deployment_id         = "tj62g3" -> (known after apply)
        id                    = "ags-halquax36h-example"
        tags                  = {}
        # (8 unchanged attributes hidden)
    }

Plan: 1 to add, 2 to change, 1 to destroy.

Changes to Outputs:
  ~ domain_url       = "curl -H 'Host: example.com' https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path1 # may take a minute to become available on initial deploy" -> "curl -H 'Host: example.com' https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path2 # may take a minute to become available on initial deploy"
  ~ stage_invoke_url = "curl https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path1" -> "curl https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path2"

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_api_gateway_rest_api.example: Modifying... [id=halquax36h]
aws_api_gateway_rest_api.example: Modifications complete after 1s [id=halquax36h]
aws_api_gateway_deployment.example: Creating...
aws_api_gateway_deployment.example: Creation complete after 1s [id=9vc6zm]
aws_api_gateway_stage.example: Modifying... [id=ags-halquax36h-example]
aws_api_gateway_stage.example: Modifications complete after 1s [id=ags-halquax36h-example]
aws_api_gateway_deployment.example: Destroying... [id=tj62g3]
aws_api_gateway_deployment.example: Destruction complete after 0s

Apply complete! Resources: 1 added, 2 changed, 1 destroyed.

Outputs:

domain_url = "curl -H 'Host: example.com' https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path2 # may take a minute to become available on initial deploy"
stage_invoke_url = "curl https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path2"

$ curl -s https://halquax36h.execute-api.us-west-2.amazonaws.com/example/path2 | jq '.createDate'
"2021-01-21-00-44-18"

$ curl -H 'Host: example.com' -s https://d-orixhuv0o9.execute-api.us-west-2.amazonaws.com/path2 | jq '.createDate'
"2021-01-21-00-44-18"
```

* docs/service/apigateway: Adjust for main branch rename

* examples/api-gateway-rest-api-openapi: Add curl_ prefix to output names
@dmurphy-github
Copy link

dmurphy-github commented Mar 19, 2021

Hi All,

Hopefully you can help us understand why we get a cycle error in the following case;

We have an API deployed that has a single method (e.g. "ANY") and single corresponding integration. This needs to be changed to two methods (e.g. "ANY" and "OPTIONS") and two integrations. Our actual implementation is quite complex but after extensive testing we think the change that is causing the error can be reproduced using the example @bflad provided above on 22 Jan.

Steps to reproduce:

  1. Apply the example configuration:
resource "aws_api_gateway_rest_api" "example" {
  name = "example"
}

resource "aws_api_gateway_resource" "example" {
  parent_id   = aws_api_gateway_rest_api.example.root_resource_id
  path_part   = "example"
  rest_api_id = aws_api_gateway_rest_api.example.id
}

resource "aws_api_gateway_method" "example" {
  authorization = "NONE"
  http_method   = "GET"
  resource_id   = aws_api_gateway_resource.example.id
  rest_api_id   = aws_api_gateway_rest_api.example.id
}

resource "aws_api_gateway_integration" "example" {
  http_method = aws_api_gateway_method.example.http_method
  resource_id = aws_api_gateway_resource.example.id
  rest_api_id = aws_api_gateway_rest_api.example.id
  type        = "MOCK"
}

resource "aws_api_gateway_deployment" "example" {
  rest_api_id = aws_api_gateway_rest_api.example.id

  triggers = {
    # NOTE: The configuration below will satisfy ordering considerations,
    #       but not pick up all future REST API changes. More advanced patterns
    #       are possible, such as using the filesha1() function against the
    #       Terraform configuration file(s) or removing the .id references to
    #       calculate a hash against whole resources. Be aware that using whole
    #       resources will show a difference after the initial implementation.
    #       It will stabilize to only change when resources change afterwards.
    redeployment = sha1(jsonencode([
      aws_api_gateway_resource.example.id,
      aws_api_gateway_method.example.id,
      aws_api_gateway_integration.example.id,
    ]))
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_stage" "example" {
  deployment_id = aws_api_gateway_deployment.example.id
  rest_api_id   = aws_api_gateway_rest_api.example.id
  stage_name    = "example"
}
  1. Modify the configuration by changing the name of the method from "example" to "new_example" and modify the integration and deployment resources accordingly:
resource "aws_api_gateway_rest_api" "example" {
  name = "example"
}
resource "aws_api_gateway_resource" "example" {
  parent_id   = aws_api_gateway_rest_api.example.root_resource_id
  path_part   = "example"
  rest_api_id = aws_api_gateway_rest_api.example.id
}
resource "aws_api_gateway_method" "new_example" {
  authorization = "NONE"
  http_method   = "GET"
  resource_id   = aws_api_gateway_resource.example.id
  rest_api_id   = aws_api_gateway_rest_api.example.id
}
resource "aws_api_gateway_integration" "example" {
  http_method = aws_api_gateway_method.new_example.http_method
  resource_id = aws_api_gateway_resource.example.id
  rest_api_id = aws_api_gateway_rest_api.example.id
  type        = "MOCK"
}
resource "aws_api_gateway_deployment" "example" {
  rest_api_id = aws_api_gateway_rest_api.example.id
triggers = {
    # NOTE: The configuration below will satisfy ordering considerations,
    #       but not pick up all future REST API changes. More advanced patterns
    #       are possible, such as using the filesha1() function against the
    #       Terraform configuration file(s) or removing the .id references to
    #       calculate a hash against whole resources. Be aware that using whole
    #       resources will show a difference after the initial implementation.
    #       It will stabilize to only change when resources change afterwards.
    redeployment = sha1(jsonencode([
      aws_api_gateway_resource.example.id,
      aws_api_gateway_method.new_example.id,
      aws_api_gateway_integration.example.id,
    ]))
  }
lifecycle {
    create_before_destroy = true
  }
}
resource "aws_api_gateway_stage" "example" {
  deployment_id = aws_api_gateway_deployment.example.id
  rest_api_id   = aws_api_gateway_rest_api.example.id
  stage_name    = "example"
}
  1. Apply the updated configuration.

Expected behaviour:
A new deployment is created and the old deployment is replaced.

Actual behaviour:
Error: Cycle: aws_api_gateway_stage.example, aws_api_gateway_method.example (destroy), aws_api_gateway_deployment.example, aws_api_gateway_deployment.example (destroy deposed d932d47f)

We would really appreciate it if you could look into this case.

Thank you

Terraform v0.12.20
Provider.aws v3.31.0

@hannes-ucsc
Copy link

hannes-ucsc commented Apr 16, 2021

Also still seeing this despite following the best practices (explicit stage instead of implicit one, create_before_destroy on the deployment, hash of the API spec as a trigger).

$ terraform -version
Terraform v0.12.24
+ provider.aws v3.36.0
+ provider.google v2.20.3
+ provider.null v2.1.2
+ provider.template v2.2.0

Can someone explain how to read the Error: Cycle line?

@Pepert
Copy link

Pepert commented Oct 22, 2021

Just putting it here, in case that helps somebody: I followed the example of bflad (using the REST API Deployment with OpenAPI version), but still had this cycle error.

I finally found that I had some aws_lambda_permission resources to bind lambdas with API gateway that were being updated at the same time as the deployment resource.

After adding depends_on = [aws_api_gateway_deployment.example] on my permission resources, the deployment went fine (ex below):

resource "aws_lambda_permission" "lambda_permission_example" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = "lambda name example"
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.example_api_gateway.execution_arn}/*/POST/whatever/*"

  depends_on = [aws_api_gateway_deployment.example]
}

@jeffbski-rga
Copy link

In my case I was able to work around the issue when trying to destroy an endpoint from an API Gateway by first removing the ids from the trigger redeploy block and then following up with removing the items from terraform in the next deploy.

@nsergeev1
Copy link

Still have the same error.

@hannes-ucsc
Copy link

It should also be considered that the workarounds may only work in certain situations i.e. when previous attempts at applying the resource changes already created some of the resources involved. What's really needed is a repeatable reproduction. It should also be said that the API Gateway v1 API is an aberration from other AWS APIs. That's probably why v2 was created but that's just speculation.

@razinlightyear
Copy link

I finally resolved this error (destroy deposed) for 2 aws_api_gateway_resource resources. I commented out resources and ran the plan locally (after downloading tfstate from TF cloud) until the error disappeared. Then I uncommented the remaining resources in the following commit/plan/apply. I did implemented the suggestions by @bflad #11344 (comment) but I was still getting the same error. Hopefully by implementing the suggestions we will see less of these type of issues in the future.

@hannes-ucsc
Copy link

hannes-ucsc commented Dec 19, 2022

I believe the complete fix for this is to upgrade to Terraform 1.3 or later and

  1. not have lifecycle.create_before_destroy on aws_api_gateway_deployment
  2. not have stage_name or stage_description on aws_api_gateway_deployment (to disable the implicit stage creation)
  3. specify a stage explicitly by adding a aws_api_gateway_stage resource
  4. set lifecycle.replace_triggered_by to ["aws_api_gateway_deployment.YOUR_DEPLOYMENT.id"] in the aws_api_gateway_stage resource
  5. set lifecycle.replace_triggered_by to ["aws_api_gateway_stage.YOUR_STAGE.id"] in the aws_api_gateway_base_path_mapping, aws_api_gateway_method_settings resources, and any other resource that's "downstream" from the stage, such as aws_wafv2_web_acl_association if you have it

Note for Chalice users: As a fix for aws/chalice#1237, Chalice added lifecycle.create_before_destroy to aws_api_gateway_deployment in its generated TF config. I think that was a mistake. You either have to post-process the Chalice-generated TF config or modify Chalice to revert that fix. Also, the Chalice-generated TF config sets stage_description to a hash so as to trigger redeployment on source code changes. To satisfy step 2 above, you need to ensure that stage_description is absent and move its value to triggers.redeployment instead, which is the official mechanism for triggerring redeployment in TF 1.3.

@ricoli
Copy link

ricoli commented Apr 25, 2023

If like me you've come to this issue because you got a cycle error while having implemented the recommended way of doing things in the docs (summarised here), then here follows how I solved things.
I was getting this cycle error when running a terraform plan to remove a resource from the body of the API gateway REST API resource (openAPI definition). I spotted the cause of the cycle fairly easily - a lambda behind a separate API Gateway (let's call it B) referenced the invoke_url of the current stage of the main API Gateway (let's call it A) in its environment variables. The deployment resource of both API Gateways had a lifecycle policy of create_before_destroy, which is a must have to ensure uptime. This caused the cycle, and as such I broke apart the cycle by manually assembling the invoke_url based on the ID of the REST API resource of API Gateway A and the variable that was used as the stage name in API Gateway A. Great stuff, but unfortunately I simply had a new cycle to contend with, though a shorter one and one where only resources for the API Gateway A were present, mentioning some deposed resources. Basically what I had here is that the remote state still had this coupling between the two API Gateway deployments (because of the stage invoke_url reference), whereas locally I didn't have it. To solve this, what I did was to change the lifecycle policy of the aws_api_gateway_deployment resource of API Gateway A in the same PR as the change to remove the resource from it:

  lifecycle {
    create_before_destroy = true
    ignore_changes = [
      triggers
    ]
  }

What the above does is to simply not trigger a deployment while still removing the resource from the API Gateway in remote state. PR merged, terraform apply executed and in the next PR I simply removed the ignore_changes block to go back to normal 🎉

@jcano-chwy
Copy link

Somehow ended up with this cycle error through modification of the IAM policy document attached to our REST API policy resource.

00:02:04.010 │ Error: error waiting for API Gateway Stage (*******) to be updated: unexpected state 'NOT_AVAILABLE', wanted target 'AVAILABLE, DELETE_IN_PROGRESS'. last error: %!s(<nil>) 00:02:04.010 │ 00:02:04.010 │ with aws_api_gateway_stage.rest_api, 00:02:04.010 │ on api_main.tf line 352, in resource "aws_api_gateway_stage" "rest_api": 00:02:04.010 │ 352: resource "aws_api_gateway_stage" "rest_api" {

Tried destroying, but ran into deposed API Gateway stages error. Was able to successfully destroy it by commenting out this block from my api_gateway_deployment resource:

lifecycle { create_before_destroy = true }

And then running a full build from scratch again.

@hjfitz
Copy link

hjfitz commented Sep 17, 2023

Still dealing with this years later. I feel like my code is pretty bog standard, too.

terraform code
resource "aws_api_gateway_rest_api" "api" {
  name = "rtb"
}

# ─────────────────────────────────────────────────
# Secret reader
# ─────────────────────────────────────────────────
resource "aws_api_gateway_resource" "resource" {
  path_part   = ""
  parent_id   = aws_api_gateway_rest_api.api.root_resource_id
  rest_api_id = aws_api_gateway_rest_api.api.id
  depends_on  = [aws_api_gateway_rest_api.api]
}

resource "aws_api_gateway_method" "read_method" {
  rest_api_id   = aws_api_gateway_rest_api.api.id
  resource_id   = aws_api_gateway_resource.resource.id
  http_method   = "GET"
  authorization = "NONE"

  request_parameters = {
    "method.request.querystring.id"   = true
  }

}


resource "aws_api_gateway_integration" "read_integration" {
  rest_api_id             = aws_api_gateway_rest_api.api.id
  resource_id             = aws_api_gateway_method.read_method.resource_id
  http_method             = aws_api_gateway_method.read_method.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.read_lambda.invoke_arn

  request_parameters = {
    "integration.request.querystring.id" = "method.request.querystring.id"
  }
}

resource "aws_api_gateway_deployment" "read" {
  rest_api_id = aws_api_gateway_rest_api.api.id


  triggers = {
    # NOTE: The configuration below will satisfy ordering considerations,
    #       but not pick up all future REST API changes. More advanced patterns
    #       are possible, such as using the filesha1() function against the
    #       Terraform configuration file(s) or removing the .id references to
    #       calculate a hash against whole resources. Be aware that using whole
    #       resources will show a difference after the initial implementation.
    #       It will stabilize to only change when resources change afterwards.
    redeployment = sha1(jsonencode([
      aws_api_gateway_resource.resource.id,
      aws_api_gateway_method.read_method.id,
      aws_api_gateway_integration.read_integration.id,
    ]))
  }


  lifecycle {
    create_before_destroy = false
  }
}

resource "aws_api_gateway_stage" "secret_reader" {
  stage_name    = "secret_reader"
  rest_api_id   = aws_api_gateway_rest_api.api.id
  deployment_id = aws_api_gateway_deployment.read.id
}

# Lambda
resource "aws_lambda_permission" "apigw_read_lambda" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.read_lambda.function_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 = aws_lambda_function.read_lambda.arn
}

Where the only difference is, I'm using Localstack. I don't think that's hugely important in this though.

code/rtb/iac                                                                                                ⍉
▶ tf --version
Terraform v1.5.6
on darwin_arm64
+ provider registry.terraform.io/hashicorp/archive v2.4.0
+ provider registry.terraform.io/hashicorp/aws v4.67.0
+ provider registry.terraform.io/hashicorp/null v3.2.1

Your version of Terraform is out of date! The latest version
is 1.5.7. You can update by downloading from https://www.terraform.io/downloads.html

Edit: I've got a hacky workaround for my issue -

resource "aws_api_gateway_resource" "resource" {
  path_part   = ""
  parent_id   = aws_api_gateway_rest_api.api.root_resource_id
  rest_api_id = aws_api_gateway_rest_api.api.id
  depends_on  = [aws_api_gateway_rest_api.api]
  lifecycle {
    ignore_changes = [ parent_id ]
  }
}

@YakDriver
Copy link
Member

As maintainers of the Terraform AWS Provider, we’ve reached a decision to close this longstanding issue. We want to assure you that this decision was made after careful consideration, and we’re committed to transparency in our actions.

Over time, this issue has seen numerous attempts at resolution (#17230, #17099, #17209) and workarounds (such as this, this, this, and, of course, this), but its complexity and longevity present significant challenges. We lack clarity on how many users are still affected and the precise nature of the remaining issues. Given these uncertainties and our limited resources, it’s difficult for us to effectively address the problem in its current state.

However, we value community feedback immensely. If you’re still encountering issues, we encourage you to open a new, focused issue outlining the specific problems you’re facing. We understand the frustration of having to restart the discussion, but the convoluted history of this particular issue necessitates a fresh approach.

While we’ve received reports from community members in the past year, it’s unclear how these relate to the broader context of this issue’s history. Moving forward, a new, well-defined problem statement will greatly increase the likelihood of prompt attention from maintainers or fellow community members.

Ultimately, our goal is to ensure that the Terraform AWS Provider remains a dependable tool for realizing your infrastructure goals. Regrettably, this prolonged issue no longer contributes to that objective. By closing it, we aim to clear the path for more effective problem-solving and a smoother experience for all users. We appreciate your understanding and continued support as we work towards a better future for our provider.

Copy link

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 19, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
service/apigateway Issues and PRs that pertain to the apigateway service.
Projects
None yet
Development

No branches or pull requests