Terraform Plan Doesn't Catch Manual Resource Changes

Context

Hello, facing an interesting issue for basic use-case

  1. Created a Security Group in AWS using module code referenced below.
  2. No issues there and the resource creation + deletion works as expected.
  3. If I modify anything referenced in the input code (in main.tf where module is being used), TF does catch those changes in TF Plan.
  4. But if I modify anything in AWS for same resource (in this case adding / modifying ingress rule), TF does NOT catch those changes for some reason.

Terraform Version

v1.0.4

Terraform Configuration Files

Security Group Module:
main.tf

// Create security groups
resource "aws_security_group" "ease_security_group" {
  for_each    = var.security_group
  name        = each.key
  vpc_id      = var.ease_vpc_id
  tags = {
    Name = each.key
  }
}

// Add ingress rules to all security groups
locals {
  ingress_rules = merge([
    for sg_key, sg in var.security_group : {
      for rule in sg["ingress"] : 
        "${sg_key}-${rule["description"]}-${rule["protocol"]}-${rule["from_port"]}-${rule["to_port"]}}" => {
            type              = "ingress"
            security_group_id = "${aws_security_group.ease_security_group[sg_key].id}"
            from_port         = rule["from_port"]
            to_port           = rule["to_port"]
            protocol          = rule["protocol"]
            cidr_blocks       = rule["cidr_blocks"]
            description       = rule["description"]
        }
    }
  ]...)
}

resource "aws_security_group_rule" "ease_security_group_ingress_rules" {
  for_each = local.ingress_rules

  type              = "ingress"
  security_group_id = each.value.security_group_id
  from_port         = each.value.from_port
  to_port           = each.value.to_port
  protocol          = each.value.protocol
  cidr_blocks       = each.value.cidr_blocks
  description       = each.value.description
  depends_on = [aws_security_group.ease_security_group]
}

// Add egress rules to all security groups
locals {
  egress_rules = merge([
    for sg_key, sg in var.security_group : {
      for rule in sg["egress"] : 
        "${sg_key}-${rule["description"]}" => {
            type              = "egress"
            security_group_id = "${aws_security_group.ease_security_group[sg_key].id}"
            from_port         = rule["from_port"]
            to_port           = rule["to_port"]
            protocol          = rule["protocol"]
            cidr_blocks       = rule["cidr_blocks"]
            description       = rule["description"]
        }
    }
  ]...)
}

resource "aws_security_group_rule" "ease_security_group_egress_rules" {
  for_each = local.egress_rules

  type              = "egress"
  security_group_id = each.value.security_group_id
  from_port         = each.value.from_port
  to_port           = each.value.to_port
  protocol          = each.value.protocol
  cidr_blocks       = each.value.cidr_blocks
  description       = each.value.description
  depends_on = [aws_security_group.ease_security_group]
}

variables.tf

// Define security groups
variable "security_group" {
  default = {}
}

// Define VPC ID
variable "ease_vpc_id" {
  default = ""
}

Using the module:
main.tf

module "securitygroups" {
  source = "modules/securitygroups"
  vpc_id = "${module.network.out_vpc_id}"
  security_group = {
	"redis" = {
		ingress = {
		    rule_1 = {
		        description = "Allow Redis",
		    	from_port   = 6379,
		    	to_port     = 6379,
		    	protocol    = "tcp",
    			cidr_blocks = [var.vpc_cidr]
		    },
		    rule_2 = {
		        description = "from eng vpn",
		    	from_port   = 6379,
		    	to_port     = 6379,
		    	protocol    = "tcp",
		    	cidr_blocks = ["172.16.0.0/16"]
		    },
		},
		egress = {
			rule_1 = {
		        description = "Allow ALL traffic",
		    	from_port   = 0,
		    	to_port     = 0,
		    	protocol    = -1,
		    	cidr_blocks = ["0.0.0.0/0"]
			}
		}
	}
  }
}

Debug Output

