Giter VIP home page Giter VIP logo

tfmigrate's Introduction

tfmigrate

License: MIT GitHub release GoDoc

A Terraform / OpenTofu state migration tool for GitOps.

Table of content

Features

  • GitOps friendly: Write terraform state mv/rm/import commands in HCL, plan and apply it.
  • Monorepo style support: Move resources to other tfstates to split and merge easily for refactoring.
  • Dry run migration: Simulate state operations with a temporary local tfstate and check to see if terraform plan has no changes after the migration without updating remote tfstate.
  • Migration history: Keep track of which migrations have been applied and apply all unapplied migrations in sequence.

You can apply terraform state operations in a declarative way.

In short, write the following migration file and save it as state_mv.hcl:

migration "state" "test" {
  dir = "dir1"
  actions = [
    "mv aws_security_group.foo aws_security_group.foo2",
    "mv aws_security_group.bar aws_security_group.bar2",
  ]
}

Then, apply it:

$ tfmigrate apply state_mv.hcl

It works as you expect, but it's just a text file, so you can commit it to git.

Why?

If you have been using Terraform in production for a long time, tfstate manipulations are unavoidable for various reasons. As you know, the terraform state command is your friend, but it's error-prone and not suitable for a GitOps workflow.

In team development, Terraform configurations are generally managed by git and states are shared via remote state storage which is outside of version control. It's a best practice for Terraform. However, most Terraform refactorings require not only configuration changes, but also state operations such as state mv/rm/import. It's not desirable to change the remote state before merging configuration changes. Your colleagues may be working on something else and your CI/CD pipeline continuously plan and apply their changes automatically. At the same time, you probably want to check to see if terraform plan has no changes after the migration before merging configuration changes.

To fit into the GitOps workflow, the answer is obvious. We should commit all terraform state operations to git. This brings us to a new paradigm, that is to say, Terraform state operation as Code!

Requirements

The tfmigrate invokes terraform or tofu command under the hood. This is because we want to support multiple Terraform / OpenTofu versions in a stable way.

Terraform

The minimum required version is Terraform v0.12 or higher, but we recommend the Terraform v1.x.

OpenTofu

If you want to use OpenTofu, a community fork of Terraform, you need to set the environment variable TFMIGRATE_EXEC_PATH to tofu.

The minimum required version is OpenTofu v1.6 or higher.

Getting Started

As you know, terraform state operations are dangerous if you don't understand what you are actually doing. If I were you, I wouldn't use a new tool in production from the start. So, we recommend you to play an example sandbox environment first, which is safe to run terraform state command without any credentials. The sandbox environment mocks the AWS API with localstack and doesn't actually create any resources. So you can safely run the tfmigrate and terraform commands, and easily understand how the tfmigrate works.

Build a sandbox environment with docker-compose and run bash:

$ git clone https://github.com/minamijoyo/tfmigrate
$ cd tfmigrate/
$ docker-compose build
$ docker-compose run --rm tfmigrate /bin/bash

In the sandbox environment, create and initialize a working directory from test fixtures:

# mkdir -p tmp && cp -pr test-fixtures/backend_s3 tmp/dir1 && cd tmp/dir1
# terraform init
# cat main.tf

This example contains two aws_security_group resources:

resource "aws_security_group" "foo" {
  name = "foo"
}

resource "aws_security_group" "bar" {
  name = "bar"
}

Apply it and confirm that the state of resources are stored in the tfstate:

# terraform apply -auto-approve
# terraform state list
aws_security_group.bar
aws_security_group.foo

Now, let's rename aws_security_group.foo to aws_security_group.baz:

# cat << EOF > main.tf
resource "aws_security_group" "baz" {
  name = "foo"
}

resource "aws_security_group" "bar" {
  name = "bar"
}
EOF

At this point, of course, there are differences in the plan:

# terraform plan
(snip.)
Plan: 1 to add, 0 to change, 1 to destroy.

Now it's time for tfmigrate. Create a migration file:

# cat << EOF > tfmigrate_test.hcl
migration "state" "test" {
  actions = [
    "mv aws_security_group.foo aws_security_group.baz",
  ]
}
EOF

Run tfmigrate plan to check to see if terraform plan has no changes after the migration without updating remote tfstate:

# tfmigrate plan tfmigrate_test.hcl
(snip.)
YYYY/MM/DD hh:mm:ss [INFO] [migrator] state migrator plan success!
# echo $?
0

The plan command computes a new state by applying state migration operations to a temporary state. It will fail if terraform plan detects any diffs with the new state. If you are wondering how the tfmigrate command actually works, you can see all terraform commands executed by the tfmigrate with log level DEBUG:

# TFMIGRATE_LOG=DEBUG tfmigrate plan tfmigrate_test.hcl

If looks good, apply it:

# tfmigrate apply tfmigrate_test.hcl
(snip.)
YYYY/MM/DD hh:mm:ss [INFO] [migrator] state migrator apply success!
# echo $?
0

The apply command computes a new state and pushes it to remote state. It will fail if terraform plan detects any diffs with the new state.

You can confirm the latest remote state has no changes with terraform plan:

# terraform plan
(snip.)
No changes. Infrastructure is up-to-date.

# terraform state list
aws_security_group.bar
aws_security_group.baz

There is no magic. The tfmigrate just did the boring work for you.

Furthermore, you can also move resources to another directory. Let's split the tfstate in two. Create a new empty directory with a different remote state path:

# mkdir dir2
# cat config.tf | sed 's/test\/terraform.tfstate/dir2\/terraform.tfstate/' > dir2/config.tf

Move the resource definition of aws_security_group.baz in main.tf to dir2/main.tf and rename it to aws_security_group.baz2:

# cat << EOF > main.tf
resource "aws_security_group" "bar" {
  name = "bar"
}
EOF

# cat << EOF > dir2/main.tf
resource "aws_security_group" "baz2" {
  name = "foo"
}
EOF

Create a multi_state migration file:

# cat << EOF > tfmigrate_multi_state_test.hcl
migration "multi_state" "test" {
  from_dir = "."
  to_dir   = "dir2"

  actions = [
    "mv aws_security_group.baz aws_security_group.baz2",
  ]
}
EOF

Run tfmigrate plan & apply:

# tfmigrate plan tfmigrate_multi_state_test.hcl
# tfmigrate apply tfmigrate_multi_state_test.hcl

You can see the tfstate was split in two:

# terraform state list
aws_security_group.bar
# cd dir2 && terraform state list
aws_security_group.baz2

Install

Homebrew

If you are macOS user:

$ brew install tfmigrate

Download

Download the latest compiled binaries and put it anywhere in your executable path.

https://github.com/minamijoyo/tfmigrate/releases

Source

If you have Go 1.22+ development environment:

$ git clone https://github.com/minamijoyo/tfmigrate
$ cd tfmigrate/
$ make install
$ tfmigrate --version

Usage

$ tfmigrate --help
Usage: tfmigrate [--version] [--help] <command> [<args>]

Available commands are:
    apply    Compute a new state and push it to remote state
    list     List migrations
    plan     Compute a new state
$ tfmigrate plan --help
Usage: tfmigrate plan [PATH]

Plan computes a new state by applying state migration operations to a temporary state.
It will fail if terraform plan detects any diffs with the new state.

Arguments:
  PATH                     A path of migration file
                           Required in non-history mode. Optional in history-mode.

Options:
  --config                 A path to tfmigrate config file
  --backend-config=path    A backend configuration, a path to backend configuration file or
                           key=value format backend configuraion.
                           This option is passed to terraform init when switching backend to remote.

  --out=path               Save a plan file after dry-run migration to the given path.
                           Note that the saved plan file is not applicable in Terraform 1.1+.
                           It's intended to use only for static analysis.
$ tfmigrate apply --help
Usage: tfmigrate apply [PATH]

Apply computes a new state and pushes it to remote state.
It will fail if terraform plan detects any diffs with the new state.

Arguments
  PATH                     A path of migration file
                           Required in non-history mode. Optional in history-mode.

Options:
  --config                 A path to tfmigrate config file
  --backend-config=path    A backend configuration, a path to backend configuration file or
                           key=value format backend configuraion.
                           This option is passed to terraform init when switching backend to remote.
$ tfmigrate list --help
Usage: tfmigrate list

List migrations.

Options:
  --config           A path to tfmigrate config file
  --status           A filter for migration status
                     Valid values are as follows:
                       - all (default)
                       - unapplied

Configurations

Environment variables

You can customize the behavior by setting environment variables.

  • TFMIGRATE_LOG: A log level. Valid values are TRACE, DEBUG, INFO, WARN, ERROR. Default to INFO.
  • TFMIGRATE_EXEC_PATH: A string how terraform command is executed. Default to terraform. It's intended to inject a wrapper command such as direnv. e.g.) direnv exec . terraform. To use OpenTofu, set this to tofu.

Some history storage implementations may read additional cloud provider-specific environment variables. For details, refer to a configuration file section for storage block described below.

Configuration file

You can customize the behavior by setting a configuration file. The path of configuration file defaults to .tfmigrate.hcl. You can change it with command line flag --config.

The syntax of configuration file is as follows:

  • A configuration file must be written in the HCL2.
  • The extension of file must be .hcl(for HCL native syntax) or .json(for HCL JSON syntax).
  • The file must contain exactly one tfmigrate block.

An example of configuration file is as follows.

tfmigrate {
  migration_dir = "./tfmigrate"
  is_backend_terraform_cloud = true
  history {
    storage "s3" {
      bucket = "tfmigrate-test"
      key    = "tfmigrate/history.json"
    }
  }
}

is_backend_terraform_cloud

Whether the remote backend specified in Terraform files references a terraform cloud remote backend, in particular specified as a cloud block within the terraform config block. This backend type was introduced in Terraform 1.1.+ and is the recommended way to specify a Terraform backend. Attribute defaults to false.

Note that when using tfmigrate with Terraform Cloud, you also need to set a workspace name in a migration file.

tfmigrate block

The tfmigrate block has the following attributes:

  • migration_dir (optional): A path to directory where migration files are stored. Default to . (current directory).

The tfmigrate block has the following blocks:

  • history (optional): Keep track of which migrations have been applied.

history block

The history block has the following blocks:

  • storage (required): A migration history data store

storage block

The storage block has one label, which is a type of storage. Valid types are as follows:

  • local: Save a history file to local filesystem.
  • s3: Save a history file to AWS S3.
  • gcs: Save a history file to GCS (Google Cloud Storage).

If your cloud provider has not been supported yet, as a workaround, you can use local storage and synchronize a history file to your cloud storage with a wrapper script.

storage block (local)

The local storage has the following attributes:

  • path (required): A path to a migration history file.

An example of configuration file is as follows.

tfmigrate {
  migration_dir = "./tfmigrate"
  history {
    storage "local" {
      path = "tmp/history.json"
    }
  }
}

storage block (s3)

