<?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/en/tags/opentofu/</link><description>Opentofu – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</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/en/tags/opentofu/index.xml" rel="self" type="application/rss+xml"/><item><title>Automating netcup eos with eosctl and OpenTofu</title><link>https://serverkueche.de/en/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/en/tutorials/netcup-eos-terraform/</guid><description>netcup eos without clicking: client credentials with minimal rights, VMs via eosctl and a complete setup of network, ACL and VM in OpenTofu.</description><content:encoded><![CDATA[<p>eos is built for people who don&rsquo;t want to create servers by clicking. So in part two we drive
netcup&rsquo;s new cloud from the command line – first with the <code>eosctl</code> tool, then with OpenTofu as
infrastructure as code. You only need the console for the access credentials themselves.</p>
<p>As in part one: this is the state of the preview, which netcup takes down on 13 November 2026. eos launches publicly in 2027.
Before that, later in 2026, every netcup customer can use the platform free of charge in the
friends &amp; family launch, on production hardware and without a later reset. The CLI and
the provider are still below version 1.0 and may change.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>We create access for programs that may only manage networks and VMs. With <code>eosctl</code> we create a
VM, run a command inside it and delete it again. Then we describe a complete setup in an OpenTofu
file: its own network, a firewall rule, a Debian VM with cloud-init and a public IPv4. One
<code>tofu apply</code> builds everything in under a minute, one <code>tofu destroy</code> tears it down just as fast.</p>
<p>We use <code>eosctl</code> 0.16.0, OpenTofu 1.13.1 and the eos provider 0.10.1. OpenTofu is the open-source
fork of Terraform and understands the same configuration language.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Access to eos and a basic grasp of the console – organisation, realm, ACL and public IP are
explained in <a href="/en/tutorials/netcup-eos-preview/">netcup eos in preview</a>.</li>
<li>A machine running Debian 13 for the tools. That can be your laptop or a small server; we used
an eos VM.</li>
<li>An SSH key (<code>ssh-keygen -t ed25519</code>), see <a href="/en/tutorials/harden-ssh/">Harden SSH</a>.</li>
<li><code>jq</code> for parsing JSON: <code>sudo apt install jq</code>.</li>
</ul>
<h2 id="step-by-step">Step by step</h2>
<h3 id="1-client-credentials-with-minimal-rights">1. Client credentials with minimal rights</h3>
<p>Programs don&rsquo;t log in to eos with your account but with <strong>client credentials</strong> – a client ID
and a secret. Create them in the console under <strong>Realms → your realm → Client Credentials →
Create credential</strong>. The page describes them as &ldquo;organization level&rdquo;; they are still created
for the realm you are in.</p>
<p><figure class="my-6"><img src="/en/tutorials/netcup-eos-terraform/eos-credentials_hu_e479dcefa5b9e4c9.webp" srcset="/en/tutorials/netcup-eos-terraform/eos-credentials_hu_3981cfa16f99dd88.webp 480w, /en/tutorials/netcup-eos-terraform/eos-credentials_hu_e479dcefa5b9e4c9.webp 768w, /en/tutorials/netcup-eos-terraform/eos-credentials_hu_93063f43fd53d4e5.webp 1200w, /en/tutorials/netcup-eos-terraform/eos-credentials_hu_19ba4481ee9f87ee.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/netcup-eos-terraform/eos-credentials_hu_ae1fd346152e40d1.webp"
    alt="The dialog for new client credentials with name, expiry date and the roles Network Manager and VM Manager" title="Client credentials: the base right is read, everything else is added as a role"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Client credentials: the base right is read, everything else is added as a role</figcaption></figure></p>
