Chris W
Chris W

Reputation: 135

MySQL firewall rules from an app service's outbound IPs on Azure using Terraform

I'm using Terraform to deploy an app to Azure, including a MySQL server and an App Service, and want to restrict database access to only the app service. The app service has a list of outbound IPs, so I think I need to create firewall rules for these on the database. I've found that in Terraform, I can't use count or for_each to dynamically create these rules, as the value isn't known in advance.

We've also considered hard coding the count but the Azure docs don't confirm the number of IPs. With this, and after seeing different numbers in stackoverflow comments, I'm worried that the number could change at some point and break future deployments.

The output error suggests using -target as a workaround, but the Terraform docs explicitly advise against this due to potential risks.

Any suggestions for a solution? Is there a workaround, or is there another approach that would be better suited?

Non-functional code I'm using so far to give a better idea of what I'm trying to do:

...
locals {
    appIps = split(",", azurerm_app_service.appService.outbound_ip_addresses)
}

resource "azurerm_mysql_firewall_rule" "appFirewallRule" {

  count = length(appIps)

  depends_on            = [azurerm_app_service.appService]
  name                  = "appService-${count.index}"
  resource_group_name   = "myResourceGroup"
  server_name           = azurerm_mysql_server.databaseServer.name
  start_ip_address      = local.appIps[count.index]
  end_ip_address        = local.appIps[count.index]
}
...

This returns the error:


Error: Invalid count argument

  on main.tf line 331, in resource "azurerm_mysql_firewall_rule" "appFirewallRule":
 331:   count = length(local.appIps)

The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the count depends on.

Upvotes: 1

Views: 1119

Answers (2)

O1da
O1da

Reputation: 173

I dig deeper there and I think I have a solution which works at least for me :)

The core of the problem here is in the necessity to do the whole thing in 2 steps (we can't have not yet known values as arguments for count and for_each). It can be solved with explicit imperative logic or actions (like using -targetonce or commenting out and then uncommenting). Besides, it's not declarative it's also not suitable for automation via CI/CD (I am using Terraform Cloud and not the local environment).

So I am doing it just with Terraform resources, the only "imperative" pattern is to trigger the pipeline (or local run) twice.

Check my snippet:

data "azurerm_resources" "web_apps_filter" {
  resource_group_name = var.rg_system_name
  type                = "Microsoft.Web/sites"
  required_tags = {
    ProvisionedWith = "Terraform"
  }
}

data "azurerm_app_service" "web_apps" {
  count = length(data.azurerm_resources.web_apps_filter.resources)

  resource_group_name = var.rg_system_name
  name                = data.azurerm_resources.web_apps_filter.resources[count.index].name
}

data "azurerm_resources" "func_apps_filter" {
  resource_group_name = var.rg_storage_name
  type                = "Microsoft.Web/sites"
  required_tags = {
    ProvisionedWith = "Terraform"
  }
}

data "azurerm_app_service" "func_apps" {
  count = length(data.azurerm_resources.func_apps_filter.resources)

  resource_group_name = var.rg_storage_name
  name                = data.azurerm_resources.func_apps_filter.resources[count.index].name
}

locals {
  # flatten ensures that this local value is a flat list of IPs, rather
  # than a list of lists of IPs.
  # distinct ensures that we have only uniq IPs

  web_ips = distinct(flatten([
    for app in data.azurerm_app_service.web_apps : [
      split(",", app.possible_outbound_ip_addresses)
    ]
  ]))

  func_ips = distinct(flatten([
    for app in data.azurerm_app_service.func_apps : [
      split(",", app.possible_outbound_ip_addresses)
    ]
  ]))
}

resource "azurerm_postgresql_firewall_rule" "pgfr_func" {
  for_each = toset(local.web_ips)

  name                = "web_app_ip_${replace(each.value, ".", "_")}"
  resource_group_name = var.rg_storage_name
  server_name         = "${var.project_abbrev}-pgdb-${local.region_abbrev}-${local.environment_abbrev}"
  start_ip_address    = each.value
  end_ip_address      = each.value
}

resource "azurerm_postgresql_firewall_rule" "pgfr_web" {
  for_each = toset(local.func_ips)

  name                = "func_app_ip_${replace(each.value, ".", "_")}"
  resource_group_name = var.rg_storage_name
  server_name         = "${var.project_abbrev}-pgdb-${local.region_abbrev}-${local.environment_abbrev}"
  start_ip_address    = each.value
  end_ip_address      = each.value
}

The most important piece there is azurerm_resources resource - I am using it to do filtering on what web apps are already existing in my resource group (and managed by automation). I am doing DB firewall rules on that list, next terraform run, when newly created web app is there, it will also whitelist the lastly created web app.

An interesting thing is also the filtering of IPs - a lot of them are duplicated.

Upvotes: 2

Jim Xu
Jim Xu

Reputation: 23141

At the moment, using -target as a workaround is a better choice. Because with how Terraform works at present, it considers this sort of configuration to be incorrect. Using resource computed outputs as arguments to count and for_each is not recommended. Instead, using variables or derived local values which are known at plan time is the preferred approach. If you choose to go ahead with using computed values for count/for_each, this will sometimes require you to work around this using -target as illustrated above. For more details, please refer to here

Besides, the bug will be fixed in the pre-release 0.14 code. For more details, please

Upvotes: 1

Related Questions