The s3 storage has the following attributes:

  • bucket (required): Name of the bucket.
  • key (required): Path to the migration history file.
  • region (optional): AWS region. This can also be sourced from the AWS_DEFAULT_REGION and AWS_REGION environment variables.
  • access_key (optional): AWS access key. This can also be sourced from the AWS_ACCESS_KEY_ID environment variable, AWS shared credentials file, or AWS shared configuration file.
  • secret_key (optional): AWS secret key. This can also be sourced from the AWS_SECRET_ACCESS_KEY environment variable, AWS shared credentials file, or AWS shared configuration file.
  • profile (optional): Name of AWS profile in AWS shared credentials file or AWS shared configuration file to use for credentials and/or configuration. This can also be sourced from the AWS_PROFILE environment variable.
  • role_arn (optional): Amazon Resource Name (ARN) of the IAM Role to assume.
  • kms_key_id (optional): Amazon Server-Side Encryption (SSE) KMS Key Id. When specified, this encryption key will be used and server-side encryption will be enabled. See the terraform s3 backend.

The following attributes are also available, but they are intended to use with localstack for testing.

  • endpoint (optional): Custom endpoint for the AWS S3 API.
  • skip_credentials_validation (optional): Skip credentials validation via the STS API.
  • skip_metadata_api_check (optional): Skip usage of EC2 Metadata API.
  • force_path_style (optional): Enable path-style S3 URLs (https://<HOST>/<BUCKET> instead of https://<BUCKET>.<HOST>).

An example of configuration file is as follows.

tfmigrate {
  migration_dir = "./tfmigrate"
  history {
    storage "s3" {
      bucket  = "tfmigrate-test"
      key     = "tfmigrate/history.json"
      region  = "ap-northeast-1"
      profile = "dev"
    }
  }
}

storage block (gcs)

The gcs storage has the following attributes:

  • bucket (required): Name of the bucket.
  • name (required): Path to the migration history file.

Note that this storage implementation refers the Application Default Credentials (ADC) for authentication.

An example of configuration file is as follows.

tfmigrate {
  migration_dir = "./tfmigrate"
  history {
    storage "gcs" {
      bucket = "tfstate-test"
      name   = "tfmigrate/history.json"
    }
  }
}

If you want to connect to an emulator instead of GCS, set the STORAGE_EMULATOR_HOST environment variable as required by the Go library for GCS.

Migration file

You can write terraform state operations in HCL. The syntax of migration file is as follows:

  • A migration file must be written in the HCL2.
  • The extension of file must be .hcl(for HCL native syntax) or .json(for HCL JSON syntax).

Although the filename can be arbitrary string, note that in history mode unapplied migrations will be applied in alphabetical order by filename. It's possible to use a serial number for a filename (e.g. 123.hcl), but we recommend you to use a timestamp as a prefix to avoid git conflicts (e.g. 20201114000000_dir1.hcl)

An example of migration file is as follows.

migration "state" "test" {
  dir = "dir1"
  actions = [
    "mv aws_security_group.foo aws_security_group.foo2",
    "mv aws_security_group.bar aws_security_group.bar2",
  ]
}

The above example is written in HCL native syntax, but you can also write them in HCL JSON syntax. This is useful when generating a migration file from other tools.

{
  "migration": {
    "state": {
      "test": {
        "dir": "dir1",
        "actions": [
          "mv aws_security_group.foo aws_security_group.foo2",
          "mv aws_security_group.bar aws_security_group.bar2"
        ]
      }
    }
  }
}

If you want to move a resource using for_each, you need to escape as follows:

migration "state" "test" {
  dir = "dir1"
  actions = [
    "mv aws_security_group.foo[0] 'aws_security_group.foo[\"baz\"]'",
  ]
}

Environment Variables

Environment variables can be accessed in migration files via the env variable:

migration "state" "test" {
  dir = "dir1"
  workspace = env.TFMIGRATE_WORKSPACE
  actions = [
    "mv aws_security_group.foo aws_security_group.foo2"
  ]
}

migration block

  • The file must contain exactly one migration block.
  • The first label is the migration type. There are two types of migration block, state and multi_state, and specify one of them.
  • The second label is the migration name, which is an arbitrary string.

The file must contain only one block, and multiple blocks are not allowed, because it's hard to re-run the file if partially failed.

migration block (state)

The state migration updates the state in a single directory. It has the following attributes.

  • dir (optional): A working directory for executing terraform command. Default to . (current directory).
  • workspace (optional): A terraform workspace. Defaults to "default".
  • actions (required): Actions is a list of state action. An action is a plain text for state operation. Valid formats are the following.
    • "mv <source> <destination>"
    • "xmv <source> <destination>"
    • "rm <addresses>...
    • "import <address> <id>"
    • "replace-provider <address> <address>"
  • force (optional): Apply migrations even if plan show changes
  • skip_plan (optional): If true, tfmigrate will not perform and analyze a terraform plan.

Note that dir is relative path to the current working directory where tfmigrate command is invoked.

We could define strict block schema for action, but intentionally use a schema-less string to allow us to easily copy terraform state command to action.

Examples of migration block (state) are as follows.

state mv

migration "state" "test" {
  dir = "dir1"
  actions = [
    "mv aws_security_group.foo aws_security_group.foo2",
    "mv aws_security_group.bar aws_security_group.bar2",
  ]
}

state xmv

The xmv command works like the mv command but allows usage of wildcards * in the source definition. The source expressions will be matched against resources defined in the terraform state. The matched value can be used in the destination definition via a dollar sign and their ordinal number (e.g. $1, $2, ...). When there is ambiguity, you need to put the ordinal number in curly braces, in this case, the dollar sign need to be escaped and therefore are placed twice (e.g. $${1}).

For example if foo and bar in the mv command example above are the only 2 security group resources defined at the top level then you can rename them using:

migration "state" "test" {
  dir = "dir1"
  actions = [
    "xmv aws_security_group.* aws_security_group.$${1}2",
  ]
}

state rm

migration "state" "test" {
  dir = "dir1"
  actions = [
    "rm aws_security_group.baz",
  ]
}

state import

migration "state" "test" {
  dir = "dir1"
  actions = [
    "import aws_security_group.qux qux",
  ]
}

state replace-provider

migration "state" "test" {
  dir = "dir1"
  actions = [
    "replace-provider registry.terraform.io/-/null registry.terraform.io/hashicorp/null",
  ]
}

migration block (multi_state)

The multi_state migration updates states in two different directories. It is intended for moving resources across states. It has the following attributes.

  • from_dir (required): A working directory where states of resources move from.
  • from_skip_plan (optional): If true, tfmigrate will not perform and analyze a terraform plan in the from_dir.
  • from_workspace (optional): A terraform workspace in the FROM directory. Defaults to "default".
  • to_dir (required): A working directory where states of resources move to.
  • to_skip_plan (optional): If true, tfmigrate will not perform and analyze a terraform plan in the to_dir.
  • to_workspace (optional): A terraform workspace in the TO directory. Defaults to "default".
  • actions (required): Actions is a list of multi state action. An action is a plain text for state operation. Valid formats are the following.
    • "mv <source> <destination>"
    • "xmv <source> <destination>"
  • force (optional): Apply migrations even if plan show changes

Note that from_dir and to_dir are relative path to the current working directory where tfmigrate command is invoked.

Example of migration block (multi_state) are as follows.

multi_state mv

migration "multi_state" "mv_dir1_dir2" {
  from_dir = "dir1"
  to_dir   = "dir2"
  actions = [
    "mv aws_security_group.foo aws_security_group.foo2",
    "mv aws_security_group.bar aws_security_group.bar2",
  ]
}

multi_state xmv

The xmv command works like the mv command but allows usage of wildcards * in the source definition. The wildcard expansion rules are the same as for the single state xmv.

migration "multi_state" "mv_dir1_dir2" {
  from_dir = "dir1"
  to_dir   = "dir2"
  actions = [
    "xmv aws_security_group.* aws_security_group.$${1}2",
  ]
}

If you want to move all resources to another dir for merging two tfstates, you can write something like this:

migration "multi_state" "merge_dir1_to_dir2" {
  from_dir = "dir1"
  to_dir   = "dir2"
  actions = [
    "xmv * $1",
  ]
}

Integrations

You can integrate tfmigrate with your favorite CI/CD services. Examples are as follows:

License

MIT

tfmigrate's People

Contributors

bhavanki avatar bouk avatar chenrui333 avatar chrissng avatar davidcardonah avatar dex4er avatar dhaven avatar goodmanben avatar kengotoda avatar lacunoc avatar luckyducky583 avatar mdb avatar minamijoyo avatar perryao avatar pvbouwel avatar rco-ableton avatar syndicut avatar teriu avatar uchida avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

tfmigrate's Issues

tfmirate says state file not found when running in github action.

2021/07/21 16:30:28 [INFO] [migrator] compute new states (./production/b => ./production/a)
2021/07/21 16:30:28 [INFO] [executor@./production/a] remove the override file
2021/07/21 16:30:28 [INFO] [executor@./production/a] switch back to remote
2021/07/21 16:30:30 [INFO] [executor@./production/b] remove the override file
2021/07/21 16:30:30 [INFO] [executor@./production/b] switch back to remote
failed to run command (exited 1): terraform state mv -state=/home/runner/work/_temp/tmp685864232 -state-out=/home/runner/work/_temp/tmp507658855 -backup=/dev/null module.alb module.alb
stdout:

stderr:
No state file was found!

State management commands require a state file. Run this command
in a directory where Terraform has been run or use the -state flag
to point the command to a specific state location.

Both the default tmp dir and github runner tmp dir drop this error.

"multi_state" failed question

Hi thanks for this awesome project! I have a quick question here.

I would like to use the "multi_state" feature but don't know if I config it correctly. this is my tfmigrate_mv_dir1_dir2.hcl file in sandbox env

bash-5.0# cat tfmigrate_mv_dir1_dir2.hcl 
migration "multi_state" "mv_dir1_dir2" {
  from_dir = "/work/tmp/dir1"
  to_dir   = "/work/tmp/dir2"
  actions = [
    "mv aws_security_group.foo aws_security_group.foo2",
    "mv aws_security_group.bar aws_security_group.bar2",
  ]
}

and this is the current state list

bash-5.0# terraform state list
aws_security_group.bar
aws_security_group.foo

/work/tmp/dir2 is a empty directory

bash-5.0# ls -al /work/tmp/dir2
total 0
drwxr-xr-x    2 root     root            64 Oct  9 02:03 .
drwxr-xr-x    4 root     root           128 Oct  8 14:32 ..

What I think multi_state will do for me is

  1. rename the resource
  2. cut from the current tfstate file from dir1 and create a new tfstate file in dir2
    But, instead, this is the result.
bash-5.0# TFMIGRATE_LOG=DEBUG tfmigrate plan tfmigrate_mv_dir1_dir2.hcl
2021/10/09 02:05:31 [DEBUG] [main] start: tfmigrate plan tfmigrate_mv_dir1_dir2.hcl
2021/10/09 02:05:31 [DEBUG] [main] tfmigrate version: 0.2.9
2021/10/09 02:05:31 [DEBUG] [command] config: &config.TfmigrateConfig{MigrationDir:".", History:(*history.Config)(nil)}
2021/10/09 02:05:31 [DEBUG] [command] option: &tfmigrate.MigratorOption{ExecPath:"", PlanOut:""}
2021/10/09 02:05:31 [INFO] [runner] load migration file: tfmigrate_mv_dir1_dir2.hcl
2021/10/09 02:05:31 [INFO] [migrator] multi start state migrator plan
2021/10/09 02:05:31 [DEBUG] [executor@/work/tmp/dir1]$ terraform version
2021/10/09 02:05:31 [INFO] [migrator@/work/tmp/dir1] terraform version: 1.0.8
2021/10/09 02:05:31 [INFO] [migrator@/work/tmp/dir1] initialize work dir
2021/10/09 02:05:32 [DEBUG] [executor@/work/tmp/dir1]$ terraform init -input=false -no-color
2021/10/09 02:05:32 [INFO] [migrator@/work/tmp/dir1] switch to remote workspace default
2021/10/09 02:05:33 [DEBUG] [executor@/work/tmp/dir1]$ terraform workspace select default
2021/10/09 02:05:33 [INFO] [migrator@/work/tmp/dir1] get the current remote state
2021/10/09 02:05:33 [DEBUG] [executor@/work/tmp/dir1]$ terraform state pull
2021/10/09 02:05:33 [INFO] [migrator@/work/tmp/dir1] override backend to local
2021/10/09 02:05:33 [INFO] [executor@/work/tmp/dir1] create an override file
2021/10/09 02:05:33 [INFO] [migrator@/work/tmp/dir1] creating local workspace folder in: /work/tmp/dir1/terraform.tfstate.d/default
2021/10/09 02:05:33 [INFO] [executor@/work/tmp/dir1] switch backend to local
2021/10/09 02:05:35 [DEBUG] [executor@/work/tmp/dir1]$ terraform init -input=false -no-color -reconfigure
2021/10/09 02:05:35 [DEBUG] [executor@/work/tmp/dir2]$ terraform version
2021/10/09 02:05:35 [INFO] [migrator@/work/tmp/dir2] terraform version: 1.0.8
2021/10/09 02:05:35 [INFO] [migrator@/work/tmp/dir2] initialize work dir
2021/10/09 02:05:35 [DEBUG] [executor@/work/tmp/dir2]$ terraform init -input=false -no-color
2021/10/09 02:05:35 [INFO] [migrator@/work/tmp/dir2] switch to remote workspace default
2021/10/09 02:05:35 [DEBUG] [executor@/work/tmp/dir2]$ terraform workspace select default
2021/10/09 02:05:35 [INFO] [migrator@/work/tmp/dir2] get the current remote state
2021/10/09 02:05:36 [DEBUG] [executor@/work/tmp/dir2]$ terraform state pull
2021/10/09 02:05:36 [INFO] [migrator@/work/tmp/dir2] override backend to local
2021/10/09 02:05:36 [INFO] [executor@/work/tmp/dir2] create an override file
2021/10/09 02:05:36 [INFO] [migrator@/work/tmp/dir2] creating local workspace folder in: /work/tmp/dir2/terraform.tfstate.d/default
2021/10/09 02:05:36 [INFO] [executor@/work/tmp/dir2] switch backend to local
2021/10/09 02:05:36 [DEBUG] [executor@/work/tmp/dir2]$ terraform init -input=false -no-color -reconfigure
2021/10/09 02:05:36 [INFO] [migrator] compute new states (/work/tmp/dir1 => /work/tmp/dir2)
2021/10/09 02:05:36 [DEBUG] [executor@/work/tmp/dir1]$ terraform state mv -state=/tmp/tmp995693296 -state-out=/tmp/tmp740880783 -backup=/dev/null aws_security_group.foo aws_security_group.foo
2021/10/09 02:05:36 [DEBUG] [executor@/work/tmp/dir2]$ terraform state mv -state=/tmp/tmp848061346 -state-out=/tmp/tmp388647065 -backup=/dev/null aws_security_group.foo aws_security_group.foo2
2021/10/09 02:05:36 [DEBUG] [executor@/work/tmp/dir1]$ terraform state mv -state=/tmp/tmp335183140 -state-out=/tmp/tmp391829555 -backup=/dev/null aws_security_group.bar aws_security_group.bar
2021/10/09 02:05:36 [DEBUG] [executor@/work/tmp/dir2]$ terraform state mv -state=/tmp/tmp514207734 -state-out=/tmp/tmp343693021 -backup=/dev/null aws_security_group.bar aws_security_group.bar2
2021/10/09 02:05:36 [INFO] [migrator@/work/tmp/dir1] check diffs
2021/10/09 02:05:37 [DEBUG] [executor@/work/tmp/dir1]$ terraform plan -state=/tmp/tmp664858264 -out=/tmp/tfplan877530391 -input=false -no-color -detailed-exitcode
2021/10/09 02:05:37 [DEBUG] [executor@/work/tmp/dir1] failed to run command: (*exec.ExitError)(0xc0003063a0)(exit status 2)
2021/10/09 02:05:37 [ERROR] [migrator@/work/tmp/dir1] unexpected diffs
2021/10/09 02:05:37 [INFO] [executor@/work/tmp/dir2] remove the override file
2021/10/09 02:05:37 [INFO] [executor@/work/tmp/dir2] remove the workspace state folder
2021/10/09 02:05:37 [INFO] [executor@/work/tmp/dir2] switch back to remote
2021/10/09 02:05:38 [DEBUG] [executor@/work/tmp/dir2]$ terraform init -input=false -no-color -reconfigure
2021/10/09 02:05:38 [INFO] [executor@/work/tmp/dir1] remove the override file
2021/10/09 02:05:38 [INFO] [executor@/work/tmp/dir1] remove the workspace state folder
2021/10/09 02:05:38 [INFO] [executor@/work/tmp/dir1] switch back to remote
2021/10/09 02:05:40 [DEBUG] [executor@/work/tmp/dir1]$ terraform init -input=false -no-color -reconfigure
terraform plan command returns unexpected diffs: failed to run command (exited 2): terraform plan -state=/tmp/tmp664858264 -out=/tmp/tfplan877530391 -input=false -no-color -detailed-exitcode
stdout:

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_security_group.bar will be created
  + resource "aws_security_group" "bar" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = (known after apply)
      + name                   = "bar"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags_all               = (known after apply)
      + vpc_id                 = (known after apply)
    }

  # aws_security_group.foo will be created
  + resource "aws_security_group" "foo" {
      + arn                    = (known after apply)
      + description            = "Managed by Terraform"
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = (known after apply)
      + name                   = "foo"
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags_all               = (known after apply)
      + vpc_id                 = (known after apply)
    }

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

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: /tmp/tfplan877530391

