How to dynamically generate NACL rule set for AWS Gateway Endpoints?

hi there,

I’m trying to slove this mystery for a while but don’t see I’m going anywhere, hence decided to ask here.
I’m creating AWS Gateway endpoints for S3 and dynamodb, which returns a set of ip address each per VPC and then I’m trying to create the NACL rules to allow each IP address through it, as part of the same plan. With two variables like this:

> keys(var.vpc_list)
tolist([
  "main",
  "pub",
])
> var.vpc_endpoints_gw
tolist([
  "dynamodb",
  "s3",
])

I can easily do something like:

// Gateway Endpoints NACL rules
resource "aws_network_acl_rule" "gw_eps" {
  for_each = merge([
    for VPC in keys(var.vpc_list) : {
      for K, V in flatten([
        for EP in var.vpc_endpoints_gw : aws_vpc_endpoint.gw_eps["${VPC}:${EP}"].cidr_blocks
      ]) :
      "${VPC}:${V}:${K}" => {
        idx  = K,
        addr = V,
        vpc  = VPC
      }
    }
  ]...)

  cidr_block     = each.value.addr
  rule_number    = 200 + (2 * each.value.idx)
  ....
}

but it won’t work when adding new VPC, as the The “for_each” value depends on resource attributes that cannot be determined until apply, hence will fail.

I’m not in a sitution to do any targetd deployment and the entier IaC automation is broken due to this. What can I do here to get around this issue?

-S

You can refactor the for_each expression so that it does not depend upon

but instead depends on whatever expression you are using to set the value of those cidr_blocks attributes.

That’s my question: How?
aws_network_acl_rule expects cidr_block (and rule_number) to be string and aws_vpc_endpoint returns cidr_blocks as a list, so one has to iterate over it, unless doing staticly for every single IP (which can change anytime). The cidr_blocks is coming straight from the aws_vpc_endpoint resourse - neither can be staticly define, nor set by outselves. That’s the way Gateway Endpoints work.

I don’t use AWS and you haven’t shown those portions of your configuration, so I just responded the best I could using general Terraform knowledge and what you have shared.

that’s fine @maxb!! Thanks for the heads up!
I did mention aws_vpc_endpoint.gw_eps in the code snippet and hopped for the best. I probably should have made it clearer.

Okay, I have sloved this issue in a different way… using aws_ip_ranges data resource to get the list of IP ranges for the GW Endpoints and then iterate ovdr that list in for_each loop.

// Get the IP ranges of AWS GW Endpoints
data "aws_ip_ranges" "gw_eps" {
  for_each = length(var.vpc_endpoints_gw) > 0 ? toset(var.vpc_endpoints_gw) : []
  regions  = [var.aws_region]
  services = [each.value]
}

then use that in a local variable to get a list of IP 5x IP adresss from each endpoints:

  // Combine list of GW Endpoints IP ranges
  gw_ep_cidrs = length(var.vpc_endpoints_gw) > 0 ? flatten([
    for EP in var.vpc_endpoints_gw : slice(
      data.aws_ip_ranges.gw_eps[EP].cidr_blocks, 0, length(
        data.aws_ip_ranges.gw_eps[EP].cidr_blocks
      ) > 5 ? 5 : length(data.aws_ip_ranges.gw_eps[EP].cidr_blocks)
    )
  ]) : []

and iterate over that list in aws_network_acl_rule resource:

resource "aws_network_acl_rule" "gw_eps" {
  for_each = length(local.vpc_endpoints_gw) > 0 ? merge([
    for type in ["egress", "ingress"] : {
      for idx, cdr in sort(local.gw_ep_cidrs) : "${type}:${idx}" => {
        "cidr" = cdr,
        "rule" = 200 + (2 * idx),
        "type" = type,
      }
    }
  ]...) : {}
  cidr_block     = each.value.cidr
  egress         = each.value.type == "egress" ? true : false
  from_port      = each.value.type == "egress" ? 443 : 1024
  network_acl_id = aws_network_acl.nacls.id
  protocol       = "tcp"
  rule_action    = "allow"
  rule_number    = each.value.rule
  to_port        = each.value.type == "egress" ? 443 : 65535
}

In that way, the for_each is not dependent on any unknown value(s).

Hope it helps if anyone land here due to the same issue.

-S