NOTE:

  1. Initially there were no manual changes for this SG, so TF Plan was showing right state which is no new changes.
  2. Then to test, I added a 3rd ingress test rule with dummy values 1 as port and 1.1.1.1/32 as CIDR.
  3. Next TF Plan still shows as no new changes, would expect TF Plan to destroy that 3rd ingress for that SG being under TF state.
  4. TF Plan from point 3 which shows no new changes
terraform plan -target=module.securitygroups
module.network.aws_vpc.ease_vpc: Refreshing state... [id=vpc-0f48a7ba42d0f92a2]
module.securitygroups.aws_security_group.ease_security_group["ENG VPN GENERAL"]: Refreshing state... [id=sg-0f9bca78b2aceffb4]
module.securitygroups.aws_security_group.ease_security_group["ease-redis"]: Refreshing state... [id=sg-0f44d46877f814d6b]
module.securitygroups.aws_security_group.ease_security_group["ENG VPN PROD"]: Refreshing state... [id=sg-052d665f8c31dd150]
module.securitygroups.aws_security_group_rule.ease_security_group_egress_rules["ENG VPN GENERAL-Allow ALL traffic"]: Refreshing state... [id=sgrule-1431550098]
module.securitygroups.aws_security_group_rule.ease_security_group_egress_rules["ENG VPN PROD-Allow ALL traffic"]: Refreshing state... [id=sgrule-1039961651]
module.securitygroups.aws_security_group_rule.ease_security_group_egress_rules["ease-redis-Allow ALL traffic"]: Refreshing state... [id=sgrule-3887475553]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ENG VPN PROD-main account eng vpn-1--1--1}"]: Refreshing state... [id=sgrule-1577332433]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ENG VPN GENERAL-Allow PostgreSQL-tcp-5432-5432}"]: Refreshing state... [id=sgrule-3534773046]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ENG VPN PROD-Allow SSH-tcp-22-22}"]: Refreshing state... [id=sgrule-4112345915]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ease-redis-Allow Redis-tcp-6379-6379}"]: Refreshing state... [id=sgrule-2847224619]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ENG VPN GENERAL-local-tcp-5432-5432}"]: Refreshing state... [id=sgrule-105634581]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ease-redis-from eng vpn-tcp-6379-6379}"]: Refreshing state... [id=sgrule-327071861]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ENG VPN PROD-Allow HTTP-tcp-80-80}"]: Refreshing state... [id=sgrule-161870213]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ENG VPN PROD-Allow RDP-tcp-3389-3389}"]: Refreshing state... [id=sgrule-3254739701]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ENG VPN PROD-opentelemetry-tcp-4317-4317}"]: Refreshing state... [id=sgrule-802725468]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ENG VPN PROD-Allow Redshift-tcp-5439-5439}"]: Refreshing state... [id=sgrule-1483419703]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ENG VPN PROD-Allow PostgreSQL-tcp-5432-5432}"]: Refreshing state... [id=sgrule-4104625858]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are
needed.

This is how terraform state looks:

module.securitygroups.aws_security_group.ease_security_group["ease-redis"]
module.securitygroups.aws_security_group_rule.ease_security_group_egress_rules["ease-redis-Allow ALL traffic"]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ease-redis-Allow Redis-tcp-6379-6379}"]
module.securitygroups.aws_security_group_rule.ease_security_group_ingress_rules["ease-redis-from eng vpn-tcp-6379-6379}"]

Expected Behavior

TF Plan to destroy 3rd ingress rule.

Actual Behavior

TF Plan shows no new changes.


No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are
needed.

Steps to Reproduce

  1. Terraform apply to create a SG resource using module.
  2. Manually modify ingress or egress rule.
  3. Terraform plan which should ideally show that it plans to destroy manual changes made in step 2.

Additional Context

No custom configuration is set apart from what’s shown here.

Please let me know if any Qs OR more input is required. Thank You

That is expected.

You are using the aws_security_group_rule resources to define the rules, so Terraform only knows about the rules you define. If anyone creates additional rules outside of Terraform it won’t know anything about them, and therefore wouldn’t try to remote them. You would need to use terraform import for Terraform to know about them.

Instead of using aws_security_group_rule you could look at using in-line rules within the aws_security_group resource, which I think may make Terraform manage all rules.