Avoid configuration drift on your terraform state when using aws_security_group
Tags: aws, terraform, devops, security
Published: 2022-12-13 18:13:08

Terraform is a fantastic tool for provisioning and maintaining infrastructure state, both on-premises and in cloud environments. However, sometimes we go through some situations where we have an expectation that terraform works one way, but terraform ends up working in another way.

![this is fine](https://media.giphy.com/media/2UCt7zbmsLoCXybx6t/giphy-downsized.gif)

That's what happened to me when using the **aws_security_group**. This resource allows you to create a Security Group within a VPC, which in practice can be applied to other resources, such as EC2 instances, databases, and Kubernetes clusters, to deny connections to specific ports and allow only some specific ports (or none).

My use case was to create a Security Group for an EC2 instance that would work as a simple job processing worker. In summary, because it works in pull-based, this instance would not need to have any ingress (incoming) rules, only one egress (outgoing) rule for any IP.

So, I did it like this:
```
resource "aws_security_group" "sg" {
  name   = "my-amazing-security-group"
  vpc_id = aws_vpc.main.id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

When running the **terraform apply** command, I got the following output:
```
gabriel@machine:~/terraform$ terraform apply

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:

  # aws_security_group.sg will be created
  + resource "aws_security_group" "sg" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = [
          + {
              + cidr_blocks      = [
                  + "0.0.0.0/0",
                ]
              + description      = ""
              + from_port        = 0
              + ipv6_cidr_blocks = []
              + prefix_list_ids  = []
              + protocol         = "-1"
              + security_groups  = []
              + self             = false
              + to_port          = 0
            },
        ]
      + id                     = (known after apply)
      + ingress                = (known after apply)
      + name                   = "my-amazing-security-group"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags_all               = (known after apply)
      + vpc_id                 = "vpc-0101010101"
    }

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

aws_security_group.sg: Creating...
aws_security_group.sg: Creation complete after 2s [id=sg-0101010101010101]
```

Excellent! Terraform created the security group and I was able to use it on the EC2 instance I created. However, I realized I had made a mistake while provisioning the EC2 instance and needed to connect to this specific instance using **ssh**.

![made a mistake](https://media.giphy.com/media/QBpnlzndcEeIS57v15/giphy.gif)

To enable this access, I needed to create a new ingress rule on the security group. So, I went directly to the AWS dashboard, and edited the security group manually, allowing connections on port 22 for just my IP Address.

![hehehe](https://media.giphy.com/media/xThtaxbIPLT9XqWcDK/giphy.gif)

After connecting to the instance and solving the problem, I thought to myself:

> _I'm gonna run terraform apply again, as terraform will notice the extra rule in the security group and create a plan to remove this rule, right?_

So when I ran the command I got the following output:

```
gabriel@machine:~/terraform$ terraform apply
aws_security_group.sg: Refreshing state... [id=sg-0101010101010101]

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.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
```

![confused](https://media.giphy.com/media/nTfdeBvfgzV26zjoFP/giphy.gif)

I was confused. Did terraform not notice the new rule in the security group? I went to the AWS dashboard and saw that the ingress rule was still present! So what?

Then I dig a little further and I understood that the aws_security_group does **not** maintain the state of the rules, if this is not **explicit**. That is, we need to make it explicit in the resource definition that we don't want any ingress rules. Correcting our code, it would look like this:

```
resource "aws_security_group" "sg" {
  name   = "my-amazing-security-group"
  vpc_id = data.aws_vpc.main.id

  ingress = []    # no ingress rules allowed

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

Note that the **ingress** attribute takes an empty list. This makes it explicit to terraform that we don't want any ingress rules for this security group. Then, running the terraform apply command again, we can finally have the guaranteed state:

```
gabriel@machine:~/terraform$ terraform apply
aws_security_group.sg: Refreshing state... [id=sg-0110522c7ec4870d0]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_security_group.sg will be updated in-place
  ~ resource "aws_security_group" "sg" {
        id                     = "sg-0110522c7ec4870d0"
      ~ ingress                = [
          - {
              - cidr_blocks      = [
                  - "169.169.169.169/32",
                ]
              - description      = "My IP"
              - from_port        = 22
              - ipv6_cidr_blocks = []
              - prefix_list_ids  = []
              - protocol         = "tcp"
              - security_groups  = []
              - self             = false
              - to_port          = 22
            },
        ]
        name                   = "my-amazing-security-group"
        tags                   = {}
        # (7 unchanged attributes hidden)
    }

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

aws_security_group.sg: Modifying... [id=sg-0110522c7ec4870d0]
aws_security_group.sg: Modifications complete after 0s [id=sg-0110522c7ec4870d0]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
```

![ufa](https://media.giphy.com/media/nXxOjZrbnbRxS/giphy.gif)

Now, terraform guarantees that this security group does not gain new rules through the AWS dashboard and maintains this state for us!