To perform exactly these actions, run the following command to apply:
    terraform apply "/tmp/tfplan877530391"

stderr:

Instead rename and move the tfstate it seem to want to create extra resource for me. and now I wondering on which step I might did wrong on multi_state.

Thank for your help and your awesome project once again!

Support automatic migration depending on module ref vs module state ref

Great tool by the way.

Please let me know if this is feasible.

Context

Previously applied module code which creates module.s3.aws_s3_bucket.that resource.

module "s3" {
  source = "some-module-source?ref=tags/0.1.0"

Now the source has been bumped to 0.2.0 which changes the name that to this

The new version contains a directory migrate/0.1.0_to_0.2.0.hcl file which can be downloaded and run via tfmigrate

Proposal

User updates the source to a new version of a module

terraform init -upgrade
terraform plan

User notices creations and destroys of similar resources

Running this or similar

tfmigrate plan --auto-detect
tfmigrate plan --target module.s3
tfmigrate plan
tfmigrate apply
  1. Detect previously applied module versions
  2. Detect if new module version will be applied
  3. Check for a standardized module migration directory migrate/ which contains the previous version and new version like x.y.z_to_a.b.c.hcl
    • could also use multiple directories to autodetect like tfmigrate, tfupgrade, upgrade, etc
  4. if plan, do not prompt user
  5. if apply, prompt user to migrate

Allow passing in a url to a tfmigrate file

tfmigrate plan https://raw.githubusercontent.com/minamijoyo/tfmigrate/master/test-fixtures/storage_s3/.tfmigrate.hcl
tfmigrate apply https://raw.githubusercontent.com/minamijoyo/tfmigrate/master/test-fixtures/storage_s3/.tfmigrate.hcl

tfmigrate reports migration plan success when Terraform plan command returns an error

Thank you for all your work on tfmigrate, it has saved our bacon many times when performing state file surgery! 🙇‍♂️

We recently encountered a scenario where tfmigrate reported a successful plan with no changes, however after applying the migration we found that there were changes shown when we ran terraform plan manually in the new state's home. We traced this down to the fact that there were non default/auto .tfvars files that were required to be specified at plan time, and if these were not specified the plan command as run by tfmigrate returns an error. This is observed as well when debug logging is turned on.

2023/08/01 16:57:54 [DEBUG] [executor@/Users/joshua.feierman/repos/something]$ terraform plan -state=/var/folders/nz/p1nscz5j1_q4xwrb7kfpr3q80000gq/T/tmp3169562229 -out=/var/folders/nz/p1nscz5j1_q4xwrb7kfpr3q80000gq/T/tfplan3479111866 -input=false -no-color -detailed-exitcode
2023/08/01 16:57:54 [DEBUG] [executor@/Users/joshua.feierman/repos/something] failed to run command: (*exec.ExitError)(0x14000030dc0)(exit status 1)
2023/08/01 16:57:54 [INFO] [migrator@/Users/joshua.feierman/repos/something] check diffs
...
2023/08/01 16:58:17 [INFO] [migrator] multi state migrator plan success!

This doesn't seem like appropriate behavior, in that I would expect tfmigrate to throw an error if it cannot run the terraform plan command successfully. Granted, I'm not sure how we'd specify those .tfvars files be used, but that's more of a feature gap in my mind.

[Feature Request] Show plan output even when force=true

Maybe this should be an optional flag, if some prefer it the way it is now.

But as I have been using tfmigrate, I often expect a few specific diffs to be in the final plan, but I still want to verify that they look correct, even when I use force = true to apply them anyway.

Currently, my workflow is to comment out the force = true line, run the plan, verify it, then turn uncomment the line to run the apply. It works but I often forget and have to run things over again!

--

Also thank you for writing this tool, it is amazing and has made my life so much easier! Another feature request is a donate button somewhere so I can show some monetary support :)

Feature request: Display of state plan for to_dir in multi-state

Hi, I have always used this software very frequently. It is a great OSS.

description

