Migrating Proxmox LXC containers from Terraform to Pulumi
This post is part of my series on migrating my Homelab from Terraform to Pulumi. In this article I walk through how I’m migrating my LXC containers and how I imported them from Terraform without any data loss.
My LCX containers
I run several containers in my homelab, mainly NixOS and Debian LXC containers. Each container has a small volume where the operating system, applications, and configuration are stored. Everything there is created automatically through Infrastructure as Code.
Some containers also have additional storage to persist application data. For example, I have a Debian LXC container running SigNoz, and I keep its ClickHouse database and logs on a separate volume.
Others share data across containers. To accomplish this, I run a dedicated container providing an SMB server, while the rest connect to it as regular SMB clients.
Some containers can be safely recreated from scratch, but a few of them contain data I want to keep. That made migrating existing containers an important requirement (rather than deleting and recreating them).
Importing existing containers
To import Proxmox resources with Pulumi, you first need to define three environment variables:
PROXMOX_VE_ENDPOINTPROXMOX_VE_PASSWORDPROXMOX_VE_USERNAME
If you connect through an insecure method (self-signed certificate or non-HTTPS), you may also need:
-
PROXMOX_VE_INSECURE
If a container is simple and has no complex child resources, you can import it directly:
pulumi import proxmoxve:CT/container:Container <NAME> proxmox/<PROXMOX_ID>Code language: Shell Session (shell)
However, more complex containers may require additional resources like copying local files or running remote scripts. In these cases I found it easier to let Pulumi create a temporary new container (with a different IP address), and then manually update the Pulumi state to point it to the original container. Afterward, you can delete the temporary container in Proxmox to keep things clear.
Manually editing Pulumi state
Editing the Pulumi state manually is dangerous, but it can significantly simplify migrations.
Export the current stack state:
pulumi stack export > state.jsonCode language: Shell Session (shell)
By default, secrets are encrypted. To export plaintext secrets pass the --show-secrets option:
pulumi stack export --show-secrets > state.jsonCode language: Shell Session (shell)
You can re-import the state with (it doesn’t matter if your state has cyphered or plaintext secrets):
pulumi stack import --file state.jsonCode language: Arduino (arduino)
Pulumi can also refresh the state from real resources (only for supported resources):
pulumi refresh
Fixing password and SSH keys differences
If you use RandomPassword to generate the root password, you must import that password before creating the new container and modifying the state. Otherwise, updating the password resource later is harder due to how Pulumi handles secrets.
You can import the RandomPassword resource like this:
pulumi import random:index/randomPassword:RandomPassword <NAME> <CLEARTEXT_PASSWORD>Code language: JavaScript (javascript)
An alternative is to define the password as a Pulumi secret, but I prefer not forcing imported containers to keep their Terraform-generated password forever. If I recreate the infrastructure from scratch, I want fresh passwords.
The advanced importing process
Combining all the steps, the import process for non-trivial containers looks like this:
- Update your Pulumi code so it creates a new container without conflicts (e.g., temporarily change the container’s IP).
- Run Pulumi and let it create the new container and all its child resources.
- Export the state:
pulumi stack export > state.json - Edit the state file: locate the new container resource and replace its ID with the ID of the original container.
- The ID appears multiple times (at least three times within the container resource and possibly more if other resources reference it).
- Update every occurrence or Pulumi may detect differences and attempt to recreate or modify the container.
- Import the updated state:
pulumi stack import --file state.json - Refresh the state:
pulumi refresh - Update your Pulumi code back to the intended configuration (undoing step 1).
- Run
pulumi previewto confirm Pulumi detects no differences.
Issues I run into
Some problems are expected, especially around remote connections.
At one point I manually edited the state but didn’t update every occurrence of a value. Pulumi then tried to run a command in the container but couldn’t connect, and I received a vague unexpected EOF error. I eventually fixed it by deleting the affected remote::Command resource from the state and letting Pulumi recreate it. Since my scripts are idempotent, rerunning them wasn’t an issue.
Make your install scripts idempotent. They might end up running more often than you expect.
Another major issue involved remote files. Pulumi needs to know the file contents to compute hashes and determine identifiers, even during previews. Some of my remote files are templates that depend on Pulumi outputs, including RandomPassword, which isn’t available during previews. I created a custom resource to delay asset creation until outputs resolved, but this led to errors like:
property asset value {<nil>} has a problem: either asset or archive must be setCode language: Swift (swift)
A final thought
This migration is taking longer than I expected. I like Pulumi’s programming model overall but there are some rough edges. The migration would be significantly easier if:
- Pulumi supported creating remote files from local templates plus input variables, similar to Terraform’s
templatefile. - Proxmox allowed creating detached volumes, so containers could be recreated without touching storage.
Maybe these features exist and I just haven’t found them yet. If I discover better solutions, I’ll write another post about it.
No replies on “Migrating Proxmox LXC containers from Terraform to Pulumi”