<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Opentofu – Serverküche</title><link>https://serverkueche.de/tags/opentofu/</link><description>Opentofu – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>de-DE</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Wed, 28 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/opentofu/index.xml" rel="self" type="application/rss+xml"/><item><title>netcup eos automatisieren: eosctl und OpenTofu</title><link>https://serverkueche.de/tutorials/netcup-eos-terraform/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/netcup-eos-terraform/</guid><description>netcup eos ohne Klicks: Client Credentials mit knappen Rechten, VMs per eosctl und ein komplettes Setup aus Netz, ACL und VM mit OpenTofu.</description><content:encoded><![CDATA[<p>eos ist für Leute gebaut, die Server nicht per Klick anlegen wollen. Im zweiten Teil steuern wir
netcups neue Cloud deshalb von der Kommandozeile – erst mit dem Werkzeug <code>eosctl</code>, dann mit
OpenTofu als Infrastruktur-als-Code. Die Konsole brauchst du nur noch für den Zugang selbst.</p>
<p>Wie im ersten Teil gilt: Das ist der Stand der Preview, die netcup am 13. November 2026 abbaut. Öffentlich startet eos 2027.
Schon vorher, noch 2026, kann jeder netcup-Kunde die Plattform im Friends-&amp;-Family-Launch
kostenlos nutzen, auf produktiver Hardware und ohne späteren Reset. Befehle und Provider
sind noch vor Version 1.0 und können sich ändern.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Wir legen einen Zugang für Programme an, der nur Netze und VMs verwalten darf. Mit <code>eosctl</code>
erzeugen wir eine VM, führen darin einen Befehl aus und löschen sie wieder. Danach beschreiben
wir ein vollständiges Setup in einer OpenTofu-Datei: eigenes Netz, Firewall-Regel, Debian-VM mit
Cloud-Init und öffentlicher IPv4. Ein <code>tofu apply</code> baut alles in unter einer Minute auf, ein
<code>tofu destroy</code> räumt es genauso schnell wieder ab.</p>
<p>Eingesetzt werden <code>eosctl</code> 0.16.0, OpenTofu 1.13.1 und der eos-Provider 0.10.1. OpenTofu ist der
quelloffene Ableger von Terraform und versteht dieselbe Konfigurationssprache.</p>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein Zugang zu eos und Grundkenntnisse der Konsole – Organisation, Realm, ACL und öffentliche
IP erklärt <a href="/tutorials/netcup-eos-preview/">netcup eos in der Preview</a>.</li>
<li>Ein Rechner mit Debian 13 für die Werkzeuge. Das kann dein Laptop sein oder ein kleiner
Server; wir haben eine eos-VM dafür genommen.</li>
<li>Ein SSH-Schlüssel (<code>ssh-keygen -t ed25519</code>), siehe <a href="/tutorials/ssh-absichern/">SSH absichern</a>.</li>
<li><code>jq</code> zum Auswerten von JSON: <code>sudo apt install jq</code>.</li>
</ul>
<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="1-client-credentials-mit-knappen-rechten">1. Client Credentials mit knappen Rechten</h3>
<p>Programme melden sich bei eos nicht mit deinem Konto an, sondern mit <strong>Client Credentials</strong> –
einer Client-ID und einem Secret. Du legst sie in der Konsole unter <strong>Realms → dein Realm →
Client Credentials → Create credential</strong> an. Die Seite beschreibt sie als „organization level“;
angelegt werden sie trotzdem für den Realm, in dem du gerade bist.</p>
<p><figure class="my-6"><img src="/tutorials/netcup-eos-terraform/eos-credentials_hu_e479dcefa5b9e4c9.webp" srcset="/tutorials/netcup-eos-terraform/eos-credentials_hu_3981cfa16f99dd88.webp 480w, /tutorials/netcup-eos-terraform/eos-credentials_hu_e479dcefa5b9e4c9.webp 768w, /tutorials/netcup-eos-terraform/eos-credentials_hu_93063f43fd53d4e5.webp 1200w, /tutorials/netcup-eos-terraform/eos-credentials_hu_19ba4481ee9f87ee.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/netcup-eos-terraform/eos-credentials_hu_ae1fd346152e40d1.webp"
    alt="Der Dialog für neue Client Credentials mit Name, Ablaufdatum und den Rollen Network Manager und VM Manager" title="Client Credentials: Basisrecht ist Lesen, alles Weitere kommt als Rolle dazu"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Client Credentials: Basisrecht ist Lesen, alles Weitere kommt als Rolle dazu</figcaption></figure></p>
