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
| Property | Value |
|---|---|
| PHP | ≥ 8.2 |
| Providers | Hetzner Cloud, INWX DNS |
| Modes | Single server, multi-node cluster |
| Encryption | AES-256-GCM, Sodium |
| State | .provision-state.json |
Installation
composer require jardisops/provisioningInitialize a new project:
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.
PROVISION_MODE=single
INFRA_PROVIDER=hetzner
SERVER_NAME=my-app
SERVER_TYPE=cpx31Workflow:
- Register SSH public key with provider
- Generate cloud-init script (hardening)
- Create server + wait until running
- Create external firewall (22, 80, 443)
- Create DNS records (optional)
- Create volume (optional)
- Save state
Multi-Node Cluster
Multiple servers with private network, internal firewall, load balancer and TLS.
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=/healthWorkflow:
- Register SSH key + generate cloud-init
- Create private network
- Create server node (10.0.1.2)
- Create agent nodes (10.0.1.3, 10.0.1.4, ...)
- External firewall (22, 80, 443) + internal firewall (K3S ports)
- Create TLS certificate (Let's Encrypt)
- Create load balancer + assign targets
- DNS records pointing to LB IP
- Create volumes (optional)
- Save state
Internal Firewall Rules (K3S):
| Port | Protocol | Service |
|---|---|---|
| 6443 | TCP | Kubernetes API |
| 8472 | UDP | Flannel/WireGuard |
| 10250 | TCP | Kubelet |
| 2379 | TCP | etcd |
| 2380 | TCP | etcd peer |
Configuration
Global Variables
| Variable | Description |
|---|---|
PROVISION_MODE | single or cluster |
INFRA_PROVIDER | Infrastructure provider (hetzner) |
DNS_PROVIDER | DNS provider (hetzner, inwx) — optional |
SSH_KEY_PATH | Path to SSH public key |
Hetzner Cloud
| Variable | Description |
|---|---|
HETZNER_API_TOKEN | API token (supports encryption) |
HETZNER_DNS_TOKEN | DNS token (optional, falls back to API token) |
HETZNER_REGION | Data center (fsn1, nbg1, hel1, ash, sin) |
HETZNER_IMAGE | Server image (ubuntu-24.04) |
HETZNER_BACKUP_ENABLED | Automatic backups (true/false) |
INWX DNS
| Variable | Description |
|---|---|
INWX_USER | INWX username |
INWX_PASSWORD | Password (supports encryption) |
DNS_ZONE | DNS zone (example.com) |
DNS_RECORDS | Records (api:A,portal:A,monitoring:A) |
DNS_TTL | Time-to-live (default: 300) |
Security / Cloud-Init
| Variable | Default | Description |
|---|---|---|
SECURITY_DEPLOY_USER | deploy | Non-root deploy user |
SECURITY_SSH_PORT | 22 | SSH port |
SECURITY_DISABLE_ROOT | true | Disable root login |
SECURITY_DISABLE_PASSWORD_AUTH | true | SSH key auth only |
SECURITY_AUTO_UPDATES | true | Automatic updates |
SECURITY_FAIL2BAN | true | Enable Fail2Ban |
Load Balancer
| Variable | Default | Description |
|---|---|---|
LOADBALANCER_ENABLED | — | Enable load balancer |
LOADBALANCER_NAME | — | Name |
LOADBALANCER_TYPE | lb11 | Type (lb11, lb21) |
LOADBALANCER_ALGORITHM | round_robin | Algorithm |
LOADBALANCER_HEALTH_CHECK_PROTOCOL | http | Protocol |
LOADBALANCER_HEALTH_CHECK_PORT | 80 | Port |
LOADBALANCER_HEALTH_CHECK_PATH | /health | Path |
LOADBALANCER_HEALTH_CHECK_INTERVAL | 15 | Interval (sec) |
LOADBALANCER_HEALTH_CHECK_TIMEOUT | 10 | Timeout (sec) |
LOADBALANCER_HEALTH_CHECK_RETRIES | 3 | Retries |
CLI Commands
vendor/bin/provision <command> [options]| Command | Description |
|---|---|
init | Bootstrap project (ENV template, Makefile) |
provision | Build infrastructure |
provision --dry-run | Preview without changes |
deprovision --force | Tear down everything |
status | Show current state |
status --json | State as JSON |
node:add | Add node to cluster |
node:remove | Remove node from cluster |
secret:generate-key | Generate encryption key |
secret:encrypt | Encrypt value (AES-256-GCM) |
secret:encrypt-sodium | Encrypt value (Sodium) |
Node Management
Add Node
vendor/bin/provision node:add \
--name=jardis-prod-agent-3 \
--role=agent \
--type=cpx31 \
--volume=50Creates server, attaches to private network, applies firewalls, adds to load balancer.
Remove Node
vendor/bin/provision node:remove \
--name=jardis-prod-agent-3 \
--delete-volumeRemoves node from LB, detaches volumes, removes firewalls, deletes server.
Secret Management
API tokens and passwords can be stored encrypted in .env:
# Generate key
vendor/bin/provision secret:generate-key
# Encrypt value
vendor/bin/provision secret:encrypt --value="hcloud-Xyz..."
# → secret(aes:k9Xp2mV8nQ3wR6yT...)Add to .env:
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:
{
"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:
- Create directory
src/Provider/<Name>/ - Implement 6–7 interfaces (
ServerProviderInterface,NetworkProviderInterface, etc.) - Create handler class (
<Name>InfraHandler) - Register in
ProvisionerFactory - Add
.env.examplein provider directory
The Provisioner, StateManager and CLI work automatically: no changes needed.
Makefile
| Target | Description |
|---|---|
make provision | Build infrastructure |
make provision-dry | Dry run |
make deprovision | Tear down everything |
make cluster-status | Show status |
make cluster-status-json | Status as JSON |
make node-add | Add node |
make node-remove | Remove node |
make generate-key-file | Generate encryption key |
make encrypt | Encrypt value (AES) |
make encrypt-sodium | Encrypt value (Sodium) |
Dependencies
| Package | Purpose |
|---|---|
jardissupport/dotenv | ENV loading with type casting |
jardissupport/secret | Secret encryption/decryption |
ext-json | JSON processing |
ext-curl | HTTP requests |
ext-xmlrpc | INWX XML-RPC API |