Semi-Automating Terraform State Imports with Plan Files and ID Maps
On more than one occasion I've longed for the means to automatically import terraform state. But this is a feature I know will likely never be added for a number of very good reasons. In some cases it is possible to use only a plan file to automate the generation of terraform import blocks though. Here is how it can be done.
Use Case
Why No Auto Import?
So What? I Need This!
How The Script Works
Requirements
Example
A fairly poor but working example of how to do this can be found in the ./example
path. Local resources do not lend themselves well to importing state so I used a local vault deployment with the hashicorp/vault terraform provider instead. Here is how you can run through this example locally:
cd examples
# Start local vault dev instance
docker compose up -d
# export the dev root token
export VAULT_TOKEN=dev-token-12345
# perform initial deployment
terraform init
terraform plan -out plan.tfplan
terraform apply plan.tfplan
# delete the local state then get your plan as json again.
rm ./terraform.tfstate ./terraform.tfstate.backup
terraform show -no-color -json plan.tfplan | jq > plan.json
At this point you will need to figure out the import id by visiting the terraform provider documentation (I know of no way to scrape import id schema automatically anywhere). So I dropped into the terraform provider website for vault and looked up the vault_mount
and vault_kv_secret_v2
resources. I discovered that that they both require just a path to import. Sweet! Lets start with the vault_mount
resource. I inspect the plan json output for the vault_mount
create resource data and zero in on the after
section for the created resource:
{
"address": "vault_mount.kv",
"mode": "managed",
"type": "vault_mount",
"name": "kv",
"provider_name": "registry.terraform.io/hashicorp/vault",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"allowed_managed_keys": null,
"allowed_response_headers": null,
"delegated_auth_accessors": null,
"description": "KV Version 2 secret engine",
"external_entropy_access": false,
"identity_token_key": null,
"listing_visibility": null,
"local": null,
"namespace": null,
"options": {
"version": "2"
},
"passthrough_request_headers": null,
"path": "kv",
"plugin_version": null,
"type": "kv"
},
...
Looks like they give us path
straight away so the start of our map file looks like this:
registry.terraform.io/hashicorp/vault:
vault_mount:
id: "{path}"
Now if we look at the next resource, vault_kv_secret_v2
, we see something like this:
{
"address": "vault_kv_secret_v2.secrets[\"user-credentials\"]",
"mode": "managed",
"type": "vault_kv_secret_v2",
"name": "secrets",
"index": "user-credentials",
"provider_name": "registry.terraform.io/hashicorp/vault",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"cas": null,
"data_json": "{\"admin_password\":\"secure_password_123\",\"admin_username\":\"admin\",\"last_backup\":\"2024-01-15T10:30:00Z\",\"user_count\":\"150\"}",
"data_json_wo": null,
"data_json_wo_version": null,
"delete_all_versions": false,
"disable_read": false,
"mount": "kv",
"name": "user-credentials",
"namespace": null,
"options": null
},
...
No path! But we can make the correct path with mount
and name
so that becomes our mapping to complete our map file.
registry.terraform.io/hashicorp/vault:
vault_mount:
id: "{path}"
vault_kv_secret_v2:
id: "{mount}/data/{name}"
NOTE: An astute reader will notice that I included the 'data' section in that path. This is just a nuance of Vault kv version 2 that I happen to know already. It is also a good example of how you can manually tweak these mappings.
Now that we have this we can create the import block file and proceed to replan with it in place to import all the existing paths.
uv run ../import-terraform.py ./plan.json kv_imports.tf --id-map ./import_map.yaml
terraform plan -out plan.tfplan
terraform apply plan.tfplan
This will pull in the existing secrets as state and recreate your state file as it was before you deleted it.