1
votes

I am trying to build a dynamic aws_security_group Terraform module. This module will take in a few variables:

variable "region" {
  default = "us-east-1"
  type    = string
}
variable "security_group_list" {
  description = "List of security groups to be made"
}

The variable "security_group_list" takes in a yaml file that has been decoded by the following:

module "test-security-group" {
  source              = "../"
  security_group_list = yamldecode(file("${path.module}/test.yaml"))
  region              = "us-west-1"
}

The YAML file that is being decode is currently structured like this:

test-security-group:
  ingress_rules:
    description: "Test description"
    is_self_source: "false"
    from_port: 80
    to_port: 80
    protocol: "tcp"
    cidr_blocks: ["0.0.0.0/0"]
  ingress_rules:
    description: "Test description 2"
    is_self_source: "false"
    from_port: 443
    to_port: 443
    protocol: "tcp"
    cidr_blocks: ["0.0.0.0/0"]
  ingress_rules:
    description: "Test description 3"
    is_self_source: "false"
    from_port: 8080
    to_port: 8080
    protocol: "tcp"
    cidr_blocks: ["0.0.0.0/0"]
  egress_rules:
    description: "Test description 4"
    is_self_source: "false"
    from_port: 80
    to_port: 80
    protocol: "tcp"
    cidr_blocks: ["0.0.0.0/0"]

The code in this module is:

#-------------------------------------------
#Dynamic Security Group
#-------------------------------------------
resource "aws_security_group" "security_group" {
  for_each = var.security_group_list

  name = each.key
  dynamic "ingress" {
    for_each = each.value.ingress_rules[*]
    content {
      description = each.value.ingress_rules.description
      from_port   = each.value.ingress_rules.from_port
      to_port     = each.value.ingress_rules.to_port
      protocol    = each.value.ingress_rules.protocol
      cidr_blocks = each.value.ingress_rules.cidr_blocks
      self        = each.value.ingress_rules.is_self_source
    }
  }
  dynamic "egress" {
    for_each = each.value.egress_rules[*]
    content {
      description = each.value.egress_rules.description
      from_port   = each.value.egress_rules.from_port
      to_port     = each.value.egress_rules.to_port
      protocol    = each.value.egress_rules.protocol
      cidr_blocks = each.value.egress_rules.cidr_blocks
      self        = each.value.egress_rules.is_self_source
    }
  }
}

I am able to successfully run terraform validate and terraform apply. When I do so I get the following output from the CLI:

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:

  # module.test-security-group.aws_security_group.security_group["test-security-group"] will be created
  + resource "aws_security_group" "security_group" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = "Test description 4"
              + from_port        = 80
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 80
            },
        ]
      + id                     = (known after apply)
      + ingress                = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = "Test description 3"
              + from_port        = 8080
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "tcp"
              + security_groups  = []
              + self             = false
              + to_port          = 8080
            },
        ]
      + name                   = "test-security-group"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + vpc_id                 = (known after apply)
    }

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

The problem that I am having is that this code keeps skipping over the other "ingress_rules" that are specified in the YAML file. It seems to always only apply the last "ingress_rule" in the list. I have tried restrucuting the YAML file in multiple ways including making it a tuple. I am new to for_each loops in Terraform and new to YAML files. If someone can help me figure out why this code is skipping over the other "ingress_rules" that would be much appreciated.

2

2 Answers

1
votes

YAML disallows identical keys in a mapping. If you want to have multiple ingress rules, put them in a list:

test-security-group:
  ingress_rules:
    - description: "Test description"
      is_self_source: "false"
      from_port: 80
      to_port: 80
      protocol: "tcp"
      cidr_blocks: ["0.0.0.0/0"]
    - description: "Test description 2"
      is_self_source: "false"
      from_port: 443
      to_port: 443
      protocol: "tcp"
      cidr_blocks: ["0.0.0.0/0"]
    - description: "Test description 3"
      is_self_source: "false"
      from_port: 8080
      to_port: 8080
      protocol: "tcp"
      cidr_blocks: ["0.0.0.0/0"]

I am not really familiar with terraform but each.value seems to point to the current item so you would do each.value.description etc.

0
votes

I would highly recommend not using this pattern, mixing DSLs.

Use of external yaml can lead to undefined behavior and will be difficult for users to troubleshoot. Use of an external YAML file means your input cannot be type checked or linted for syntax at runtime by terraform.

Terraform already provides map data types. These can be used in your for-each loops in a very similar manner.

If you want to include these maps in your module, try input variables and tfvars files.

AWS uses a similar pattern in their module here.