Refactor Terraform code with Moved Blocks - a new way without manually modifying the state
Tags: terraform, cloud, devops
Published: 2022-07-08 11:07:49

Most software and IT infrastructure projects which have been deployed to production have to deal with requirement changes during their lifetime. User expectations change, new use cases appear, traffic patterns are different than expected or new technology becomes available. Refactoring of existing code (application code as well as infrastructure-as-code) has always been an important task but also one of the major pain points in IT. A good support of refactoring tools and patterns can make a difference for a framework like Terraform compared with its competitors.

**Setting the stage**

Terraform by HashiCorp -- one of the major players in the
infrastructure-as-code framework world - has been around since 2014. It has been used to setup a lot of small, medium, and large projects all over the world. It provides a rich feature set to define infrastructure in a concise manner. One of its strengths is the way to create identical/similar resources using either the meta-argument `count` or the newer version `for_each`.

`count` makes it very easy to define identical resources like shown in the listing below which defines a very basic setup for 3 EC2 instances running on AWS:

```terraform
locals {
  server_names = ["webserver1", "webserver2", "webserver3"]
}

resource "aws_instance" "web" {
  count = length(local.server_names)

  ami                       = "ami-0a1ee2fb28fe05df3"
  instance_type             = "t3.micro"

  tags = {
    Name = local.server_names[count.index]
  }
} 
```

Terraform stores references to resources created by using the `count` meta-argument in its internal state in an array using an index-based approach.

![Terraform state listing showing three web server instance details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6f0qk32w98fdpfcmv28m.png)

This works fine if a single instance must not be replaced or deleted. Such an action will affect all resources which are located on a higher index in the array due to the nature Terraform manages its state.

Trying to remove "webserver2" in the example above
 
```terraform
locals {
  server_names = ["webserver1", "webserver2", "webserver3"]
}
...
```

will result in the destruction of the EC2 instance tagged "webserver3" and a renaming of the previous named "webserver2" instance into "webserver3". The result does not correspond to the expressed intention.

![Terraform listing showing unintended result of refactoring a structure build upon `count`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kl5ec2v4gifkz0dfpmg3.png)

Version 0.12.6 of Terraform introduced the `for_each` meta-argument - a more flexible way to create identical/similar resources. 

```terraform
resource "aws_instance" "web" {
  for_each = toset(local.server_names)

  ami                         = "ami-0a1ee2fb28fe05df3"
  instance_type               = "t3.micro"

  tags = {
    Name = each.value 
  }
} 
```

The Terraform state references the resources no longer based on an index but by using a key-based approach. It is now possible to address a single resource without affecting others.

![Terraform listing showing correctly refactored structure based upon `for_each`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/stiice0glkzwlcwkxqm9.png)

The removal of "webserver2" can now be performed successfully without affecting other resources.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v3ljhaic16c74yw4cswd.png)

Due to the greater flexibility of `for_each` it might be helpful or even required to refactor existing code (migrate from `count` to `for_each`). This has been possible in the past by manipulating the Terraform state directly using the `terraform state mv` CLI command. However, all manual state manipulations are brittle and prone to errors which make them as a kind of last resort.

**From imperative to explicit**

HashiCorp introduced an improved refactoring experience with version 1.1 of Terraform: the [`moved block`](https://www.terraform.io/language/modules/develop/refactoring) syntax which allows to express refactoring steps in code instead of using an imperative attempt via CLI.

The `moved block` allows to specify the old and new reference of a resource like shown in the following example which has been rewritten to use `for_each` instead of `count`:

```terraform
locals {
  server_names = ["webserver1", "webserver2", "webserver3"]
}

moved {
  from = aws_instance.web[0]
  to   = aws_instance.web["webserver1"]
}

moved {
  from = aws_instance.web[1]
  to   = aws_instance.web["webserver2"]
}

moved {
  from = aws_instance.web[2]
  to   = aws_instance.web["webserver3"]
}

resource "aws_instance" "web" {
  for_each = toset(local.server_names)

  ami           = "ami-0a1ee2fb28fe05df3"
  instance_type = "t3.micro"

  tags = {
    Name = each.value
  }
} 
```

A following `terraform plan/apply` reveals that no instance will be destroyed or modified in any way but only moved in the state from its old reference to its new one created by the way `for_each` works. No need for any manual state manipulation anymore but everything can be done securely using Terraforms native way to work.

![Terraform listing showing how moved blocks work](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4vi8u35moj0xiu6gul18.png)
 
`moved blocks` cannot only be applied to refactor `count` into
`for_each` syntax but also be used to rename resources, to move resources into modules and so on. Not everything is possible using the new language element, but many (not extremely complex) refactoring tasks can benefit from using it. Terraforms documentation contains different examples and use cases with further details.

### Wrap-up

`moved blocks` have made refactoring existing Terraform projects easier and safer to perform. No manual steps are required any longer for many use cases even though `terraform state mv` is still there to solve problems which cannot be tackled by using the new element. It is helpful to have tooling/framework elements like this on at hand.   

Depending on the type and size of the project (internal project or public module) it might make sense respectively it is even recommended by HashiCorp not to delete the blocks after having applied the changes. Not everyone using the module might already have fetched the latest version. Apart from avoiding trouble for users it might be helpful to document any significant changes on the project structure for later reviews. A short well written and dated comment combined with the `moved block` syntax might answer your question or the one of a colleague six months down the road.