<p>Every credential can <strong>read</strong> everything. Write access comes as roles: Compute Network Manager,
VM Manager, Volume Manager, Object Storage Manager and Realm Admin. Two are enough for this
tutorial:</p>
<ul>
<li><code>compute:network-manager</code> – networks, ACLs and public IPs</li>
<li><code>compute:vm-manager</code> – create, power and delete VMs</li>
</ul>
<p>Also set an <strong>expiry date</strong>. Credentials for experiments don&rsquo;t need a lifetime of a year, and a
forgotten key then expires by itself.</p>
<p>After saving, eos shows the secret <strong>exactly once</strong>. Copy both values straight into a file only
you can read:</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=YOUR_CLIENT_ID
</span></span></span><span class="line"><span class="cl"><span class="s">export EOS_CLIENT_SECRET=YOUR_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> makes sure the file is created with mode <code>600</code>. Both <code>eosctl</code> and the OpenTofu
provider read exactly these two variables – so the secret never has to appear in a
configuration file.</p>
<h3 id="2-install-eosctl">2. Install eosctl</h3>
<p>netcup ships <code>eosctl</code> as a Debian package from its own repository. First fetch the signing key
so apt only accepts packages signed by netcup:</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>Then add the package source and install the package:</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> binds the key to this one source only – it doesn&rsquo;t apply to Debian&rsquo;s own packages.
Updates arrive with <code>apt upgrade</code> from now on. At the time, the eos documentation still named
version 0.12.1 while the repository already shipped 0.16.0.</p>
<h3 id="3-log-in-and-tame-the-output">3. Log in and tame the output</h3>
<p>Load the credentials and log in:</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>The warning appears on any server without a keyring: <code>eosctl</code> stores the access token as a file
in your home directory. On a machine only you use, that&rsquo;s fine; on a shared system, log out with
<code>eosctl logout</code> when you&rsquo;re done.</p>
<p>By default <code>eosctl</code> prints coloured tables. For scripts there are <code>-o json</code>, <code>-o yaml</code> and
<code>-o template</code>; <code>NO_COLOR=1</code> turns the colours off. This lists the available 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>The products reveal what the console keeps quiet about:</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>A fourth size with 8 cores and 16 GB already exists, it just isn&rsquo;t enabled yet. Likewise, the API
knows a second block (<code>at-preview1-db</code>) next to the one in the console.</p>
<h3 id="4-create-a-vm-with-eosctl">4. Create a VM with eosctl</h3>
<p>A VM needs four IDs: block, product, image and network. Instead of copying them by hand, look
them up by name with <code>jq</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"><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> takes the first network in the realm – if you have several, select it by name like the
others. As the cloud-init file, use the <code>user-data.yaml</code> from part one: it creates a user with
your key and installs the SSH server the cloud images lack.</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> is the disk size in <strong>megabytes</strong>, so 10000 is 10 GB – the minimum. <code>-U</code> expects the file&rsquo;s
content, not its path. With <code>-o template -t '{{.id}}'</code> the command prints only the new VM&rsquo;s ID,
which you keep using.</p>
<p>Check whether it runs with the same template trick:</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;running&#34;</span></span></span></code></pre></div>
</div>
<p>The VM was up after just over 20 seconds. But <code>started</code> only means it&rsquo;s running. The next step
also needs the agent inside the VM to be ready, which takes about another half 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-run-commands-inside-the-vm">5. Run commands inside the VM</h3>
<p><code>eosctl compute vm exec</code> runs a command directly inside the VM – no SSH, no public IP, no ACL
rule. Wrap several commands 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>The inner quotes must be escaped double quotes (<code>\&quot;</code>). With single quotes, only the first
command&rsquo;s output came back.</p>
<p>The command runs as <strong>root</strong>. That&rsquo;s the same back door as the web console from part one, just
scriptable – and the reason credentials with the <code>vm-manager</code> role need to be guarded like a root
password.</p>
<h3 id="6-deleting-a-vm-power-off-first">6. Deleting a VM: power off first</h3>
<p>The obvious command does <strong>nothing</strong> on a running VM:</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>No error message, and the VM simply keeps running. eos only deletes VMs that are powered off.
The right order is power off, wait, delete:</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>The root volume disappears with the VM. Check with <code>eosctl compute vm list</code> that it&rsquo;s gone – in
scripts, silently doing nothing is more dangerous than an error.</p>
<h3 id="7-install-opentofu">7. Install OpenTofu</h3>
<p>For anything beyond a single VM, a declarative description is more convenient: you write down
what should exist, and OpenTofu works out what to create, change or delete. OpenTofu is available
as a <code>.deb</code> on 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>You don&rsquo;t need to install the eos provider – OpenTofu fetches it itself in the next step.</p>
<h3 id="8-the-setup-as-code">8. The setup as code</h3>
<p>Create a directory and copy the <code>user-data.yaml</code> from part one into it:</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>Then the file <code>main.tf</code>. We go through it in sections; together they form one file. First the
provider and the input:</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"># Credentials come from 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;my_ip&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  description</span> <span class="o">=</span> <span class="s2">&#34;Public IPv4 allowed to connect via SSH&#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>The provider doesn&rsquo;t come from the OpenTofu registry but straight from netcup&rsquo;s server
<code>dl.eoscloud.io</code>. <code>~&gt; 0.10.0</code> allows bug fixes within 0.10 but no jump to 0.11 – below version
1.0, every minor release may break things. The empty <code>provider</code> block is intentional: it reads
the credentials from the environment variables.</p>
<p>Instead of hard-coding IDs, the configuration looks up block, size and image by 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;all&#34;</span> {}
</span></span><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;eoscloud_products&#34; &#34;all&#34;</span> {}
</span></span><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;eoscloud_vm_images&#34; &#34;all&#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.all.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.all.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.all.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>Now the firewall and the network. The ACL gets a single rule, SSH from your address; everything
else stays blocked, as usual on eos:</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.my_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-net&#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>Unlike in the console, the rule needs no destination here: it applies to the whole network the
ACL is attached to. The network gets its own private address range.</p>
<p>Finally the VM, the public IP and the link between them:</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;public_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>The IP and its attachment are two resources, just like the two steps in the console. That way
you can keep an address while the VM behind it is replaced.</p>
<h3 id="9-init-plan-apply">9. init, plan, apply</h3>
<p><code>tofu init</code> downloads the provider and verifies its signature:</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>This creates <code>.terraform.lock.hcl</code> with the exact version and its checksums. Commit this file to
your Git repository – that way everyone who rolls out the setup gets the same provider.</p>
<p>Pass your public IPv4 as a variable. <code>tofu plan</code> shows what would happen without creating
anything:</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">my_ip</span><span class="o">=</span>YOUR_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>If the plan looks right, build it:</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">my_ip</span><span class="o">=</span>YOUR_IP</span></span></code></pre></div>
</div>
<p>After the confirmation (<code>yes</code>), OpenTofu creates the resources in the right order:</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">public_ip = &#34;203.0.113.20&#34;</span></span></code></pre></div>
</div>
<p>Around 40 seconds for a complete setup. Cloud-init then needs about another minute to install
the SSH server – so the first connection attempts go nowhere. Then:</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 public_ip<span class="k">)</span></span></span></code></pre></div>
</div>
<p>The VM has the address <code>10.10.0.2</code> from its own network, and <code>sudo sshd -T</code> confirms
<code>passwordauthentication no</code> and <code>permitrootlogin no</code> from the cloud-init file.</p>
<h3 id="10-change-and-tear-down">10. Change and tear down</h3>
<p>A second <code>tofu plan</code> without changes must report <code>No changes.</code> – otherwise reality has drifted
from your file. Now a real change: port 80 should be open as well. 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">my_ip</span><span class="o">=</span>YOUR_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>The rule is changed in place and the VM stays untouched – done after 6 seconds. <code>user_data</code> is a
different story: cloud-init only runs on first boot, so OpenTofu <strong>replaces</strong> the VM as soon as
you change the file. Look for <code>must be replaced</code> in the plan before you type <code>yes</code>.</p>
<p>To tear everything down:</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">my_ip</span><span class="o">=</span>YOUR_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>That took just under a minute, most of it for the VM. Unlike <code>eosctl</code>, the provider powers the VM
off by itself before deleting it.</p>
<h2 id="when-it-doesnt-work">When it doesn&rsquo;t work</h2>
<div class="troubleshoot not-prose">
<p><strong><code>eosctl compute vm delete</code> reports nothing and the VM keeps running.</strong> eos only deletes VMs that
are powered off and doesn&rsquo;t say so. Run <code>eosctl compute vm power-off</code> first, wait until
<code>power_state</code> is <code>stopped</code>, then delete (step 6).</p>
<p><strong><code>exec</code> doesn&rsquo;t work right after creation.</strong> <code>started</code> only means the VM is running. The agent
<code>exec</code> talks to needs another half minute – wait until <code>agent_ready</code> is <code>true</code> (step 4).</p>
<p><strong>Only the first of several commands returns output.</strong> The inner quotes in <code>bash -c</code> are single
quotes. Use escaped double quotes as in step 5.</p>
<p><strong>OpenTofu aborts with a permission error while creating resources.</strong> Every credential can read,
but writing needs a role. This setup needs <code>compute:network-manager</code> and <code>compute:vm-manager</code>.
Also check the client credentials list to see whether it has expired, and create a new one if
necessary.</p>
<p><strong><code>tofu init</code> can&rsquo;t find the provider.</strong> The <code>source</code> must include the hostname:
<code>dl.eoscloud.io/eos/eoscloud</code>. Without it, OpenTofu searches its own registry, where the provider
doesn&rsquo;t live.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Your problem is not listed?
  <a href="/en/errors/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Search all error messages →</a>