migration "multi_state" "mv_dir1_dir2" {
  from_dir = "./old"
  to_dir   = "./new"
  force = true
  actions = [
    "mv aws_iam_user.test_user aws_iam_user.test2_user",
  ]
}

For example, if we want to perform a state operation across multiple directories as described above, the terraform plan is currently executed only on the directory set on from_dir, and the results are output.
In this case, the source directory can be guaranteed to be normal, but if we want to guarantee the normality of the destination directory, there is no way to check this with the current tfmigrate. Therefore, when executing the tfmigrate plan, is it possible to execute the terraform plan for the to_dir at the same time as the terraform plan for the from_dir? Thank you!

Feature request: return count of unapplied migrations

This relates to #64 as a pattern for using tfmigrate in CI/CD.

Request

It would be helpful to return the number of unapplied migrations in addition to the current list command. I currently rely on the list command to determine whether I need to conditionally execute tfmigrate within a workflow. This works but does require some additional processing in order to arrive at a count that can be used in subsequent conditional expressions.

num_migrations=$(tfmigrate list --status unapplied | grep -c '.hcl' || true)

It would be much nicer to avoid the extra processing and error handling if tfmigrate could return the count directly such as:

num_migrations=$(tfmigrate count --status unapplied)

Ideally, a count command could also be executed in a terraform directory without tfmigrate configuration present and still return a count of 0. This would avoid any need to first parse/detect if tfmigrate configuration is present before executing a count command.

Background

I have developed a pattern for incorporating tfmigrate into our CDKTF & Terraform workflows that works quite well for both workflows that have state migrations to apply as well as workflows that do not need state migrations or have not yet needed migrations. This was desired as we have source repositories and workflows that manage multiple Terraform stacks or root modules - some of them where tfmigrate is applicable and others where it is not. Knowing the migration needs of these stacks and root modules can and will change over time, I wanted to create a CI/CD workflow that was consistent and adaptable to both situations. This design was also needed in order to take advantage of matrix builds or parallel executions of jobs in CI/CD where each Terraform workflow was the same and will conditionally execute tfmigrate plan and tfmigrate apply when migrations are configured and present.

I began using the list command and processing the count of migrations after I noticed tfmigrate will skip the terraform plan when no unapplied migrations are present to process. I would like my CI/CD workflow to execute a terraform plan regardless of state migrations being present so I can expose and surface the plan as part of the Pull request or as part of the release.

This is the result of running tfmigrate plan when no migrations need to be processed:

2022/02/24 17:11:15 [INFO] AWS Auth provider used: "EnvProvider"
2022/02/24 17:11:16 [INFO] [runner] no unapplied migrations

Desired CI/CD workflow

Pull request / Build

graph TD
    pr((pull_request)) --> setup-tf[setup terraform]
    setup-tf --> setup-migrate[install tfmigrate]
    setup-migrate --> init[terraform init]
    init --> count[tfmigrate count --unapplied]
    count --> unapplied{unapplied state migations > 0}
    unapplied --> |Yes| migrate-plan[tfmigrate plan]
    unapplied --> |No| tf-plan[terraform plan]

Release / Deployment

graph TD
    deployment((deployment)) --> setup-tf[setup terraform]
    setup-tf --> setup-migrate[install tfmigrate]
    setup-migrate --> init[terraform init]
    init --> count[tfmigrate count --unapplied]
    count --> unapplied{unapplied state migations > 0}
    unapplied --> |Yes| migrate-apply[tfmigrate apply]
    unapplied --> |No| tf-plan[terraform plan]
    migrate-apply --> tf-plan
    tf-plan --> tf-apply[terraform apply]

Integration in CI/CD pipeline when there are also non-migration changes

Hi, this looks like the tool I need, but I'm having some trouble integrating it into our CI/CD pipeline, perhaps you can help.

Current situation

Currently we have to do our migrations (state mv, import etc) manually, outside of the CI/CD pipeline. But aside from the migrations it works like this:

  • Changes in terraform code are made on a separate branch.
  • A merge/pull request is created for the branch.
  • CI/CD pipeline does a terraform plan.
  • Reviewer checks the terraform code and the output of the plan.
  • Branch is merged into the main branch.
  • CI/CD pipeline does another terraform plan (to ensure no changes were done on the main branch in the mean time).
  • Reviewer checks the plan again.
  • Reviewer starts manual job in the CI/CD pipeline that runs terraform apply with the previously created plan.

Desired situation

I would love to use tfmigrate in history mode to do all our migrations for us at the same time as the terraform code for the migrations is actually merged and applied by the CI/CD pipeline. So I came up with this:

  • Changes in terraform code are made on a separate branch.
  • A merge/pull request is created for the branch.
  • CI/CD pipeline does a tfmigrate plan.
  • Reviewer checks the terraform code and the output of the plan.
  • Branch is merged into the main branch.
  • CI/CD pipeline does tfmigrate apply to apply the migrations that were merged (together with the terraform code causing the migrations)
  • CI/CD pipeline does another terraform plan (to ensure no changes were done on the main branch in the mean time).
  • Reviewer checks the plan again.
  • Reviewer starts manual job in the CI/CD pipeline that runs terraform apply with the previously created plan.

Difficulties

  • Our merge/pull requests will contain more than just migration code. For example there may be a mv/rename of a resource, but in the same merge request that resource' attributes are also changed. So tfmigrate plan results in a non-empty plan causing the CI/CD pipeline to fail.
  • And even if we would make separate merge/pull requests for migrations and 'normal' terraform changes, how would we combine both tfmigrate and 'normal' terraform plan/apply into the same pipeline without the tfmigrate part failing?
  • I tried using a wrapper script around the terraform command with TFMIGRATE_EXEC_PATH that always exits with a 0 exit code for the terraform plan command, but then tfmigrate doesn't seem to output the plan results to the console.

A potential solution might be to always have tfmigrate plan output the plan results and have an option for it to not fail when there are diffs. But perhaps I misunderstood and that is the wrong way of using the tool. Would you have any suggestions on how to properly integrate tfmigrate into our development pipeline while allowing 'normal' terraform changes besides the migrations?

Allow migrations with resources including a space

It seems tfmigrate splits on a space for a mv command and only allows 1 space after mv , however, this basically means we cannot write state mv commands for resources in a for_each with a key that includes a space

[Feature Request] Azure history storage

AWS is the primary cloud provider for a lot of Terraform users, but for many others it's Azure. It would be a lot easier for those people to start using this tool if we were able to use an Azure blob to store history, similar to the Terraform azurerm backend.

If we don't want to set up an AWS account and include more credentials in our CI then the only other option is the local storage option. A local storage backend won't help if you use ephemeral CI agents, or even if you just have more than one and don't want to set up some sort of remote mounted filesystem.

[Feature Request] GCP Cloud Storage History Backend

Current State:
At present, tfmigrate only supports remote storage in Amazon's S3 service. This limits the usage for projects that are based on GCP.

Desired New State:
Support GCP's Cloud Storage as a remote storage service.

Feature request, a flag to ignore output changes

In many cases, output changes are caused by a wrong output order, or other trivial causes. By scripting and disabling output before and after this can be circumvented. Still I think it would be wishful to have a flag to ignore output diff, that would save a little bit of time.

Kudos for this very awesome tool!

Doesn't seem to work with indexed resources

Hello! I'm super excited about this tool, but I haven't been able to find a way for it to work with indexed resources (e.g. aws_security_group.many["foo"]). The error message I get when I try to migrate one of these is

╷
│ Error: Index value required
│
│   on  line 1:
│   (source code not available)
│
│ Index brackets must contain either a literal number or a literal string.
╵

Full replication follows your example in the readme:

git clone https://github.com/minamijoyo/tfmigrate
cd tfmigrate/
docker-compose build
docker-compose run --rm tfmigrate /bin/bash

In the sandbox environment, set up the initial statefile:

# mkdir -p tmp/dir1 && cd tmp/dir1
# terraform init -from-module=../../test-fixtures/backend_s3/
# cat << eof > main.tf
> resource "aws_security_group" "many" {
>   for_each = toset(["foo", "bar"])
>   name = each.key
> }
> eof
# terraform apply

A terraform state list will show two resources:

aws_security_group.many["bar"]
aws_security_group.many["foo"]

Now we set up a second root:

# cd .. && mkdir dir2 && cd dir2
# cat ../cir1/config.tf | sed 's/test\/terraform.tfstate/dir2\/terraform.tfstate/' > config.tf
# touch main.tf
# terraform init

Move a resource out of dir1:

# cd ..
# cat << eof > dir1/main.tf
> resource "aws_security_group" "many" {
>   for_each = toset(["foo"])
>   name = each.key
> }
> eof
# cd dir1 && terraform plan
...
Plan: 0 to add, 0 to change, 1 to destroy.
# cd ..

Define a resource in dir2:

# cat << eof > dir2/main.tf
> resource "aws_security_group" "many" {
>   for_each = toset(["bar"])
>   name = each.key
> }
> eof
# cd dir2 && terraform plan
...
Plan: 1 to add, 0 to change, 0 to destroy.
# cd ..

Write a migration file describing this change:

# cat << eof > tfmigrate_multi_state_test.hcl
> migration "multi_state" "test" {
>   from_dir = "dir1"
>   to_dir = "dir2"
> 
>   actions = [
>     "mv aws_security_group.many[\"bar\"] aws_security_group.many[\"bar\"]"
>   ]
> }
> eof

Run tfmigrate plan & apply:

# TFMIGRATE_LOG=DEBUG tfmigrate plan tfmigrate_multi_state_test.hcl
...
failed to run command (exited 1): terraform state mv -state=/tmp/tmp849889331 -state-out=/tmp/tmp278533622 -backup=/dev/null aws_security_group.many[bar] aws_security_group.many[bar]
stdout:

stderr:
╷
│ Error: Index value required
│
│   on  line 1:
│   (source code not available)
│
│ Index brackets must contain either a literal number or a literal string.
╵

╷
│ Error: Index value required
│
│   on  line 1:
│   (source code not available)
│
│ Index brackets must contain either a literal number or a literal string.
╵

Add support for `role_arn` in S3 Storage

We use role assumption to allow our pipelines and devs to access S3 Terraform state, and as part of that we use role_arn in S3. Would be great to support this.

Migrating resources from 1 state to another.

I've got a few resources and modules I am trying to move from a monolithic state into smaller broken up states.

New state is entirely new, has just the resources and modules I want to move from the old state. I am using this tfmigrate config to test:

migration "multi_state" "mv_test" {
  from_dir = "."
  to_dir   = "../new_state"
  actions = [
    "mv aws_security_group.foo aws_security_group.foo",
    "mv aws_security_group.bar aws_security_group.bar",
    "mv module.s3-bucket-test module.s3-bucket-test",
  ]
}

When I test planned this, it always showed the new_state state had unexpected diffs, with it creating all the resources new and which failed the tfmigrate plan. I tried adding the force flag, which still showed the diff, but went ahead and applied it since this is a small test.

