hashicorp / terraform-provider-azurerm

Terraform provider for Azure Resource Manager
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs
Mozilla Public License 2.0
4.52k stars 4.6k forks source link

Support for azurerm_cdn_frontdoor_route data resource #21639

Open PavelPikat opened 1 year ago

PavelPikat commented 1 year ago

Is there an existing issue for this?

Community Note

Description

We would like to manage Azure Front Door custom domains from appliation-specific Terraform configuration and target existing AFD routes which were created by a different, central Terraform configuration.

Currently, it's not possible, because there is no data resource for azurerm_cdn_frontdoor_route. Existing Terraform example for custom domain assumes that the route is created in the same Terraform configuration:

resource "azurerm_cdn_frontdoor_custom_domain_association" "example" {
  cdn_frontdoor_custom_domain_id = azurerm_cdn_frontdoor_custom_domain.contoso.id
  cdn_frontdoor_route_ids        = [azurerm_cdn_frontdoor_route.contoso.id, azurerm_cdn_frontdoor_route.fabrikam.id]
}

New or Affected Resource(s)/Data Source(s)

azurerm_cdn_frontdoor_route

Potential Terraform Configuration

data "azurerm_cdn_frontdoor_endpoint" "example" {
  name                = "existing-endpoint"
  profile_name        = "existing-cdn-profile"
  resource_group_name = "existing-resources"
}

data "azurerm_cdn_frontdoor_route" "example" {
  name                      = "my-route"
  cdn_frontdoor_endpoint_id = data.azurerm_cdn_frontdoor_endpoint.example.id
}

resource "azurerm_cdn_frontdoor_custom_domain" "example" {
  name                     = "example-customDomain"
  <omitted>
}

resource "azurerm_cdn_frontdoor_custom_domain_association" "example" {
  cdn_frontdoor_custom_domain_id = azurerm_cdn_frontdoor_custom_domain.example.id
  cdn_frontdoor_route_ids        = [data.azurerm_cdn_frontdoor_route.example.id]
}

References

No response

WodansSon commented 1 year ago

@PavelPikat, thanks for opening this issue. That said, even when I expose this new data source I do not believe it will solve the issue you are running up against unfortunately. The reason being is that the azurerm_cdn_frontdoor_custom_domain still needs to be "associated" with the azurerm_cdn_frontdoor_route via the routes cdn_frontdoor_custom_domain_ids field. Meaning that the custom domain resource needs to be defined and "associated" with the route in the same configuration file as the route. The azurerm_cdn_frontdoor_custom_domain_association resource is named, somewhat poorly, as it is really a disassociation resource rather than an association resource. The azurerm_cdn_frontdoor_custom_domain_association sole purpose within the provider is to allow custom domain(s) to be "disassociated" with a route prior to the route being deleted to avoid a service side error that would be raised if the custom domain was still "associated" with the route.

NOTE:

The behavior you are requesting/expecting from the azurerm_cdn_frontdoor_custom_domain_association resource (in your Potential Terraform Configuration example above) is not what that resource actually does. I am currently trying to implement that exact behavior in the new azurerm_cdn_frontdoor_rule_sets_association PR #20859 due to a similar issue being raised about the rule set resource (issue #20744).

In order to get your Potential Terraform Configuration example to work, the azurerm_cdn_frontdoor_custom_domain_association resource would have to "own" the routes cdn_frontdoor_custom_domain_ids field, which it currently does not.

ADDITIONAL:

I have written a test case that follows the Potential Terraform Configuration example and it still will not work even with the azurerm_cdn_frontdoor_route data source being exposed. You will still receive the error '_the CDN FrontDoor Route(Name: "acctestRoute-XXX") is currently not associated with the CDN FrontDoor Custom Domain(Name: "acctest-contoso-XXX"). Please remove the CDN FrontDoor Route from your 'cdn_frontdoor_custom_domainassociation' configuration block' due to the issues I have already mentioned above.

PavelPikat commented 1 year ago

@WodansSon, thank you for looking into it so quickly. I reviewed a potential workaround via usage of azcli provider, but quickly found that az afd route update required a list of IDs of all custom domains, like you mention. This makes my use case difficult to achieve.

Speaking of the use case, let me expand on it and describe our setup: We have a centrally placed Platform Engineering team that provides managed services to the rest of the dev organization within a company. That team manages global resources like AKS clusters and Front Door profile. AFD routes are pre-configured and hooked up with AKS via Terraform config in a Git repo that this team owns. The intention here is that every dev & product team can consume these services from their own product-specific Terraform configurations and add their own custom domains for each app to the existing AFD profile, endpoint and route. Since there is a dedicated azurerm_cdn_frontdoor_custom_domain, we thought it would be possible to create custom domains from Terraform configurations separate to the one with AFD route. But unfortunaly it seems to be Azrure API design limitation.

