EBS Volume attachments are being reassigned

I have the following configuration in vars file which defines an EC2 configuration with ebs volumes.

vars file

SQLEC2s = {
  "Machine1" = {
    name                      = "QAIftik1"
    subnetId                  = "subnet-0dc5bca32c0ca73b7"
    defaultSecurityGroupId    = "sg-0f8a3b8dc370fa481"
    additionalSecurityGroupId = ""
    regionName                = "east-1"
    EBSVolumes = [
      {
        deviceName  = "xvdg"
        driveLetter = "M"
        size        = 500
      },
      {
        deviceName  = "xvdh"
        driveLetter = "N"
        size        = 100
      },
      {
        deviceName  = "xvdi"
        driveLetter = "D"
        size        = 100
      },

    ]
  },
  "Machine2" = {
    name                      = "QRETSQLWEB1"
    subnetId                  = "subnet-0dc5bca32c0ca73b7"
    defaultSecurityGroupId    = "sg-0f8a3b8dc370fa481"
    additionalSecurityGroupId = ""
    regionName                = "east-1"
    EBSVolumes = [
      {
        deviceName  = "xvdg"
        driveLetter = "M"
        size        = 500
      },
      {
        deviceName  = "xvdh"
        driveLetter = "N"
        size        = 100
      },
      {
        deviceName  = "xvdi"
        driveLetter = "D"
        size        = 100
      },

    ]
  },

#Create EC2 Instance.

resource "aws_instance" "SQLWebEditionEast1" {
  for_each = { for key, val in var.SQLEC2s :
  key => val if val.regionName == "east-1" }
  ami                    = var.amiSQLServerWebEditionEast1
  subnet_id              = each.value.subnetId
  iam_instance_profile   = aws_iam_instance_profile.ReturnsSQLInstanceProfile.id
  vpc_security_group_ids = [var.default_security_group_id, aws_security_group.CloudReturnsEC2Sg.id] #[each.value.defaultSecurityGroupId, each.value.additionalSecurityGroupId]
  instance_type          = var.instance_type
  key_name               = var.key_name
  root_block_device {

    delete_on_termination = true
    volume_size           = 70
    volume_type           = "gp3"
    tags = {
      EC2DriveLetter = "C"
      Name           = each.value.name
    }
  }
  tags = {
    "Name"      = each.value.name
    "Component" = "ReturnsDatabaseServer"
  }
}

#Ebs Volume Attachment

locals {
  # A list of objects with one object per instance.
  flattened_volumesList = flatten([
    for key, machine in var.SQLEC2s : [
      for vol in machine.EBSVolumes :
      {
        regionName  = machine.regionName
        name        = machine.name
        subnetId    = machine.subnetId
        driveLetter = vol.driveLetter
        size        = vol.size
        volNameTag  = machine.name
        key         = key
        deviceName  = vol.deviceName
      }
    ]
  ])
}


resource "aws_ebs_volume" "EBSVolumesEast1" {
  for_each = { for key, val in local.flattened_volumesList :
  key => val if val.regionName == "east-1" }

  size              = each.value.size
  type              = "gp3"
  availability_zone = aws_instance.SQLWebEditionEast1[each.value.key].availability_zone
  tags = {
    EC2DriveLetter = each.value.driveLetter
    Name           = aws_instance.SQLWebEditionEast1[each.value.key].tags["Name"]
  }
}
resource "aws_volume_attachment" "AttachEast1EBSVolumes" {
  for_each = { for key, val in local.flattened_volumesList :
  key => val if val.regionName == "east-1" }
  device_name  = each.value.deviceName
  volume_id    = aws_ebs_volume.EBSVolumesEast1[index(local.flattened_volumesList, each.value)].id
  instance_id  = aws_instance.SQLWebEditionEast1[each.value.key].id
  skip_destroy = "false"
}

Issue
When I remove “Machine1” from SQLEc2s var file, the terraform plan shows that Volume attached to Machine 2 will also be reassigned. I only want Machine1 and it’s related EBS volumes be removed. I see in the state file ebs volume attachments are stored as indexkey = “0” … I think that why tf is getting confused. What is the best way to handle this situation?

Yeah, when you use a list with a for expression and two temporary symbols, the first one is the index:

You can also use the two-symbol form with lists and tuples, in which case the additional symbol is the index of each element starting from zero, which conventionally has the symbol name i or idx

Source: For Expressions - Configuration Language | Terraform | HashiCorp Developer

You probably want to create a map instead where each key is unique for each combination of server and EBS volume, i.e {machine.name}-{vol.deviceName} so whenever that server is removed from the vars file, only the keys containing that machine name will be removed.

Now, if you can recreate the EBS volumes and attachments, that will be easier, otherwise you can use terraform state mv or a moved block in your code.

Thanks macmiranda for your response. If I have volumes defined as

Machine1-xvdg = {
              + deviceName  = "xvdg"
              + driveLetter = "M"
              + key         = "Machine1"
              + name        = "QAIftik1"
              + regionName  = "east-1"
              + size        = 500
              + subnetId    = "subnet-0dc5bca32c0ca73b7"
              + volNameTag  = "QAIftik1"

Terraform will some how match it?

No, you’ll still need to move the object describing the resource in the state file with:

terraform state mv 'aws_volume_attachment.AttachEast1EBSVolumes[0]' 'aws_volume_attachment.AttachEast1EBSVolumes["Machine1-xvdg"]'

assuming that they correspond to each other after your code changes in the for expression.

After doing if for all volumes and for all attachments, terraform plan should show no changes needed.

Thanks it’s working now. I end up redestroying the env and rebuilt (green field). One confusion I had was between for and for_each. I created a map from flat list like this

locals {    
    machineScopedVolumeList = {
   
    
      for tm in local.flattened_volumesList : 
        "${tm.key}-${tm.deviceName}" => {"a" = tm}
    
    }
}
resource "aws_ebs_volume" "EBSVolumesEast1" {
  for_each = { for key, val in local.machineScopedVolumeList :
  key => val if val.a.regionName == "east-1" }

  size              = each.value.a.size
  type              = "gp3"
  availability_zone = aws_instance.SQLWebEditionEast1[each.value.a.key].availability_zone
  tags = {
    EC2DriveLetter = each.value.a.driveLetter
    Name           = aws_instance.SQLWebEditionEast1[each.value.a.key].tags["Name"]
  }
}

The for_each in local produced something else so I had to use for. Can you explain differece between for and for_each in this context, if possible.

The for_each meta-argument accepts a map or a set of strings, and creates an instance (of a resource or module) for each item in that map or set.

The for expression (in this context) is what is used to construct the map or set from other variables, by iterating through them.

Is there a reason you added the “a” above instead of just => tm ?

Thanks. “a” vs just => tm? I was trying bunch of different things. If there was no “a”, I think the system produced

Roughly:
machineName-deviceName {
fields
}

vs machineName-deviceName {
a = {
fields
}
}

I believe I was getting error “no property {fieldName}” so I added a “a”. No other reason. Though I have to try again to really be sure.