Skip to content

Add Hetzner API/MCP support for agentic infrastructure provisioning #2

Description

@tacman

Goal

Make Hetzner infrastructure manageable agentically instead of manually through the Hetzner UI.

The immediate use case is spinning up a temporary Hetzner Cloud server to test Elasticsearch. Elasticsearch has relatively heavy memory requirements, so this is a good example of infrastructure we may want to create for an experiment, use for a few hours or days, and then destroy.

Doing this manually through the Hetzner Console is tedious and error-prone. We should be able to tell an agent:

Create an appropriately sized Hetzner instance for Elasticsearch testing, configure access, return the IP/connection details, and later destroy it.

Start with a safe, inspectable implementation and avoid building a large abstraction layer prematurely.

Initial approach

Investigate using an existing Hetzner MCP server first.

Candidate:

  • hetzner-mcp
  • backed by the official Hetzner APIs/OpenAPI specifications
  • potentially covers Hetzner Cloud plus other Hetzner services
  • usable by Codex, Claude, and other MCP clients

If the MCP implementation is mature enough, prefer it over writing our own provisioning client.

Symfony/PHP integration can come later if an application itself needs runtime access to Hetzner.

Hetzner credentials

Create a dedicated Hetzner Cloud API token for agentic infrastructure work.

Hetzner tokens are project-scoped.

For the first pass:

  • use a dedicated Hetzner project if practical
  • create a token specifically for agentic/dev infrastructure
  • initially test read-only access
  • enable write access only when provisioning is ready to test
  • keep the token outside the repository
  • expose it through environment configuration

Example:

export HETZNER_TOKEN=...

Do not put the token in .env files that can be committed.

Phase 1: inventory

Before provisioning anything, verify that the MCP/API integration can inspect the project.

We should be able to retrieve at least:

  • servers
  • server types
  • locations
  • images
  • SSH keys
  • firewalls
  • volumes
  • networks
  • public IPs

Test queries such as:

List all servers in this Hetzner project.

Show available server types with RAM, CPUs, disk and architecture.

Show available Ubuntu images.

List existing SSH keys.

List existing firewalls.

Show servers currently running in Ashburn.

This gives us confidence that credentials and project scope are correct before allowing mutations.

Phase 2: Elasticsearch test server

Add a repeatable workflow for creating an Elasticsearch test machine.

Initial requirements:

  • Ubuntu 24.04 LTS
  • x86_64 unless ARM compatibility has been explicitly verified
  • enough RAM to make Elasticsearch useful
  • public IPv4
  • existing SSH key installed
  • sensible hostname
  • firewall permitting SSH only from appropriate sources
  • optionally allow HTTP/HTTPS or Elasticsearch ports later if necessary

Do not expose Elasticsearch port 9200 publicly by default.

A reasonable initial machine should probably have at least:

RAM: 8 GB
CPU: 4 vCPU or better
Disk: 80+ GB

The exact Hetzner server type should be selected from current availability rather than hard-coded permanently.

Example desired interaction:

Create an Elasticsearch test machine in Hetzner.

Requirements:
- Ubuntu 24.04
- at least 8 GB RAM
- x86_64
- use our existing SSH key
- use Ashburn if an appropriate server type is available
- call it elastic-test

The agent should return:

Server: elastic-test
Type: ...
Location: ...
RAM: ...
IPv4: ...
SSH: ssh root@...

Phase 3: bootstrap Elasticsearch

Once provisioning works, automate software installation separately from Hetzner resource creation.

Do not make the Hetzner provisioning layer responsible for Elasticsearch internals.

Possible bootstrap mechanisms:

  1. cloud-init
  2. Ansible
  3. shell bootstrap script
  4. Docker/Compose

Cloud-init is attractive for the first experiment because Hetzner can apply it when the machine is created.

Example responsibilities:

apt update
install Docker or Elasticsearch packages
configure swap/memory settings if required
start Elasticsearch
verify cluster health

Keep the bootstrap configuration in the repository so it can be reviewed and reproduced.

Prefer Docker for the first Elasticsearch experiment

For an experimental machine, Elasticsearch via Docker is probably the quickest path.

Something approximately like:

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:9.x
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms4g -Xmx4g
    volumes:
      - esdata:/usr/share/elasticsearch/data
    ports:
      - "127.0.0.1:9200:9200"

volumes:
  esdata:

The exact Elasticsearch release should be chosen at implementation time.

Binding to localhost is deliberate. Access it through SSH tunneling unless we later have a reason to expose it.

