Terraform: Resolving AWS Security Group Cyclic Dependencies

If you have ever used Terraform to create security groups in AWS, you may have come across a situation where you're trying to do something like this:

  • Create security group A with an ingress rule from security group B

  • Create security group B with an ingress rule from security group A

No big deal, right? My first reaction was to use Terraform's aws security group resource to create both security groups and define some inline ingress rules. The code would look something like this:

provider "aws" {
    region = "${var.region}"
}

resource "aws_security_group" "group_A" {
    name = "Group A"
    vpc_id = "${var.vpc}"

    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        security_group_id = ["${aws_security_group.group_B.id}"]
    }
}

resource "aws_security_group" "group_B" {
    name = "Group B"
    vpc_id = "${var.vpc}"

    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        security_group_id = ["${aws_security_group.group_A.id}"]
    }
}

After hitting terraform plan, however, I received an error message indicating the following:

Error: 1 error(s) occurred:

* Cycle: aws_security_group.group_B, aws_security_group.group_A

As you can see, creating security groups with inline ingress rules that depend on each other creates a problem for us. As of this time, it doesn't appear from the GitHub issues that the development team at HashiCorp is able to fix the issue with this inline declaration approach. Luckily, there is a relatively easy approach to getting around this issue.

The solution to the aforementioned problem is to use the aws_security_group resource to simply create the AWS security groups first without defining any inline ingress or egress rules. You can then use the aws_security_group_rule resource to add ingress and egress rules to your respective security groups. Here is an example of a solution that should work for you in this scenario:

provider "aws" {
    region = "${var.region}"
}

resource "aws_security_group" "group_A" {
    name = "Group A"
    vpc_id = "${var.vpc}"
}

resource "aws_security_group" "group_B" {
    name = "Group B"
    vpc_id = "${var.vpc}"
}

resource "aws_security_group_rule" "allow_group_B" {
    type = "ingress"
    from_port = 80
    to_port = 80
    protocol = "tcp"
    security_group_id = "${aws_security_group.group_A.id}"
    source_security_group_id = "${aws_security_group.group_B.id}"
}

resource "aws_security_group_rule" "allow_group_A" {
    type = "ingress"
    from_port = 80
    to_port = 80
    protocol = "tcp"
    security_group_id = "${aws_security_group.group_B.id}"
    source_security_group_id = "${aws_security_group.group_A.id}"
}

While this requires writing an extra bit of code, it does get the job done. Some may even contend that it is the cleaner approach.

If you found this article useful, be sure to follow me on Twitter for more content that I regularly put out.