# How to move Terraform root module state inside other root module


I ran into the problem that I wanted to make an existing [root module](https://learn.hashicorp.com/tutorials/terraform/module#what-is-a-terraform-module) (I'll call this **source**) a child module of another root module (I'll call this **destination**). Both modules used a remote state backend. In my case a GCP storage bucket.

## Example
Let's say we have 2 root modules. `redis` & `databases`. The `databases` module contains a `postgresql` child module. We want to make the `redis` module a child module of the `databases` module.

This requires that we move the state of the existing source module into the destination module's state.

Our before & after Terraform module state:

([SVG version](https://raw.githubusercontent.com/orlandothoeny/blog/main/images/2021-08-03/terraform-migrate-module-state.svg) for less pixel artifacts)
![Terraform migrate module state into other module](https://cdn.hashnode.com/res/hashnode/image/upload/v1628090465840/fPlWRCz3L.jpeg)

### Before

#### File structure
```
~/
├── databases/
│   └── main.tf
├── redis/
│   └── main.tf
└── modules/
    └── postgresql/
        └── main.tf
```

#### Code

__~/databases/main.tf__:
```terraform
...

resource "module" "postgresql" {
  source = "../modules/postresql"
}
```

__~/redis/main.tf__:
```terraform
...

resource "google_redis_instance" "my_redis_instance" {
  ...
}

resource "random_password" "my_redis_pw" {
...
}
```

__~/modules/postgresql/main.tf__:
```terraform
...

resource "google_sql_database_instance" "postgresql" {
  ...
}

resource "random_password" "my_sql_pw" {
...
}
```

### After

#### File structure
```
~/
├── databases/
│   └── main.tf
└── modules/
    ├── redis/
    │   └── main.tf
    └── postgresql/
        └── main.tf
```

#### Code

__~/databases/main.tf__:
```terraform
...
// Has the resource address "module.redis" 
resource "module" "redis" {
  source = "../modules/redis"
}

resource "module" "postgresql" {
  source = "../modules/postresql"
}
```

__~/modules/redis/main.tf__:
```terraform
...

resource "google_redis_instance" "my_redis_instance" {
  ...
}

resource "random_password" "my_redis_pw" {
...
}
```

__~/modules/postgresql/main.tf__:
```terraform
...

resource "google_sql_database_instance" "postgresql" {
  ...
}

resource "random_password" "my_sql_pw" {
...
}
```

## Making module child of other module
Resolving the problem described above can be achieved by running a combination of commands:
* [`terraform state pull`](https://www.terraform.io/docs/cli/commands/state/pull.html): Pull the remote states of the source & destination modules down as state files to your local drive.
* [`terraform state mv`](https://www.terraform.io/docs/cli/commands/state/mv.html): Move all resources from the pulled down local source module state to the local destination module state.
* [`terraform state push`](https://www.terraform.io/docs/cli/commands/state/push.html): Push the local destination module state file back up to the remote.

As explained in this [Stackoverflow answer](https://stackoverflow.com/a/51489058/6653862).

This approach works fine for small modules that don't contain a whole lot of resources. It can be cumbersome if that's not the case for you. When you're working with large modules. Especially if you need to move multiple such modules.

I created a small script that automates this process.

It will also create a backup of your existing state before changing anything. Just in case (Terraform additionally also creates backup files before every `terraform state mv`).

It takes in a couple of arguments:
1. The directory of our destination module. In our example: `~/databases`.
2. The directory of our source module. In our example: `~/redis`.
3. The [resource address](https://www.terraform.io/docs/cli/state/resource-addressing.html) inside the destination module where the source module should be moved to. In our example: `module.redis`.

And the optional option `-dry-run`. Which does what it indicates. Does a dry run. No state is moved, but you can see what the script will do on an actual run.

__migrate-state.sh__:
```shell
#!/usr/bin/env bash
set -euo pipefail

# Support aborting via SIGINT, without this bash will not exit the for loop until it's finished
trap 'exit 0' INT

# Usage example:
# bash migrate-state.sh '/home/myuser/terraform/destination-module' '/home/myuser/terraform/source-module' 'module.source' 'config/dev_backend.tfvars' 'config/dev_backend.tfvars'
# Arguments:
# $1 - destinationModuleDirectory: The directory in which the module resides where state should be moved to
# $2 - sourceModuleDirectory: The directory in which the module resides where state should be moved from
# $3 - destinationModuleSourceParentAddress: The resource address inside the destination module that is used as parent for all resources that are moved from the source module
# $4 (optional) - sourceModuleBackendConfig: The value that should be used for the "-backend-config" parameter when running  terraform init for the source module
# $5 (optional) - destinationModuleBackendConfig: The value that should be used for the "-backend-config" parameter when running  terraform init for the destination module
# Options:
# -dry-run
#   Show changes that will be made, does not actually change anything

function migrateState() {
  local destinationModuleDirectory="${1:?'The destinationModuleDirectory argument is missing'}"
  local sourceModuleDirectory="${2:?'The sourceModuleDirectory argument is missing'}"
  local destinationModuleSourceParentAddress="${3:?'The destinationModuleSourceParentAddress argument is missing'}"
  local sourceModuleBackendConfig="${4:-''}"
  local destinationModuleBackendConfig="${5:-''}"

  local isDryRun=0
  for arg in "${@}"
  do
    if [[ "${arg}" == '-dry-run' ]];
    then
      isDryRun=1
      break
    fi
  done

  local lightBlue='\e[38;5;26m'
  local colorEnd='\e[0m'

  local destinationModuleLocalStateFile="${destinationModuleDirectory}/destination-module.tfstate"

  cd "${destinationModuleDirectory}" || false

  local destinationTerraformInitArgs=''
  if [[ "${destinationModuleBackendConfig}" != '' ]];
  then
    destinationTerraformInitArgs="-backend-config=${destinationModuleBackendConfig}"
  fi
  terraform init "${destinationTerraformInitArgs}"

  terraform state pull > "${destinationModuleLocalStateFile}"
  cp "${destinationModuleLocalStateFile}" "${destinationModuleLocalStateFile}.bak"

  cd "${sourceModuleDirectory}" || false
  local sourceTerraformInitArgs=''
  if [[ "${sourceModuleBackendConfig}" != '' ]];
  then
    sourceTerraformInitArgs="-backend-config=${sourceModuleBackendConfig}"
  fi
  terraform init "${sourceTerraformInitArgs}"
  terraform state pull > "${sourceModuleDirectory}/source-module.tfstate.bak"

  for sourceResourceAddress in $(terraform state list)
  do
    local destinationResourceAddress="${destinationModuleSourceParentAddress}.${sourceResourceAddress}"
    printf "Moving ${lightBlue}%-100s${colorEnd} to ${lightBlue}%-120s${colorEnd}\n" "${sourceResourceAddress}" "${destinationResourceAddress}"

    if [[ ${isDryRun} == 1 ]];
    then
      terraform state mv -state-out="${destinationModuleLocalStateFile}" -dry-run "${sourceResourceAddress}" "${destinationResourceAddress}"
    else
      terraform state mv -state-out="${destinationModuleLocalStateFile}" "${sourceResourceAddress}" "${destinationResourceAddress}"
    fi

    printf '\n'
  done

  if [[ ${isDryRun} == 0 ]];
  then
    cd "${destinationModuleDirectory}" || false

    terraform init "${destinationTerraformInitArgs}"
    terraform state push "${destinationModuleLocalStateFile}"
  fi
}

migrateState "${@}"
```

The script is also available on [GitHub](https://gist.github.com/orlandothoeny/5403ec7304bcba1590e7561818afea5e).

To get back to our example. We'd run
```shell
bash migrate-state.sh '~/databases' '~/redis' 'module.redis' -dry-run
```
to see what will happen.

And then remove the `-dry-run` option:
```shell
bash migrate-state.sh '~/databases' '~/redis' 'module.redis'
```