Just for the record, a couple of potential workarounds that I can think of (and reject):

I wonder if others have a similar use case with centrally placed AFD and custom domains managed somewhere else and how they solve the limitation with Azure API. 🤔

Regarding Azure API design, I wish that Front Door routes/custom domain association was structured in a way that one can POST/DELETE domain one by one, instead of PUT entire route with a list of all custom domains.

Imagine if it worked that way with Storage Accounts - in order to add a blob container, one must pass IDs of all other containers already added to the account :smirk:

PavelPikat commented 1 year ago

@WodansSon Actually, came to think about azurerm_cdn_frontdoor_custom_domain_association and Azure API. Would it not make sense for the azurerm_cdn_frontdoor_custom_domain_association resource to satisfy Azure API's requirement of knowing a full list of custom domain IDs attached to the route by invoking a couple of internal requests under the hood, something like:

  1. Given AFD profile exists with an endpoint and a route, and the route has 10 custom domains already
  2. The user creates one more azurerm_cdn_frontdoor_custom_domain_association resource with 11th domain referencing the route ID
  3. The azurerm provider does an internal lookup against route ID to get a list of all existing (10) custom domains
  4. It then does a PUT request to update the route with 11th domain
  5. In case the use removes azurerm_cdn_frontdoor_custom_domain_association, then the provider does another internal lookup to fetch the full list of 11 domains, remove the one user wants to delete, then update the route with 10 domains
WodansSon commented 1 year ago

@PavelPikat, that is kind of sort of what I have it doing now, but I still have it taking a list of custom domains and from reading your use case is sounds like you want it to be an iterative addition to the route instead of monolithic list of custom domains. The issue with that design is that no one resource "owns" the list of custom domains on the route at that point and if you pass me one custom domain I don't know if you want me to add it to the existing list of custom domains or replace the list of existing custom domains with this one new one that you just passed in. Plus, once the custom domain is added, this would cause a diff in all the other association resources as at that point the other configuration files would be out of sync and the next time they run their terraform config it would want to remove the custom domain you just added. This is a complex problem, which on the surface sounds like it would be a simple fix, but unfortunately it's not. Let me think on it a bit and I will get back with you. 🚀

WodansSon commented 1 year ago

@PavelPikat, I have given it some thought and I don't see a way around this natively within Terraform for these two reasons.

  1. The way the CDN Front Door API is designed to work.
  2. The way Terraform works.

That said, the only thing I could come up with that would workaround your issue is if you kicked off some other custom tool, after running a terraform -refresh command to update the values in the data source, that you or one of your devs can write that would take a custom domain ID as input, if the ID was not passed in it would just skip the function that adds the custom domain to the routes custom domain association list. It would then fetch the list of custom domains from the azurerm_cdn_frontdoor_route data source which I have exposed with this PR, or call directly into Azure to get the values from the route itself. It could then check the list to see if the passed in custom domain ID was already it the routes custom domain list or not. If not the tool would then add it to the custom domain list and then write that back to the configuration files azurerm_cdn_frontdoor_custom_domain_association cdn_frontdoor_custom_domain_ids field. That way, you can still have your one off dev/product team control their own custom domain(s) creation/destruction. Keep in mind that this tool would have to be run prior to terraform being run every single time by every single team else you would end up deleting a custom domain association in the route without knowing that some other team had created another custom domain without your knowledge. It's a little hacky, but I do believe it would work and solve your current issue.

Another way of doing this would be to set up a small CosmosDB or an Excel Spreadsheet where all the devs/teams would add and remove their own custom domain IDs to it. Then all the tool would have to do is call into that DB/Spreadsheet and pull the latest list of Custom Domain IDs from there and insert them into the azurerm_cdn_frontdoor_custom_domain_association cdn_frontdoor_custom_domain_ids field.

I believe that the safest way to do this one would be to do it in two runs, the first run is where the dev/team would actually create the Custom Domain, so it would exist in Azure. Then they would update the list of custom domain IDs it the DB/Spreadsheet, adding their new one to the list and then run Terraform again to associate the new custom domain with the route via the azurerm_cdn_frontdoor_custom_domain_association resource.

zioproto commented 1 year ago

Creating a dedicate AFD route per app would hit the AFD limit of 100 routes per profile instantly (we have 4000 apps).

Where did you find documented a 100 routes per Azure Front Door profile limit ?

From the docs: https://learn.microsoft.com/en-us/azure/frontdoor/front-door-routing-limits