Example:

ssh -L 9200:localhost:9200 root@SERVER_IP

Then locally:

curl http://localhost:9200

Lifecycle commands/workflow

The important part is not merely creating a server. The lifecycle should be easy enough that temporary infrastructure actually stays temporary.

Desired operations:

create elastic-test
inspect elastic-test
stop elastic-test
start elastic-test
snapshot elastic-test
destroy elastic-test

Destruction should be simple and explicit.

Before destroying a server, report:

  • server name
  • IP
  • attached volumes
  • snapshots/backups if relevant

Do not accidentally destroy persistent volumes.

Cost awareness

When creating infrastructure, the agent should report the selected Hetzner server type and its current hourly/monthly price if that information is available from the API.

Example:

Creating:

elastic-test
8 GB RAM
4 vCPU
80 GB disk

Estimated Hetzner cost:
€X/hour
€Y/month maximum

This matters because the intended usage is often short-lived experimentation.

MCP safety model

MCP gives an agent potentially destructive infrastructure capabilities.

We should distinguish operations conceptually:

Read-only

Safe for normal agent use:

list
get
inspect
describe
inventory

Mutating but reversible

Examples:

create server
power on/off
attach volume
change firewall

Destructive

Examples:

delete server
delete volume
rebuild server
delete snapshot

For destructive actions, the agent should clearly identify the target resource before executing them.

If the MCP server supports restricting available tools, consider exposing read/create operations broadly while treating destructive operations more carefully.

Multiple Hetzner projects

Hetzner Cloud API tokens are scoped to a project.

Do not assume one global Hetzner account API gives us everything.

Eventually we may want configuration like:

hetzner:
  projects:
    dev:
      token: '%env(HETZNER_DEV_TOKEN)%'

    fortepan:
      token: '%env(HETZNER_FORTEPAN_TOKEN)%'

    survos:
      token: '%env(HETZNER_SURVOS_TOKEN)%'

But this is not required for the initial Elasticsearch experiment.

Start with one development project.

PHP/Symfony fallback

If MCP turns out to be insufficient, use the Hetzner Cloud REST API directly.

Candidate PHP SDK:

composer require lkdevelopment/hetzner-cloud-php-sdk

Alternatively, Symfony HttpClient can call:

https://api.hetzner.cloud/v1/

directly.

Avoid creating a large Symfony bundle until we have a concrete application-level requirement.

If we eventually do add Symfony integration, keep it thin:

HetznerClient
HetznerProjectRegistry
hetzner:inventory
hetzner:server:list
hetzner:server:create
hetzner:server:delete

No Doctrine mirror of Hetzner resources.

Hetzner remains the source of truth.

First implementation target

The first end-to-end success criterion is intentionally small:

Agent receives:

"Create a temporary Hetzner machine for Elasticsearch testing."

Agent:

1. checks available Hetzner server types
2. selects one with >= 8 GB RAM
3. creates Ubuntu 24.04 server
4. installs our SSH key
5. configures a restrictive firewall
6. returns its IP
7. verifies SSH availability
8. bootstraps Elasticsearch
9. verifies Elasticsearch responds

Then:

"Destroy the elastic-test machine."

should remove the temporary infrastructure cleanly.

Acceptance criteria

  • Identify and install/configure a suitable Hetzner MCP server.
  • Authenticate against a dedicated Hetzner project.
  • Successfully list existing servers.
  • List server types and memory sizes.
  • List available Ubuntu images.
  • List SSH keys and firewalls.
  • Provision an Ubuntu 24.04 test server with at least 8 GB RAM.
  • Install the appropriate SSH key automatically.
  • Apply a restrictive firewall.
  • Return server IP and SSH command.
  • Bootstrap Elasticsearch.
  • Verify Elasticsearch health from the development machine.
  • Report the server type and approximate cost.
  • Destroy the temporary server through the same agentic workflow.
  • Confirm no unintended persistent resources remain.
  • Document MCP configuration and required environment variables.

Non-goals

For this first issue, do not build:

  • a generic cloud abstraction
  • Terraform infrastructure
  • a full Symfony Hetzner bundle
  • a local database mirroring Hetzner resources
  • production Elasticsearch infrastructure
  • public Elasticsearch endpoints
  • multi-cloud support

The objective is much simpler:

make disposable Hetzner infrastructure trivial to create and destroy from an agent.

Elasticsearch is the first concrete test case because its memory requirements make local development inconvenient and repeatedly provisioning it through the Hetzner UI is unnecessary friction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions