I need to rename a terraform resource created with a for_each
over a loop. I can just change the terraform and apply, which results in a destroy/create. Not terrible in this case, but I want something better just by changing state. Best of all, I want to script this.
Situation
My resource needs to go from:
resource "aws_ssm_parameter" "ecs_api_parameter" {
for_each = var.ecs_api_parameters
name = each.key
type = "String"
value = each.value
}
To:
resource "aws_ssm_parameter" "ecs_parameter" {
for_each = var.ecs_parameters
name = each.key
type = "String"
value = each.value
}
Terraform plan tells me it’ll be destroy/create –
$ terraform plan
# aws_ssm_parameter.ecs_api_parameter["one"] will be destroyed
# (because aws_ssm_parameter.ecs_api_parameter is not in configuration)
- resource "aws_ssm_parameter" "ecs_api_parameter" {
<snip>
# aws_ssm_parameter.ecs_parameter["one"] will be created
+ resource "aws_ssm_parameter" "ecs_parameter" {
<snip>
Plan: 4 to add, 0 to change, 4 to destroy.
That would be okay in this case – these are just parameters, there’s no interesting history I want to keep, there’s no impact at all. There will be times you don’t want to do this – that DynamoDB table may well have a backup, but if you don’t have to restore it then life’s easier.
Instead we can just tell terraform the resources have changed their names, and we do this by working on the state file.
Changing resources in state
We cam work out what needs to happen just be looking at the code, but inspecting the plan output is rather helpful. We can see that we have these resources:
aws_ssm_parameter.ecs_api_parameter["one"]
aws_ssm_parameter.ecs_api_parameter["two"]
aws_ssm_parameter.ecs_api_parameter["ninety-nine"]
aws_ssm_parameter.ecs_api_parameter["5"]
Which need to become these:
aws_ssm_parameter.ecs_parameter["one"]
aws_ssm_parameter.ecs_parameter["two"]
aws_ssm_parameter.ecs_parameter["ninety-nine"]
aws_ssm_parameter.ecs_parameter["5"]
The terraform state mv
command lets us do this easily enough. The basic solution is going to be along the lines of:
terraform state mv aws_ssm_parameter.ecs_api_parameter["one"] aws_ssm_parameter.ecs_parameter["one"]
terraform state mv aws_ssm_parameter.ecs_api_parameter["two"] aws_ssm_parameter.ecs_parameter["two"]
terraform state mv aws_ssm_parameter.ecs_api_parameter["ninety-nine"] aws_ssm_parameter.ecs_parameter["ninety-nine"]
terraform state mv aws_ssm_parameter.ecs_api_parameter["5"] aws_ssm_parameter.ecs_parameter["5"]
Four resources, I can do some copy and paste, it’s easy. The thing is there could be an awful lot more than four, and manual stuff can easily go wrong. Okay, scripting can too, but bear with me here.
Scripting this
I start with a dead basic one-liner to iterate over the output of terraform state list
, grepping for the resource type, saying what I’ve found:
$ for param in terraform state list | grep aws_ssm_parameter.ecs_api_parameter; do echo $param ; done
aws_ssm_parameter.ecs_api_parameter["one"]
aws_ssm_parameter.ecs_api_parameter["two"]
aws_ssm_parameter.ecs_api_parameter["ninety-nine"]
aws_ssm_parameter.ecs_api_parameter["5"]
Next I want to get just that string so I can construct my terraform state mv
command, I’ll use cut
here to only give me what’s between the quotes:
$ for param in terraform state list | grep aws_ssm_parameter.ecs_api_parameter | cut -d '"' -f 2 ; do echo $param ; done
one
two
ninety-nine
5
Now to test I’ll try a terraform state show
– something totally non-destructive. I need to escape the quotes in the terragrunt command or it doesn’t work. Try it and see…
$ for param in terraform state list | grep aws_ssm_parameter.ecs_api_parameter | cut -d '"' -f 2 ; do terraform state show aws_ssm_parameter.ecs_api_parameter[\"$param\"] ; done
# aws_ssm_parameter.ecs_api_parameter["one"]:
resource "aws_ssm_parameter" "ecs_api_parameter" {
arn = "arn:aws:ssm:eu-west-2:xxx:parameter/one"
data_type = "text"
id = "/one"
name = "/one"
<snip>
Then we can switch out the show
with a mv
to try the terraform state mv
command – but I’m going to use the -dry-run
option because I’m a coward:
$ for param in terraform state list | grep aws_ssm_parameter.ecs_api_parameter | cut -d '"' -f 2 ; do terraform state mv -dry-run aws_ssm_parameter.ecs_api_parameter[\"$param\"] aws_ssm_parameter.ecs_parameter[\"$param\"]; done
Would move "aws_ssm_parameter.ecs_api_parameter[\"one\"]" to "aws_ssm_parameter.ecs_parameter[\"one\"]"
Would move "aws_ssm_parameter.ecs_api_parameter[\"two\"]" to "aws_ssm_parameter.ecs_parameter[\"two\"]"
Would move "aws_ssm_parameter.ecs_api_parameter[\"ninety-nine\"]" to "aws_ssm_parameter.ecs_parameter[\"ninety-nine\"]"
Would move "aws_ssm_parameter.ecs_api_parameter[\"5\"]" to "aws_ssm_parameter.ecs_parameter[\"5\"]"
If I were being a real coward I’d change my loop more to pick a single resource before going for it, but I’m happy with this.
$ for param in `terraform state list | grep aws_ssm_parameter.ecs_api_parameter | cut -d '"' -f 2` ; do terraform state mv aws_ssm_parameter.ecs_api_parameter[\"$param\"] aws_ssm_parameter.ecs_parameter[\"$param\"]; done
Move "aws_ssm_parameter.ecs_api_parameter[\"one\"]" to "aws_ssm_parameter.ecs_parameter[\"one\"]"
Successfully moved 1 object(s).
Move "aws_ssm_parameter.ecs_api_parameter[\"two\"]" to "aws_ssm_parameter.ecs_parameter[\"two\"]"
Successfully moved 1 object(s).
Move "aws_ssm_parameter.ecs_api_parameter[\"ninety-nine\"]" to "aws_ssm_parameter.ecs_parameter[\"ninety-nine\"]"
Successfully moved 1 object(s).
Move "aws_ssm_parameter.ecs_api_parameter[\"5\"]" to "aws_ssm_parameter.ecs_parameter[\"5\"]"
Successfully moved 1 object(s).
And now if I try a plan?
$ terraform plan
aws_ssm_parameter.ecs_parameter["one"]: Refreshing state... [id=/one]
aws_ssm_parameter.ecs_parameter["one"]: Refreshing state... [id=/two]
aws_ssm_parameter.ecs_parameter["ninety-nine"]: Refreshing state... [id=/ninety-nine]
aws_ssm_parameter.ecs_parameter["5"]: Refreshing state... [id=/5]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
So yes, it worked!