How to add new instance and remove old instance without destroying all instances

Hi,

We use:

Terraform v0.12.16
+ provider.aws v2.31.0

The use case is that we have a single instance of our webserver. We need to add a new server in order to renew some token for an external api. After adding the new server and the token is renewed successfully we want to destroy the old server.

Our initial setup is as follow (simplified):

module "web-server" {
  source               = "../modules/web-server"
  hostnames            = ["s01.web.com"]
  private_ip_addresses            = ["172.25.25.25"]
}

The module web-server:

resource "aws_instance" "web" {
  count = length(var.hostnames)
  private_ip = var.private_ip_addresses[count.index]
  tags = {
    Name = var.hostnames[count.index]
  }
}

Each hostnames binds to a fixed ip address, which we later need to provision the servers via Ansible.

When we create this, all is well. To add another server we do:

module "web-server" {
  source               = "../modules/web-server"
  hostnames            = ["s01.web.com", "s02.web.com"]
  private_ip_addresses            = ["172.25.25.25", "172.25.25.26"]
}

And run terraform apply and all is still well. Now we want to remove the s01.web.com, but removing that from the list will cause the destruction of s02.web.com and the “re-creation” of s01.web.com as s02.web.com. This is not acceptable for us since that has downtime.

This seems like a typical scenario: adding one server with specific values for variables and later removing an old one. What is the Terraform way of implementing this scenario?

Best regards,
Lars

Hi @lvonk,

The behavior you are describing is a typical result for using count against a list because it causes Terraform to identify the individual instances by the indexes into the list, and so removing an item will assign new indices to all items appearing after it.

The answer is to use resource for_each instead, which associates one instance with each key in a map or with each string in a set of strings.

The most direct change from what you have already would be to use for_each with zipmap to produce a suitable map:

resource "aws_instance" "web" {
  for_each = zipmap(var.hostnames, var.private_ip_addresses)

  private_ip = each.value
  tags = {
    Name = each.key
  }
}

zipmap will produce a map from hostnames to corresponding IP addresses, and so each.key will be the hostname and each.value the IP address of each instance. Terraform will identify these instances with addresses like aws_instance.web["s01.web.com"] so that it can better understand the intended meaning of adding and removing hostnames.


With that said, I suspect that having the module itself accept the map from hostnames to private IP addresses would be a more convenient interface for callers of this module anyway, so you might prefer to consolidate your two variables into one, declared like this:

variable "host_ip_addresses" {
  type = map(string)
}

Then you can call the module like this instead:

module "web-server" {
  source               = "../modules/web-server"

  host_ip_addresses = {
    "s01.web.com" = "172.25.25.25"
  }
}

Because in this case v.host_ip_addresses is already a suitable map, you can skip the zipmap call and just use for_each = var.host_ip_addresses directly, with the same result.

2 Likes

Wow thanks @apparentlymart for the quick reply and explanation. This works like a charm and it also makes much more sense with the map instead of two lists.

I would like to add that, like in my case, if you previously created the aws_instance.web[0] you can’t simply refactor this to aws_instance.web["s01.web.com"]:

If you do:

terraform state mv module.web-server.aws_instance.web[0] module.web-server.aws_instance.web[\"s01.web.com\"]

It results to

Error: Invalid target address

Cannot move to
module.web-server.aws_instance.web["s01.web.com"]:
module.web-server.aws_instance.web does not exist in the current
state.

The issue https://github.com/hashicorp/terraform/issues/21346 pointed me to importing the current aws_instance as module.web-server.aws_instance.web["s01.web.com"] and later removing the module.web-server.aws_instance.web[0]

terraform import module.web-server.aws_instance.web[\"s01.web.com\"] i-1234567654323456
terraform state rm module.web-server.aws_instance.web[0]

Again thanks a lot!