Terraform keeps removing empty security group ingress rules each plan/apply

I see weird behavior in my terraform plan and apply. I’m creating a aws_security_group resource with two ingress block settings and one dynamic ingress block, for the ingress block I’m passing as variable the additional sg rules.

resource "aws_security_group" "db" {
  name_prefix            = "${var.name}-db-"
  description            = "Allow db access"
  vpc_id                 = var.vpc_id
  revoke_rules_on_delete = false

  ingress {
    from_port       = 3306
    to_port         = 3306
    description     = "X"
    protocol        = "tcp"
    security_groups = [var.x-sg]
  }

  ingress {
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    description = "Y"
    cidr_blocks = [var.cidr_block]
  }

  dynamic "ingress" {
    for_each = var.db_sg_ingress_rules
    content {
      from_port   = ingress.value["port"]
      to_port     = ingress.value["port"]
      protocol    = ingress.value["protocol"]
      description = ingress.value["description"]
      cidr_blocks = ingress.value["cidr_blocks"]
    }
  }
}

All the var passed correctly, setting var:

db_sg_ingress_rules = [{
        port        = 3306
        protocol    = "tcp"
        description = "A"
        cidr_blocks = ["10.10.0.0/16"]
    },
    {
        port        = 3306
        protocol    = "tcp"
        description = "B"
        cidr_blocks = ["180.20.0.0/16"]
    },
    {
        port        = 3306
        protocol    = "tcp"
        description = "C"
        cidr_blocks = ["12.11.10.0/24"]
    }]

configure this way:

variable "db_sg_ingress_rules" {
  type = list(object({
    port        = number
    protocol    = string
    description = string
    cidr_blocks = list(string)
  }))
}

Once I run the terraform apply and approve it, I see each time the same suggestion for changes, terraform is trying to remove empty rule cidr_block definition:

  # module.security_group.aws_security_group.db will be updated in-place
  ~ resource "aws_security_group" "db" {
        id                     = "sg-xxxxxxxxxxxxxx"
      ~ ingress                = [
          - {
              - cidr_blocks      = []
              - description      = "B"
              - from_port        = 3306
              - ipv6_cidr_blocks = []
              - prefix_list_ids  = []
              - protocol         = "tcp"
              - security_groups  = []
              - self             = false
              - to_port          = 3306
            },
            # (5 unchanged elements hidden)
        ]
        name                   = "db-xxxxxxxxxxxxxxx"
        tags                   = {}
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

Another thing that I see is that if I ran it multiple times without changing any code the plan returns the same changes but for a different rule, for example not description = “B”, but with description = “X”. The terraform state show for this resource not showing any empty definition. I don’t want to use the aws_security_group_rule resources as I’m looking to manage the sg resource to see any drifts in the sg.

How can I solve this issue, I would like to see the plan empty without any changes.

I have tried to sort the list so it will be passed in the same order each time with:

sorted_values = sort(var.db_sg_ingress_rules[*].description)
sorted_list = tolist(flatten(
    [for value in local.sorted_values:
        [ for elem in var.db_sg_ingress_rules: 
              elem if value == elem.description
        ]     
    ]))

and then pass the sorted_list as the var to for_each.

UPDATE: I have tried to combine all rules into one list and pass it into a singly dynamic block as well as tried to separate all of them into 5 blocks of ingress rules(under the security group resource), but I still see the same pattern with the empty rule that is being removed in terraform plan/apply

I do not use AWS myself, so I can’t help much with that side of it, but what I can point out is that from your plan:

Terraform seems to be seeing the AWS API report that there are 6 items (1 to be deleted plus 5 unchanged).

Naturally, if your configuration defines 5 items but the remote API says there are 6, Terraform is going to plan to delete one - so the next thing to investigate is why there are 6.

Perhaps, have a look at what the AWS API, CLI, or web UI thinks exists - confirm there are indeed 6, and see if the specifics provide any hint about how there somehow continue to be 6 even after Terraform tries to remove one.