2

I'm doing a proof-of-concept work with terraform to move our infrastructure code to it. It's my second day and I feel like I'm doing something extremely wrong or missing some points while trying to setup network ACL's because the code became very complex quickly and didn't even solve all the repetition.

I've tried to create a network-acl-rule module which I can re-use throughout the environments. Currently it looks like this;

# modules/acl/main.tf
resource "aws_network_acl_rule" "acl-rule-example" {
  network_acl_id = "${var.network_acl_id}"
  count = "${length(var.cidrs) * length(var.rules)}"

  rule_number    = "${var.rule_number + count.index}"
  from_port      = "${element(var.all_acl_rules[var.rules[floor(count.index / length(var.cidrs))]], 0)}"
  to_port        = "${element(var.all_acl_rules[var.rules[floor(count.index / length(var.cidrs))]], 1)}"
  egress         = "${element(var.all_acl_rules[var.rules[floor(count.index / length(var.cidrs))]], 2)}"
  protocol       = "${element(var.all_acl_rules[var.rules[floor(count.index / length(var.cidrs))]], 3)}"
  rule_action    = "${element(var.all_acl_rules[var.rules[floor(count.index / length(var.cidrs))]], 4)}"
  cidr_block     = "${element(var.cidrs, count.index)}"
}

I use this with the following variables and module declaration, to make it easier for you to understand.

# variables.tf
variable "all_acl_rules" {
  type = "map"

  # [from_port, to_port, egress, protocol, action, description]
  default = {
    # ephemeral outbound
    ephemeral_outbound = [1024, 65535, true, "tcp", "allow", "ephemeral-outbound"]

    # basic inbounds
    http_inbound  = [80, 80, false, "tcp", "allow", "http-inbound"]
    https_inbound = [443, 443, false, "tcp", "allow", "https-inbound"]
    ssh_inbound   = [22, 22, false, "tcp", "allow", "https-inbound"]

    # :::
  }
}

variable "cidr_blocks" {
  type = "map"
  default = {
    "all"     = ["0.0.0.0/0"],
    "vpc"     = ["10.0.0.0/8"],
    "clients" = ["x.x.x.x/32", "x.x.x.x/32", "x.x.x.x/30"],

    # :::
  }
}

and below is how I call the module

# main.tf
module "clients-acl-rule" {
 source = "modules/acl"

 network_acl_id = "${aws_network_acl.public-acl.id}"

 all_acl_rules = "${var.acl_rules}"
 cidrs = "${var.cidr_blocks["clients"]}"
 rules = ["http_inbound", "https_inbound", "ephemeral_outbound"]
 rule_number = 20
}

I'm okay to have a bloated module implementation because that will be write once and never look back again for something like network acl. This implementation is nice for grouping rules per some cidr blocks. But it has the drawback that I need to repeat calling the module multiple times for every different cidr block I need the rule which will yield a lot of duplication.

In the end, what I'd like to achieve is, having a module which I can say http_inbound for this cidr blocks, ssh inbound for this cidr blocks and ephemeral outbound for all vpc kind of flexibility.

I could fight for bloating the module code a little bit more, but it felt to me It's not a correct way of doing the ACL's. Maybe smarter variable definitions with more duplication there rather than the duplication when calling the module. How people solve this kind of problems with terraform?

Muhammet Can
  • 1,304
  • 2
  • 16
  • 30

2 Answers2

1

Until support for count in modules is implemented, you don't have a lot of options. In the past I have generated .tf files at runtime using other scripting tools (bash/python) to get around this DRY problem.

RaGe
  • 22,696
  • 11
  • 72
  • 104
  • I've considered templating with python + jinja, but I didn't want to commit to it before fully understanding terraform. I think I'm going to have a little bit of repetition along with some strange modules and will achieve something acceptable. I'll post the solution if it works out well. – Muhammet Can Nov 21 '17 at 21:45
0

Terraform 0.12 has support for Dynamic Nested Blocks.

For example you use it like this:

resource "aws_network_acl" "public_tier" {
  vpc_id = aws_vpc.my_vpc.id
  subnet_ids = [for s in aws_subnet.public : s.id]

  tags = {
    Name = "my-nacl"
  }

  dynamic "egress" {
    for_each = [for rule_obj in local.nacl_rules : {
      port       = rule_obj.port
      rule_no    = rule_obj.rule_num
      cidr_block = rule_obj.cidr
    }]
    content {
      protocol   = "tcp"
      rule_no    = egress.value["rule_no"]
      action     = "allow"
      cidr_block = egress.value["cidr_block"]
      from_port  = egress.value["port"]
      to_port    = egress.value["port"]
    }
  }

  dynamic "ingress" {
    for_each = [for rule_obj in local.nacl_rules : {
      port       = rule_obj.port
      rule_no    = rule_obj.rule_num
      cidr_block = rule_obj.cidr
    }]
    content {
      protocol   = "tcp"
      rule_no    = ingress.value["rule_no"]
      action     = "allow"
      cidr_block = ingress.value["cidr_block"]
      from_port  = ingress.value["port"]
      to_port    = ingress.value["port"]
    }
  }

}


locals {
  nacl_rules = [
    { port : 22,   rule_num : 100, cidr : "0.0.0.0/0" },
    { port : 80,   rule_num : 110, cidr : "0.0.0.0/0" },
    { port : 443,  rule_num : 120, cidr : "0.0.0.0/0" }
  ]
}

Notice that you might need to add the egress block below:

  egress{
     protocol   = "tcp"
      rule_no    = 300
      action     = "allow"
      cidr_block = "0.0.0.0/0"
      from_port  = 1024
      to_port    = 65535
  }

As mentioned here:

To enable the connection to a service running on an instance, the associated network ACL must allow both inbound traffic on the port that the service is listening on as well as allow outbound traffic from ephemeral ports. When a client connects to a server, a random port from the ephemeral port range (1024-65535) becomes the client's source port.


We can in the console that the (*)DENY ALL rule is added automatically:

enter image description here

enter image description here

Rot-man
  • 18,045
  • 12
  • 118
  • 124