DNS
The rjkorteschiel.nl zone already exists at Infomaniak — a live, registered domain. Terraform can't create it, only adopt it. This page is the proven procedure, run for real on 2026-08-14 — not the original speculative plan; a few details only became clear by actually doing it.
1. Import the zone
pass-cli run --env-file .env -- terraform import infomaniak_zone.rjkorteschiel_nl rjkorteschiel.nl
The fqdn worked directly — no need for the numeric-ID fallback some docs suggest.
The zone resource's fqdn must exactly match what the live API returns: "rjkorteschiel.nl", no trailing dot. A dot mismatch forces a destroy-and-recreate of the zone on apply, which would very likely wipe every record this config doesn't manage — NS, MX, mail auth records, every other subdomain. Caught via terraform plan before ever applying; dns.tofu's zone resource has a comment explaining why the dot is deliberately absent.
2. Get record IDs
Records live under a sub-path, not the zone lookup itself — and the response's data is directly an array, not data.records:
pass-cli run --env-file .env -- bash -c 'curl -s -H "Authorization: Bearer $INFOMANIAK_TOKEN" "https://api.infomaniak.com/2/zones/rjkorteschiel.nl/records"'
The bash -c '...' wrapper (single-quoted) matters — it defers $INFOMANIAK_TOKEN expansion to the subprocess pass-cli actually injects it into, rather than your outer shell expanding it to empty first.
3. Experiment with one record first
rss was picked as the deliberate low-stakes test case before trusting any of this with something real — see Architectures for why the choice of what to test with mattered. Prove the mechanics on just this one record before touching anything else:
Identifier is zone_fqdn,id — comma-separated, not just the bare numeric id:
pass-cli run --env-file .env -- terraform import infomaniak_record.personal_services_rss_a "rjkorteschiel.nl,<id>"
pass-cli run --env-file .env -- terraform import infomaniak_record.personal_services_rss_aaaa "rjkorteschiel.nl,<id>"
Expect a target → computed_target diff on the next plan — harmless, an in-place update where the provider reconciles the raw value import captured against how data.ip computes it. Not a sign anything's wrong; safe to apply.
Once import itself is proven, go one step further and prove an actual cutover works too — not just the mechanics of adopting an existing record. rss was briefly flipped to the new server's IP and back (see Migrating a subdomain for real below for the mechanism) purely to confirm apply sends a real update and Infomaniak's API reflects it — a fresh updated_at timestamp, not a no-op.
4. Importing many records at once
Once both — import and a real cutover — are proven on one record, importing everything else is mechanical. This is how the remaining 13 subdomains (26 records) actually got done, all in one go rather than one at a time: dashboard, docs, cloud, openwebui, code, ai, docs2, docs3, docs4, design, status, auth, auth2.
-
Add all the resource blocks first — one
A/AAAApair per subdomain, same shape asrss. -
Run every
terraform importinside a singlebash -c '...', instead of invokingpass-cli runseparately per record — two dozen separatepass-cliinvocations would each spin up and re-authenticate for no reason:pass-cli run --env-file .env -- bash -c 'set -eterraform import infomaniak_record.dashboard_a "<zone_fqdn>,<id>"terraform import infomaniak_record.dashboard_aaaa "<zone_fqdn>,<id>"terraform import infomaniak_record.docs_a "<zone_fqdn>,<id>"terraform import infomaniak_record.docs_aaaa "<zone_fqdn>,<id>"# ...one A/AAAA pair per remaining subdomain'set -estops the whole batch at the first failure instead of plowing through with something already broken — worth having when running a couple dozen imports unattended. Every ID needed came from the single step 2 API call; no need to re-query per record. -
Confirm the whole batch with one
plan— expect the same benigntarget→computed_targetreconciliation on every newly-imported record, and critically zero adds, zero destroys across all of them. Ifplanwants to create or destroy anything, stop before applying — something's wrong.
That's every service-related record — not the entire zone. NS, MX, and the mail-auth TXT/CNAME records (SPF, DMARC, ProtonMail verification and DKIM) stay outside Terraform entirely, same as always; nothing to do with personal-services.
5. Migrating for real
No apex A/AAAA cutover record is declared — the live zone doesn't have one today, every current service sits on a subdomain — so this was never a single flip. Every record's data.ip referenced a variable, not a literal IP (variables.tofu): personal_services_ipv4/_ipv6 for the new server, and (while they still existed) _ipv4_old/_ipv6_old for the old one.
rss proved the mechanism first — imported, briefly flipped to the new server and back, confirming apply sends a real update rather than a no-op. Once proven, all 14 subdomains (28 records) flipped from _old to the plain variable in one batch rather than staged one at a time — single-VM model, no benefit to spreading it out. The _old variables were removed from variables.tofu once nothing referenced them anymore; every record now points at the new server unconditionally.