This seemed to properly migrate the 3 resources from the old state into the new state, with the new state showing no changes on plan.

Is this the intended workflow when doing a multi_state migration? Is the force flag required?

Migrating states from codebases with different terraform version

Hi, Currently have a use case similar to this where we are trying to move from a monolithic repo/state into smaller broken micro repositories/state

but the catch here is the monolithic repo is in an older version of terraform(0.13.x) and the new repo is in the latest terraform version(1.3.x)

The installed terraform exposed via TFMIGRATE_EXEC_PATH is the new terraform 1.3.x, due to this when the terraform plan runs in the old repo there are a lot of errors (for example map() function was removed and replaced with tomap() function etc)

Is there a way in the multi_state mode that we can specify two different terraform versions and ensure a smooth migration?

Bug: Error when Terraform Cloud is Remote Backend

Current State:
Terraform Cloud is an enterprise cloud solution from Hashicorp that includes a remote state backend. When initializing terraform, the -reconfigure flag does not work when the remote backend is specified to be Terraform Cloud. This error occurs here, and leads to tfmigrate not working with Terraform Cloud as a backend.

image

Solution Plan:
When Terraform Cloud is the desired backend, running the initialization command without the -reconfigure flag leads to tfmigrate running exactly as desired. If the user specifies the new parameter "is_backend_terraform_cloud" to be true in their configuration file, then this is the init command that will be run in place of the c.Init(ctx, "-input=false", "-no-color", "-reconfigure") command that is run currently.

Assigning myself to work on this issue.

Reusability of terraform state command

migration "multi_state" "mv_dir1_dir2" {
  from_dir = "new_folder"
  to_dir   = "."
  actions = [
    "mv aws_vpc.vpc1-1 aws_vpc.vpc1-1",
    "mv aws_subnet.pvt-subnet1-1 aws_subnet.pvt-subnet1-1",
    "mv aws_subnet.pvt-subnet2-1 aws_subnet.pvt-subnet2-1",
    "mv aws_subnet.pvt-subnet3-1 aws_subnet.pvt-subnet3-1",
    "mv aws_subnet.pvt-subnet4-1 aws_subnet.pvt-subnet4-1"
  ]
}

Is there a way to move the entire terraform states in a directory/ terraform file in a one-line command?

eg: shell script I used,

for i in $(terraform state list);
do terraform state mv -state=terraform.tfstate -state-out=../test/terraform.tfstate $i $i;
done

Feature Request keeping temp plan output

Hi,

it would be awesome if we could have the option to keep the temp plan output file from /tmp/....
We would like to use it as a input for our Terraform plan stage.

[Question] Don't apply state migrations to fresh workspaces in history mode

After going through the readme, it's unclear if this tool is designed to handle newly provisioned instances of a configuration (such as a new workspace).

For the sake of simplicity, I'll just call each of these provisioned instances workspaces, although there are other ways to do this without using workspaces.

It's great that this gives us the ability to migrate the state of multiple workspaces in tandem with GitOps-friendly way with history mode, but will a newly provisioned workspace that needs no migrations have all of the historical migrations applied? To me, knowing not to apply migrations to new environments is a core requirement of a tool like this, because development teams will want to create new feature branches along with feature environments regularly.

If history mode isn't capable of handling this already, I can think of at least one way it could be done. Otherwise, if this is already possible with tfmigrate then could we document it in the readme?

TF_CLI_ARGS_plan="-out=tfplan.out" doesn't work

Environment

$ terraform version
Terraform v1.2.9
on darwin_arm64
+ provider registry.terraform.io/hashicorp/null v3.1.1

$ tfmigrate --version
0.3.7

Overview

I would like to test the result of tfmigrate plan with Conftest. ref

e.g.

terraform plan -out tfplan.out
terraform show -json tfplan.out > tfplan.json
conftest test tfplan.json

To do it, we have to run Conftest against a plan file.
To create a plan file with tfmigrate, I passed the environment variable TF_CLI_ARGS_plan=-out=tfplan.out.

$ env TF_CLI_ARGS_plan="-out=tfplan.out" tfmigrate plan
2022/09/12 22:34:08 [INFO] AWS Auth provider used: "SharedCredentialsProvider"
2022/09/12 22:34:10 [INFO] [runner] unapplied migration files: [20220912214947_mv_fooo.hcl]
2022/09/12 22:34:10 [INFO] [runner] load migration file: tfmigrate/20220912214947_mv_fooo.hcl
2022/09/12 22:34:10 [INFO] [migrator] start state migrator plan
2022/09/12 22:34:10 [INFO] [migrator@.] terraform version: 1.2.9
2022/09/12 22:34:10 [INFO] [migrator@.] initialize work dir
2022/09/12 22:34:11 [INFO] [migrator@.] get the current remote state
2022/09/12 22:34:13 [INFO] [migrator@.] override backend to local
2022/09/12 22:34:13 [INFO] [executor@.] create an override file
2022/09/12 22:34:13 [INFO] [migrator@.] creating local workspace folder in: terraform.tfstate.d/default
2022/09/12 22:34:13 [INFO] [executor@.] switch backend to local
2022/09/12 22:34:13 [INFO] [migrator@.] compute a new state
2022/09/12 22:34:14 [INFO] [migrator@.] check diffs
2022/09/12 22:34:14 [INFO] [executor@.] remove the override file
2022/09/12 22:34:14 [INFO] [executor@.] remove the workspace state folder
2022/09/12 22:34:14 [INFO] [executor@.] switch back to remote
2022/09/12 22:34:16 [INFO] [migrator] state migrator plan success!

But unfortunately, a plan file isn't created.

I think TF_CLI_ARGS_plan should work. #27 (comment)

How to reproduce

resource "null_resource" "foo" {}

