Terraform refactoring
Published: 2022-10-08 03:55:51

Estava trabalhando com Terraform gerando dados para testes da nova versão do [**Netbox**](https://github.com/netbox-community/netbox).

Em vez de destruir todos os dados para reexecutar o código Terraform final , aproveitei para fazer uma demonstração de **Terraform Refactoring** com as diretivas **moved**.

## Contexto

Originalmente, dois recursos **netbox_region** eram criados:

```terraform
# Região de São Paulo
resource "netbox_region" "sp" {
  name = "São Paulo"
  description = "Estado de São Paulo"
  slug = "sp"
}

# Região de Brasília
resource "netbox_region" "df" {
  name = "Distrito Federal"
  description = "Distrito Federal"
  slug = "df"
}
```

O resultado da aplicação do código acima pode ser inspecionado no arquivo de estados do Terraform:

```bash
$ terraform state list | fgrep netbox_region
netbox_region.df
netbox_region.sp
```

## Refactor para variáveis e objetos

Não é algo que agrada a todo mundo, mas você pode parametrizar as configurações usando objetos complexos.

Regra geral, eu **não recomendo** complexidade excessiva na definição de variáveis porque deixa tudo confuso e difícil para ler ou adaptar posteriormente. Mas vamos lá:

Podemos deixar o arquivo **variables.tf**:

```terraform
variable "regions" {
    type = map(
        object({
            name = string
            description = string
            slug = string
    }))
    description = "Estados do Brasil"

    default = {
        sp = {
          name = "São Paulo"
          description = "Estado de São Paulo"
          slug = "sp"
        },
        df = {
            name = "Distrito Federal"
            description = "Distrito Federal"
            slug = "df"
        }    
    }
}
```

Tudo isso me permite substituir os dois recursos **netbox_region** por um único bloco usando **for_each**.

(Vou ser honesto, não me parece uma troca muito inteligente!)

```
resource "netbox_region" "estado" {
  for_each = var.regions
  name = each.value.name
  description = each.value.description
  slug = each.value.slug
}
```

Obviamente, se você fizer apenas isso, vai literalmente destruir ambos os recursos originais para criar dois novos. Dependendo do tipo de API com que o Terraform irá lidar, pode ser que nem seja possível fazer as alterações. 

Neste código, o que acontece é isso aqui:

```bash
$ terraform plan | egrep 'Plan|# netbox'
  # netbox_region.df will be destroyed
  # netbox_region.estado["df"] will be created
  # netbox_region.estado["sp"] will be created
  # netbox_region.sp will be destroyed
  # netbox_site.df will be updated in-place
  # netbox_site.df_spc_1 will be updated in-place
  # netbox_site.sp will be updated in-place
  # netbox_site.sp_spc_1 will be updated in-place
Plan: 2 to add, 4 to change, 2 to destroy.
```

Mas eu não estou modificando nada, não estou recriando nada, eu quero apenas substituir uma maneira de criar os recursos por outra.

Tradicionalmente, seria necessário fazer mudanças com o comando **terraform state mv**

```bash
$ terraform state mv netbox_region.sp 'netbox_region.estado["sp"]'
Move "netbox_region.sp" to "netbox_region.estado[\"sp\"]" 
Successfully moved 1 object(s).
$ terraform state mv netbox_region.df 'netbox_region.estado["df"]'
Move "netbox_region.df" to "netbox_region.estado[\"df\"]" 
Successfully moved 1 object(s).

$ terraform plan 
...
No changes. Your infrastructure matches the configuration.
```

Desde a versão 1.1, entretanto, temos uma outra maneira mais legal que executar dezenas de comandos: os blocos **moved**.

Os blocos abaixo substituem os comandos **terraform state mv**:

```terraform
moved {
  from = netbox_region.df
  to = netbox_region.estado["df"]
}

moved {
  from = netbox_region.sp
  to = netbox_region.estado["sp"]
}
```

Com este bloco, o Terraform modifica o state e não faz operações na API remota:

```bash
$ terraform plan -out plan
...
Terraform will perform the following actions:

  # netbox_region.df has moved to netbox_region.estado["df"]
    resource "netbox_region" "estado" {
        id               = "1"
        name             = "Distrito Federal"
        # (3 unchanged attributes hidden)
    }

  # netbox_region.sp has moved to netbox_region.estado["sp"]
    resource "netbox_region" "estado" {
        id               = "2"
        name             = "Estao de São Paulo"
        # (3 unchanged attributes hidden)
    }

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

$ terraform apply plan

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

$ terraform state list | fgrep netbox_region
netbox_region.estado["df"]
netbox_region.estado["sp"]
```

Você pode se perguntar: mas para que eu substituiria dois comandos simples por um monte de blocos poluindo meu código Terraform?

Bem, primeiramente, trabalhar com **blocos de configurações declarativas** não é apenas temático, mas certamente uma alternativa muito melhor que um conjunto isolados de comandos imperativos.

Vários comandos imperativos quebram a "atomicidade" das operações, além de ser bem incômodo para execução de rollbacks.

(Nem sempre será possível contemplar 100% dos casos com blocos **move**, mas é sempre melhor ter uma alternativa declarativa a não ter nenhuma!)

Além disso, se você codifica um módulo que é usado por dezenas de instâncias de códigos diferentes, você precisa executar os comandos de **terraform state mv** em **cada uma das instâncias**. 

Na maioria dos ambientes, além deste tipo de prática não ser bem vista, normalmente é particularmente difícil de conseguir os acessos a todos os arquivos.

Ao operar "dentro" de um módulo, agora você consegue abstrair boa parte das dores de cabeça que os usuários dos seus módulos podem ter.

Se você usa Terraform e pretende continuar usando no futuro, é bem provável que precise destes recursos nos seus códigos!

---

A página que fez o anúncio dos blocos **moved** é [esta aqui](https://www.terraform.io/language/modules/develop/refactoring) e conta com vários outros exemplos em que as técnicas podem ajudar.

Também há um tutorial que pode ser seguido [aqui](https://developer.hashicorp.com/terraform/tutorials/configuration-language/move-config) para ajudar a praticar esta técnica.