cloudposse / terraform-aws-security-group

Terraform module to provision an AWS Security Group
https://cloudposse.com/accelerate
Apache License 2.0
36 stars 35 forks source link

Cycle dependency #76

Open nabilbendafi opened 1 day ago

nabilbendafi commented 1 day ago

Describe the Bug

Trying to move from https://github.com/terraform-aws-modules/terraform-aws-security-group, a simple declaration of Security Group rules for two new Security Group in order to:

creates a cycle and Terraform is not able to perform a plan.

Use case:

[EC2 with SG A]--> outbound port 22
[EC2 with SG B]<-- inbound port 22

(where setting allow_all_egress to true is not an option, from a security point of view)


terraform plan
╷
│ Error: Cycle: module.b.aws_security_group.default, module.b.local.all_ingress_rules (expand), module.b.local.sg_rules_lists (expand), module.b.local.sg_exploded_rules (expand), module.b.local.all_resource_rules (expand), module.b.local.keyed_resource_rules (expand), module.b.random_id.rule_change_forces_new_security_group, module.b.local.sg_name_prefix_forced (expand), module.b.local.sg_name_prefix (expand), module.a.aws_security_group.default, module.a.local.all_ingress_rules (expand), module.a.local.sg_rules_lists (expand), module.a.local.sg_exploded_rules (expand), module.a.local.all_resource_rules (expand), module.a.local.keyed_resource_rules (expand), module.a.random_id.rule_change_forces_new_security_group, module.a.local.sg_name_prefix_forced (expand), module.a.local.sg_name_prefix (expand), module.b.local.security_group_id (expand), module.b.output.id (expand), module.a.var.rules (expand), module.a.local.rules (expand), module.a.local.norm_rules (expand), module.a.local.all_inline_rules (expand), module.a.local.all_egress_rules (expand), module.a.aws_security_group.cbd, module.a.local.created_security_group (expand), module.a.local.security_group_id (expand), module.a.output.id (expand), module.b.var.rules (expand), module.b.local.rules (expand), module.b.local.norm_rules (expand), module.b.local.all_inline_rules (expand), module.b.local.all_egress_rules (expand), module.b.aws_security_group.cbd, module.b.local.created_security_group (expand)

Expected Behavior

Terraform plan finishes without error about cycle dependency.

Steps to Reproduce

Run

terraform init
terraform plan

with following code:

locals {
  ssh-tcp = {
    key         = "ssh-tcp"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    description = "SSH"
  }
}

module "a" {
  source  = "cloudposse/security-group/aws"
  version = "~> 2.2.0"

  create_before_destroy      = true  # Enforce default behavior
  preserve_security_group_id = false # Enforce default behavior
  tags                       = {}

  vpc_id = "vpc-0000"

  allow_all_egress = false

  rules = [
    merge({ type = "egress" }, local.ssh-tcp, { source_security_group_id = module.b.id })
  ]
}

module "b" {
  source  = "cloudposse/security-group/aws"
  version = "~> 2.2.0"

  create_before_destroy      = true  # Enforce default behavior
  preserve_security_group_id = false # Enforce default behavior
  tags                       = {}

  vpc_id = "vpc-0000"

  allow_all_egress = false

  rules = [
    merge({ type = "ingress" }, local.ssh-tcp, { source_security_group_id = module.a.id })
  ]
}

Screenshots

No response

Environment

Terraform v1.7.4 on darwin_arm64

Additional Context

Same "code" with terraform-aws-modules/security-group/aws implementation produces no error

locals {
  ssh-tcp = {
    key         = "ssh-tcp"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    description = "SSH"
  }
}

module "a" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "~> 5.1.0"

  name   = "a"
  vpc_id = "vpc-0000"

  egress_with_source_security_group_id = [
    merge(local.ssh-tcp, { source_security_group_id = module.b.security_group_id })
  ]
}

module "b" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "~> 5.1.0"

  name   = "b"
  vpc_id = "vpc-0000"

  ingress_with_source_security_group_id = [
    merge(local.ssh-tcp, { source_security_group_id = module.b.security_group_id })
  ]
}

and plan seems consistent with expected output

terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.a.aws_security_group.this_name_prefix[0] will be created
  + resource "aws_security_group" "this_name_prefix" {
      + arn                    = (known after apply)
      + description            = "Security Group managed by Terraform"
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = (known after apply)
      + name                   = (known after apply)
      + name_prefix            = "a-"
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags                   = {
          + "Name" = "a"
        }
      + tags_all               = {
          + "Name" = "a"
        }
      + vpc_id                 = "vpc-0000"

      + timeouts {
          + create = "10m"
          + delete = "15m"
        }
    }

  # module.a.aws_security_group_rule.egress_with_source_security_group_id[0] will be created
  + resource "aws_security_group_rule" "egress_with_source_security_group_id" {
      + description              = "SSH"
      + from_port                = 22
      + id                       = (known after apply)
      + prefix_list_ids          = []
      + protocol                 = "tcp"
      + security_group_id        = (known after apply)
      + security_group_rule_id   = (known after apply)
      + self                     = false
      + source_security_group_id = (known after apply)
      + to_port                  = 22
      + type                     = "egress"
    }

  # module.b.aws_security_group.this_name_prefix[0] will be created
  + resource "aws_security_group" "this_name_prefix" {
      + arn                    = (known after apply)
      + description            = "Security Group managed by Terraform"
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = (known after apply)
      + name                   = (known after apply)
      + name_prefix            = "b-"
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags                   = {
          + "Name" = "b"
        }
      + tags_all               = {
          + "Name" = "b"
        }
      + vpc_id                 = "vpc-0000"

      + timeouts {
          + create = "10m"
          + delete = "15m"
        }
    }

  # module.b.aws_security_group_rule.ingress_with_source_security_group_id[0] will be created
  + resource "aws_security_group_rule" "ingress_with_source_security_group_id" {
      + description              = "SSH"
      + from_port                = 22
      + id                       = (known after apply)
      + prefix_list_ids          = []
      + protocol                 = "tcp"
      + security_group_id        = (known after apply)
      + security_group_rule_id   = (known after apply)
      + self                     = false
      + source_security_group_id = (known after apply)
      + to_port                  = 22
      + type                     = "ingress"
    }

Plan: 4 to add, 0 to change, 0 to destroy.
vavdoshka commented 11 hours ago

This is causing this problem https://github.com/cloudposse/terraform-aws-security-group/blob/276a4949330eba485147edc56c5f584b1298cbab/main.tf#L81-L109

The fact that the module can handle inline, even if inline is disabled, that does not matter. Here is simplistic example to demonstrate that:

  1. Two invocation of the same module, passing cross-reference from the output

    
    module "sg_1" {
    
    source = "./module"
    rules = [module.sg_2.id]
    }

module "sg_2" {

source = "./module"
rules = [module.sg_1.id]

}


2. module itself
```hcl
variable "rules" {
    type = list(any)

}

resource "aws_security_group" "default" {

    dynamic "does_not_matter_what" {
        for_each = var.rules # it can be local behind some flag, it does not matter
        content {
            does_not_matter_what = ""
        }
    }
}

output "id" {
    value = aws_security_group.default.id 

}
  1. result
    ╷
    │ Error: Cycle: module.sg_2.aws_security_group.default, module.sg_2.output.id (expand), module.sg_1.var.rules (expand), module.sg_1.aws_security_group.default, module.sg_1.output.id (expand), module.sg_2.var.rules (expand)
    │ 
    │ 
    ╵