github.com/pix4d/terravalet@v0.8.1-0.20240131132849-abcd6a79eeeb/README.md (about)

     1  # Terravalet
     2  
     3  A tool to help with advanced, low-level [Terraform](https://www.terraform.io/) operations:
     4  
     5  - Rename resources within the same Terraform state, with optional fuzzy match.
     6  - Move resources from one Terraform state to another.
     7  - Import existing resources into Terraform state.
     8  - Remove existing resources from Terraform state.
     9  
    10  **DISCLAIMER Manipulating Terraform state is inherently dangerous. It is your responsibility to be careful and ensure you UNDERSTAND what you are doing**.
    11  
    12  ## Status
    13  
    14  This is BETA code, although we already use it in production.
    15  
    16  The project follows [semantic versioning](https://semver.org/). In particular, we are currently at major version 0: anything MAY change at any time. The public API SHOULD NOT be considered stable.
    17  
    18  ## Overall approach and migration scripts
    19  
    20  The overall approach is for Terravalet to generate migration scripts, not to perform any change directly. This for two reasons:
    21  
    22  1. Safety. The operator can review the migration scripts for correctness.
    23  2. Gitops-style. The migration scripts are meant to be stored in git in the same branch (and thus same PR) that performs the Terraform changes and can optionally be hooked to an automatic deployment system.
    24  
    25  Terravalet takes as input the output of `terraform plan` for each involved root module and generates one UP and one DOWN migration script.
    26  
    27  ### Remote and local state
    28  
    29  At least until Terraform 0.14, `terraform state mv` has a bug: if a remote backend for the state is configured (which will always be the case for prod), it will remove entries from the remote state, but it will not add entries to it.
    30  It will fail silently and leave an empty backup file, so you will lose your state.
    31  
    32  For this reason Terravalet operates on local state and leaves to the operator the task of performing `terraform state pull` and `terraform state push`.
    33  
    34  ### Terraform workspaces
    35  
    36  Be careful when using Terraform workspaces, since they are invisible and persistent global state :-(. Remember to always explicitly run `terraform workspace select` before anything else.
    37  
    38  ### Interactions with the "moved" block
    39  
    40  After the creation of Terravalet, Terraform introduced the `moved` block, which can be seen as an alternative to certain usages of Terravalet. See [Terraform: refactoring](https://developer.hashicorp.com/terraform/language/modules/develop/refactoring)) for more information.
    41  
    42  ## Install
    43  
    44  ### Install from binary package
    45  
    46  1. Download the archive for your platform from the [releases page](https://github.com/Pix4D/terravalet/releases).
    47  2. Unarchive and copy the `terravalet` executable to a directory in your `$PATH`.
    48  
    49  ### Install from source
    50  
    51  1. Install [Go](https://golang.org/).
    52  2. Install [Task](https://taskfile.dev/).
    53  3. Run `task`:
    54     ```
    55     $ task test build
    56     ```
    57  4. Copy the executable `bin/terravalet` to a directory in your `$PATH`.
    58  
    59  ## Usage
    60  
    61  Terravalet supports multiple operations:
    62  
    63  - [Rename resources](#rename-resources-within-the-same-state) within the same Terraform state, with optional fuzzy match.
    64  - [Move resources](#-move-resources-from-one-state-to-another) from one Terraform state to another.
    65  - [Import existing resources](#-import-existing-resources) into Terraform state.
    66  - [Remove existing resources](#removing-existing-resources) from Terraform state.
    67  
    68  They will be explained in the following sections.
    69  
    70  You can also look at the tests and in particular at the files below [testdata/](testdata) for a rough idea.
    71  
    72  # Rename resources within the same state
    73  
    74  Only one Terraform root module (and thus only one state) is involved. This actually covers two different use cases:
    75  
    76  1. Renaming resources within the same root module.
    77  2. Moving resources to/from a non-root Terraform module (this will actually _rename_ the resources, since they will get or lose the `module.` prefix).
    78  
    79  ## Collect information and remote state
    80  
    81  ```
    82  $ cd $ROOT_MODULE
    83  $ terraform workspace select $WS
    84  $ terraform plan -no-color 2>&1 | tee plan.txt
    85  
    86  $ terraform state pull > local.tfstate
    87  $ cp local.tfstate local.tfstate.BACK
    88  ```
    89  
    90  The backup is needed to recover in case of errors. It must be done now.
    91  
    92  ## Generate migration scripts: exact match, success
    93  
    94  Take as input the Terraform plan `plan.txt` (explicit) and the local state `local.tfstate` (implicit) and generate UP and DOWN migration scripts:
    95  
    96  ```
    97  $ terravalet rename \
    98      --plan plan.txt --up 001_TITLE.up.sh --down 001_TITLE.down.sh
    99  ```
   100  
   101  ## Generate migration scripts: exact match, failure
   102  
   103  Depending on _how_ the elements have been renamed in the Terraform configuration, it is possible that the exact match will fail:
   104  
   105  ```
   106  $ terravalet rename \
   107      --plan plan.txt --up 001_TITLE.up.sh --down 001_TITLE.down.sh
   108  match_exact:
   109  unmatched create:
   110    aws_route53_record.private["foo"]
   111  unmatched destroy:
   112    aws_route53_record.foo_private
   113  ```
   114  
   115  In this case, you can attempt fuzzy matching.
   116  
   117  ## Generate migration scripts: fuzzy match
   118  
   119  **WARNING** Fuzzy match can make mistakes. It is up to you to validate that the migration makes sense.
   120  
   121  If the exact match failed, it is possible to enable [q-gram distance](https://github.com/dexyk/stringosim) fuzzy matching with the `-fuzzy-match` flag:
   122  
   123  ```
   124  $ terravalet rename -fuzzy-match \
   125      --plan plan.txt --up 001_TITLE.up.sh --down 001_TITLE.down.sh
   126  WARNING fuzzy match enabled. Double-check the following matches:
   127   9 aws_route53_record.foo_private -> aws_route53_record.private["foo"]
   128  ```
   129  
   130  ## Run the migration script
   131  
   132  1. Review the contents of `001_TITLE.up.sh`.
   133  2. Run it: `sh ./001_TITLE.up.sh`
   134  
   135  ## Push the migrated state
   136  
   137  1. `terraform state push local.tfstate`. In case of error, DO NOT FORCE the push unless you understand very well what you are doing.
   138  
   139  ## Recovery in case of error
   140  
   141  Push the `local.tfstate.BACK`.
   142  
   143  # Move resources from one state to another
   144  
   145  Two Terraform root modules (and thus two states) are involved. The names of the resources stay the same, but we move them from one root module to another.
   146  
   147  ## Understanding move-after and move-before
   148  
   149  Consider root environment (name `1`), represented as list element:
   150  
   151  ![](docs/list.png)
   152  
   153  There are two ways to split it:
   154  
   155  1. By putting some of its contents AFTER itself.
   156    ![](docs/list-append.png)
   157  2. By putting some of its contents BEFORE itself.
   158    ![](docs/list-prepend.png)
   159  
   160  Move AFTER is more frequent and easier to reason about.
   161  
   162  On the other hand, you will know when you need to move BEFORE `terraform plan` in the BEFORE module (1' in the figure above) will fail with a message similar to this one:
   163  
   164  ```
   165  Error: Unsupported attribute
   166  │
   167  │ on main.tf line 11, in resource "null_resource" "res2":
   168  │ 11:     dep = data.terraform_remote_state.prepend_p0.outputs.res1_id
   169  │ data.terraform_remote_state.prepend_p0.outputs is object with 1 attribute "pet"
   170  │
   171  │ This object does not have an attribute named "res1_id".
   172  ╵
   173  ```
   174  
   175  The error is because we didn't run terraform apply in `p0`, so `p0.outputs` doesn't have yet the attribute `res1_id`.
   176  
   177  When this happens and you convince yourself that this is expected and not an error on your part, you can still move the state, by using command `move-before`.
   178  
   179  ## Collect information and remote state
   180  
   181  Perform all operations in `topdir`, the directory containing the two root modules.
   182  
   183  Something like:
   184  
   185  ```
   186  topdir/
   187      BEFORE/    <== root module
   188      AFTER/     <== root module
   189  ```
   190  
   191  ### Collect information
   192  
   193  If this is a terravalet move-after (the default):
   194  
   195  ```
   196  $ terraform -chdir=BEFORE plan -no-color > BEFORE.tfplan
   197  $ terraform -chdir=AFTER  plan -no-color > AFTER.tfplan
   198  ```
   199  
   200  In this case, you can also perform a basic validation: the number of elements to add compared to the number of elements to destroy must be the same:
   201  
   202     ```
   203     $ grep "Plan:" BEFORE.tfplan
   204     Plan: 229 to add, 0 to change, 229 to destroy.
   205     ```
   206  If there is a mismatch, then it means that you have missed something. Go back to editing the Terraform files.
   207  
   208  
   209  If this is a terravalet move-before (special case):
   210  
   211  ```
   212  $ terraform -chdir=BEFORE plan -no-color > BEFORE.tfplan
   213  ```
   214  
   215  ### Collect remote state
   216  
   217  ```
   218  $ terraform -chdir=BEFORE state pull > BEFORE.tfstate
   219  $ terraform -chdir=AFTER  state pull > AFTER.tfstate
   220  ```
   221  
   222  Backup the two remote states. This is needed to recover in case of errors and must be done now.
   223  
   224  ```
   225  $ cp BEFORE.tfstate BEFORE.tfstate.BACK
   226  $ cp AFTER.tfstate AFTER.tfstate.BACK
   227  ```
   228  
   229  ## Generate migration scripts
   230  
   231  Take as input the one or two Terraform plans (BEFORE.tfplan and AFTER.tfplan) and the two state files (BEFORE.tfstate and AFTER.tfstate) and generate the 01-migrate-foo_up.sh and 01-migrate-foo_down.sh migration scripts.
   232  
   233  If move-after:
   234  
   235  ```
   236  $ terravalet move-after  --script=01-migrate-foo --before=BEFORE --after=AFTER
   237  ```
   238  
   239  If move-before:
   240  
   241  ```
   242  $ terravalet move-before --script=01-migrate-foo --before=BEFORE --after=AFTER
   243  ```
   244  
   245  ## Run the migration script
   246  
   247  1. Review the contents of `01-migrate-foo_up.sh`.
   248  2. Run it: `sh ./01-migrate-foo_up.sh`
   249  
   250  ## Push the migrated states
   251  
   252  In case of error, DO NOT FORCE the push unless you understand very well what you are doing.
   253  
   254  ```
   255  $ terraform -chdir=BEFORE state push - < BEFORE.tfstate
   256  $ terraform -chdir=AFTER  state push - < AFTER.tfstate
   257  ```
   258  
   259  ## Recovery in case of error
   260  
   261  Push the two backups:
   262  
   263  ```
   264  $ terraform -chdir=BEFORE state push - < BEFORE.tfstate.BACK
   265  $ terraform -chdir=AFTER  state push - < AFTER.tfstate.BACK
   266  ```
   267  
   268  # Import existing resources
   269  
   270  The `terraform import` command can import existing resources into Terraform state, but requires to painstakingly write by hand the arguments, one per resource. This is error-prone and tedious.
   271  
   272  Thus, `terravalet import` creates the import commands for you.
   273  
   274  You must first add to the Terraform configuration the resources that you want to import, and then import them: neither `terraform` nor `terravalet` are able to write Terraform configuration, they only add to the Terraform state.
   275  
   276  Since each Terraform provider introduces its own resources, it would be impossible for Terravalet to know all of them. Instead, you write a simple [resource definitions file](#writing-a-resource-definitions-file), so that Terravalet can know how to proceed.
   277  
   278  For concreteness, the examples below refer to the [Terraform GitHub provider](https://registry.terraform.io/providers/integrations/github/latest/docs).
   279  
   280  ## Generate a plan in JSON format
   281  
   282  terraform plan:
   283  
   284  ```
   285  $ cd $ROOT_MODULE
   286  $ terraform plan -no-color 2>&1 -out plan.txt
   287  $ terraform show -json plan.txt | tee plan.json
   288  ```
   289  
   290  ## Generate import/remove scripts
   291  
   292  Take as input the Terraform plan in JSON format `plan.json` and generate UP and DOWN import scripts:
   293  
   294  ```
   295  $ terravalet import \
   296      --res-defs  my_definitions.json \
   297      --src-plan  plan.json \
   298      --up import.up.sh --down import.down.sh
   299  ```
   300  
   301  ## Review the scripts
   302  
   303  1. Ensure that the **parent** resources are placed at the top of the `up` script, followed by their **children**.
   304  2. Ensure that the **child** resources are placed at the top of the `down` script, followed by their **parents**.
   305  3. Ensure the correctness of parameters.
   306  
   307  NOTE: The script modifies the remote state, but it is not dangerous because it only imports new resources if they already exist and it doesn't create or destroy anything.
   308  
   309  Terraform will try to import as much as possible, if the corresponding address in state doesn't exist yet, it means it should be created later using `terraform apply`, actually the resource is in `.tf` configuration, but not yet in real world.
   310  
   311  ## Run the import script
   312  
   313  ```
   314  sh ./import.up.sh
   315  ```
   316  
   317  ### Example
   318  
   319  Here is a new plan, scripts have been already generated:
   320  
   321  ```
   322   $ terraform plan
   323   .....
   324   Plan: 6 to add, 0 to change, 0 to destroy.
   325  ```
   326  These are new resources, let's run the import script and run the plan again:
   327  
   328  ```
   329  $ sh import.up.sh
   330  module.github.github_repository.repos["test-import-gh"]: Importing from ID "test-import-gh"...
   331  module.github.github_repository.repos["test-import-gh"]: Import prepared!
   332    Prepared github_repository for import
   333  module.github.github_repository.repos["test-import-gh"]: Refreshing state... [id=test-import-gh]
   334  
   335  Import successful!
   336  .....
   337  ```
   338  
   339  During the run the following error can happen:
   340  
   341  ```
   342  Error: Cannot import non-existent remote object
   343  
   344  While attempting to import an existing object to
   345  github_team_repository.all_teams["test-import-gh.integration"], the provider
   346  detected that no object exists with the given id. Only pre-existing objects
   347  can be imported; check that the id is correct and that it is associated with
   348  the provider's configured region or endpoint, or use "terraform apply" to
   349  create a new remote object for this resource.
   350  ```
   351  
   352  In this specific case the out-of-band resource didn't have a setting yet about teams, so it's normal.
   353  
   354  Next plan should be different:
   355  
   356  ```
   357  $ terraform plan
   358  .....
   359  Plan: 3 to add, 2 to change, 0 to destroy.
   360  ```
   361  
   362  In conclusion, the plan now is close to real resources states and terraform is now aware of them.
   363  In every case plan doesn't contain any `destroy` sentence.
   364  
   365  ## Rollback
   366  
   367  Run `import.down.sh` script that remove the same resources from terraform state that have been imported with `import.up.sh`.
   368  
   369  ## Writing a resource definitions file
   370  
   371  Terravalet doesn't know anything about resources, it just parses the plan and uses the resources configuration file passed via the flag `res-defs`. An example can be found in [testdata/import/terravalet_imports_definitions.json](testdata/import/terravalet_imports_definitions.json).
   372  
   373  The idea is to tell Terravalet where to search the data to build the up/down scripts. The correct information can be found on the [specific provider documentation](https://registry.terraform.io/browse/providers). Under the hood, Terravalet matches the parsed plan and resources definition file.
   374  
   375  1. The JSON resources definition is a map of resources type objects identified by their own name as a key.
   376  2. The resource type object has an optional `priority`: import statement for that resource must be placed at the top of up.sh and at the bottom of down.sh (resources that must be imported before others).
   377  3. The resource type object has an optional `separator`: in case of multiple arguments it is mandatory and it will be used to join them. Using the example below, `tag, owner` will be joined into the string `<tag_value>:<owner_value>`.
   378  4. The resource type object must have `variables`: a list of fields names that are the keys in the plan to retrieve the correct values building the import statement. Using the example below, Terravalet will search for keys `tag` and `owner` in terraform plan for that resource.
   379  
   380  ```json
   381  {
   382    "dummy_resource1": {
   383      "priority": 1,
   384      "separator": ":",
   385      "variables": [
   386        "tag",
   387        "owner"
   388      ]
   389    }
   390  }
   391  ```
   392  
   393  ## Error cases
   394  
   395  Ignorable errors:
   396  
   397  1. Resource X doesn't exist yet, it resides only in new terraform configuration.
   398  2. Resource X exists, but depends on resource Y that has not been imported yet (should be fine setting the priority).
   399  
   400  NON ignorable errors:
   401  
   402  1. Provider specific argument ID is wrong.
   403  
   404  # Removing existing resources
   405  
   406  Although `terraform state rm` allows to remove _individual_ resources, but when a real-world resource is composed of multiple terraform resources, using `terraform state rm` becomes tedious and error-prone. Even worse when multiple high-level resources are removed together.
   407  
   408  Thus, `terravalet remove` parses a plan file and creates all the `state rm` commands for you.
   409  
   410  1. Remove the resources in the Terraform configuration files.
   411  2. Generate the plan file:
   412     ```
   413     $ terraform -chdir=<the tf root> plan -no-color > remove-plan.txt
   414     ```
   415  3. Run `terravalet remove`. As usual, this is a safe operation, since it will only generate a script file:
   416     ```
   417     $ terravalet remove --up=remove.sh --plan=remove-plan.txt
   418     ```
   419  4. Carefully examine the generated script file `remove.sh`!
   420  5. Execute the scrip.
   421     ```
   422     $ sh ./remove.sh
   423     ```
   424  
   425  # Making a release
   426  
   427  ## Setup
   428  
   429  1. Install [gopass](https://github.com/gopasspw/gopass) or equivalent.
   430  2. Configure a fine-grained personal access token, scoped to the terravalet repository:
   431      * Go to [Fine-grained personal access tokens](https://github.com/settings/tokens?type=beta)
   432      * Click on "Generate new token"
   433      * Give it a name like "terravalet-releases"
   434      * Select "Resource owner" -> Pix4D
   435      * Select "Repository access" -> "Only select repositories" -> Terravalet
   436      * Select "Repository permissions"
   437        * "Contents" -> RW
   438      * Generate the token 
   439  3. Store the token securely with a tool like `gopass`. The name `GITHUB_TOKEN` is expected by `github-release`
   440     ```
   441     $ gopass insert gh/terravalet/GITHUB_TOKEN
   442     ```
   443  
   444  ## Each time
   445  
   446  1. Update [CHANGELOG](CHANGELOG.md)
   447  2. Update this README and/or additional documentation.
   448  3. Make and merge a PR.
   449  4. Ensure your local master branch is up-to-date:
   450     ```
   451     $ git checkout master
   452     $ git pull
   453     ```
   454  5. Begin the release process with
   455     ```
   456     $ RELEASE_TAG=v0.1.0 gopass env gh/terravalet task release
   457     ```
   458  6. Finish the release process by following the instructions printed by `task` above.
   459  7. To recover from a half-baked release, see the hints in the [Taskfile](Taskfile.yml).
   460  
   461  # History and credits
   462  
   463  The idea of migrations comes from [tfmigrate](https://github.com/minamijoyo/tfmigrate). Then this blog [post](https://medium.com/@lynnlin827/moving-terraform-resources-states-from-one-remote-state-to-another-c76f8b76a996)  made me realize that `terraform state mv` had a bug and how to workaround it.
   464  
   465  # License
   466  
   467  This code is released under the MIT license, see file [LICENSE](LICENSE).