Skip to content

Provisioning

Cloud infrastructure as code: servers, networks, firewalls, DNS, load balancers, TLS certificates and volumes. Single server or multi-node cluster. Provider-independent, ENV-driven, with state tracking.

Overview

jardisops/provisioning automates complete cloud infrastructure: from a single server to a multi-node cluster with private network, load balancer and automatic DNS. Everything is configured via .env files, the current state is persisted in .provision-state.json.

GitHub: jardisOps/provisioning

PropertyValue
PHP≥ 8.2
ProvidersHetzner Cloud, INWX DNS
ModesSingle server, multi-node cluster
EncryptionAES-256-GCM, Sodium
State.provision-state.json

Installation

bash
composer require jardisops/provisioning

Initialize a new project:

bash
vendor/bin/provision init --project-root=.

Creates .env template, Makefile and .gitignore.

Architecture

┌──────────────────────────────────────────────────────┐
│              CLI (vendor/bin/provision)               │
└─────────────────────────┬────────────────────────────┘

┌─────────────────────────▼────────────────────────────┐
│          Provisioner (Orchestrator)                   │
│       Provider-independent workflow coordination      │
└──┬────────┬─────────┬─────────┬─────────┬────────┬───┘
   │        │         │         │         │        │
 Server  Network  Firewall  LoadBal.   DNS    Volume
   │        │         │         │         │        │
   └────────┴─────────┴─────────┴─────────┴────────┘

              ┌───────────▼───────────┐
              │   Provider Adapters   │
              │  Hetzner │ INWX │ ... │
              └───────────────────────┘

The Provisioner has no knowledge of provider details. It works exclusively through interfaces from Support/Contract/.

Deployment Modes

Single Server

One server with external firewall, optional DNS and optional volume.

ini
PROVISION_MODE=single
INFRA_PROVIDER=hetzner

SERVER_NAME=my-app
SERVER_TYPE=cpx31

Workflow:

  1. Register SSH public key with provider
  2. Generate cloud-init script (hardening)
  3. Create server + wait until running
  4. Create external firewall (22, 80, 443)
  5. Create DNS records (optional)
  6. Create volume (optional)
  7. Save state

Multi-Node Cluster

Multiple servers with private network, internal firewall, load balancer and TLS.

ini
PROVISION_MODE=cluster
INFRA_PROVIDER=hetzner
CLUSTER_NAME=jardis-prod
CLUSTER_NODE_COUNT=3

CLUSTER_NODE_1_ROLE=server
CLUSTER_NODE_1_TYPE=cpx31
CLUSTER_NODE_1_NAME=jardis-prod-server-1

CLUSTER_NODE_2_ROLE=agent
CLUSTER_NODE_2_TYPE=cpx31
CLUSTER_NODE_2_NAME=jardis-prod-agent-1

CLUSTER_NODE_3_ROLE=agent
CLUSTER_NODE_3_TYPE=cpx31
CLUSTER_NODE_3_NAME=jardis-prod-agent-2

# Private Network
PRIVATE_NETWORK_NAME=jardis-prod-net
PRIVATE_NETWORK_SUBNET=10.0.1.0/24
PRIVATE_NETWORK_ZONE=eu-central

# Load Balancer
LOADBALANCER_ENABLED=true
LOADBALANCER_NAME=jardis-prod-lb
LOADBALANCER_TYPE=lb11
LOADBALANCER_ALGORITHM=round_robin
LOADBALANCER_HEALTH_CHECK_PATH=/health