Each Front Door profile has a composite route limit.
The composite route metric for each Front Door profile can't exceed 5000.

If you are serving only HTTPS and each route only has 1 path your 4000 apps will fit into the 5000 composite route limit.

Please let me know if I missed anything. Thanks

zioproto commented 1 year ago

I found the documented 100 routes limit: https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-front-door-standard-and-premium-tier-service-limits

It is actually 100 in AFD Standard tier and 200 in Premium Tier

PavelPikat commented 1 year ago

@WodansSon, @zioproto appreciate your quick response guys. Let me take these findings and discuss them with my team internally and see what other potential workarounds we can come up with, either with Terraform or outside of it. I'll let you know by the end of this week

RolfMoleman commented 5 months ago

@reganlives take a look at this

RolfMoleman commented 5 months ago

Hi we have been experiencing some issues with thsi also and have a potentiaal work around but terraform (via Azure Devops) gives a pretty useless error

error with names redacted:

  Error: creating Front Door Custom Domain Association: (Association Name "****-***-afd" / Profile Name "**-****-***" / Resource Group "rg-****-***-uicq"): the CDN FrontDoor Route(Name: "****-**") is currently not associated with the CDN FrontDoor Custom Domain(Name: "****-***-***"). Please remove the CDN FrontDoor Route from your 'cdn_frontdoor_custom_domain_association' configuration block
│ 
│   with module.azurerm_front_door.azurerm_cdn_frontdoor_custom_domain_association.***["****-***-association"],
│   on ../../../../terraform-azurerm-frontdoor/modern.tf line 575, in resource "azurerm_cdn_frontdoor_custom_domain_association" "***":
│  575: resource "azurerm_cdn_frontdoor_custom_domain_association" "***" {

the way we have attempted to get aroundd the lack of a data source for routes is to do the following


resource "azurerm_cdn_frontdoor_custom_domain_association" "cag" {
  for_each = var.modern_frontdoor_custom_domain_associations # Loop over each association defined in the variable

  cdn_frontdoor_custom_domain_id = data.azurerm_cdn_frontdoor_custom_domain.frontdoor_association_custom_domain_name[each.value.cdn_frontdoor_custom_domain_name].id # Set the custom domain ID
  cdn_frontdoor_route_ids        = [for route_name in each.value.cdn_frontdoor_route_names : local.route_ids_map[route_name]] # Set the route IDs

  depends_on = [
    azurerm_cdn_frontdoor_custom_domain.this,
    azurerm_cdn_frontdoor_route.this
  ]
}

locals{

  cdn_frontdoor_domain_association_route_ids = flatten([
    for config in var.modern_frontdoor_custom_domain_associations : [
      for route_name in config.cdn_frontdoor_route_names : {
        route_name = route_name
        route_id   = format("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Cdn/profiles/%s/afdEndpoints/%s/routes/%s", data.azurerm_client_config.this.subscription_id, var.resource_group_name, config.cdn_frontdoor_profile_name, config.cdn_frontdoor_endpoint_name, route_name)
      }
    ]
  ])

route_ids_map = { for route in local.cdn_frontdoor_domain_association_route_ids : route.route_name => route.route_id }

}

}

which seems to be fine but complains about a lack of association which makes no sense as the associations exist like so


