Terraform apply detect changes where there is no changes

Viewed 1578

I have a Terraform module to deploy instances on AWS whenever a coworker needs one. For each person I deploy :

  1. An ec2 instance
  2. An elastic IP
  3. A private security group

In order to centralize everything I have made the organisation of the module like so :

.
├── README.md
├── coworker1
│   ├── main.tf
│   └── variables.tf
├── keys
├── coworker2
│   ├── main.tf
│   └── variables.tf
├── templates
│   ├── cloud-init.tpl
│   ├── pubkey_rsa.pub
│   └── strat.json
├── terraform.tf
├── terraform.tfstate
└── terraform.tfstate.backup

I use the teraform.tf file in the root module and use a module block to reference submodules.

module "corworker1" {
  source = "./coworker1"
}

I run terraform apply from the root module and its ressources are correctly created. However when I run another terraform apply it says that it will need to recreate the instance and modify the elastic ip because of changes. Even when I did not change anything.

 # module.coworker1.aws_instance.ec2_instance must be replaced
-/+ resource "aws_instance" "ec2_instance" {
      ~ arn                                  = "arn:aws:ec2:eu-central-1:XXXXX:instance/i-0f498ac75ad66199e" -> (known after apply)
      ~ associate_public_ip_address          = true -> (known after apply)
      ~ cpu_core_count                       = 1 -> (known after apply)
      ~ cpu_threads_per_core                 = 1 -> (known after apply)
      - disable_api_termination              = false -> null
      - ebs_optimized                        = false -> null
      - hibernation                          = false -> null
      + host_id                              = (known after apply)
      ~ id                                   = "i-0f498ac75ad66199e" -> (known after apply)
      ~ instance_initiated_shutdown_behavior = "stop" -> (known after apply)
      ~ instance_state                       = "running" -> (known after apply)
      ~ ipv6_address_count                   = 0 -> (known after apply)
      ~ ipv6_addresses                       = [] -> (known after apply)
      + key_name                             = (known after apply)
      - monitoring                           = false -> null
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      ~ primary_network_interface_id         = "eni-06debade3df83944b" -> (known after apply)
      ~ private_dns                          = "ip-172-31-12-126.eu-central-1.compute.internal" -> (known after apply)
      ~ private_ip                           = "172.31.12.126" -> (known after apply)
      ~ public_dns                           = "ec2-3-67-119-191.eu-central-1.compute.amazonaws.com" -> (known after apply)
      ~ public_ip                            = "3.67.119.191" -> (known after apply)
      ~ secondary_private_ips                = [] -> (known after apply)
      ~ security_groups                      = [
          - "ec2 coworker1.example.com",
        ] -> (known after apply)
      ~ subnet_id                            = "subnet-3e48ca72" -> (known after apply)
        tags                                 = {
            "HiddenName" = "coworker1"
            "Name"       = "coworker1.example.com"
            "Operator"   = "coworker1@example.com"
        }
      ~ tenancy                              = "default" -> (known after apply)
        # (9 unchanged attributes hidden)

      ~ capacity_reservation_specification {
          ~ capacity_reservation_preference = "open" -> (known after apply)

          + capacity_reservation_target {
              + capacity_reservation_id = (known after apply)
            }
        }

      - credit_specification {
          - cpu_credits = "standard" -> null
        }

      + ebs_block_device { # forces replacement
          + delete_on_termination = true
          + device_name           = "/dev/sda1"
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + throughput            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = 200
          + volume_type           = (known after apply)
        }
      - ebs_block_device { # forces replacement
          - delete_on_termination = true -> null
          - device_name           = "/dev/sda1" -> null
          - encrypted             = false -> null
          - iops                  = 600 -> null
          - snapshot_id           = "snap-0d8c6e92a59dec0c9" -> null
          - tags                  = {} -> null
          - throughput            = 0 -> null
          - volume_id             = "vol-007f3d2d27b510740" -> null
          - volume_size           = 200 -> null
          - volume_type           = "gp2" -> null
        }

      ~ enclave_options {
          ~ enabled = false -> (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      ~ metadata_options {
          ~ http_endpoint               = "enabled" -> (known after apply)
          ~ http_put_response_hop_limit = 1 -> (known after apply)
          ~ http_tokens                 = "optional" -> (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      ~ root_block_device {
          ~ delete_on_termination = true -> (known after apply)
          ~ device_name           = "/dev/sda1" -> (known after apply)
          ~ encrypted             = false -> (known after apply)
          ~ iops                  = 600 -> (known after apply)
          + kms_key_id            = (known after apply)
          ~ tags                  = {} -> (known after apply)
          ~ throughput            = 0 -> (known after apply)
          ~ volume_id             = "vol-007f3d2d27b510740" -> (known after apply)
          ~ volume_size           = 200 -> (known after apply)
          ~ volume_type           = "gp2" -> (known after apply)
        }
    }

This is the file of a submodule main.tf

data "aws_ami" "ubuntu" {
  owners = ["amazon"]
  filter {
    name   = "name"
    values = ["Deep Learning Base AMI (Ubuntu 18.04) Version 38.0"]
  }
}

resource "aws_eip" "lb" {
  instance = aws_instance.ec2_instance.id
  vpc      = true
}

resource "aws_security_group" "instance_private_group" {
  description = "Private security group for the ${var.username}.example.com instance"
  name        = "ec2 ${var.username}.example.com"
  vpc_id      = data.aws_vpc.debian-ec2-env.id
  ingress {
    description = "description"
    protocol    = "TCP"
    from_port   = 22
    to_port     = 22
    cidr_blocks = ["XX.XX.XX.XX/32"]
  }
}
data "aws_vpc" "debian-ec2-env" {
  id = var.vpc_id
}
data "aws_iam_instance_profile" "instance_profile" {
  name  = var.instance_profile_name
}

resource "aws_instance" "ec2_instance" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.instance_private_group.id]
  availability_zone      = var.az
  iam_instance_profile   = data.aws_iam_instance_profile.instance_profile.name 
  user_data              = templatefile("${path.root}/templates/cloud-init.tpl", { username = var.username, admins = var.admins })
  ebs_block_device {
    device_name = "/dev/sda1"
    volume_size = var.storage_size
  }
  tags = {
    Name       = "${var.username}.example.com"
    HiddenName = var.username
    Operator   = "${var.username}@example.com"
  }
}

What do you think is the cause of this ?

EDIT 1 :

I tried to re run apply after tweaking some logging in order to reduce the amount of logging preventing me to see an important detail. The user data does'nt appear to have changed. BUT I now see this :

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the last "terraform apply":
  # module.coworker1.aws_eip.lb has been changed
  ~ resource "aws_eip" "lb" {
        id                   = "eipalloc-52f1ee6c"
      + tags                 = {}
        # (12 unchanged attributes hidden)
    }
  # module.coworker1.aws_instance.ec2_instance has been changed
  ~ resource "aws_instance" "ec2_instance" {
        id                                   = "i-0f498ac75ad66199e"
      ~ public_dns                           = "ec2-18-197-141-73.eu-central-1.compute.amazonaws.com" -> "ec2-3-67-119-191.eu-central-1.compute.amazonaws.com"
      ~ public_ip                            = "18.197.141.73" -> "3.67.119.191"
        tags                                 = {
            "HiddenName" = "coworker1"
            "Name"       = "coworker1.example.com"
            "Operator"   = "coworker1@example.com"
        }
        # (28 unchanged attributes hidden)






        # (6 unchanged blocks hidden)
    }

So if I understand correctly the public IP is changed after the Elastic IP is created and it is causing diff from the results that Terraform displayed. Should I place a dependance on the Elastic IP on the ec2 instance ?

1 Answers

I think there are two different categories of surprising behavior in your example:

  1. The IP address and hostname of your EC2 instance changed after it was already running.
  2. The provider reported that tags changed from being unset to being set to an empty map, even though being unset and being an empty map mean the same thing for that argument.

The rest of your question suggests that you're primarily interested in the first of these, so I'm going to focus on that here.

The crux of this situation is that attaching an Elastic IP to an EC2 instance after you created it causes the EC2 API to begin reporting the Elastic IP and its associated hostname as the public_ip and public_dns. This means that although these values didn't technically "change outside of Terraform", they did change outside of the definition of aws_instance.ec2_instance because that change to the IP address occurred after the EC2 API reported the instance as having been successfully created.

This behavior of changing the public_ip is unfortunately a fundamental behavior of attaching an Elastic IP address to an EC2 instance after it already booted. This is mentioned as part of the public_ip documentation, and so the message from Terraform here is really just reinforcing that warning from the docs. If you aren't actually using aws_instance.ec2_instance.public_ip anywhere else in your module then this change of value is harmless and you can think of it as Terraform just correcting its own record of what the IP address is. Accepting a plan which includes this message should then commit the updated value to a new Terraform state snapshot and not report the change again unless you detach the elastic IP address later.

Related