<p>Jeder Zugang darf grundsätzlich alles <strong>lesen</strong>. Schreibrechte kommen als Rollen dazu: Compute
Network Manager, VM Manager, Volume Manager, Object Storage Manager und Realm Admin. Für dieses
Tutorial genügen zwei:</p>
<ul>
<li><code>compute:network-manager</code> – Netze, ACLs und öffentliche IPs</li>
<li><code>compute:vm-manager</code> – VMs anlegen, schalten, löschen</li>
</ul>
<p>Setze außerdem ein <strong>Ablaufdatum</strong>. Ein Zugang für Experimente braucht keine Laufzeit von einem
Jahr, und ein vergessener Schlüssel verfällt dann von selbst.</p>
<p>Nach dem Speichern zeigt eos das Secret <strong>genau einmal</strong>. Kopiere beides sofort in eine Datei,
die nur du lesen darfst:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">umask</span> <span class="m">077</span>
</span></span><span class="line"><span class="cl">cat &gt; ~/.eos.env <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">export EOS_CLIENT_ID=DEINE_CLIENT_ID
</span></span></span><span class="line"><span class="cl"><span class="s">export EOS_CLIENT_SECRET=DEIN_CLIENT_SECRET
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre></div>
</div>
<p><code>umask 077</code> sorgt dafür, dass die Datei mit den Rechten <code>600</code> entsteht. Sowohl <code>eosctl</code> als auch
der OpenTofu-Provider lesen genau diese beiden Variablen – das Secret muss also nie in einer
Konfigurationsdatei stehen.</p>
<h3 id="2-eosctl-installieren">2. eosctl installieren</h3>
<p>netcup bietet <code>eosctl</code> als Debian-Paket aus einem eigenen Repository an. Zuerst den
Signaturschlüssel holen, damit apt nur Pakete akzeptiert, die netcup signiert hat:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt-get install -y ca-certificates curl
</span></span><span class="line"><span class="cl">curl -sSL https://dl.eoscloud.io/debian.gpg <span class="p">|</span> sudo tee /usr/share/keyrings/eoscloud-public.gpg &gt; /dev/null</span></span></code></pre></div>
</div>
<p>Dann die Paketquelle eintragen und das Paket installieren:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tee /etc/apt/sources.list.d/eos.sources &gt; /dev/null <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">Types: deb
</span></span></span><span class="line"><span class="cl"><span class="s">URIs: https://dl.eoscloud.io/debian
</span></span></span><span class="line"><span class="cl"><span class="s">Suites: trixie
</span></span></span><span class="line"><span class="cl"><span class="s">Components: main
</span></span></span><span class="line"><span class="cl"><span class="s">Signed-By: /usr/share/keyrings/eoscloud-public.gpg
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">sudo apt-get update <span class="o">&amp;&amp;</span> sudo apt-get install -y eosctl</span></span></code></pre></div>
</div>
<p><code>Signed-By</code> bindet den Schlüssel nur an diese eine Quelle – er gilt nicht für Debians eigene
Pakete. Updates kommen künftig mit <code>apt upgrade</code>. Die eos-Dokumentation nannte zu diesem
Zeitpunkt noch Version 0.12.1, das Repository lieferte bereits 0.16.0.</p>
<h3 id="3-anmelden-und-die-ausgabe-zähmen">3. Anmelden und die Ausgabe zähmen</h3>
<p>Lade die Zugangsdaten und melde dich an:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">source</span> ~/.eos.env
</span></span><span class="line"><span class="cl">eosctl login</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Client credentials flow is configured. To logout, run: &#39;eosctl logout&#39;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">WARNING: storing token insecure in file.
</span></span><span class="line"><span class="cl">Consider installing keyring software like gnome-keyring if possible.</span></span></code></pre></div>
</div>
<p>Die Warnung kommt auf jedem Server ohne Schlüsselbund: <code>eosctl</code> legt das Zugriffstoken als Datei
in deinem Home-Verzeichnis ab. Auf einem Rechner, den nur du nutzt, ist das in Ordnung; auf
einem geteilten System meldest du dich nach der Arbeit mit <code>eosctl logout</code> ab.</p>
<p>Standardmäßig gibt <code>eosctl</code> bunte Tabellen aus. Für Skripte gibt es <code>-o json</code>, <code>-o yaml</code> und
<code>-o template</code>; die Farben schaltest du mit <code>NO_COLOR=1</code> ab. So listest du die verfügbaren Images:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">NO_COLOR</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl">eosctl compute vm image list -o json <span class="p">|</span> jq -r <span class="s1">&#39;.[].name&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Alpine Edge
</span></span><span class="line"><span class="cl">Alpine Edge Cloud
</span></span><span class="line"><span class="cl">Arch Linux
</span></span><span class="line"><span class="cl">Arch Linux Cloud
</span></span><span class="line"><span class="cl">Debian 13
</span></span><span class="line"><span class="cl">Debian 13 Cloud
</span></span><span class="line"><span class="cl">Ubuntu 26.04
</span></span><span class="line"><span class="cl">Ubuntu 26.04 Cloud</span></span></code></pre></div>
</div>
<p>Die Produkte zeigen, was die Oberfläche verschweigt:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">eosctl compute product list -o json <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> jq -r <span class="s1">&#39;.[] | select(.kind==&#34;vm&#34;) | [.name, .active, .product_data.cpu_count, .product_data.ram_size] | @tsv&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">vServer Mini	true	1	2048
</span></span><span class="line"><span class="cl">vServer Small	true	2	4096
</span></span><span class="line"><span class="cl">vServer Medium	true	4	8192
</span></span><span class="line"><span class="cl">vServer Large	false	8	16384</span></span></code></pre></div>
</div>
<p>Eine vierte Größe mit 8 Kernen und 16 GB ist also schon angelegt, nur noch nicht freigeschaltet.
Ebenso kennt die API einen zweiten Block (<code>at-preview1-db</code>) neben dem aus der Konsole.</p>
<h3 id="4-eine-vm-per-eosctl-anlegen">4. Eine VM per eosctl anlegen</h3>
<p>Eine VM braucht vier IDs: Block, Produkt, Image und Netz. Statt sie abzuschreiben, holst du sie
per <code>jq</code> anhand des Namens:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">block_id</span><span class="o">=</span><span class="k">$(</span>eosctl compute block list -o json <span class="p">|</span> jq -r <span class="s1">&#39;.[] | select(.name==&#34;at-preview1-da&#34;) | .id&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">product_id</span><span class="o">=</span><span class="k">$(</span>eosctl compute product list -o json <span class="p">|</span> jq -r <span class="s1">&#39;.[] | select(.name==&#34;vServer Mini&#34;) | .id&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">image_id</span><span class="o">=</span><span class="k">$(</span>eosctl compute vm image list -o json <span class="p">|</span> jq -r <span class="s1">&#39;.[] | select(.name==&#34;Debian 13 Cloud&#34;) | .id&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">network_id</span><span class="o">=</span><span class="k">$(</span>eosctl compute network list -o json <span class="p">|</span> jq -r <span class="s1">&#39;.[0].id&#39;</span><span class="k">)</span></span></span></code></pre></div>
</div>
<p><code>.[0]</code> nimmt das erste Netz im Realm – hast du mehrere, wähle es wie die anderen per Name aus.
Als Cloud-Init-Datei nimmst du die <code>user-data.yaml</code> aus dem ersten Teil: Sie legt einen Benutzer
mit deinem Schlüssel an und installiert den SSH-Server, der den Cloud-Images fehlt.</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">vm_id</span><span class="o">=</span><span class="k">$(</span>eosctl compute vm create <span class="se">\
</span></span></span><span class="line"><span class="cl">  -B <span class="s2">&#34;</span><span class="nv">$block_id</span><span class="s2">&#34;</span> -P <span class="s2">&#34;</span><span class="nv">$product_id</span><span class="s2">&#34;</span> -I <span class="s2">&#34;</span><span class="nv">$image_id</span><span class="s2">&#34;</span> -N <span class="s2">&#34;</span><span class="nv">$network_id</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -S <span class="m">10000</span> --name cli-demo <span class="se">\
</span></span></span><span class="line"><span class="cl">  -U <span class="s2">&#34;</span><span class="k">$(</span>cat user-data.yaml<span class="k">)</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o template -t <span class="s1">&#39;{{.id}}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$vm_id</span><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<p><code>-S</code> ist die Plattengröße in <strong>Megabyte</strong>, 10000 also 10 GB – das Minimum. <code>-U</code> erwartet den
Inhalt der Datei, nicht ihren Pfad. Mit <code>-o template -t '{{.id}}'</code> gibt der Befehl nur die ID
der neuen VM aus, die du gleich weiterverwendest.</p>
<p>Ob sie läuft, fragst du mit demselben Template-Trick ab:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">until</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="k">$(</span>eosctl compute vm get <span class="s2">&#34;</span><span class="nv">$vm_id</span><span class="s2">&#34;</span> -o template -t <span class="s1">&#39;{{.power_state}}&#39;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;started&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  sleep <span class="m">3</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;läuft&#34;</span></span></span></code></pre></div>
</div>
<p>Nach gut 20 Sekunden war die VM gestartet. <code>started</code> heißt aber nur, dass sie läuft. Für den
nächsten Schritt muss zusätzlich der Agent in der VM bereit sein, und das dauert noch etwa eine
halbe Minute:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">until</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="k">$(</span>eosctl compute vm get <span class="s2">&#34;</span><span class="nv">$vm_id</span><span class="s2">&#34;</span> -o template -t <span class="s1">&#39;{{.agent_ready}}&#39;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;true&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  sleep <span class="m">3</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span></span></span></code></pre></div>
</div>
<h3 id="5-befehle-in-der-vm-ausführen">5. Befehle in der VM ausführen</h3>
<p><code>eosctl compute vm exec</code> führt einen Befehl direkt in der VM aus, ohne SSH, ohne öffentliche IP
und ohne ACL-Regel. Mehrere Befehle verpackst du in <code>bash -c</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">eosctl compute vm <span class="nb">exec</span> <span class="s2">&#34;</span><span class="nv">$vm_id</span><span class="s2">&#34;</span> <span class="s2">&#34;bash -c \&#34;hostname; whoami\&#34;&#34;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">cli-demo
</span></span><span class="line"><span class="cl">root</span></span></code></pre></div>
</div>
<p>Die inneren Anführungszeichen müssen maskierte doppelte sein (<code>\&quot;</code>). Mit einfachen kam nur die
Ausgabe des ersten Befehls zurück.</p>
<p>Der Befehl läuft als <strong>root</strong>. Das ist dieselbe Hintertür wie die Web-Konsole aus dem ersten
Teil, nur skriptbar – und der Grund, warum der Zugang mit der Rolle <code>vm-manager</code> so sorgfältig
verwahrt werden muss wie ein Root-Passwort.</p>
<h3 id="6-vm-löschen-erst-ausschalten">6. VM löschen: erst ausschalten</h3>
<p>Der naheliegende Befehl tut bei einer laufenden VM <strong>nichts</strong>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">eosctl compute vm delete <span class="s2">&#34;</span><span class="nv">$vm_id</span><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<p>Es kommt keine Fehlermeldung, und die VM läuft einfach weiter. eos löscht nur ausgeschaltete VMs.
Die richtige Reihenfolge ist also ausschalten, warten, löschen:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">eosctl compute vm power-off <span class="s2">&#34;</span><span class="nv">$vm_id</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">until</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="k">$(</span>eosctl compute vm get <span class="s2">&#34;</span><span class="nv">$vm_id</span><span class="s2">&#34;</span> -o template -t <span class="s1">&#39;{{.power_state}}&#39;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;stopped&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  sleep <span class="m">3</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">eosctl compute vm delete <span class="s2">&#34;</span><span class="nv">$vm_id</span><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<p>Das Root-Volume verschwindet mit der VM. Prüfe mit <code>eosctl compute vm list</code>, dass sie weg ist –
in Skripten ist ein stilles Nichtstun gefährlicher als ein Fehler.</p>
<h3 id="7-opentofu-installieren">7. OpenTofu installieren</h3>
<p>Für alles, was über eine einzelne VM hinausgeht, ist eine deklarative Beschreibung bequemer:
Du schreibst auf, was existieren soll, und OpenTofu rechnet aus, was dafür anzulegen, zu ändern
oder zu löschen ist. OpenTofu gibt es als <code>.deb</code> auf GitHub:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -sSLo /tmp/tofu.deb https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_amd64.deb
</span></span><span class="line"><span class="cl">sudo apt-get install -y /tmp/tofu.deb <span class="o">&amp;&amp;</span> rm /tmp/tofu.deb
</span></span><span class="line"><span class="cl">tofu version</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">OpenTofu v1.13.1</span></span></code></pre></div>
</div>
<p>Den eos-Provider musst du nicht installieren – den lädt OpenTofu im nächsten Schritt selbst.</p>
<h3 id="8-das-setup-als-code">8. Das Setup als Code</h3>
<p>Lege ein Verzeichnis an und kopiere die <code>user-data.yaml</code> aus dem ersten Teil hinein:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir ~/eos-tf <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/eos-tf
</span></span><span class="line"><span class="cl">cp ~/user-data.yaml .</span></span></code></pre></div>
</div>
<p>Dann die Datei <code>main.tf</code>. Wir gehen sie in Abschnitten durch; zusammen ergeben sie eine Datei.
Zuerst Provider und Eingabe:</p>
<div class="sk-code">
  <span class="sk-code-head">HCL</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">terraform</span> {
</span></span><span class="line"><span class="cl">  <span class="k">required_providers</span> {
</span></span><span class="line"><span class="cl"><span class="n">    eoscloud</span> <span class="o">=</span> {
</span></span><span class="line"><span class="cl"><span class="n">      source</span>  <span class="o">=</span> <span class="s2">&#34;dl.eoscloud.io/eos/eoscloud&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">      version</span> <span class="o">=</span> <span class="s2">&#34;~&gt; 0.10.0&#34;</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}<span class="c1">
</span></span></span><span class="line"><span class="cl"><span class="c1">
</span></span></span><span class="line"><span class="cl"><span class="c1"># Zugangsdaten kommen aus EOS_CLIENT_ID / EOS_CLIENT_SECRET.
</span></span></span><span class="line"><span class="cl"><span class="k">provider</span> <span class="s2">&#34;eoscloud&#34;</span> {}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;meine_ip&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  description</span> <span class="o">=</span> <span class="s2">&#34;Öffentliche IPv4, von der aus SSH erlaubt ist&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  type</span>        <span class="o">=</span> <span class="k">string</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Der Provider kommt nicht aus der OpenTofu-Registry, sondern direkt von netcups Server
<code>dl.eoscloud.io</code>. <code>~&gt; 0.10.0</code> erlaubt Fehlerkorrekturen innerhalb von 0.10, aber keinen Sprung
auf 0.11 – vor Version 1.0 darf jede Unterversion Bestehendes umwerfen. Der leere
<code>provider</code>-Block ist Absicht: Die Zugangsdaten liest er aus den Umgebungsvariablen.</p>
<p>Statt IDs fest einzutragen, sucht die Konfiguration Block, Größe und Image per Name:</p>
<div class="sk-code">
  <span class="sk-code-head">HCL</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;eoscloud_blocks&#34; &#34;alle&#34;</span> {}
</span></span><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;eoscloud_products&#34; &#34;alle&#34;</span> {}
</span></span><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;eoscloud_vm_images&#34; &#34;alle&#34;</span> {}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">locals</span> {
</span></span><span class="line"><span class="cl"><span class="n">  block_id</span>   <span class="o">=</span><span class="n"> [for b in data.eoscloud_blocks.alle.blocks : b.id if b.name</span> <span class="o">==</span> <span class="s2">&#34;at-preview1-da&#34;</span><span class="p">][</span><span class="m">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  product_id</span> <span class="o">=</span><span class="n"> [for p in data.eoscloud_products.alle.products : p.id if p.active &amp;&amp; p.kind</span> <span class="o">==</span><span class="n"> &#34;vm&#34; &amp;&amp; p.name</span> <span class="o">==</span> <span class="s2">&#34;vServer Mini&#34;</span><span class="p">][</span><span class="m">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  image_id</span>   <span class="o">=</span><span class="n"> [for i in data.eoscloud_vm_images.alle.vm_images : i.id if i.name</span> <span class="o">==</span> <span class="s2">&#34;Debian 13 Cloud&#34;</span><span class="p">][</span><span class="m">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Jetzt die Firewall und das Netz. Die ACL bekommt eine einzige Regel, SSH von deiner Adresse; alles
andere bleibt wie bei eos üblich gesperrt:</p>
<div class="sk-code">
  <span class="sk-code-head">HCL</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;eoscloud_acl&#34; &#34;web&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  block_id</span> <span class="o">=</span> <span class="k">local</span><span class="p">.</span><span class="k">block_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  name</span>     <span class="o">=</span> <span class="s2">&#34;web-acl&#34;</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;eoscloud_acl_rule&#34; &#34;ssh&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  acl_id</span>            <span class="o">=</span> <span class="k">eoscloud_acl</span><span class="p">.</span><span class="k">web</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="cl"><span class="n">  action</span>            <span class="o">=</span> <span class="s2">&#34;allow&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  protocol</span>          <span class="o">=</span> <span class="s2">&#34;tcp&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  sources</span>           <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;${var.meine_ip}/32&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  destination_ports</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;22&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;eoscloud_network&#34; &#34;web&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  block_id</span>  <span class="o">=</span> <span class="k">local</span><span class="p">.</span><span class="k">block_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  name</span>      <span class="o">=</span> <span class="s2">&#34;web-netz&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  v4_prefix</span> <span class="o">=</span> <span class="s2">&#34;10.10.0.0/24&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  acl_ids</span>   <span class="o">=</span> <span class="p">[</span><span class="k">eoscloud_acl</span><span class="p">.</span><span class="k">web</span><span class="p">.</span><span class="k">id</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>Anders als in der Konsole braucht die Regel hier kein Ziel: Sie gilt für das ganze Netz, an das
die ACL gehängt ist. Das Netz bekommt einen eigenen privaten Adressbereich.</p>
<p>Zum Schluss VM, öffentliche IP und deren Verknüpfung:</p>
<div class="sk-code">
  <span class="sk-code-head">HCL</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;eoscloud_vm&#34; &#34;web&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  block_id</span>   <span class="o">=</span> <span class="k">local</span><span class="p">.</span><span class="k">block_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  product_id</span> <span class="o">=</span> <span class="k">local</span><span class="p">.</span><span class="k">product_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  network_id</span> <span class="o">=</span> <span class="k">eoscloud_network</span><span class="p">.</span><span class="k">web</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="cl"><span class="n">  image_id</span>   <span class="o">=</span> <span class="k">local</span><span class="p">.</span><span class="k">image_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  name</span>       <span class="o">=</span> <span class="s2">&#34;web&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  disk_size</span>  <span class="o">=</span> <span class="m">10000</span><span class="c1"> # MB
</span></span></span><span class="line"><span class="cl"><span class="n">  user_data</span>  <span class="o">=</span> <span class="k">file</span><span class="p">(</span><span class="s2">&#34;${path.module}/user-data.yaml&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;eoscloud_public_ip&#34; &#34;web&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  network_id</span> <span class="o">=</span> <span class="k">eoscloud_network</span><span class="p">.</span><span class="k">web</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="cl"><span class="n">  ip_family</span>  <span class="o">=</span> <span class="m">4</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;eoscloud_public_ip_attachment&#34; &#34;web&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  public_ip_id</span> <span class="o">=</span> <span class="k">eoscloud_public_ip</span><span class="p">.</span><span class="k">web</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="cl"><span class="n">  vm_id</span>        <span class="o">=</span> <span class="k">eoscloud_vm</span><span class="p">.</span><span class="k">web</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">output</span> <span class="s2">&#34;oeffentliche_ip&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  value</span> <span class="o">=</span> <span class="k">eoscloud_public_ip</span><span class="p">.</span><span class="k">web</span><span class="p">.</span><span class="k">ip</span>
</span></span><span class="line"><span class="cl">}</span></span></code></pre></div>
</div>
<p>IP und Anhängen sind zwei Ressourcen, genau wie die zwei Schritte in der Konsole. So kannst du
eine Adresse behalten, während die VM dahinter ausgetauscht wird.</p>
<h3 id="9-init-plan-apply">9. init, plan, apply</h3>
<p><code>tofu init</code> lädt den Provider und prüft seine Signatur:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">source</span> ~/.eos.env
</span></span><span class="line"><span class="cl">tofu init</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">- Installing dl.eoscloud.io/eos/eoscloud v0.10.1...
</span></span><span class="line"><span class="cl">- Installed dl.eoscloud.io/eos/eoscloud v0.10.1 (signed, key ID 4650503580F8B221)
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">OpenTofu has been successfully initialized!</span></span></code></pre></div>
</div>
<p>Dabei entsteht <code>.terraform.lock.hcl</code> mit der exakten Version und ihren Prüfsummen. Diese Datei
gehört ins Git-Repository – so bekommt jeder, der das Setup ausrollt, denselben Provider.</p>
<p>Deine öffentliche IPv4 übergibst du als Variable. <code>tofu plan</code> zeigt, was passieren würde, ohne
etwas anzulegen:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tofu plan -var <span class="nv">meine_ip</span><span class="o">=</span>DEINE_IP</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Plan: 6 to add, 0 to change, 0 to destroy.</span></span></code></pre></div>
</div>
<p>Sieht der Plan richtig aus, baust du auf:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tofu apply -var <span class="nv">meine_ip</span><span class="o">=</span>DEINE_IP</span></span></code></pre></div>
</div>
<p>Nach der Rückfrage (<code>yes</code>) legt OpenTofu die Ressourcen in der richtigen Reihenfolge an:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">eoscloud_acl.web: Creation complete after 6s
</span></span><span class="line"><span class="cl">eoscloud_acl_rule.ssh: Creation complete after 5s
</span></span><span class="line"><span class="cl">eoscloud_network.web: Creation complete after 5s
</span></span><span class="line"><span class="cl">eoscloud_public_ip.web: Creation complete after 5s
</span></span><span class="line"><span class="cl">eoscloud_vm.web: Creation complete after 20s
</span></span><span class="line"><span class="cl">eoscloud_public_ip_attachment.web: Creation complete after 6s
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Outputs:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">oeffentliche_ip = &#34;203.0.113.20&#34;</span></span></code></pre></div>
</div>
<p>Rund 40 Sekunden für ein vollständiges Setup. Cloud-Init braucht danach noch etwa eine Minute,
um den SSH-Server zu installieren – die ersten Verbindungsversuche laufen deshalb ins Leere.
Dann:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh koch@<span class="k">$(</span>tofu output -raw oeffentliche_ip<span class="k">)</span></span></span></code></pre></div>
</div>
<p>Die VM hat die Adresse <code>10.10.0.2</code> aus dem eigenen Netz, und <code>sudo sshd -T</code> bestätigt
<code>passwordauthentication no</code> und <code>permitrootlogin no</code> aus der Cloud-Init-Datei.</p>
<h3 id="10-ändern-und-abreißen">10. Ändern und abreißen</h3>
<p>Ein zweites <code>tofu plan</code> ohne Änderung muss <code>No changes.</code> melden – sonst weicht die Wirklichkeit
von deiner Datei ab. Jetzt eine echte Änderung: Port 80 soll zusätzlich offen sein. In <code>main.tf</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">HCL</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="n">  destination_ports</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;22&#34;, &#34;80&#34;</span><span class="p">]</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tofu apply -var <span class="nv">meine_ip</span><span class="o">=</span>DEINE_IP</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">  # eoscloud_acl_rule.ssh will be updated in-place
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Apply complete! Resources: 0 added, 1 changed, 0 destroyed.</span></span></code></pre></div>
</div>
<p>Die Regel wird an Ort und Stelle geändert, die VM bleibt unberührt – nach 6 Sekunden war das
erledigt. Anders bei <code>user_data</code>: Cloud-Init läuft nur beim ersten Start, deshalb <strong>ersetzt</strong>
OpenTofu die VM, sobald du die Datei änderst. Achte im Plan auf <code>must be replaced</code>, bevor du
<code>yes</code> tippst.</p>
<p>Zum Abräumen:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tofu destroy -var <span class="nv">meine_ip</span><span class="o">=</span>DEINE_IP</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Destroy complete! Resources: 6 destroyed.</span></span></code></pre></div>
</div>
<p>Das dauerte knapp eine Minute, den Großteil davon die VM. Anders als <code>eosctl</code> schaltet der
Provider sie vor dem Löschen selbst aus.</p>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong><code>eosctl compute vm delete</code> meldet nichts, die VM läuft weiter.</strong> eos löscht nur ausgeschaltete
VMs und sagt das nicht. Erst <code>eosctl compute vm power-off</code>, warten bis <code>power_state</code> auf
<code>stopped</code> steht, dann löschen (Schritt 6).</p>
<p><strong><code>exec</code> klappt direkt nach dem Anlegen nicht.</strong> <code>started</code> heißt nur, dass die VM läuft. Der
Agent, über den <code>exec</code> arbeitet, braucht eine halbe Minute länger – warte, bis <code>agent_ready</code> auf
<code>true</code> steht (Schritt 4).</p>
<p><strong>Von mehreren Befehlen kommt nur die erste Ausgabe zurück.</strong> Die inneren Anführungszeichen in
<code>bash -c</code> sind einfache. Nimm maskierte doppelte, wie in Schritt 5.</p>
<p><strong>OpenTofu bricht beim Anlegen mit einem Berechtigungsfehler ab.</strong> Lesen darf jeder Zugang,
schreiben nur mit Rolle. Für dieses Setup braucht er <code>compute:network-manager</code> und
<code>compute:vm-manager</code>. Prüfe außerdem in der Liste der Client Credentials, ob er abgelaufen ist,
und lege notfalls einen neuen an.</p>
<p><strong><code>tofu init</code> findet den Provider nicht.</strong> Die <code>source</code> muss den Hostnamen enthalten:
<code>dl.eoscloud.io/eos/eoscloud</code>. Ohne ihn sucht OpenTofu in seiner eigenen Registry, wo der
Provider nicht liegt.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Dein Problem ist nicht dabei?
  <a href="/fehler/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Alle Fehlermeldungen durchsuchen →</a>
</p>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<ul>
<li><strong>Den State schützen.</strong> <code>terraform.tfstate</code> enthält alle IDs, die IP-Adressen und den Inhalt
deiner Cloud-Init-Datei im Klartext. Er gehört <strong>nicht</strong> in ein öffentliches Repository. Für
ein Team lohnt ein Remote-Backend, etwa ein S3-Bucket – eos bringt S3 mit; sperr den Zugriff
darauf aber genauso ein wie die Zugangsdaten. Geht der State verloren, weiß OpenTofu nicht mehr,
was es angelegt hat, und du räumst von Hand auf.</li>
<li><strong>Geheimnisse bleiben draußen.</strong> Client-ID und Secret stehen in <code>~/.eos.env</code>, nie in <code>.tf</code>-Dateien.
Ein Passwort in <code>user_data</code> landet im State – Cloud-Init-Dateien also nur mit Schlüsseln.</li>
<li><strong>Zugänge rotieren.</strong> Setze jedem Client-Credential ein Ablaufdatum und lege rechtzeitig einen
neuen an. Einer pro Zweck (CI, Laptop, Experimente) lässt sich einzeln widerrufen.</li>
<li><strong>Provider-Updates bewusst einspielen.</strong> <code>tofu init -upgrade</code> holt die neueste erlaubte
Version. Solange der Provider unter 1.0 ist, lies vor jedem Sprung auf eine neue Unterversion
die Änderungen und prüfe mit <code>tofu plan</code>, dass nichts unerwartet ersetzt wird.</li>
<li><strong><code>eosctl</code> mit apt aktuell halten.</strong> Es kommt mit <code>apt upgrade</code>. Weil die Dokumentation
hinterherläuft, ist <code>eosctl &lt;befehl&gt; --help</code> die verlässlichere Quelle.</li>
<li><strong>Abräumen nicht vergessen.</strong> Abgerechnet wird minutengenau, und eine öffentliche IP kostet
auch ohne VM. <code>tofu destroy</code> erwischt alles, was in der Datei steht – Ressourcen aus der
Konsole oder per <code>eosctl</code> musst du selbst löschen.</li>
</ul>
<p>Unser Eindruck: Hier spielt eos seine Stärke aus. Alles, was wir in der Konsole gebraucht haben,
geht auch per <code>eosctl</code> und OpenTofu, der Provider ist signiert und ein komplettes Setup steht in unter einer Minute.
Die Kanten sind die einer Preview – ein stilles <code>delete</code>, eine Dokumentation, die der Software
hinterherläuft. Wer heute schon Server für den Betrieb braucht, nimmt einen netcup-VPS oder
Root-Server; wer seine Infrastruktur als Code verwalten will, findet in eos ab dem
Friends-&amp;-Family-Launch eine Plattform, die genau dafür gebaut ist.</p>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /tutorials/netcup-eos-terraform/" data-content-piece="VPS 1000 G12.5">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Empfehlung</span>
    <span title="Mit * markierte Links sind Affiliate-Links.">Anzeige</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCore · 8 GB RAM · 128 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">ab 14,50 €/Monat</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Für den Betrieb heute die sichere Wahl: der klassische netcup-VPS.</p>
    </div>
    <a href="https://www.netcup.com/de/server/vps/vps-1000-g12.5-24m-eu?ref=44083#vps-1000-g12.5-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12.5 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Zu netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200"
     data-voucher-api="vps-1000" data-voucher-msg-empty="Gerade ist für diesen Tarif kein Code frei – nimm den 5,95-€-Code." data-voucher-msg-limit="Für heute sind keine weiteren Codes drin – nimm den 5,95-€-Code oder versuch es morgen." data-voucher-msg-error="Der Code ließ sich gerade nicht abrufen – nimm den 5,95-€-Code."
     data-voucher-hint="Warenkorb vorher leeren – der Code legt den Server selbst hinein.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 Monat gratis</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Zum Kopieren klicken"
            class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">Code anzeigen</button>
    <button type="button" data-voucher-next hidden
            class="rounded border border-herb-500/40 px-2 py-0.5 text-xs text-herb-800 hover:bg-white dark:text-herb-400 dark:hover:bg-slate-800">anderer Code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Einmal einlösbar, gilt für VPS 1000 G12.5.
    <a href="https://www.netcup.com/de/checkout/warenkorb?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|VPS 1000 G12.5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Im Warenkorb einlösen →</a>
  </p><p class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-herb-500/30 pt-2">
    <span>💶 <span class="font-semibold">5,95 € Gutschein</span> für netcup-Neukunden:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Zum Kopieren klicken" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">immer gültig · nicht für Domains und VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/netcup-gutschein/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Alle netcup-Gutscheine →</a></p>
</div></div>
</div>

]]></content:encoded></item></channel></rss>