Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Unable to "state mv" to a for_each resource #22301

Closed
jkburges opened this issue Aug 2, 2019 · 27 comments · Fixed by #23582
Closed

Unable to "state mv" to a for_each resource #22301

jkburges opened this issue Aug 2, 2019 · 27 comments · Fixed by #23582
Labels
bug cli v0.12 Issues (primarily bugs) reported against v0.12 releases

Comments

@jkburges
Copy link
Contributor

jkburges commented Aug 2, 2019

It doesn't seem possible to migrate existing code to use for_each without having to destroy/create resources.

Terraform Version

Terraform v0.12.6
+ provider.aws v2.22.0

Expected Behaviour

I should be able to move a resource to a for_each name to avoid having to destroy/create it.

Actual Behaviour

Trying to state mv an existing (non for_each) resource, e.g.:

$ terraform state mv aws_instance.web-bar 'aws_instance.web["bar"]'
Move "aws_instance.web-bar" to "aws_instance.web[\"bar\"]"

Error: Invalid target address

Cannot move to aws_instance.web["bar"]: aws_instance.web does not exist in the
current state.

Terraform Configuration Files

Version 1:

provider "aws" {
  region = "us-west-2"
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

resource "aws_instance" "web-bar" {
  ami           = "${data.aws_ami.ubuntu.id}"
  instance_type = "t2.micro"

  tags = {
    Name = "bar"
  }
}

resource "aws_instance" "web-foo" {
  ami           = "${data.aws_ami.ubuntu.id}"
  instance_type = "t2.micro"

  tags = {
    Name = "foo"
  }
}

Version 2 (converting to for_each format):

provider "aws" {
  region = "us-west-2"
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

variable "instance_names" {
  default = [
    "foo",
    "bar"
  ]
}

resource "aws_instance" "web" {
  for_each      = toset(var.instance_names)
  ami           = "${data.aws_ami.ubuntu.id}"
  instance_type = "t2.micro"

  tags = {
    Name = each.value
  }
}

Steps to Reproduce

  1. terraform init
  2. terraform apply # with <version 1 from above>
  3. terraform plan # with <version 2 from above>

At this point, we have some destroys and creates for EC2 instances:

  # aws_instance.web["bar"] will be created
  + resource "aws_instance" "web" {
  <snip>

  # aws_instance.web["foo"] will be created
  + resource "aws_instance" "web" {
  <snip>

  # aws_instance.web-bar will be destroyed
  - resource "aws_instance" "web-bar" {
  <snip>

  # aws_instance.web-foo will be destroyed
  - resource "aws_instance" "web-foo" {
  <snip>

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

So, we try to do a terraform state mv as above, to avoid the planned changes.

@teamterraform
Copy link
Contributor

Thank you for reporting this bug @jkburges !

We get an interesting different error when wrapping the resource in single quotes:

# terraform state mv random_pet.zoo[0] 'random_pet.for_each_zoo["linus"]'
Move "random_pet.zoo[0]" to "random_pet.for_each_zoo[\"linus\"]"

Error: Invalid target address

Cannot move to random_pet.for_each_zoo["linus"]: random_pet.for_each_zoo does
not exist in the current state.

Here is a configuration one can use to reproduce this without creating resources that cost money - just comment out the for_each resource for the first apply:

variable "list" {
  default = ["linus", "cheetarah", "li-shou"]
}

resource random_pet "zoo" {
  count = 3
  prefix = var.list[count.index]
}

// comment this out for the first apply, then try to state mv
resource random_pet "for_each_zoo" {
  for_each = toset(var.list)
  prefix = each.key
}

This might be a red herring, but it's worth looking at addrs.ParseTargetStr to make sure addrs properly parses random_pet.for_each_zoo["linus"]

@Tenzer
Copy link

Tenzer commented Aug 2, 2019

I think there are more problems around this than just rename resources in the state file. From what I've seen you can't remove resources in the state file either, and it's not possible to terraform import any resource you want to manage with for_each.

Here's the output I get when I try to remove a resource from a state:

$ terraform state rm github_team_repository.developers["babaganush"]

Error: Index value required

  on  line 1:
  (source code not available)

Index brackets must contain either a literal number or a literal string.

@teamterraform
Copy link
Contributor

Hi @Tenzer! I think you are running into something different - how different shells escape quotes.
If you are using bash, wrapping the resource in a single quotes will help (using the same code example from above):

terraform state rm 'random_pet.for_each_zoo["cheetarah"]'
Removed random_pet.for_each_zoo["cheetarah"]
Successfully removed 1 resource instance(s).

@Tenzer
Copy link

Tenzer commented Aug 2, 2019

Oh, that's interesting. I tried random_pet.for_each_zoo[cheetarah] and random_pet.for_each_zoo.cheetarah but didn't think to wrap the entire identifier.

I wonder if that's something that can be detected by Terraform so it can provide more helpful information? That probably belongs in another bug report though.

@jkburges
Copy link
Contributor Author

jkburges commented Aug 3, 2019

Hi @teamterraform,

We get an interesting different error when wrapping the resource in single quotes:

I am using single quotes, also - but your error looks the same to me apart from the names?

Your example differs from mine (aside from the resource type) in that you've used count = n whereas in mine, I've explicitly defined multiple instances of the resource (this is required in my "real" code due to different configuration between each resource instance).

@michalschott
Copy link

michalschott commented Aug 12, 2019

$ terraform-0.12.6 state mv module.XXX.github_repository_collaborator.admin 'module.XXX.github_repository_collaborator.admin["michalschott"]'

Error: Invalid target address

Cannot move module.street-manager-api.github_repository_collaborator.admin to
<invalid>..: the target must also be a whole resource.

@haodeon
Copy link

haodeon commented Aug 14, 2019

I am able to move resources within my state file, converting from count to for_each.

$ terraform state mv "azurerm_storage_container.infinity[1]" 'azurerm_storage_container.infinity["backups"]'
Move "azurerm_storage_container.infinity[1]" to "azurerm_storage_container.infinity[\"backups\"]"
Successfully moved 1 object(s).

@jkburges
Copy link
Contributor Author

I am able to move resources within my state file, converting from count to for_each.

I think this is a different scenario to what I describe. I am trying to move two explicitly/separately defined resources to a for_each. Same as with #22301 (comment)

@devblackops
Copy link

Hi @Tenzer! I think you are running into something different - how different shells escape quotes.
If you are using bash, wrapping the resource in a single quotes will help (using the same code example from above):

terraform state rm 'random_pet.for_each_zoo["cheetarah"]'
Removed random_pet.for_each_zoo["cheetarah"]
Successfully removed 1 resource instance(s).

When importing state, wrapping with single quotes isn't working with PowerShell. Escaping the double quotes with a backtick (how you'd normally escape things in pwsh) also doesn't work.

What would be the correct syntax in that scenario?

terraform import 'azurerm_role_assignment.owners["foo-group"]' /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleAssignments/00000000-0000-0000-0000-000000000000

Error: Index value required

  on <import-address> line 1:
   1: azurerm_role_assignment.owners[foo-group]

Index brackets must contain either a literal number or a literal string.
For information on valid syntax, see:


terraform import 'azurerm_role_assignment.owners[`"foo-group`"]' /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleAssignments/00000000-0000-0000-0000-000000000000

Error: Index value required

  on <import-address> line 1:
   1: azurerm_role_assignment.owners[foo-group]

@bflad
Copy link
Contributor

bflad commented Aug 15, 2019

@devblackops you might want to give the following syntax a try with PowerShell (since it requires extra escaping):

terraform import 'azurerm_role_assignment.owners[\"foo-group\"]' /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleAssignments/00000000-0000-0000-0000-000000000000

The documentation will be updated shortly to reflect this: #22318

For any further questions, it might be best to open a topic on the community forum so this bug report can stay on topic. 👍

@bastiandg
Copy link

Related Issue: #22375

There seems to be an oddity when using lists with only one element, as in the initial example. This doesn't happen when there are multiple list elements (see #22375 (comment)). I posted a dirty workaround for that in the related issue (#22375 (comment)). It adds the missing index with jq.

@hashibot hashibot added the v0.12 Issues (primarily bugs) reported against v0.12 releases label Aug 28, 2019
@pdecat
Copy link
Contributor

pdecat commented Sep 5, 2019

I'm facing the Invalid target address error reported in #22301 (comment) where the target resource does not exist in the current state, e.g.:

# terraform state mv -state terraform.tfstate 'module.pagerduty_team1.pagerduty_escalation_policy.bh' 'module.pagerduty.pagerduty_escalation_policy.bh["team1"]'
Move "module.pagerduty_team1.pagerduty_escalation_policy.bh" to "module.pagerduty.pagerduty_escalation_policy.bh[\"team1\"]"

Error: Invalid target address

Cannot move to module.pagerduty.pagerduty_escalation_policy.bh["team1"]:
module.pagerduty.pagerduty_escalation_policy.bh does not exist in the current
state.

Apparently, this is intentional:

If we're moving to an address without an index then that suggests the user's intent is to establish both the resource and the instance at the same time (since the address covers both), but if there's an index in the target then the resource must already exist.

fmt.Sprintf("Cannot move to %s: %s does not exist in the current state.", addrTo, addrTo.ContainingResource()),

Is there any hope to get that changed, e.g. create the target resource with an index in the state even if it is the first instance at that resource address?

For context, this prevents me from performing a large refactoring (> 400 resources) when moving from resources definitions (using count or not) to resource definitions using for_each.
A work-around is to do terraform state rm $SRC and terraform import $DST $ID for the first item but that only works for resources supporting import and takes much more time than a simple state mv as remote API calls are involved.
Also, terraform import does not currently respect -state when a remote backend is configured: #8014

Update: another work-around to allow the terraform state mv is to alter the state by creating the target resource with an empty instances list, e.g.:

    {
      "module": "module.pagerduty",
      "mode": "managed",
      "type": "pagerduty_escalation_policy",
      "name": "bh",
      "each": "map",
      "provider": "provider.pagerduty",
      "instances": []
    },
# terraform state mv -state terraform.tfstate 'module.pagerduty_team1.pagerduty_escalation_policy.bh' 'module.pagerduty.pagerduty_escalation_policy.bh["team1"]'
Move "module.pagerduty_team1.pagerduty_escalation_policy.bh" to "module.pagerduty.pagerduty_escalation_policy.bh[\"team1\"]"
Successfully moved 1 object(s).

@ricohomewood
Copy link

@teamterraform Is there an update on a likely fix for this issue? It looks like it's not just a for_each but any count index moving into the state for the first time as @pdecat suggests.
This used to work in terraform v11 but makes it very difficult to refactor if moving to a new resource or module in the state that is the first entry in an index. Thanks

@tmccombs
Copy link
Contributor

If we're moving to an address without an index then that suggests the user's intent is to establish both the resource and the instance at the same time (since the address covers both)

that means for count you can just leave the index off for the first one. But for a for_each there isn't any way to do this, since there is no "first" item.

@cha7ri
Copy link

cha7ri commented Oct 8, 2019

We are currently refactoring some modules to use for_each instead of count, and we are facing the same issue:
state mv 'aws_subnet.public_az_a[0]' 'aws_subnet.subnets["public_1_az_a"]' :

Move "aws_subnet.public_az_a[1]" to "aws_subnet.subnets[\"public_1_az_a\"]"

Error: Invalid target address

Cannot move to aws_subnet.subnets["public_1_az_a"]: aws_subnet.subnets does
not exist in the current state. 

@teamterraform Any update on this issue, It make it really difficult to upgrade to TF 0.12 and to refactor the code.

The workaround that I found is:

  • Pull the state
  • Take a backup in case of something went wrong.
  • Edit the state and add a list of subnets with one element in it like this:
{
      "mode": "managed",
      "type": "aws_subnet",
      "name": "subnets",
      "each": "list",
      "provider": "provider.aws",
      "instances": [
        {
          "index_key": "public_1_az_a",
          "schema_version": 1,
          "attributes": {
        ...
       }
     ]
}
  • Push the state

Now we will be able to other subnets using terraform mv command. e.g in my case:

terraform state mv 'aws_subnet.public_az_a[1]' 'aws_subnet.subnets["public_2_az_a"]' 

@psalaberria002
Copy link

Just bumped into this one :(

@morgante
Copy link

This is a major issue which basically blocks migrating to for_each for existing resources. Can we get an idea of how to workaround this?

@tmccombs
Copy link
Contributor

The best workaround I've been able to find is @pdecat's hack of manually editing the state to add the resource object with an empty "instances"

@bastiandg
Copy link

@morgante I posted a workaround in the related issue #22375 .

Here is the current version of the script we are using to handle such cases. It uses jq to add the missing index, which you provide as the second parameter.

# Example call: cat state.tfstate | ./move-single-element-tf012.sh "module.k8s.google_container_node_pool.pool" "pool1" > newstate.tfstate

resource_id="$1"
index_key="$2"
module="$(cut -d '.' -f2 <<< "$resource_id" )"
resource_type="$(cut -d '.' -f3 <<< "$resource_id" )"
resource_name="$(cut -d '.' -f4 <<< "$resource_id" )"

jq -M ".resources[
		.resources | map(.type == \"${resource_type}\" and .module == \"module.${module}\" and .name == \"$resource_name\") | index(true)
	].instances[0] +=
	{\"index_key\": \"$index_key\"}" < /dev/stdin

As we are only using resources from modules it only works with resources located in modules. But it should be easy enough to make it work for other structures as well.

@morganchristiansson
Copy link

morganchristiansson commented Oct 28, 2019

Another workaround is to add a dummy element to for_each and run terraform apply -target 'aws_subnet.subnets["dummy"]'

This creates a aws_subnet.subnets[] resource and terraform state mv now works

edit: another workaround is terraform state rm ; terraform import

@pexaorj
Copy link

pexaorj commented Nov 7, 2019

Same here:

terraform state list
aws_iam_user.api-users["api-test"]

terraform show
# aws_iam_user.api-users["api-test"]:
resource "aws_iam_user" "api-test" {
    arn           = "arn:aws:iam::000000000000:user/api-test"
    force_destroy = false
    id            = "api-test"
    name          = "api-test"
    path          = "/"
    unique_id     = "AIDAT5X3NZWUZBCRJAFYE"
}


terraform state rm 'aws_iam_user.api-users["test"]'
Removed aws_iam_user.api-users["test"]
Successfully removed 1 resource instance(s).

terraform import  'aws_iam_user.api-users["test"]'
The import command expects two arguments.
Usage: terraform import [options] ADDR ID

terraform import aws_iam_user.api-users["api-test"]
The import command expects two arguments.
Usage: terraform import [options] ADDR ID

The destroy command also works:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_iam_user.api-users["api-test"] will be destroyed
  - resource "aws_iam_user" "api-users" {
      - arn           = "arn:aws:iam::0000000000000:user/api-test" -> null
      - force_destroy = false -> null
      - id            = "api-test" -> null
      - name          = "api-test" -> null
      - path          = "/" -> null
      - tags          = {} -> null
      - unique_id     = "AIDAT5X3NZWU75XKNFBMO" -> null
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_iam_user.api-users["api-test"]: Destroying... [id=api-test]
aws_iam_user.api-users["api-test"]: Destruction complete after 1s

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

Terraform file:

variable "api_user_names" {
  description = "Map api-name users"
  default = {
    api-test                 = "api-test"
  }
}

resource "aws_iam_user" "api-users" {
  for_each = var.api_user_names
  name     = each.value
}

So it is possible to create users and delete from state, but if I need to import then, it's impossible, causing a lot of problems.

terraform -v
Terraform v0.12.13
+ provider.aws v2.34.0

@tabacco
Copy link

tabacco commented Nov 15, 2019

Another workaround is to add a dummy element to for_each and run terraform apply -target 'aws_subnet.subnets["dummy"]'

This creates a aws_subnet.subnets[] resource and terraform state mv now works

This is definitely the easiest workaround that doesn't involve editing the state directly and doesn't destroy the resource.

@MetricMike
Copy link

You can also move the entire collection:

terraform state list
aws_route53_record.ipv4["dev"]
aws_route53_record.ipv4["test"]

terraform state mv aws_route53_record.ipv4 aws_route53_record.ipv6

terraform state list
aws_route53_record.ipv6["dev"]
aws_route53_record.ipv6["test"]

@tmccombs
Copy link
Contributor

Assuming you are moving a for_each collection and not an individual item, or a count collection.

@taylorludwig
Copy link

Was able to find a somewhat simple workaround for going from a count to for_each in different modules.

Takes two mv steps.

First, as @MetricMike suggested, move the whole collection (works even though its from count's integer key to for_each string key)

terraform state mv 'resource.collection_name' 'module.example.resource.collection_name'

Then, move each integer index to the string index (notice it's already in the desired module, just the wrong key)

terraform state mv 'module.example.resource.collection_name[0]' 'module.example.resource.collection_name["first"]

terraform state mv 'module.example.resource.collection_name[1]' 'module.example.resource.collection_name["second"]

@dantman
Copy link

dantman commented Nov 27, 2019

Same problem. This makes converting a bunch of individual imported CloudFlare dns entries into a simple for_each a pain.

Additionally because the only way to make resources conditional is count = var.enabled ? 1 : 0 this means you have to move to a [0] resource which triggers this bug again.

@ghost
Copy link

ghost commented Mar 28, 2020

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@ghost ghost locked and limited conversation to collaborators Mar 28, 2020
# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
bug cli v0.12 Issues (primarily bugs) reported against v0.12 releases
Projects
None yet
Development

Successfully merging a pull request may close this issue.