on
Bulk renaming terraform resources
Background
Back in December last year I joined a project where there was no consistency in the local name given to terraform resources and datasources, some things were kebab case such as;
resource "azurerm_key_vault" "team-a-secrets" {
...
}
other resources were in snake case, for example;
resource "azurerm_storage_account" "team_a_files" {
...
}
I even found one instance where both -
and _
were used in the same name.
While terraform doesn’t care whether you used kebab case or snake case in your names, I do like local consistency within the same the same codebase. Considering that the resource names, datasource names and argument names defined in providers are always snake case this is generally my preference also.
Last week we had a couple of “Yak days” at work where we are allowed to focus on the smaller maintenance tasks that rarely get prioritised against other work and I chose to shave this particular Yak.
The work to be done
Before Terraform 1.1 refactoring the local name of resources like this would have either triggered a recreate of the resource, not ideal for infrastructure containing state, or required manually removing and importing resources from the state which is risky if you make any mistakes.
Since Terraform 1.1 we can now use moved
blocks to do this refactoring in a
safe manner. Correcting the kebab case example above would require the following
code changes.
moved {
from = azurerm_key_vault.team-a-secrets
to = azurerm_key_vault.team_a_secrets
}
resource "azurerm_key_vault" "team_a_secrets" {
...
}
When running terraform plan with this you get the following output in the terraform plan;
# azurerm_key_vault.team-a-secrets has moved to azurerm_key_vault.team_a_secrets
Once applied you can then remove the moved
block.
Scripting creation of the moved
blocks
After a couple of hours of manually refactoring a couple of modules I realised this would take me a week of work if I wasn’t able to automate the job in some way. That’s when I set to work writing a little script to make my job easier. Luckily the HCL language is easily parsable with standard CLI tooling.
We always start our scripts with a shebang so we know what interpreter it will run with and tell it to exit immediately on errors. Then I defined a variable to hold the current root module folder to work in as we have a monorepo and I didn’t want to break everything at once.
#!/usr/bin/env bash
set -e
SEARCH_FOLDER=./prod/base-infra/
Then we need to find all of the terraform files in the module root (using nulls as separators for safe piping to the next stage)
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0
Then for each file we found, extract all the lines that start with the string resource "
. This is the resurce definitions.
xargs \
--null \
grep '^resource "'
Next we parse those lines with awk
, specifying "
as the delimiter. If the
fourth token in the line contains a -
then we output a moved block for the
resource.
awk \
--field-separator=\" \
'$4 ~ "-" { printf "moved {\n from = %s.%s\n to = %s.%s\n}\n\n", $2, $4, $2, gensub("-", "_", "g", $4) }'
This output can then be redirected to a file for later use. I used a temp filename for now as we don’t want this file to be picked up by other commands later in the script.
The same strategy can be used for creating moved blocks for data sources and module references.
Changing the resource names
In order to change the resource names themselves in the terraform files I used
a similar strategy to create a sed
script that I could run towards the end of
the script. This looks the same as the code above but the awk
command changed
to output valid lines of a sed
script.
awk \
--field-separator=\" \
'$4 ~ "-" { printf "s/resource \"%s\" \"%s\"/resource \"%s\" \"%s\"/g\n", $2, $4, $2, gensub("-", "_", "g", $4) }'
This produces a sed
script that looks like the following
s/resource "azurerm_key_vault" "team-a-secrets"/resource "azurerm_key_vault" "team_a_secrets"/g
And references to the resources
Just changing the resource names isn’t much use if there are references to those resources in other areas of the terraform code for example
resource "azurerm_key_vault_secret" "db_password" {
key_vault_id = azurerm_key_vault.team-a-secrets.id
...
}
These can also be changed by extending the sed script
awk \
--field-separator=\" \
'$4 ~ "-" { printf "s/%s\\.%s/%s.%s/g\n", $2, $4, $2, gensub("-", "_", "g", $4) }'
Which generates the following line
s/azurerm_key_vault\.team-a-secrets/azurerm_key_vault.team_a_secrets/g
Putting it all together
These last two commands need to be repeated for data sources and modules too.
Then we can run the sed
script performing an inline replacement for all
terraform files. Finally we can move the refactoring.tf file to the correct
location.
All of this together looks like this;
#!/usr/bin/env bash
set -e
SEARCH_FOLDER=./prod/base-infra/
# Generate moved blocks for resources
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
| \
xargs \
--null \
grep '^resource "' \
| \
awk \
--field-separator=\" \
'$4 ~ "-" { printf "moved {\n from = %s.%s\n to = %s.%s\n}\n\n", $2, $4, $2, gensub("-", "_", "g", $4) }' \
> \
$SEARCH_FOLDER/refactoring.tf-soon
# Generate moved blocks for data sources
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
| \
xargs \
--null \
grep '^data "' \
| \
awk \
--field-separator=\" \
'$4 ~ "-" { printf "moved {\n from = data.%s.%s\n to = data.%s.%s\n}\n\n", $2, $4, $2, gensub("-", "_", "g", $4) }' \
>> \
$SEARCH_FOLDER/refactoring.tf-soon
# Generate moved blocks for modules
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
|
xargs \
--null \
grep '^module "' \
| \
awk \
--field-separator=\" \
'$2 ~ "-" { printf "moved {\n from = module.%s\n to = module.%s\n}\n\n", $2, gensub("-", "_", "g", $2) }' \
>> \
$SEARCH_FOLDER/refactoring.tf-soon
# Generate sed for resource usage
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
| \
xargs \
--null \
grep '^resource "' \
| \
awk \
--field-separator=\" \
'$4 ~ "-" { printf "s/%s\\.%s/%s.%s/g\n", $2, $4, $2, gensub("-", "_", "g", $4) }' \
> \
$SEARCH_FOLDER/refactoring.sed
# Generate sed for resource definition
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
| \
xargs \
--null \
grep '^resource "' \
| \
awk \
--field-separator=\" \
'$4 ~ "-" { printf "s/resource \"%s\" \"%s\"/resource \"%s\" \"%s\"/g\n", $2, $4, $2, gensub("-", "_", "g", $4) }' \
>> \
$SEARCH_FOLDER/refactoring.sed
# Generate sed for data source usage
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
| \
xargs \
--null \
grep '^data "' \
| \
awk \
--field-separator=\" \
'$4 ~ "-" { printf "s/data\\.%s\\.%s/data.%s.%s/g\n", $2, $4, $2, gensub("-", "_", "g", $4) }' \
>> \
$SEARCH_FOLDER/refactoring.sed
# Generate sed for data source definition
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
| \
xargs \
--null \
grep '^data "' \
| \
awk \
--field-separator=\" \
'$4 ~ "-" { printf "s/data \"%s\" \"%s\"/data \"%s\" \"%s\"/g\n", $2, $4, $2, gensub("-", "_", "g", $4) }' \
>> \
$SEARCH_FOLDER/refactoring.sed
# Generate sed for module usage
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
| \
xargs \
--null \
grep '^module "' \
| \
awk \
--field-separator=\" \
'$2 ~ "-" { printf "s/module\\.%s/module.%s/g\n", $2, $4, $2, gensub("-", "_", "g", $4) }' \
>> \
$SEARCH_FOLDER/refactoring.sed
# Generate sed for module definition
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
| \
xargs \
--null \
grep '^module "' \
| \
awk \
--field-separator=\" \
'$2 ~ "-" { printf "s/module \"%s\"/module \"%s\"/g\n", $2, gensub("-", "_", "g", $2) }' \
>> \
$SEARCH_FOLDER/refactoring.sed
# Apply sed script
find \
$SEARCH_FOLDER \
-name "*.tf" \
-print0 \
| \
xargs \
--null
sed -i -f $SEARCH_FOLDER/refactoring.sed
# Rename refactoring.tf to correct name
mv \
$SEARCH_FOLDER/refactoring.tf-soon \
$SEARCH_FOLDER/refactoring.tf
I’m sure this script could be optimised or written prettier but it was a one-time job and worked well enough for what I needed.