Skip to main content

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 targetcomputed_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.

  1. Add all the resource blocks first — one A/AAAA pair per subdomain, same shape as rss.

  2. Run every terraform import inside a single bash -c '...', instead of invoking pass-cli run separately per record — two dozen separate pass-cli invocations would each spin up and re-authenticate for no reason:

    pass-cli run --env-file .env -- bash -c '
    set -e
    terraform 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 -e stops 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.

  3. Confirm the whole batch with one plan — expect the same benign targetcomputed_target reconciliation on every newly-imported record, and critically zero adds, zero destroys across all of them. If plan wants 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.