Workflow:

  1. Register SSH key + generate cloud-init
  2. Create private network
  3. Create server node (10.0.1.2)
  4. Create agent nodes (10.0.1.3, 10.0.1.4, ...)
  5. External firewall (22, 80, 443) + internal firewall (K3S ports)
  6. Create TLS certificate (Let's Encrypt)
  7. Create load balancer + assign targets
  8. DNS records pointing to LB IP
  9. Create volumes (optional)
  10. Save state

Internal Firewall Rules (K3S):

PortProtocolService
6443TCPKubernetes API
8472UDPFlannel/WireGuard
10250TCPKubelet
2379TCPetcd
2380TCPetcd peer

Configuration

Global Variables

VariableDescription
PROVISION_MODEsingle or cluster
INFRA_PROVIDERInfrastructure provider (hetzner)
DNS_PROVIDERDNS provider (hetzner, inwx) — optional
SSH_KEY_PATHPath to SSH public key

Hetzner Cloud

VariableDescription
HETZNER_API_TOKENAPI token (supports encryption)
HETZNER_DNS_TOKENDNS token (optional, falls back to API token)
HETZNER_REGIONData center (fsn1, nbg1, hel1, ash, sin)
HETZNER_IMAGEServer image (ubuntu-24.04)
HETZNER_BACKUP_ENABLEDAutomatic backups (true/false)

INWX DNS

VariableDescription
INWX_USERINWX username
INWX_PASSWORDPassword (supports encryption)
DNS_ZONEDNS zone (example.com)
DNS_RECORDSRecords (api:A,portal:A,monitoring:A)
DNS_TTLTime-to-live (default: 300)

Security / Cloud-Init

VariableDefaultDescription
SECURITY_DEPLOY_USERdeployNon-root deploy user
SECURITY_SSH_PORT22SSH port
SECURITY_DISABLE_ROOTtrueDisable root login
SECURITY_DISABLE_PASSWORD_AUTHtrueSSH key auth only
SECURITY_AUTO_UPDATEStrueAutomatic updates
SECURITY_FAIL2BANtrueEnable Fail2Ban

Load Balancer

VariableDefaultDescription
LOADBALANCER_ENABLEDEnable load balancer
LOADBALANCER_NAMEName
LOADBALANCER_TYPElb11Type (lb11, lb21)
LOADBALANCER_ALGORITHMround_robinAlgorithm
LOADBALANCER_HEALTH_CHECK_PROTOCOLhttpProtocol
LOADBALANCER_HEALTH_CHECK_PORT80Port
LOADBALANCER_HEALTH_CHECK_PATH/healthPath
LOADBALANCER_HEALTH_CHECK_INTERVAL15Interval (sec)
LOADBALANCER_HEALTH_CHECK_TIMEOUT10Timeout (sec)
LOADBALANCER_HEALTH_CHECK_RETRIES3Retries

CLI Commands

bash
vendor/bin/provision <command> [options]
CommandDescription
initBootstrap project (ENV template, Makefile)
provisionBuild infrastructure
provision --dry-runPreview without changes
deprovision --forceTear down everything
statusShow current state
status --jsonState as JSON
node:addAdd node to cluster
node:removeRemove node from cluster
secret:generate-keyGenerate encryption key
secret:encryptEncrypt value (AES-256-GCM)
secret:encrypt-sodiumEncrypt value (Sodium)

Node Management

Add Node

bash
vendor/bin/provision node:add \
  --name=jardis-prod-agent-3 \
  --role=agent \
  --type=cpx31 \
  --volume=50

Creates server, attaches to private network, applies firewalls, adds to load balancer.

Remove Node

bash
vendor/bin/provision node:remove \
  --name=jardis-prod-agent-3 \
  --delete-volume

Removes node from LB, detaches volumes, removes firewalls, deletes server.

Secret Management

API tokens and passwords can be stored encrypted in .env:

bash
# Generate key
vendor/bin/provision secret:generate-key

# Encrypt value
vendor/bin/provision secret:encrypt --value="hcloud-Xyz..."
# → secret(aes:k9Xp2mV8nQ3wR6yT...)

Add to .env:

ini
HETZNER_API_TOKEN=secret(aes:k9Xp2mV8nQ3wR6yT...)
INWX_PASSWORD=secret(sodium:A7bQ9c...)

Decryption happens automatically when loading configuration via jardissupport/dotenv + jardissupport/secret.

State Tracking

The current infrastructure state is persisted in .provision-state.json:

json
{
  "version": 1,
  "mode": "cluster",
  "cluster_name": "jardis-prod",
  "provider": "hetzner",
  "region": "fsn1",
  "created_at": "2024-01-15T10:30:00+00:00",
  "resources": {
    "ssh_key": { "id": 123, "name": "jardis-prod" },
    "nodes": [
      { "name": "server-1", "role": "server", "public_ip": "49.12.xx.xx", "private_ip": "10.0.1.2" }
    ],
    "firewalls": [...],
    "network": { "name": "jardis-prod-net", "subnet": "10.0.1.0/24" },
    "load_balancer": { "name": "jardis-prod-lb", "ip": "49.12.xx.xx" },
    "dns_records": [...]
  }
}

Idempotent: Running provision multiple times does not create duplicates: the state prevents double resource creation.

Extending Providers

Adding a new provider:

  1. Create directory src/Provider/<Name>/
  2. Implement 6–7 interfaces (ServerProviderInterface, NetworkProviderInterface, etc.)
  3. Create handler class (<Name>InfraHandler)
  4. Register in ProvisionerFactory
  5. Add .env.example in provider directory

The Provisioner, StateManager and CLI work automatically: no changes needed.

Makefile

TargetDescription
make provisionBuild infrastructure
make provision-dryDry run
make deprovisionTear down everything
make cluster-statusShow status
make cluster-status-jsonStatus as JSON
make node-addAdd node
make node-removeRemove node
make generate-key-fileGenerate encryption key
make encryptEncrypt value (AES)
make encrypt-sodiumEncrypt value (Sodium)

Dependencies

PackagePurpose
jardissupport/dotenvENV loading with type casting
jardissupport/secretSecret encryption/decryption
ext-jsonJSON processing
ext-curlHTTP requests
ext-xmlrpcINWX XML-RPC API