</p>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Protect the state.</strong> <code>terraform.tfstate</code> contains all IDs, the IP addresses and the content of
your cloud-init file in plain text. It does <strong>not</strong> belong in a public repository. For a team, a
remote backend such as an S3 bucket is worth it – eos has S3 built in; lock access to it down
just like the credentials. If the state is lost, OpenTofu no longer knows what it created, and
you clean up by hand.</li>
<li><strong>Keep secrets out.</strong> Client ID and secret live in <code>~/.eos.env</code>, never in <code>.tf</code> files. A
password in <code>user_data</code> ends up in the state – so cloud-init files should only carry keys.</li>
<li><strong>Rotate credentials.</strong> Give every client credential an expiry date and create a new one in
time. One per purpose (CI, laptop, experiments) can be revoked individually.</li>
<li><strong>Update the provider deliberately.</strong> <code>tofu init -upgrade</code> fetches the newest allowed version.
While the provider is below 1.0, read the changes before every jump to a new minor version and
check with <code>tofu plan</code> that nothing is replaced unexpectedly.</li>
<li><strong>Keep <code>eosctl</code> current with apt.</strong> It updates with <code>apt upgrade</code>. Since the documentation lags
behind, <code>eosctl &lt;command&gt; --help</code> is the more reliable source.</li>
<li><strong>Don&rsquo;t forget to clean up.</strong> Billing is per minute, and a public IP costs money even without a
VM. <code>tofu destroy</code> catches everything in the file – resources created in the console or with
<code>eosctl</code> you delete yourself.</li>
</ul>
<p>Our impression: this is where eos plays to its strengths. Everything we needed in the console
also works via <code>eosctl</code> and OpenTofu, the provider is signed and a complete setup stands in under
a minute. The rough edges are those of a preview – a silent <code>delete</code>, documentation lagging
behind the software. If you need servers for production today, take a netcup VPS or root server;
if you want to manage your infrastructure as code, eos will be a platform built for exactly that
from the friends &amp; family launch onwards.</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 · /en/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>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</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 vCores · 8 GB RAM · 128 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €14.50/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">The safe choice for production today: the classic netcup VPS.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12.5-iv-24m-eu?ref=44083#vps-1000-g12.5-iv-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">
  Go to 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="No code is available for this plan right now – use the €5.95 code." data-voucher-msg-limit="No more codes today – use the €5.95 code or try again tomorrow." data-voucher-msg-error="The code couldn&#39;t be fetched right now – use the €5.95 code."
     data-voucher-hint="Empty the cart first – the code adds the server itself.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 month free</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Click to copy"
            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">Show code</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">another code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Single use, valid for VPS 1000 G12.5.
    <a href="https://www.netcup.com/en/checkout/cart?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">Redeem in the cart →</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 voucher</span> for new netcup customers:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Click to copy" 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">always valid · not for domains or VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/en/netcup-voucher/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">All netcup vouchers →</a></p>
</div></div>
</div>

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