# Define the resource for Azure CDN Frontdoor Route
resource "azurerm_cdn_frontdoor_route" "this" {
  # Loop over each route config defined in the variable
  for_each = var.modern_front_door_route_configs

  # Basic configuration
  name = each.key # Name of the route

  # Cache configuration
  dynamic "cache" {
    for_each = each.value.cache != null ? [each.value.cache] : [] # Check if cache is defined
    content {
      compression_enabled           = cache.value.compression_enabled != null ? cache.value.compression_enabled : null                     # Whether compression is enabled
      content_types_to_compress     = cache.value.content_types_to_compress != null ? cache.value.content_types_to_compress : null         # Content types to compress
      query_string_caching_behavior = cache.value.query_string_caching_behavior != null ? cache.value.query_string_caching_behavior : null # Query string caching behavior
    }
  }

  # CDN Frontdoor configuration
  cdn_frontdoor_custom_domain_ids = [for domain_name in each.value.cdn_frontdoor_custom_domain_names : data.azurerm_cdn_frontdoor_custom_domain.frontdoor_custom_domain_name[domain_name].id] # IDs of the custom domains
  cdn_frontdoor_endpoint_id     = data.azurerm_cdn_frontdoor_endpoint.frontdoor_route_endpoint[each.key].id
  cdn_frontdoor_origin_group_id = data.azurerm_cdn_frontdoor_origin_group.frontdoor_route_origin_group[each.key].id            # ID of the origin group
  cdn_frontdoor_origin_path     = each.value.cdn_frontdoor_origin_path != null ? each.value.cdn_frontdoor_origin_path : null   # Path of the origin
  cdn_frontdoor_origin_ids      = local.origin_ids                                                                             # IDs of the origins
  cdn_frontdoor_rule_set_ids    = each.value.cdn_frontdoor_rule_set_ids != null ? each.value.cdn_frontdoor_rule_set_ids : null # IDs of the rule sets
  enabled                       = each.value.enabled != null ? each.value.enabled : null                                       # Whether the route is enabled
  forwarding_protocol           = each.value.forwarding_protocol != null ? each.value.forwarding_protocol : null               # Forwarding protocol
  https_redirect_enabled        = each.value.https_redirect_enabled != null ? each.value.https_redirect_enabled : null         # Whether HTTPS redirect is enabled
  link_to_default_domain        = each.value.link_to_default_domain != null ? each.value.link_to_default_domain : null         # Whether to link to default domain
  patterns_to_match             = each.value.patterns_to_match != null ? each.value.patterns_to_match : null                   # Patterns to match
  supported_protocols           = each.value.supported_protocols != null ? each.value.supported_protocols : null               # Supported protocols

  # Add depends_on block
  depends_on = [
    azurerm_cdn_frontdoor_profile.this,
    azurerm_cdn_frontdoor_endpoint.this,
    azurerm_cdn_frontdoor_origin_group.this,
    azurerm_cdn_frontdoor_origin.this,
    azurerm_cdn_frontdoor_rule_set.this #,
    #azurerm_cdn_frontdoor_custom_domain.this #removed to prevent circular dependency
  ]
}

locals {

  tags = {
    Application = var.application_name
    ADO-Project = var.azdo_project_name
    Repository  = var.azdo_repo_name
    Environment = var.environment_tag
    Managed-By  = "Terraform"
    Owner       = join(" ", [var.azdo_project_name, "Contributors"]) # Join project name and "Contributors" with a space
  }

  endpoint_name     = distinct([for config in values(var.modern_front_door_route_configs) : config.cdn_frontdoor_endpoint_name])
  origin_group_name = distinct([for config in values(var.modern_front_door_route_configs) : config.cdn_frontdoor_origin_group_name])
  origin_ids = flatten([
    for config in values(var.modern_front_door_route_configs) : [
      for cdn_frontdoor_origin_name in config.cdn_frontdoor_origin_names :
      join("/", [data.azurerm_cdn_frontdoor_origin_group.frontdoor_route_origin_group[cdn_frontdoor_origin_name].id, "origins", cdn_frontdoor_origin_name])
    ]
  ])

  flat_route_configs = flatten([
    for config_key, config in var.modern_front_door_route_configs : [
      for domain in config.cdn_frontdoor_custom_domain_names : {
        domain       = domain
        profile_name = config.cdn_frontdoor_profile_name
      }
    ]
  ])

data "azurerm_cdn_frontdoor_origin_group" "frontdoor_route_origin_group" {
  for_each = {
    for key, config in var.modern_front_door_route_configs : key => config
    if contains(local.origin_group_name, config.cdn_frontdoor_origin_group_name)
  }

  name                = each.value.cdn_frontdoor_origin_group_name
  profile_name        = each.value.cdn_frontdoor_profile_name
  resource_group_name = var.resource_group_name

  depends_on = [
    azurerm_cdn_frontdoor_origin_group.this,
    azurerm_cdn_frontdoor_profile.this
  ]
}

data "azurerm_cdn_frontdoor_endpoint" "frontdoor_route_endpoint" {
  for_each = {
    for key, config in var.modern_front_door_route_configs : key => config
    if contains(local.endpoint_name, config.cdn_frontdoor_endpoint_name)
  }

  name                = each.value.cdn_frontdoor_endpoint_name
  profile_name        = each.value.cdn_frontdoor_profile_name
  resource_group_name = var.resource_group_name

  depends_on = [
    azurerm_cdn_frontdoor_endpoint.this,
    azurerm_cdn_frontdoor_profile.this
  ]
}

data "azurerm_cdn_frontdoor_custom_domain" "frontdoor_custom_domain_name" {
  for_each = { for item in local.flat_route_configs : item.domain => item }

  name                = each.value.domain
  profile_name        = each.value.profile_name
  resource_group_name = var.resource_group_name

  depends_on = [azurerm_cdn_frontdoor_profile.this,
  azurerm_cdn_frontdoor_custom_domain.this]
}