{"id":13505875,"url":"https://github.com/minamijoyo/tfmigrate","last_synced_at":"2025-05-14T12:11:20.741Z","repository":{"id":38353265,"uuid":"275169025","full_name":"minamijoyo/tfmigrate","owner":"minamijoyo","description":"A Terraform / OpenTofu state migration tool for GitOps","archived":false,"fork":false,"pushed_at":"2025-04-28T02:00:47.000Z","size":701,"stargazers_count":1200,"open_issues_count":18,"forks_count":63,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-28T03:19:55.219Z","etag":null,"topics":["gitops","go","opentofu","terraform","tfstate"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/minamijoyo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-06-26T13:59:27.000Z","updated_at":"2025-04-28T02:00:54.000Z","dependencies_parsed_at":"2024-04-16T10:35:32.349Z","dependency_job_id":"9b58d846-a40b-4ed0-ac0d-2e7d4fd76c0a","html_url":"https://github.com/minamijoyo/tfmigrate","commit_stats":{"total_commits":518,"total_committers":21,"mean_commits":"24.666666666666668","dds":"0.18725868725868722","last_synced_commit":"e3817a877de8d9feb240abdf75dcc3b12f4b67df"},"previous_names":[],"tags_count":48,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minamijoyo%2Ftfmigrate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minamijoyo%2Ftfmigrate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minamijoyo%2Ftfmigrate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/minamijoyo%2Ftfmigrate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/minamijoyo","download_url":"https://codeload.github.com/minamijoyo/tfmigrate/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254140760,"owners_count":22021219,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gitops","go","opentofu","terraform","tfstate"],"created_at":"2024-08-01T00:01:15.992Z","updated_at":"2025-05-14T12:11:20.728Z","avatar_url":"https://github.com/minamijoyo.png","language":"Go","funding_links":[],"categories":["Go","Tools","terraform","Terraform"],"sub_categories":["State","Community providers","Kubernetes // Storage"],"readme":"# tfmigrate\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![GitHub release](https://img.shields.io/github/release/minamijoyo/tfmigrate.svg)](https://github.com/minamijoyo/tfmigrate/releases/latest)\n[![GoDoc](https://godoc.org/github.com/minamijoyo/tfmigrate/tfmigrate?status.svg)](https://godoc.org/github.com/minamijoyo/tfmigrate)\n\nA Terraform / OpenTofu state migration tool for GitOps.\n\n## Table of content\n\u003c!--ts--\u003e\n   * [Features](#features)\n   * [Why?](#why)\n   * [Requirements](#requirements)\n   * [Getting Started](#getting-started)\n   * [Install](#install)\n      * [Homebrew](#homebrew)\n      * [Download](#download)\n      * [Source](#source)\n   * [Usage](#usage)\n   * [Configurations](#configurations)\n      * [Environment variables](#environment-variables)\n      * [Configuration file](#configuration-file)\n         * [tfmigrate block](#tfmigrate-block)\n         * [history block](#history-block)\n         * [storage block](#storage-block)\n         * [storage block (local)](#storage-block-local)\n         * [storage block (s3)](#storage-block-s3)\n         * [storage block (gcs)](#storage-block-gcs)\n   * [Migration file](#migration-file)\n      * [Environment Variables](#environment-variables-1)\n      * [migration block](#migration-block)\n      * [migration block (state)](#migration-block-state)\n         * [state mv](#state-mv)\n         * [state xmv](#state-xmv)\n         * [state rm](#state-rm)\n         * [state import](#state-import)\n         * [state replace-provider](#state-replace-provider)\n      * [migration block (multi_state)](#migration-block-multi_state)\n         * [multi_state mv](#multi_state-mv)\n         * [multi_state xmv](#multi_state-xmv)\n   * [Integrations](#integrations)\n   * [License](#license)\n\u003c!--te--\u003e\n\n## Features\n\n- GitOps friendly: Write terraform state mv/rm/import commands in HCL, plan and apply it.\n- Monorepo style support: Move resources to other tfstates to split and merge easily for refactoring.\n- 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.\n- Migration history: Keep track of which migrations have been applied and apply all unapplied migrations in sequence.\n\nYou can apply terraform state operations in a declarative way.\n\nIn short, write the following migration file and save it as `state_mv.hcl`:\n\n```hcl\nmigration \"state\" \"test\" {\n  dir = \"dir1\"\n  actions = [\n    \"mv aws_security_group.foo aws_security_group.foo2\",\n    \"mv aws_security_group.bar aws_security_group.bar2\",\n  ]\n}\n```\n\nThen, apply it:\n\n```\n$ tfmigrate apply state_mv.hcl\n```\n\nIt works as you expect, but it's just a text file, so you can commit it to git.\n\n## Why?\n\nIf 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.\n\nIn 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.\nHowever, 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.\n\nTo fit into the GitOps workflow, the answer is obvious. We should commit all terraform state operations to git.\nThis brings us to a new paradigm, that is to say, Terraform state operation as Code!\n\n## Requirements\n\nThe tfmigrate invokes `terraform` or `tofu` command under the hood. This is because we want to support multiple Terraform / OpenTofu versions in a stable way.\n\n### Terraform\n\nThe minimum required version is Terraform v0.12 or higher, but we recommend the Terraform v1.x.\n\n### OpenTofu\n\nIf you want to use OpenTofu, a community fork of Terraform, you need to set the environment variable `TFMIGRATE_EXEC_PATH` to `tofu`.\n\nThe minimum required version is OpenTofu v1.6 or higher.\n\n### Terragrunt\n\n#### Without dynamic state\n\nIf you are not leveraging terragrunt's [dynamic state generation](https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#remote_state), the environment variable `TF_MIGRATE_EXEC_PATH` must be set based on your Terragrunt version.\n\n- For Terragrunt \u003c v0.73.0:\n```shell\n# As part of the command or via exporting the variable to your shell.\nTFMIGRATE_EXEC_PATH=terragrunt tfmigrate $OTHEROPTIONS\n```\n- For Terragrunt \u003e= v0.73.0 (due to the CLI redesign):\n```shell\n# As part of the command or via exporting the variable to your shell.\nTFMIGRATE_EXEC_PATH=\"terragrunt run --\" tfmigrate $OTHEROPTIONS\n```\n\n#### With dynamic state\n\nAs tfmigrate uses the local backend for planning, some command line flags for the remote state backend cannot be used. If you are leveraging terragrunt's [dynamic state generation](https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#remote_state), the `remote_state` block must include a `generate` block.\n\n```hcl\nremote_state {\n  backend = \"s3\"\n\n  config = {\n    bucket         = \"highway-terraform-state\"\n    # Other config here\n  }\n  # This ensures that a file instead of command line flags are used.\n  # allowing tfmigrate to work as expected.\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n}\n```\n\n## Getting Started\n\nAs 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.\n\nBuild a sandbox environment with docker compose and run bash:\n\n```\n$ git clone https://github.com/minamijoyo/tfmigrate\n$ cd tfmigrate/\n$ docker compose build\n$ docker compose run --rm tfmigrate /bin/bash\n```\n\nIn the sandbox environment, create and initialize a working directory from test fixtures:\n\n```\n# mkdir -p tmp \u0026\u0026 cp -pr test-fixtures/backend_s3 tmp/dir1 \u0026\u0026 cd tmp/dir1\n# terraform init\n# cat main.tf\n```\n\nThis example contains two `aws_security_group` resources:\n\n```hcl\nresource \"aws_security_group\" \"foo\" {\n  name = \"foo\"\n}\n\nresource \"aws_security_group\" \"bar\" {\n  name = \"bar\"\n}\n```\n\nApply it and confirm that the state of resources are stored in the tfstate:\n\n```\n# terraform apply -auto-approve\n# terraform state list\naws_security_group.bar\naws_security_group.foo\n```\n\nNow, let's rename `aws_security_group.foo` to `aws_security_group.baz`:\n\n```\n# cat \u003c\u003c EOF \u003e main.tf\nresource \"aws_security_group\" \"baz\" {\n  name = \"foo\"\n}\n\nresource \"aws_security_group\" \"bar\" {\n  name = \"bar\"\n}\nEOF\n```\n\nAt this point, of course, there are differences in the plan:\n\n```\n# terraform plan\n(snip.)\nPlan: 1 to add, 0 to change, 1 to destroy.\n```\n\nNow it's time for tfmigrate. Create a migration file:\n\n```\n# cat \u003c\u003c EOF \u003e tfmigrate_test.hcl\nmigration \"state\" \"test\" {\n  actions = [\n    \"mv aws_security_group.foo aws_security_group.baz\",\n  ]\n}\nEOF\n```\n\nRun `tfmigrate plan` to check to see if `terraform plan` has no changes after the migration without updating remote tfstate:\n\n```\n# tfmigrate plan tfmigrate_test.hcl\n(snip.)\nYYYY/MM/DD hh:mm:ss [INFO] [migrator] state migrator plan success!\n# echo $?\n0\n```\n\nThe 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`:\n\n```\n# TFMIGRATE_LOG=DEBUG tfmigrate plan tfmigrate_test.hcl\n```\n\nIf looks good, apply it:\n\n```\n# tfmigrate apply tfmigrate_test.hcl\n(snip.)\nYYYY/MM/DD hh:mm:ss [INFO] [migrator] state migrator apply success!\n# echo $?\n0\n```\n\nThe apply command computes a new state and pushes it to remote state.\nIt will fail if terraform plan detects any diffs with the new state.\n\nYou can confirm the latest remote state has no changes with terraform plan:\n\n```\n# terraform plan\n(snip.)\nNo changes. Infrastructure is up-to-date.\n\n# terraform state list\naws_security_group.bar\naws_security_group.baz\n```\n\nThere is no magic. The tfmigrate just did the boring work for you.\n\nFurthermore, you can also move resources to another directory. Let's split the tfstate in two.\nCreate a new empty directory with a different remote state path:\n\n```\n# mkdir dir2\n# cat config.tf | sed 's/test\\/terraform.tfstate/dir2\\/terraform.tfstate/' \u003e dir2/config.tf\n```\n\nMove the resource definition of `aws_security_group.baz` in `main.tf` to `dir2/main.tf` and rename it to `aws_security_group.baz2`:\n\n```\n# cat \u003c\u003c EOF \u003e main.tf\nresource \"aws_security_group\" \"bar\" {\n  name = \"bar\"\n}\nEOF\n\n# cat \u003c\u003c EOF \u003e dir2/main.tf\nresource \"aws_security_group\" \"baz2\" {\n  name = \"foo\"\n}\nEOF\n```\n\nCreate a `multi_state` migration file:\n\n```\n# cat \u003c\u003c EOF \u003e tfmigrate_multi_state_test.hcl\nmigration \"multi_state\" \"test\" {\n  from_dir = \".\"\n  to_dir   = \"dir2\"\n\n  actions = [\n    \"mv aws_security_group.baz aws_security_group.baz2\",\n  ]\n}\nEOF\n```\n\nRun tfmigrate plan \u0026 apply:\n\n```\n# tfmigrate plan tfmigrate_multi_state_test.hcl\n# tfmigrate apply tfmigrate_multi_state_test.hcl\n```\n\nYou can see the tfstate was split in two:\n\n```\n# terraform state list\naws_security_group.bar\n# cd dir2 \u0026\u0026 terraform state list\naws_security_group.baz2\n```\n\n## Install\n\n### Homebrew\n\nIf you are macOS user:\n\n```\n$ brew install tfmigrate\n```\n\n### Download\n\nDownload the latest compiled binaries and put it anywhere in your executable path.\n\nhttps://github.com/minamijoyo/tfmigrate/releases\n\n### Source\n\nIf you have Go 1.24+ development environment:\n\n```\n$ git clone https://github.com/minamijoyo/tfmigrate\n$ cd tfmigrate/\n$ make install\n$ tfmigrate --version\n```\n\n## Usage\n\n```\n$ tfmigrate --help\nUsage: tfmigrate [--version] [--help] \u003ccommand\u003e [\u003cargs\u003e]\n\nAvailable commands are:\n    apply    Compute a new state and push it to remote state\n    list     List migrations\n    plan     Compute a new state\n```\n\n```\n$ tfmigrate plan --help\nUsage: tfmigrate plan [PATH]\n\nPlan computes a new state by applying state migration operations to a temporary state.\nIt will fail if terraform plan detects any diffs with the new state.\n\nArguments:\n  PATH                     A path of migration file\n                           Required in non-history mode. Optional in history-mode.\n\nOptions:\n  --config                 A path to tfmigrate config file\n  --backend-config=path    A backend configuration, a path to backend configuration file or\n                           key=value format backend configuraion.\n                           This option is passed to terraform init when switching backend to remote.\n\n  --out=path               Save a plan file after dry-run migration to the given path.\n                           Note that the saved plan file is not applicable in Terraform 1.1+.\n                           It's intended to use only for static analysis.\n```\n\n```\n$ tfmigrate apply --help\nUsage: tfmigrate apply [PATH]\n\nApply computes a new state and pushes it to remote state.\nIt will fail if terraform plan detects any diffs with the new state.\n\nArguments\n  PATH                     A path of migration file\n                           Required in non-history mode. Optional in history-mode.\n\nOptions:\n  --config                 A path to tfmigrate config file\n  --backend-config=path    A backend configuration, a path to backend configuration file or\n                           key=value format backend configuraion.\n                           This option is passed to terraform init when switching backend to remote.\n```\n\n```\n$ tfmigrate list --help\nUsage: tfmigrate list\n\nList migrations.\n\nOptions:\n  --config           A path to tfmigrate config file\n  --status           A filter for migration status\n                     Valid values are as follows:\n                       - all (default)\n                       - unapplied\n```\n\n## Configurations\n### Environment variables\n\nYou can customize the behavior by setting environment variables.\n\n- `TFMIGRATE_LOG`: A log level. Valid values are `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. Default to `INFO`.\n- `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`.\n- `TFMIGRATE_CONFIG`: A path to the tfmigrate configuration file. Default to `.tfmigrate.hcl`.\n\nSome history storage implementations may read additional cloud provider-specific environment variables. For details, refer to a configuration file section for storage block described below.\n\n### Configuration file\n\nYou can customize the behavior by setting a configuration file.\nThe path of configuration file defaults to `.tfmigrate.hcl`. You can change it with command line flag `--config` or `TFMIGRATE_CONFIG` environment variable.\n\nThe syntax of configuration file is as follows:\n\n- A configuration file must be written in the HCL2.\n- The extension of file must be `.hcl`(for HCL native syntax) or `.json`(for HCL JSON syntax).\n- The file must contain exactly one `tfmigrate` block.\n\nAn example of configuration file is as follows.\n\n```hcl\ntfmigrate {\n  migration_dir = \"./tfmigrate\"\n  is_backend_terraform_cloud = true\n  history {\n    storage \"s3\" {\n      bucket = \"tfmigrate-test\"\n      key    = \"tfmigrate/history.json\"\n    }\n  }\n}\n```\n\nEnvironment variables can be accessed in the configuration file via the `env` variable:\n\n```hcl\ntfmigrate {\n  migration_dir = \"./tfmigrate\"\n  is_backend_terraform_cloud = true\n  history {\n    storage \"s3\" {\n      bucket = \"tfmigrate-test\"\n      key    = \"tfmigrate/${env.VAR_NAME}/history.json\"\n    }\n  }\n}\n```\n\n#### is_backend_terraform_cloud\nWhether the remote backend specified in Terraform files references a\n[terraform cloud remote backend](https://www.terraform.io/language/settings/terraform-cloud),\nin particular specified as a `cloud` block within the `terraform` config block. This backend\ntype was introduced in Terraform 1.1.+ and is the recommended way to specify a Terraform backend.\nAttribute defaults to `false`.\n\nNote that when using tfmigrate with Terraform Cloud, you also need to set a workspace name in a migration file.\n\n#### tfmigrate block\n\nThe `tfmigrate` block has the following attributes:\n\n- `migration_dir` (optional): A path to directory where migration files are stored. Default to `.` (current directory).\n\nThe `tfmigrate` block has the following blocks:\n\n- `history` (optional): Keep track of which migrations have been applied.\n\n#### history block\n\nThe `history` block has the following blocks:\n\n- `storage` (required): A migration history data store\n\n#### storage block\n\nThe storage block has one label, which is a type of storage. Valid types are as follows:\n\n- `local`: Save a history file to local filesystem.\n- `s3`: Save a history file to AWS S3.\n- `gcs`: Save a history file to GCS (Google Cloud Storage).\n\nIf 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.\n\n#### storage block (local)\n\nThe `local` storage has the following attributes:\n\n- `path` (required): A path to a migration history file.\n\nAn example of configuration file is as follows.\n\n```hcl\ntfmigrate {\n  migration_dir = \"./tfmigrate\"\n  history {\n    storage \"local\" {\n      path = \"tmp/history.json\"\n    }\n  }\n}\n```\n\n#### storage block (s3)\n\nThe `s3` storage has the following attributes:\n\n- `bucket` (required): Name of the bucket.\n- `key` (required): Path to the migration history file.\n- `region` (optional): AWS region. This can also be sourced from the `AWS_DEFAULT_REGION` and `AWS_REGION` environment variables.\n- `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.\n- `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.\n- `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.\n- `role_arn` (optional): Amazon Resource Name (ARN) of the IAM Role to assume.\n- `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](https://www.terraform.io/language/settings/backends/s3#kms_key_id).\n\nThe following attributes are also available, but they are intended to use with `localstack` for testing.\n\n- `endpoint` (optional): Custom endpoint for the AWS S3 API.\n- `skip_credentials_validation` (optional): Skip credentials validation via the STS API.\n- `skip_metadata_api_check` (optional): Skip usage of EC2 Metadata API.\n- `force_path_style` (optional): Enable path-style S3 URLs (`https://\u003cHOST\u003e/\u003cBUCKET\u003e` instead of `https://\u003cBUCKET\u003e.\u003cHOST\u003e`).\n\nAn example of configuration file is as follows.\n\n```hcl\ntfmigrate {\n  migration_dir = \"./tfmigrate\"\n  history {\n    storage \"s3\" {\n      bucket  = \"tfmigrate-test\"\n      key     = \"tfmigrate/history.json\"\n      region  = \"ap-northeast-1\"\n      profile = \"dev\"\n    }\n  }\n}\n```\n\n#### storage block (gcs)\n\nThe `gcs` storage has the following attributes:\n\n- `bucket` (required): Name of the bucket.\n- `name` (required): Path to the migration history file.\n\nNote that this storage implementation refers the Application Default Credentials (ADC) for authentication.\n\nAn example of configuration file is as follows.\n\n```hcl\ntfmigrate {\n  migration_dir = \"./tfmigrate\"\n  history {\n    storage \"gcs\" {\n      bucket = \"tfstate-test\"\n      name   = \"tfmigrate/history.json\"\n    }\n  }\n}\n```\n\nIf 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](https://pkg.go.dev/cloud.google.com/go/storage).\n\n## Migration file\n\nYou can write terraform state operations in HCL. The syntax of migration file is as follows:\n\n- A migration file must be written in the HCL2.\n- The extension of file must be `.hcl`(for HCL native syntax) or `.json`(for HCL JSON syntax).\n\nAlthough 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`)\n\nAn example of migration file is as follows.\n\n```hcl\nmigration \"state\" \"test\" {\n  dir = \"dir1\"\n  actions = [\n    \"mv aws_security_group.foo aws_security_group.foo2\",\n    \"mv aws_security_group.bar aws_security_group.bar2\",\n  ]\n}\n```\n\nThe above example is written in HCL native syntax, but you can also write them in HCL JSON syntax.\nThis is useful when generating a migration file from other tools.\n\n```json\n{\n  \"migration\": {\n    \"state\": {\n      \"test\": {\n        \"dir\": \"dir1\",\n        \"actions\": [\n          \"mv aws_security_group.foo aws_security_group.foo2\",\n          \"mv aws_security_group.bar aws_security_group.bar2\"\n        ]\n      }\n    }\n  }\n}\n```\n\nIf you want to move a resource using `for_each`, you need to escape as follows:\n\n```hcl\nmigration \"state\" \"test\" {\n  dir = \"dir1\"\n  actions = [\n    \"mv aws_security_group.foo[0] 'aws_security_group.foo[\\\"baz\\\"]'\",\n  ]\n}\n```\n\n### Environment Variables\n\nEnvironment variables can be accessed in migration files via the `env` variable:\n\n```hcl\nmigration \"state\" \"test\" {\n  dir = \"dir1\"\n  workspace = env.TFMIGRATE_WORKSPACE\n  actions = [\n    \"mv aws_security_group.foo aws_security_group.foo2\"\n  ]\n}\n```\n\n### migration block\n\n- The file must contain exactly one `migration` block.\n- The first label is the migration type. There are two types of `migration` block, `state` and `multi_state`, and specify one of them.\n- The second label is the migration name, which is an arbitrary string.\n\nThe file must contain only one block, and multiple blocks are not allowed, because it's hard to re-run the file if partially failed.\n\n### migration block (state)\n\nThe `state` migration updates the state in a single directory. It has the following attributes.\n\n- `dir` (optional): A working directory for executing terraform command. Default to `.` (current directory).\n- `workspace` (optional): A terraform workspace. Defaults to \"default\".\n- `actions` (required): Actions is a list of state action. An action is a plain text for state operation. Valid formats are the following.\n  - `\"mv \u003csource\u003e \u003cdestination\u003e\"`\n  - `\"xmv \u003csource\u003e \u003cdestination\u003e\"`\n  - `\"rm \u003caddresses\u003e...`\n  - `\"import \u003caddress\u003e \u003cid\u003e\"`\n  - `\"replace-provider \u003caddress\u003e \u003caddress\u003e\"`\n- `force` (optional): Apply migrations even if plan show changes\n- `skip_plan` (optional): If true, `tfmigrate` will not perform and analyze a `terraform plan`.\n\nNote that `dir` is relative path to the current working directory where `tfmigrate` command is invoked.\n\nWe 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.\n\nExamples of migration block (state) are as follows.\n\n#### state mv\n\n```hcl\nmigration \"state\" \"test\" {\n  dir = \"dir1\"\n  actions = [\n    \"mv aws_security_group.foo aws_security_group.foo2\",\n    \"mv aws_security_group.bar aws_security_group.bar2\",\n  ]\n}\n```\n\n#### state xmv\n\nThe `xmv` command works like the `mv` command but allows usage of wildcards `*` in the source definition.\nThe source expressions will be matched against resources defined in the terraform state.\nThe matched value can be used in the destination definition via a dollar sign and their ordinal number (e.g. `$1`, `$2`, ...).\nWhen 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}`).\n\nFor example if `foo` and `bar` in the `mv` command example above are the only 2 security group resources\ndefined at the top level then you can rename them using:\n\n```hcl\nmigration \"state\" \"test\" {\n  dir = \"dir1\"\n  actions = [\n    \"xmv aws_security_group.* aws_security_group.$${1}2\",\n  ]\n}\n```\n\n#### state rm\n\n```hcl\nmigration \"state\" \"test\" {\n  dir = \"dir1\"\n  actions = [\n    \"rm aws_security_group.baz\",\n  ]\n}\n```\n\n#### state import\n\n```hcl\nmigration \"state\" \"test\" {\n  dir = \"dir1\"\n  actions = [\n    \"import aws_security_group.qux qux\",\n  ]\n}\n```\n\n#### state replace-provider\n\n```hcl\nmigration \"state\" \"test\" {\n  dir = \"dir1\"\n  actions = [\n    \"replace-provider registry.terraform.io/-/null registry.terraform.io/hashicorp/null\",\n  ]\n}\n```\n\n### migration block (multi_state)\n\nThe `multi_state` migration updates states in two different directories. It is intended for moving resources across states. It has the following attributes.\n\n- `from_dir` (required): A working directory where states of resources move from.\n- `from_skip_plan` (optional): If true, `tfmigrate` will not perform and analyze a `terraform plan` in the `from_dir`.\n- `from_workspace` (optional): A terraform workspace in the FROM directory. Defaults to \"default\".\n- `to_dir` (required): A working directory where states of resources move to.\n- `to_skip_plan` (optional): If true, `tfmigrate` will not perform and analyze a `terraform plan` in the `to_dir`.\n- `to_workspace` (optional): A terraform workspace in the TO directory. Defaults to \"default\".\n- `actions` (required): Actions is a list of multi state action. An action is a plain text for state operation. Valid formats are the following.\n  - `\"mv \u003csource\u003e \u003cdestination\u003e\"`\n  - `\"xmv \u003csource\u003e \u003cdestination\u003e\"`\n- `force` (optional): Apply migrations even if plan show changes\n\nNote that `from_dir` and `to_dir` are relative path to the current working directory where `tfmigrate` command is invoked.\n\nExample of migration block (multi_state) are as follows.\n\n#### multi_state mv\n\n```hcl\nmigration \"multi_state\" \"mv_dir1_dir2\" {\n  from_dir = \"dir1\"\n  to_dir   = \"dir2\"\n  actions = [\n    \"mv aws_security_group.foo aws_security_group.foo2\",\n    \"mv aws_security_group.bar aws_security_group.bar2\",\n  ]\n}\n```\n\n#### multi_state xmv\n\nThe `xmv` command works like the `mv` command but allows usage of\nwildcards `*` in the source definition.\nThe wildcard expansion rules are the same as for the single state xmv.\n\n```hcl\nmigration \"multi_state\" \"mv_dir1_dir2\" {\n  from_dir = \"dir1\"\n  to_dir   = \"dir2\"\n  actions = [\n    \"xmv aws_security_group.* aws_security_group.$${1}2\",\n  ]\n}\n```\n\nIf you want to move all resources to another dir for merging two tfstates, you can write something like this:\n\n```hcl\nmigration \"multi_state\" \"merge_dir1_to_dir2\" {\n  from_dir = \"dir1\"\n  to_dir   = \"dir2\"\n  actions = [\n    \"xmv * $1\",\n  ]\n}\n```\n\n## Integrations\n\nYou can integrate tfmigrate with your favorite CI/CD services. Examples are as follows:\n\n- [Atlantis](https://github.com/runatlantis/atlantis): [minamijoyo/tfmigrate-atlantis-example](https://github.com/minamijoyo/tfmigrate-atlantis-example)\n- [Digger](https://github.com/diggerhq/digger): [minamijoyo/tfmigrate-digger-example](https://github.com/minamijoyo/tfmigrate-digger-example)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fminamijoyo%2Ftfmigrate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fminamijoyo%2Ftfmigrate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fminamijoyo%2Ftfmigrate/lists"}