terraform {
  backend "s3" {
    bucket = "***"
    key    = "github/foo/v1/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

Create a resource for migration.

$ terraform init
$ terraform apply

Change the resource address and create a migration file.

resource "null_resource" "fooo" {}

.tfmigrate.hcl

tfmigrate {
  migration_dir = "./tfmigrate"
  history {
    storage "s3" {
      bucket = "***"
      key    = "github/foo/history.json"
    }
  }
}

tfmigrate/20220912214947_mv_fooo.hcl

migration "state" "mv_foo" {
  actions = [
    "mv null_resource.foo null_resource.fooo",
  ]
}

Run tfmigrate plan.

$ env TF_CLI_ARGS_plan="-out=tfplan.out" tfmigrate plan

Expected Behaviour

tfplan.out is created.

Actual Behaviour

tfplan.out isn't created.

Of course, when we run terraform plan -out=tfplan.out, plan file is created properly.

$ terraform plan -out=tfplan.out
null_resource.foo: Refreshing state... [id=1351938300054449217]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  + create
  - destroy

Terraform will perform the following actions:

  # null_resource.foo will be destroyed
  # (because null_resource.foo is not in configuration)
  - resource "null_resource" "foo" {
      - id = "1351938300054449217" -> null
    }

  # null_resource.fooo will be created
  + resource "null_resource" "fooo" {
      + id = (known after apply)
    }

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

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan.out

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan.out"

Reference

Feature request pre-plan / pre-apply - run command

I would love a feature flag / config feature to run a command in affected dir's before plans / migrations
Why: For folks who leverage terragrunt or other terraform wrappers not all required files may exist in the folder. eg autogenerated aws providers etc.
So for us being able to run terragrunt init in the affected dir's would be a big plus.
In a git ops pipeline there may be other uses like forcing a hard reset of the git dir to make sure there are no unexpected files in place

Breaks with Terraform Cloud

Reproduction:

Initialization:

git clone https://github.com/hashicorp/tfc-getting-started
cd tfc-getting-started
./scripts/setup.sh
# ... enter / enter / enter / enter / ...

Token for fakewebservice API (it is set in TFC already but we need it for local run too):

cat ~/.terraform.d/credentials.tfrc.json | jq '.credentials["app.terraform.io"].token' | sed 's/^/provider_token = /' > terraform.tfvars 

Change to new cloud backed:

sed -i 's/backend "remote"/cloud/' backend.tf
terraform init
# Enter a value: yes

./.tfmigrate.hcl

tfmigrate {
  migration_dir = "./tfmigrate"
  is_backend_terraform_cloud = false
  history {
    storage "local" {
      path = "./history.json"
    }
  }
}

./tfmigrate/test.hcl

migration "state" "test" {
  dir       = "."
  workspace = "getting-started"

  actions = [
    "mv fakewebservices_database.prod_db fakewebservices_database.prod_db2",
  ]

  force = true
}

tfmigrate plan:

v0.3.4 with is_backend_terraform_cloud = false:

$  tfmigrate plan
2022/08/04 10:34:03 [INFO] [runner] unapplied migration files: [test.hcl]
2022/08/04 10:34:03 [INFO] [runner] load migration file: tfmigrate/test.hcl
2022/08/04 10:34:03 [INFO] [migrator] start state migrator plan
2022/08/04 10:34:03 [INFO] [migrator@.] terraform version: 1.2.5
2022/08/04 10:34:03 [INFO] [migrator@.] initialize work dir
2022/08/04 10:34:06 [INFO] [migrator@.] get the current remote state
2022/08/04 10:34:09 [INFO] [migrator@.] override backend to local
2022/08/04 10:34:09 [INFO] [executor@.] create an override file
2022/08/04 10:34:09 [INFO] [migrator@.] creating local workspace folder in: terraform.tfstate.d/getting-started
2022/08/04 10:34:09 [INFO] [executor@.] switch backend to local
2022/08/04 10:34:09 [INFO] [migrator@.] compute a new state
2022/08/04 10:34:09 [INFO] [migrator@.] check diffs
2022/08/04 10:34:11 [INFO] [migrator@.] unexpected diffs, ignoring as force option is true: failed to run command (exited 2): terraform plan -state=/tmp/tmp1593536331 -out=/tmp/tfplan771370973 -input=false -no-color -detailed-exitcode
stdout:
fakewebservices_database.prod_db2: Refreshing state... [id=fakedb-We792NeCUZBoGjCm]
fakewebservices_vpc.primary_vpc: Refreshing state... [id=fakevpc-NUohxvfnu36ayu4j]
fakewebservices_server.servers[0]: Refreshing state... [id=fakeserver-nsUSsCUumb1Z4itt]
fakewebservices_server.servers[1]: Refreshing state... [id=fakeserver-LXMvK4tbtoGA3cd3]
fakewebservices_load_balancer.primary_lb: Refreshing state... [id=fakelb-PMJfhAwA8BDrXNop]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  - destroy

Terraform will perform the following actions:

  # fakewebservices_database.prod_db will be created
  + resource "fakewebservices_database" "prod_db" {
      + id   = (known after apply)
      + name = "Production DB"
      + size = 256
    }

  # fakewebservices_database.prod_db2 will be destroyed
  # (because fakewebservices_database.prod_db2 is not in configuration)
  - resource "fakewebservices_database" "prod_db2" {
      - id   = "fakedb-We792NeCUZBoGjCm" -> null
      - name = "Production DB" -> null
      - size = 256 -> null
    }

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

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: /tmp/tfplan771370973

To perform exactly these actions, run the following command to apply:
    terraform apply "/tmp/tfplan771370973"

stderr:
2022/08/04 10:34:11 [INFO] [executor@.] remove the override file
2022/08/04 10:34:11 [INFO] [executor@.] remove the workspace state folder
2022/08/04 10:34:11 [INFO] [executor@.] switch back to remote
2022/08/04 10:34:11 [ERROR] [executor@.] failed to switch back to remote: failed to run command (exited 1): terraform init -input=false -no-color -reconfigure -reconfigure
stdout:

Initializing Terraform Cloud...

stderr:

Error: Invalid command-line option

The -reconfigure option is for in-place reconfiguration of state backends
only, and is not needed when changing Terraform Cloud settings.

When using Terraform Cloud, initialization automatically activates any new
Cloud configuration settings.


2022/08/04 10:34:11 [ERROR] [executor@.] please re-run terraform init -reconfigure
2022/08/04 10:34:11 [INFO] [migrator] state migrator plan success!
$  terraform init

Initializing Terraform Cloud...
Migrating from backend "local" to Terraform Cloud.

v0.3.4 with is_backend_terraform_cloud = true:

Exactly the same as above.

#98 with is_backend_terraform_cloud = false:

Exactly the same as above.

#98 with is_backend_terraform_cloud = true:

Migrating from backend "local" to Terraform Cloud.

Initializing provider plugins...
- Reusing previous version of hashicorp/fakewebservices from the dependency lock file
- Using previously-installed hashicorp/fakewebservices v0.2.3

Terraform Cloud has been successfully initialized!

You may now begin working with Terraform Cloud. Try running "terraform plan" to
see any changes that are required for your infrastructure.

If you ever set or change modules or Terraform Settings, run "terraform init"
again to reinitialize your working directory.
 dex4er  ~  Sources/terraform/tfc-getting-started  piotr.roszatycki  dev  main  1✎  4+  $  tfmigrate.dex4er plan
2022/08/04 10:37:14 [INFO] [runner] unapplied migration files: [test.hcl]
2022/08/04 10:37:14 [INFO] [runner] load migration file: tfmigrate/test.hcl
2022/08/04 10:37:14 [INFO] [migrator] start state migrator plan
2022/08/04 10:37:14 [INFO] [migrator@.] terraform version: 1.2.5
2022/08/04 10:37:14 [INFO] [migrator@.] initialize work dir
2022/08/04 10:37:17 [INFO] [migrator@.] get the current remote state
2022/08/04 10:37:19 [INFO] [migrator@.] override backend to local
2022/08/04 10:37:19 [INFO] [executor@.] create an override file
2022/08/04 10:37:19 [INFO] [migrator@.] creating local workspace folder in: terraform.tfstate.d/getting-started
2022/08/04 10:37:19 [INFO] [executor@.] switch backend to local
2022/08/04 10:37:19 [INFO] [migrator@.] compute a new state
2022/08/04 10:37:20 [INFO] [migrator@.] check diffs
2022/08/04 10:37:21 [INFO] [migrator@.] unexpected diffs, ignoring as force option is true: failed to run command (exited 2): terraform plan -state=/tmp/tmp1348777574 -out=/tmp/tfplan1588401082 -input=false -no-color -detailed-exitcode
stdout:
fakewebservices_vpc.primary_vpc: Refreshing state... [id=fakevpc-NUohxvfnu36ayu4j]
fakewebservices_database.prod_db2: Refreshing state... [id=fakedb-We792NeCUZBoGjCm]
fakewebservices_server.servers[0]: Refreshing state... [id=fakeserver-nsUSsCUumb1Z4itt]
fakewebservices_server.servers[1]: Refreshing state... [id=fakeserver-LXMvK4tbtoGA3cd3]
fakewebservices_load_balancer.primary_lb: Refreshing state... [id=fakelb-PMJfhAwA8BDrXNop]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  - destroy

Terraform will perform the following actions:

  # fakewebservices_database.prod_db will be created
  + resource "fakewebservices_database" "prod_db" {
      + id   = (known after apply)
      + name = "Production DB"
      + size = 256
    }

  # fakewebservices_database.prod_db2 will be destroyed
  # (because fakewebservices_database.prod_db2 is not in configuration)
  - resource "fakewebservices_database" "prod_db2" {
      - id   = "fakedb-We792NeCUZBoGjCm" -> null
      - name = "Production DB" -> null
      - size = 256 -> null
    }

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

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: /tmp/tfplan1588401082

To perform exactly these actions, run the following command to apply:
    terraform apply "/tmp/tfplan1588401082"

stderr:
2022/08/04 10:37:21 [INFO] [executor@.] remove the override file
2022/08/04 10:37:21 [INFO] [executor@.] remove the workspace state folder
2022/08/04 10:37:21 [INFO] [executor@.] switch back to remote
2022/08/04 10:37:25 [INFO] [migrator] state migrator plan success!

Handle partial backend config

Hi! I'm looking for ideas/help in using tfmigrate with partial backend configuration for remote state.

We are storing terraform state in S3 and use a shared backend config file to reduce duplication (specifying bucket name, dynamodb table for locking, role names to assume for access, etc.).

When executing tfmigrate plan, the initialization of the workspace for planning fails, as the backend config is not specified.
I can fix this by providing the required backend-config via environment variable TF_CLI_ARGS_init=..., which allows tfmigrate to initialize, fetch remote state and do an initial plan.

Unfortunately there is strict validation of the backend config and when tfmigrate switches to a local backend override, the reconfiguration fails, as the backend-config is still set through environment variables.

Do you see a workaround for this?

I'm afraid tfmigrate might have to accept backend-config to pass through to support this use case, but maybe there is a more lightweight solution?

tfmigrate reports success despite backend not properly configured

We forgot to configure the terraform backend properly and got the following logs:

 2023/04/06 12:57:09 [INFO] [runner] unapplied migration files: [2023-04-06-import-puc-hr-records.hcl]
2023/04/06 12:57:09 [INFO] [runner] load migration file: migrations/2023-04-06-import-puc-hr-records.hcl
2023/04/06 12:57:09 [INFO] [migrator] start state migrator plan
2023/04/06 12:57:09 [INFO] [migrator@.] terraform version: 1.4.4
2023/04/06 12:57:09 [INFO] [migrator@.] initialize work dir
2023/04/06 12:57:11 [INFO] [migrator@.] get the current remote state
2023/04/06 12:57:12 [INFO] [migrator@.] override backend to local
2023/04/06 12:57:12 [INFO] [executor@.] create an override file
2023/04/06 12:57:12 [INFO] [migrator@.] creating local workspace folder in: terraform.tfstate.d/default
2023/04/06 12:57:12 [INFO] [executor@.] switch backend to local
2023/04/06 12:57:13 [INFO] [migrator@.] compute a new state
2023/04/06 12:57:17 [INFO] [migrator@.] check diffs
2023/04/06 12:57:20 [INFO] [executor@.] remove the override file
2023/04/06 12:57:20 [INFO] [executor@.] remove the workspace state folder
2023/04/06 12:57:20 [INFO] [executor@.] switch back to remote
Error: /06 12:57:20 [ERROR] [executor@.] failed to switch back to remote: failed to run command (exited 1): /home/runner/work/_temp/0366c15f-c413-424b-89f7-f63bc6dbe449/terraform-bin init -input=false -no-color -reconfigure
stdout:

Initializing the backend...

stderr:

Error: "bucket": required field is not set



Error: /06 12:57:20 [ERROR] [executor@.] please re-run terraform init -reconfigure
2023/04/06 12:57:20 [INFO] [migrator] state migrator plan success!

I think tfmigrate should not report sucess if one of the terraform commands fails.

[Feature Request] Documentation on Acceptance Criteria for Contributions

Current State: There is no documentation in the repo pertaining to how contributors should contribute to tfmigrate.

Desired State: As this repo gains a larger following, it will be helpful to have a readme for contributors that guides on acceptance criteria. Some thoughts as to what would might be helpful to see in a contributor document:

  • Desired formatting for new issues and PRs.
  • Whether PRs should be linked to issues to help prevent duplication of work.
  • Proper way to run test suite (this led to some confusion for me personally while working on this PR).
  • List of prioritized new features that the community can work on for upcoming desired releases.
  • Known licensing issues to be aware of (if any exist)

Error asking for state migration action: input is disabled

Hi thanks last time testing with me on the sandbox! it was great!

I am moving it to my codebase now but I hit another bug, can you take a look for me when you have time? thank you!

% cat tfmigrate_test.hcl 
migration "state" "test" {
  actions = [
    "mv github_repository.tfer--infra-002D-staging-002D-terraform github_repository.infra-staging-terraform",
  ]
}

 % TFMIGRATE_LOG=DEBUG tfmigrate plan tfmigrate_test.hcl
2021/10/12 16:44:17 [DEBUG] [main] start: tfmigrate plan tfmigrate_test.hcl
2021/10/12 16:44:17 [DEBUG] [main] tfmigrate version: 0.2.9
2021/10/12 16:44:17 [DEBUG] [command] config: &config.TfmigrateConfig{MigrationDir:".", History:(*history.Config)(nil)}
2021/10/12 16:44:17 [DEBUG] [command] option: &tfmigrate.MigratorOption{ExecPath:"", PlanOut:""}
2021/10/12 16:44:17 [INFO] [runner] load migration file: tfmigrate_test.hcl
2021/10/12 16:44:17 [INFO] [migrator] start state migrator plan
2021/10/12 16:44:17 [DEBUG] [executor@.]$ terraform version
2021/10/12 16:44:17 [INFO] [migrator@.] terraform version: 1.0.6
2021/10/12 16:44:17 [INFO] [migrator@.] initialize work dir
2021/10/12 16:44:22 [DEBUG] [executor@.]$ terraform init -input=false -no-color
2021/10/12 16:44:22 [INFO] [migrator@.] switch to remote workspace default
2021/10/12 16:44:23 [DEBUG] [executor@.]$ terraform workspace select default
2021/10/12 16:44:23 [INFO] [migrator@.] get the current remote state
2021/10/12 16:44:26 [DEBUG] [executor@.]$ terraform state pull
2021/10/12 16:44:26 [INFO] [migrator@.] override backend to local
2021/10/12 16:44:26 [INFO] [executor@.] create an override file
2021/10/12 16:44:26 [INFO] [migrator@.] creating local workspace folder in: terraform.tfstate.d/default
2021/10/12 16:44:26 [INFO] [executor@.] switch backend to local
2021/10/12 16:44:31 [DEBUG] [executor@.]$ terraform init -input=false -no-color -reconfigure
2021/10/12 16:44:31 [DEBUG] [executor@.] failed to run command: (*exec.ExitError)(0xc0002ca140)(exit status 1)
failed to switch backend to local: failed to run command (exited 1): terraform init -input=false -no-color -reconfigure
stdout:

Initializing the backend...

stderr:

Error: Error asking for state migration action: input is disabled

but "terraform init -input=false -no-color -reconfigure" run seems fine by itself

% terraform init -input=false -no-color -reconfigure

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of integrations/github from the dependency lock file
- Using previously-installed integrations/github v4.14.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

checking file context

% cat repository.tf | grep tfer--infra-002D-staging-002D-terraform
resource "github_repository" "tfer--infra-002D-staging-002D-terraform" {
 % cat terraform.tfstate | grep  github_repository.tfer--infra-002D-staging-002D-terraform
    "github_repository_tfer--infra-002D-staging-002D-terraform_id": {

Dynamic workspace

I would like the workspace to be dynamic in hcl files.

For example :

migration "state" "migration1" {
  workspace = "${env.TFMIGRATE_WORKSPACE}"
  actions = [
    "mv aws_security_group.foo[0] 'aws_security_group.foo[\"baz\"]'"
  ]
}

Error when migration script contains state manipulations for list items

Hi,

For a state manipulation containing brackets (i.e. when moving list items), I saw the following error:
Index brackets must contain either a literal number or a literal string

Example migration script:

migration "state" "test" {
  actions = [
    "mv module.peering.aws_route.routes[0] module.peering.aws_route.routes['rtb-1234567']"
  ]
}

I was upgrading terraform versions and one of my changes was updating a block using count to use for_each. I think a list had become a set. I noticed the underlying command tfmigrate was running on my behalf was missing quotes around the resources with brackets as well as quotes within the brackets:
terraform state mv -state=/var/folders/9l/9n0dly0d7yz_qg1jzbfjl9wh0000gn/T/tmp803107815 -backup=/dev/null module.peering.aws_route.routes[0] module.peering.aws_route.routes-common[rtb-1234567]

Importing some resource types has always changes in plan and cannot be applied

The current implementation of tfmigrate apply command requires that a plan has no changes for safe refactoring. However, importing some resource types has always changes in plan and cannot be applied.

For example, a force_destroy attribute in aws_s3_bucket is a provider implementation control parameter which cannot be imported from a real resource and will be computed default to false. In this case, it will change from null to false in the plan after import.

The first idea I came up with in my mind was to add an option like tfmigrate apply -force, but it's not suitable for GitOps because we always need to set -force in a CI/CD pipeline. Obviously, it's not safe to use.

At least, a force parameters should be set for each migration file as a migration block attribute. Furthermore, it would be great if we could declare which diffs could be ignored like ignore_changes attribute in Terraform configurations.

As a workaround, you can do it by splitting two phases:

  1. Import a resource by temporarily adding ignore_changes to *.tf and tfmigrate apply
  2. Remove ignore_changes from *.tf and terraform apply

I know it's not ideal but it should work.

Failed to terraform apply with tfplan generated by tfmigrate plan --out=tfplan in Terraform 1.1

bash-5.1# tfmigrate -v
0.2.13

bash-5.1# terraform -v
Terraform v1.1.0
on linux_amd64

bash-5.1# TEST_ACC=1 go test -v ./tfmigrate -run=TestAccStateMigratorApply

2021/12/10 04:45:12 [DEBUG] [executor@/tmp/workDir4152694674]$ terraform apply -input=false -no-color /tmp/tmp774935950
2021/12/10 04:45:13 [TRACE] [executor@/tmp/workDir4152694674] cmd=(*tfexec.command)(0xc0003821b0)({
 osExecCmd: (*exec.Cmd)(0xc000594160)(/usr/local/bin/terraform apply -input=false -no-color /tmp/tmp774935950),
 stdout: (*bytes.Buffer)(0xc0001843c0)(),
 stderr: (*bytes.Buffer)(0xc0001843f0)(
Error: Saved plan does not match the given state

The given plan file can not be applied because it was created from a
different state lineage.
)
})

2021/12/10 04:45:13 [DEBUG] [executor@/tmp/workDir4152694674] failed to run command: (*exec.ExitError)(0xc0000be0e0)(exit status 1)
    state_migrator_test.go:298: failed to apply the saved plan file: failed to run command (exited 1): terraform apply -input=false -no-color /tmp/tmp774935950
        stdout:

        stderr:

        Error: Saved plan does not match the given state

        The given plan file can not be applied because it was created from a
        different state lineage.

Running migration from other than default workspace doesn't work

Today I noticed that when using workspaces migration doesn't work if other than default workspace is selected:

Some command log:
terraform workspace select eu-central-1
tfmigrate plan --config ./tfmigrate_eu-central-1.hcl

Will give error:

2021/06/10 15:05:56 [INFO] AWS Auth provider used: "EnvProvider"
2021/06/10 15:05:57 [INFO] [runner] unapplied migration files: [202106081244_messaging.hcl]
2021/06/10 15:05:57 [INFO] [runner] load migration file: migrations/202106081244_messaging.hcl
2021/06/10 15:05:57 [INFO] [migrator] start state migrator plan
2021/06/10 15:05:58 [INFO] [migrator@.] terraform version: 0.15.0
2021/06/10 15:05:58 [INFO] [migrator@.] initialize work dir
2021/06/10 15:06:03 [INFO] [migrator@.] get the current remote state
2021/06/10 15:06:05 [INFO] [migrator@.] override backend to local
2021/06/10 15:06:05 [INFO] [executor@.] create an override file
2021/06/10 15:06:05 [INFO] [executor@.] switch backend to local
failed to switch backend to local: failed to run command (exited 1): terraform init -input=false -no-color -reconfigure
stdout:
Initializing modules...

Initializing the backend...

Successfully configured the backend "local"! Terraform will automatically
use this backend unless the backend configuration changes.

The currently selected workspace (eu-central-1) does not exist.
  This is expected behavior when the selected workspace did not have an
  existing non-empty state. Please enter a number to select a workspace:

  1. default

  Enter a value:
stderr:

Error: Failed to select workspace: EOF

Some background about my config:

  • State is stored to s3
  • State it splitted so that each region has its own state in same s3 bucket
  • Workspaces is used to get separate state for each region

I'm more than happy to provide more details if needed to fix this.

Cannot use `*` with tfmigrate actions

❯ tfmigrate --version
0.3.11
migration "state" "test" {
  actions = [
    "rm aws_s3_bucket.test[*]",
  ]
}
failed to run command (exited 1): terraform state rm -state=/tmp/tmp164712774 -backup=/dev/null aws_s3_bucket.test[*]
stdout:

stderr:

│ Error: Attribute name required

│   on  line 1:
│   (source code not available)

│ Splat expressions ([*]) may not be used here.

I can use * with terraform state rm command with quote like below.

terraform state rm 'aws_s3_bucket.test[*]'

Expected behavior: I can use * with tfmigrate actions.

Partial history storage configuration

I would like to be able to configure the history storage config as we can do in terraform with partial configurations.

  • Storage config parameters that never changes goes in .tfmigrate.hcl.
  • Storage config parameters depending on environment can be pass with cli parameters or an env variable (maybe TFMIGRATE_init).

Unable to create cloudformation stack in sandbox environment

When testing in sandbox environment, I get an error when running terraform apply -auto-approve

Error: creating CloudFormation stack failed: : 
	status code: 404, request id: 

resource "aws_cloudformation_stack" "aws_stack" {

I added a mock endpoint in the config file for the aws provider for cloudformation.

Am i missing something?

[Feature Request] Support migrating to modules

Would be great if you could move resources to modules with tool.
Typically when reorganising the resources terraform needs to destroy and recreate them, which is not nice for databases.

Only difference in the state with resource in module or outside is field 'module' which has the name of the module the resource is in.

Pre-migrate Hook

We have a custom command we run in every Terraform project directory before running any other Terraform command which generates some custom Terraform configuration in that specific directory. Specifically, this command generates the backend configuration automatically, amongst other functionality it uses the relative path in the Git Repo to set the path in S3 for the state store for example.

Without these generated files we wouldn't be able to do state migrations/imports in our projects so ideally We'd want to run this command in each directory affected by tfmigrate. When doing migrations manually that'd work, but we'd want to integrate this into Atlantis and then we'd have to either run the command into every directory in the repository to generate the configuration everywhere possible just in case or we would have to parse all migrations to see which directories are affected by the currently unapplied migrations.

I was thinking a solution for this would either be a hook to specify this command and have tfmigrate automatically run the command or perhaps simply a subcommand for tfmigrate which outputs all the directories the currently unapplied migrations are expecting to touch.

"compute a new state" takes a very long time

Planning a state migration with ~150 operations (about 50% import and 50% rm) takes a very long time (~2 hours in a 4 vCPU / 16 GiB container). A bash script running the same operations takes about 25 minutes, and that's a very naive script which locks and unlocks remote state for every operation! I would expect tfmigrate to be much faster since it works on a local copy of the state.

I set TF_CLI_ARGS_plan='-refresh=false -parallelism=32' but it didn't seem to help.

Here's the log of a run which ended up failing after about 2 hours due to the provider documentation about the import ID being wrong. 🤦🏻

exit status 1: running "cd $(git rev-parse --show-toplevel) && TF_CLI_ARGS_plan='-refresh=false -parallelism=32' tfmigrate plan && touch $PLANFILE" in "/var/lib/atlantis/repos/REDACTED/default/tfmigrate": 
2022/03/01 01:31:19 [INFO] Attempting to use session-derived credentials
2022/03/01 01:31:19 [INFO] Successfully derived credentials from session
2022/03/01 01:31:19 [INFO] AWS Auth provider used: "CredentialsEndpointProvider"
2022/03/01 01:31:20 [INFO] [runner] unapplied migration files: [redacted.hcl]
2022/03/01 01:31:20 [INFO] [runner] load migration file: tfmigrate/redacted.hcl
2022/03/01 01:31:20 [INFO] [migrator] start state migrator plan
2022/03/01 01:31:20 [INFO] [migrator@.] terraform version: 1.1.6
2022/03/01 01:31:20 [INFO] [migrator@.] initialize work dir
2022/03/01 01:32:50 [INFO] [migrator@.] get the current remote state
2022/03/01 01:33:12 [INFO] [migrator@.] override backend to local
2022/03/01 01:33:12 [INFO] [executor@.] create an override file
2022/03/01 01:33:12 [INFO] [migrator@.] creating local workspace folder in: terraform.tfstate.d/default
2022/03/01 01:33:12 [INFO] [executor@.] switch backend to local
2022/03/01 01:33:18 [INFO] [migrator@.] compute a new state
2022/03/01 03:31:53 [INFO] [migrator@.] check diffs
2022/03/01 03:35:13 [INFO] [executor@.] remove the override file
2022/03/01 03:35:13 [INFO] [executor@.] remove the workspace state folder
2022/03/01 03:35:13 [INFO] [executor@.] switch back to remote

Timestamps of the slow part emphasised:

2022/03/01 01:33:18 [INFO] [migrator@.] compute a new state
2022/03/01 03:31:53 [INFO] [migrator@.] check diffs

I am currently running a plan with TFMIGRATE_LOG=DEBUG and will update the ticket when it completes in a few hours.

Tool not working on windows

I have built tfmigrate from source for windows.

However, I receive:

failed to run command (exited 1): terraform state mv -state=C:\Users\i016249\AppData\Local\Temp\tmp3995434150 -state-out=C:\Users\i016249\AppData\Local\Temp\tmp3354553505 -backup=/dev/null module.network.data.aws_availability_zones.available module.network.data.aws_availability_zones.available
stdout:
Move "module.network.data.aws_availability_zones.available" to "module.network.data.aws_availability_zones.available"

stderr:
Error saving the state: failed to create local state backup file: open /dev/null: The system cannot find the path specified.

The state was not saved. No items were removed from the persisted
state. No backup was created since no modification occurred. Please
resolve the issue above and try again.

How can I make it work for windows as it does not have /dev/null ?

Bypass "workspace select" if the workspace state is already the desired one

Hey!

In our organization, we're using Scalr https://docs.scalr.com/en/latest/ to perform terraform CI/CD actions.

One of the limitations we're facing to adopt tfmigrate is that we can't use terraform workspace select command because remote workspaces are configured as

terraform {
  backend "remote" {
    hostname = "<account>.scalr.io"
    organization = "<org-id>"

    workspaces {
      name = "<workspace-name>"
    }
  }
}

But still terraform workspace show is available and all other commands that are not related to workspace operations.

So I wonder if it's possible/reasonable to add a simple logical check: if selected workspace is already the one that's required by tfmigrate configuration (using terraform workspace show maybe?) then bypass terraform workspace select command 🙏

Upd: actually, the issue is not with Scalr, it's core terraform code, workspace select is only available for workspaces.prefix which is not how we'd like to configure our remote

Running in a Terraform Cloud Agent

Hi!
First off, thanks for creating this tool. I don't understand how Terraform can't have better support for state migration (moved blocks can only do so much).
I have a use case where I would have liked to run tfmigrate in the context of Terraform Cloud and it seems like the best way would be to add the functionality to a self-hosted Terraform Cloud Agent.

Has anyone been able to use tfmigrate successfully in a tfc-agent?
Getting stuck with this issue for the moment:

2022/09/13 13:10:23 [DEBUG] [main] start: tfmigrate plan tfmigrate.hcl
2022/09/13 13:10:23 [DEBUG] [main] tfmigrate version: 0.3.7
2022/09/13 13:10:23 [DEBUG] [command] load configuration file: .tfmigrate.hcl
2022/09/13 13:10:23 [DEBUG] [command] config: &config.TfmigrateConfig{MigrationDir:".", IsBackendTerraformCloud:false, History:(*history.Config)(nil)}
2022/09/13 13:10:23 [DEBUG] [command] option: &tfmigrate.MigratorOption{ExecPath:"", PlanOut:"", IsBackendTerraformCloud:false, BackendConfig:[]string(nil)}
2022/09/13 13:10:23 [INFO] [runner] load migration file: tfmigrate.hcl
2022/09/13 13:10:23 [INFO] [migrator] start state migrator plan
2022/09/13 13:10:23 [DEBUG] [executor@.]$ terraform version
2022/09/13 13:10:23 [INFO] [migrator@.] terraform version: 1.2.9
2022/09/13 13:10:23 [INFO] [migrator@.] initialize work dir
2022/09/13 13:10:23 [DEBUG] [executor@.]$ terraform init -input=false -no-color
2022/09/13 13:11:08 [DEBUG] [executor@.]$ terraform workspace show
2022/09/13 13:11:08 [DEBUG] [migrator@.] currentWorkspace = ***, workspace = ***
2022/09/13 13:11:08 [INFO] [migrator@.] get the current remote state
2022/09/13 13:11:08 [DEBUG] [executor@.]$ terraform state pull
2022/09/13 13:11:15 [INFO] [migrator@.] override backend to local
2022/09/13 13:11:15 [INFO] [executor@.] create an override file
2022/09/13 13:11:15 [INFO] [migrator@.] creating local workspace folder in: terraform.tfstate.d/***
2022/09/13 13:11:15 [INFO] [executor@.] switch backend to local
2022/09/13 13:11:15 [DEBUG] [executor@.]$ terraform init -input=false -no-color -reconfigure
2022/09/13 13:11:20 [DEBUG] [executor@.] failed to run command: (*exec.ExitError)(0xc0000d8040)(exit status 1)
failed to switch backend to local: failed to run command (exited 1): terraform init -input=false -no-color -reconfigure
stdout:
Initializing modules...

Initializing Terraform Cloud...

stderr:

Error: Invalid command-line option

The -reconfigure option is for in-place reconfiguration of state backends
only, and is not needed when changing Terraform Cloud settings.

When using Terraform Cloud, initialization automatically activates any new
Cloud configuration settings.

Feature request: ability to skip plan on (e.g. force) migrations

Thanks for the very useful tool.

Background

I'm using the force option in most of my migrations, because while I appreciate the safety of planning and looking for no changes, I find I often want to break up my migration files, and end up applying more than one for the same TF code change (i.e. the TF code has changes A, B, C, and there are three migration files that correspond; if tfmigrate goes to plan before all have happened it'll show "expected" diff changes).

Another piece of context: I have a Terraform configuration that can take three minutes to perform terraform plan (it's quite slow 😢 ), even with TF_CLI_ARGS_plan="-target=module.blah.module.blah" to try to speed it up (🤔 I guess I should try TF_CLI_ARGS_plan="-refresh=false" too while I'm at it - most of the plan time is state refresh time.)

Request

  1. While tfmigrate is running plan, there's no log feedback, even at trace level (it looks hung at [INFO] [migrator@.] check diffs, even though it's busy running plan in the background), and tfmigrate doesn't output what terraform plan commands it is running.
    • It would be nice to have some indication of progress
  2. (I assume) the plan information isn't used anyway (because force = true) - and this happens for each migration (so if I'm splitting a migration into 3 logical "parts", to apply all 3 will cost me 3 times the terraform plan time)
    • It would be nice to be able to skip the plan, and have a mode to use tfmigrate to only apply state migrations (without checking their effects in the same tool)

Workaround

In the meantime, I figure I can make an exec wrapper that just exits when asked to plan, and use it with TFMIGRATE_EXEC_PATH - so I'm not completely blocked or anything

[Feature Request] Besides Terraform, I would like to migrate state by using Terragrunt.

sorry about bad english.

The terragrunt divides into several modules and dry the code when running plan and apply.
But I found out that it is not compatible with tfmigrate.

When managing terraforms in multiple modules, the remote state declaration can be redundant and duplication.
Instead of declaring this setting, terragrunt helps you use it simply.

# ./service/terragrunt.hcl
dependency "network" {
  config_path = "../network"
}
# terragrunt inserts a dependency output(from data remote state) with variable vpc_id within the terraform.
inputs = {
  vpc_id = dependency.network.output.vpc_id
}

# ./service/vars.tf
variable "vpc_id" {}

However, if I just try to run it on tfmigrate, the terraform plan fails because it does not have variable.

I think it's simple to run a terragrunt plan in that folder.
Will it be difficult to support this?

migration crashes with workspace after getting remote state

I'm trying to create migration for remote state using terraform 0.13.6. Currently the migration crashes. Looks like it does not find the workspace anymore after getting the remote state.

Env: OSX
Dir: .
Terraform is inited with remote state

$terraform workspace show
cf
$ tfmigrate plan migration_1.hcl

2021/01/11 11:03:50 [INFO] [runner] load migration file: migration_1.hcl
2021/01/11 11:03:50 [INFO] [migrator] start state migrator plan
2021/01/11 11:03:52 [INFO] [migrator@.] terraform version: 0.13.6
2021/01/11 11:03:52 [INFO] [migrator@.] initialize work dir
2021/01/11 11:04:03 [INFO] [migrator@.] get the current remote state
2021/01/11 11:04:11 [INFO] [migrator@.] override backend to local
2021/01/11 11:04:11 [INFO] [executor@.] create an override file
2021/01/11 11:04:11 [INFO] [executor@.] switch backend to local
failed to switch backend to local: failed to run command (exited 1): terraform init -input=false -no-color -reconfigure
stdout:
Initializing modules...

Initializing the backend...

Successfully configured the backend "local"! Terraform will automatically
use this backend unless the backend configuration changes.

The currently selected workspace (cf) does not exist.
This is expected behavior when the selected workspace did not have an
existing non-empty state. Please enter a number to select a workspace:

  1. default

Enter a value:

stderr:

Error: Failed to select workspace: input not a valid number

[Feature request] extra args injection to terraform command

Hello,
Your tool looks promising but to make it even more flexible I would like to suggest following new feature:

Is it possible to add configuration which allows flags to be passed to terraform plan/apply command.

Use case:
We use .tfvars files which holds values for each environment so when terraform plan or terraform apply
is executed it will need -var-file parameter which have environment specific values.
For example final apply command looks like:

terraform plan -var-file=devel.tfvars

Is it possible to add configuration parameter to .tfmigrate.hcl or directly tfmigrate command which enables these
extra parameters injected to terraform plan/apply calls?

Suggestion:

tfmigrate {
  migration_dir = "./migrations"
  plan_extra_args = "-var-file=somevars.tfvars"
  apply_extra_args="-var-file=somevars.tfvars"
}

[Feature Request] Supports workspaces and var files

Thank you for creating a great tool.

This tool allows you to specify directories using dir, from_dir, and to_dir.
But that doesn't work if you want to specify a workspace.

For example, this would be the case if you want to terraform workspace select hogehuga for each directory.

$ terraform init -reconfigure
$ terraform workspace select workspace1 # I want to add this operation
$ terraform state pull > tmp.tfstate

# switch backend to local ...

$ terraform init -reconfigure
$ terraform state mv -state=tmp.tfstate aws_security_group.foo aws_security_group.foo1
$ terraform plan -state=tmp.tfstate -detailed-exitcode

# switch backend to remote ...

$ terraform init -reconfigure
$ terraform workspace select workspace1 # I want to add this operation too
$ terraform state push tmp.tfstate

It would be nice to have the option to specify workspace and var_file for each type, but what do you think?

example

migration "state" "test" {
  dir = "dir1"
  workspace = "workspace1"
  var_file = "./variable. tfvars"
  actions = [
    "rm aws_security_group.baz",
  ]
}

migration "multi_state" "mv_dir1_dir2" {
  from_dir = "dir1"
  from_workspace = "workspace1"
  to_dir = "dir2"
  to_workspace = "workspace2"
  actions = [
    "mv aws_security_group.foo aws_security_group.foo2",
    "mv aws_security_group.bar aws_security_group.bar2",
  ]
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.