Update logging level for plugin loading and extend static example documentation
- Changed logging level from `info` to `debug` in plugin loading to reduce verbosity during runtime. - Significantly expanded `examples/static/README.md` with new sections, examples (basic, certbot, deny pages, JWT validator), and detailed usage instructions. - Improved `examples/docker/docker-compose-changed-label.yml` with clearer redirect syntax using JSON for better readability. - Fixed handling of empty or invalid JSON in labels within `easymapping` with fallback to default values and error logging. - Added additional entries to `.gitignore` to exclude dynamic and temporary files like auto-generated configuration files and certificates.
This commit is contained in:
parent
3a4b428e46
commit
99f4ff325b
7 changed files with 384 additions and 403 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -6,3 +6,11 @@ __pycache__
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.env
|
||||||
|
/examples/static/conf/config.yml
|
||||||
|
/examples/docker/certs/haproxy/.place_holder_cert.pem
|
||||||
|
/examples/static/host1.local.pem
|
||||||
|
/examples/swarm/certs/host1.local.pem
|
||||||
|
/examples/docker/host2.local.pem
|
||||||
|
/examples/swarm/certs/host2.local.pem
|
||||||
|
/examples/docker/jwt_private.pem
|
||||||
|
/examples/docker/jwt_pubkey.pem
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,25 @@ labels:
|
||||||
myproxy.http.port: 80
|
myproxy.http.port: 80
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose-changed-label.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test:**
|
||||||
|
```bash
|
||||||
|
# Test load balancing (hostname changes between containers)
|
||||||
|
curl -H "Host: www.helloworld.com" localhost:19901
|
||||||
|
# Response: f6d8d45b7411
|
||||||
|
curl -H "Host: www.helloworld.com" localhost:19901
|
||||||
|
# Response: 59b213cb8592
|
||||||
|
|
||||||
|
# Test redirect
|
||||||
|
curl -I -H "Host: google.helloworld.com" localhost:19901
|
||||||
|
# Should redirect to: www.google.com/
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Portainer Integration (`docker-compose-portainer.yml`)
|
### 5. Portainer Integration (`docker-compose-portainer.yml`)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ services:
|
||||||
container:
|
container:
|
||||||
image: byjg/static-httpserver
|
image: byjg/static-httpserver
|
||||||
labels:
|
labels:
|
||||||
haproxy.http.redirect: host1.local--https://host1.local
|
haproxy.http.redirect: '{"host1.local": "https://host1.local"}'
|
||||||
haproxy.http.host: host1.local
|
haproxy.http.host: host1.local
|
||||||
haproxy.http.port: 80
|
haproxy.http.port: 80
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,312 @@
|
||||||
# Static Configuration Example
|
# Static Configuration Example
|
||||||
|
|
||||||
This directory demonstrates EasyHAProxy using **static configuration** mode instead of dynamic service discovery.
|
This directory demonstrates EasyHAProxy using **static YAML configuration** instead of dynamic service discovery.
|
||||||
|
|
||||||
## What is Static Mode?
|
Static mode is useful for:
|
||||||
|
- Non-containerized backends (VMs, bare metal)
|
||||||
Static mode uses a YAML configuration file (`config.yml`) to define HAProxy routing rules instead of discovering services automatically from Docker/Kubernetes/Swarm labels.
|
- Fixed infrastructure
|
||||||
|
- Explicit routing control
|
||||||
**Use cases:**
|
|
||||||
- Non-containerized backends
|
|
||||||
- Mixed environments (containers + VMs + bare metal)
|
|
||||||
- Fixed infrastructure where services don't change frequently
|
|
||||||
- Testing HAProxy configurations
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Files in This Example
|
## Prerequisites
|
||||||
|
|
||||||
- `conf/config.yml` - Static configuration defining hosts and routing
|
### 1. Generate SSL Certificates
|
||||||
- `docker-compose.yml` - EasyHAProxy container mounting the config file
|
|
||||||
- `host1.local.pem` - Example SSL certificate
|
|
||||||
|
|
||||||
---
|
```bash
|
||||||
|
# From repository root
|
||||||
## Configuration Structure
|
./examples/generate-keys.sh
|
||||||
|
|
||||||
### docker-compose.yml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
haproxy:
|
|
||||||
image: byjg/easy-haproxy:4.6.0
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./conf:/etc/haproxy/static # Mount static config
|
|
||||||
- ./host1.local.pem:/certs/haproxy/host1.local.pem
|
|
||||||
environment:
|
|
||||||
EASYHAPROXY_DISCOVER: static # Use static mode
|
|
||||||
EASYHAPROXY_SSL_MODE: "loose"
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
- "1936:1936"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### conf/config.yml
|
### 2. Add Host Entry
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "127.0.0.1 host1.local www.host1.local" | sudo tee -a /etc/hosts
|
||||||
|
echo "127.0.0.1 host2.local www.host2.local" | sudo tee -a /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scenario 1: Basic (HTTP → HTTPS Redirect)
|
||||||
|
|
||||||
|
**What it does:** Simple HTTP to HTTPS redirect with SSL termination.
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/static
|
||||||
|
|
||||||
|
# 1. Copy the basic config
|
||||||
|
cp conf/config-basic.yml conf/config.yml
|
||||||
|
|
||||||
|
# 2. Start backend container
|
||||||
|
docker run -d --name container -p 8080:8080 byjg/static-httpserver
|
||||||
|
|
||||||
|
# 3. Start EasyHAProxy
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test HTTP redirect
|
||||||
|
curl -I http://host1.local
|
||||||
|
# Expected: HTTP/1.1 301 Moved Permanently
|
||||||
|
# Expected: Location: https://host1.local
|
||||||
|
|
||||||
|
# Test HTTPS
|
||||||
|
curl -k https://host1.local
|
||||||
|
# Expected: Hello from Static HTTP Server!
|
||||||
|
|
||||||
|
# Test www redirect
|
||||||
|
curl -I http://www.host1.local
|
||||||
|
# Expected: HTTP/1.1 301 Moved Permanently
|
||||||
|
# Expected: Location: https://host1.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stats Interface
|
||||||
|
|
||||||
|
Open: http://localhost:1936
|
||||||
|
- Username: `admin`
|
||||||
|
- Password: `password`
|
||||||
|
|
||||||
|
### Clean Up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
docker stop container && docker rm container
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scenario 2: Certbot (Let's Encrypt SSL)
|
||||||
|
|
||||||
|
**What it does:** Automatic SSL certificates from Let's Encrypt using ACME HTTP-01 challenge.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Public IP address
|
||||||
|
- Domain pointing to your IP
|
||||||
|
- Ports 80/443 publicly accessible
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/static
|
||||||
|
|
||||||
|
# 1. Copy the certbot config
|
||||||
|
cp conf/config-certbot.yml conf/config.yml
|
||||||
|
|
||||||
|
# 2. Edit config.yml and change:
|
||||||
|
# - Replace "example.com" with your real domain
|
||||||
|
# - Update EASYHAPROXY_CERTBOT_EMAIL in docker-compose.yml
|
||||||
|
|
||||||
|
# 3. Start backend container
|
||||||
|
docker run -d --name container -p 8080:8080 byjg/static-httpserver
|
||||||
|
|
||||||
|
# 4. Start EasyHAProxy
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 5. Check logs for certificate generation
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### What to Expect
|
||||||
|
|
||||||
|
```
|
||||||
|
# Certbot will:
|
||||||
|
# 1. Request certificate from Let's Encrypt
|
||||||
|
# 2. Complete HTTP-01 challenge
|
||||||
|
# 3. Save certificate in /certs/certbot/
|
||||||
|
# 4. Reload HAProxy with new certificate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test HTTPS with real certificate
|
||||||
|
curl https://your-domain.com
|
||||||
|
# Expected: No certificate warnings (valid SSL)
|
||||||
|
|
||||||
|
# Test HTTP redirect
|
||||||
|
curl -I http://your-domain.com
|
||||||
|
# Expected: HTTP/1.1 301 Moved Permanently
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean Up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
docker stop container && docker rm container
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Certificates are stored in Docker volume `certs_certbot` and persist across restarts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scenario 3: Deny Pages (Block Specific Paths)
|
||||||
|
|
||||||
|
**What it does:** Blocks access to sensitive paths like `/admin`, `/wp-login.php`, etc.
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/static
|
||||||
|
|
||||||
|
# 1. Copy the deny-pages config
|
||||||
|
cp conf/config-deny-pages.yml conf/config.yml
|
||||||
|
|
||||||
|
# 2. Start backend container
|
||||||
|
docker run -d --name container -p 8080:8080 byjg/static-httpserver
|
||||||
|
|
||||||
|
# 3. Start EasyHAProxy
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test normal page (should work)
|
||||||
|
curl -k https://host1.local/
|
||||||
|
# Expected: Hello from Static HTTP Server!
|
||||||
|
|
||||||
|
# Test blocked path (should fail)
|
||||||
|
curl -I -k https://host1.local/admin
|
||||||
|
# Expected: HTTP/1.1 404 Not Found
|
||||||
|
|
||||||
|
curl -I -k https://host1.local/wp-login.php
|
||||||
|
# Expected: HTTP/1.1 404 Not Found
|
||||||
|
|
||||||
|
curl -I -k https://host1.local/.env
|
||||||
|
# Expected: HTTP/1.1 404 Not Found
|
||||||
|
```
|
||||||
|
|
||||||
|
### What's Blocked
|
||||||
|
|
||||||
|
The example blocks these paths:
|
||||||
|
- `/admin`
|
||||||
|
- `/wp-admin`
|
||||||
|
- `/wp-login.php`
|
||||||
|
- `/.env`
|
||||||
|
- `/config`
|
||||||
|
|
||||||
|
### Customize Blocked Paths
|
||||||
|
|
||||||
|
Edit `conf/config.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugin_config:
|
||||||
|
deny_pages:
|
||||||
|
paths: /admin,/private,/internal
|
||||||
|
status_code: 403 # or 404
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean Up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
docker stop container && docker rm container
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scenario 4: JWT Validator (API Authentication)
|
||||||
|
|
||||||
|
**What it does:** Validates JWT tokens in Authorization header before allowing access.
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/static
|
||||||
|
|
||||||
|
# 1. Copy the JWT validator config
|
||||||
|
cp conf/config-jwt-validator.yml conf/config.yml
|
||||||
|
|
||||||
|
# 2. JWT keys were already generated by generate-keys.sh
|
||||||
|
# Location: examples/docker/jwt_pubkey.pem and jwt_private.pem
|
||||||
|
|
||||||
|
# 3. Start backend container
|
||||||
|
docker run -d --name container -p 8080:8080 byjg/static-httpserver
|
||||||
|
|
||||||
|
# 4. Start EasyHAProxy
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Without Token (Should Fail)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -k https://host1.local/
|
||||||
|
# Expected: Missing Authorization HTTP header
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test With Valid Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Generate a test JWT token using jwt_private.pem
|
||||||
|
# You can use https://jwt.io or a JWT library
|
||||||
|
|
||||||
|
# 2. Example with valid token:
|
||||||
|
TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
curl -k -H "Authorization: Bearer $TOKEN" https://host1.local/
|
||||||
|
# Expected: Hello from Static HTTP Server! (if token is valid)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate Test Token
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python example using PyJWT
|
||||||
|
import jwt
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
with open('examples/docker/jwt_private.pem', 'r') as f:
|
||||||
|
private_key = f.read()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'iss': 'https://auth.example.com/',
|
||||||
|
'aud': 'https://api.example.com',
|
||||||
|
'exp': datetime.utcnow() + timedelta(hours=1)
|
||||||
|
}
|
||||||
|
|
||||||
|
token = jwt.encode(payload, private_key, algorithm='RS256')
|
||||||
|
print(token)
|
||||||
|
```
|
||||||
|
|
||||||
|
### What's Validated
|
||||||
|
|
||||||
|
- Authorization header must be present
|
||||||
|
- Token must be valid JWT format
|
||||||
|
- Signature must match public key (`jwt_pubkey.pem`)
|
||||||
|
- Issuer must match: `https://auth.example.com/`
|
||||||
|
- Audience must match: `https://api.example.com`
|
||||||
|
- Token must not be expired
|
||||||
|
|
||||||
|
### Customize JWT Settings
|
||||||
|
|
||||||
|
Edit `conf/config.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugin_config:
|
||||||
|
jwt_validator:
|
||||||
|
algorithm: RS256
|
||||||
|
issuer: https://your-auth-server.com/
|
||||||
|
audience: https://your-api.com
|
||||||
|
pubkey_path: /certs/haproxy/jwt_pubkey.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clean Up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
docker stop container && docker rm container
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration File Reference
|
||||||
|
|
||||||
|
All scenarios use `/etc/haproxy/static/config.yml` mounted from `./conf/config.yml`.
|
||||||
|
|
||||||
|
### Basic Structure
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
stats:
|
stats:
|
||||||
|
|
@ -54,416 +317,102 @@ stats:
|
||||||
customerrors: true
|
customerrors: true
|
||||||
|
|
||||||
easymapping:
|
easymapping:
|
||||||
# HTTP Port 80 - Redirects to HTTPS
|
|
||||||
- port: 80
|
- port: 80
|
||||||
redirect:
|
redirect:
|
||||||
host1.local: https://host1.local
|
host1.local: https://host1.local
|
||||||
www.host1.local: https://host1.local
|
|
||||||
|
|
||||||
# HTTPS Port 443
|
|
||||||
- port: 443
|
- port: 443
|
||||||
ssl: true
|
ssl: true
|
||||||
hosts:
|
hosts:
|
||||||
host1.local:
|
host1.local:
|
||||||
containers:
|
containers:
|
||||||
- container:8080 # Backend container
|
- container:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### With Plugins
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### 1. Port Definitions
|
|
||||||
|
|
||||||
Each item in `easymapping` defines a listening port:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
easymapping:
|
easymapping:
|
||||||
- port: 80 # Listen on port 80
|
- port: 443
|
||||||
redirect: {...} # Optional redirects
|
ssl: true
|
||||||
|
hosts:
|
||||||
- port: 443 # Listen on port 443
|
host1.local:
|
||||||
ssl: true # Enable SSL
|
containers:
|
||||||
hosts: {...} # Virtual hosts
|
- container:8080
|
||||||
```
|
plugins:
|
||||||
|
- deny_pages
|
||||||
### 2. Redirect Configuration
|
plugin_config:
|
||||||
|
deny_pages:
|
||||||
Redirect specific domains to different URLs:
|
paths: /admin,/private
|
||||||
|
status_code: 404
|
||||||
```yaml
|
|
||||||
- port: 80
|
|
||||||
redirect:
|
|
||||||
host1.local: https://host1.local # HTTP → HTTPS
|
|
||||||
www.host1.local: https://host1.local # www → non-www + HTTPS
|
|
||||||
old.domain.com: https://new.domain.com # Domain change
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Virtual Hosts
|
|
||||||
|
|
||||||
Define hosts and their backend containers:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- port: 443
|
|
||||||
ssl: true
|
|
||||||
hosts:
|
|
||||||
host1.local: # Virtual host domain
|
|
||||||
containers:
|
|
||||||
- container:8080 # Backend: container_name:port
|
|
||||||
- another_container:3000 # Multiple backends = load balancing
|
|
||||||
|
|
||||||
host2.local:
|
|
||||||
containers:
|
|
||||||
- webserver:80
|
|
||||||
```
|
|
||||||
|
|
||||||
**Backend formats:**
|
|
||||||
- `container_name:port` - Docker container by name
|
|
||||||
- `ip_address:port` - Direct IP address
|
|
||||||
- `hostname:port` - Hostname resolution
|
|
||||||
|
|
||||||
### 4. SSL Configuration
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- port: 443
|
|
||||||
ssl: true # Enable SSL on this port
|
|
||||||
hosts:
|
|
||||||
secure.example.com:
|
|
||||||
containers:
|
|
||||||
- app:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
SSL certificates must be placed in:
|
|
||||||
- `/certs/haproxy/<domain>.pem` inside container
|
|
||||||
- `./certs/<domain>.pem` on host (if volume mounted)
|
|
||||||
|
|
||||||
Certificate format: PEM file containing both certificate and private key.
|
|
||||||
|
|
||||||
### 5. Stats Interface
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
stats:
|
|
||||||
username: admin
|
|
||||||
password: password
|
|
||||||
port: 1936
|
|
||||||
```
|
|
||||||
|
|
||||||
Access at: `http://localhost:1936`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Running the Example
|
|
||||||
|
|
||||||
### Prerequisites: Generate SSL Certificates
|
|
||||||
|
|
||||||
Before running the examples, you need to generate the required SSL certificates:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From the repository root
|
|
||||||
./examples/generate-keys.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This script automatically generates:
|
|
||||||
- SSL certificates for host1.local and host2.local
|
|
||||||
- JWT keys for authentication examples
|
|
||||||
- All other .pem files needed for testing
|
|
||||||
|
|
||||||
### 1. Start the Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd examples/static
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Create Backend Container
|
|
||||||
|
|
||||||
The static config references `container:8080`. Create a container with this name:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d --name container \
|
|
||||||
-p 8080:8080 \
|
|
||||||
byjg/static-httpserver
|
|
||||||
```
|
|
||||||
|
|
||||||
Or add to `docker-compose.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
# ... haproxy service ...
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: byjg/static-httpserver
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add to /etc/hosts:
|
|
||||||
# 127.0.0.1 host1.local www.host1.local
|
|
||||||
|
|
||||||
# Test HTTP redirect
|
|
||||||
curl -I http://host1.local
|
|
||||||
# Should return: HTTP/1.1 301 Moved Permanently
|
|
||||||
# Location: https://host1.local
|
|
||||||
|
|
||||||
# Test HTTPS
|
|
||||||
curl -k https://host1.local
|
|
||||||
|
|
||||||
# Access stats
|
|
||||||
open http://localhost:1936
|
|
||||||
# Username: admin
|
|
||||||
# Password: password
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Advanced Configuration
|
## Advanced: Multiple Backends
|
||||||
|
|
||||||
### Load Balancing Multiple Backends
|
Load balance across multiple containers:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
hosts:
|
hosts:
|
||||||
api.example.com:
|
api.example.com:
|
||||||
containers:
|
containers:
|
||||||
- api_server_1:8080
|
- api1:8080
|
||||||
- api_server_2:8080
|
- api2:8080
|
||||||
- api_server_3:8080
|
- api3:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Default algorithm: round-robin
|
---
|
||||||
|
|
||||||
### Custom Balance Algorithm
|
## Advanced: External Backends
|
||||||
|
|
||||||
```yaml
|
Route to non-Docker backends:
|
||||||
hosts:
|
|
||||||
api.example.com:
|
|
||||||
balance: leastconn # Use least connections instead of round-robin
|
|
||||||
containers:
|
|
||||||
- api_1:8080
|
|
||||||
- api_2:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
**Available algorithms:**
|
|
||||||
- `roundrobin` - Distribute evenly (default)
|
|
||||||
- `leastconn` - Send to server with fewest connections
|
|
||||||
- `source` - Same client IP always goes to same server
|
|
||||||
|
|
||||||
### External Backends (Non-Docker)
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
hosts:
|
hosts:
|
||||||
legacy.example.com:
|
legacy.example.com:
|
||||||
containers:
|
containers:
|
||||||
- 192.168.1.100:8080 # VM
|
- 192.168.1.100:8080
|
||||||
- 192.168.1.101:8080 # Another VM
|
- 192.168.1.101:8080
|
||||||
- database.local:5432 # Database server
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
hosts:
|
|
||||||
webapp.example.com:
|
|
||||||
containers:
|
|
||||||
- server1:8080
|
|
||||||
- server2:8080
|
|
||||||
healthcheck:
|
|
||||||
path: /health
|
|
||||||
interval: 5s
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multiple Domains, Same Backend
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
hosts:
|
|
||||||
example.com:
|
|
||||||
containers:
|
|
||||||
- webapp:8080
|
|
||||||
www.example.com:
|
|
||||||
containers:
|
|
||||||
- webapp:8080 # Same backend
|
|
||||||
app.example.com:
|
|
||||||
containers:
|
|
||||||
- webapp:8080 # Same backend
|
|
||||||
```
|
|
||||||
|
|
||||||
### Path-Based Routing
|
|
||||||
|
|
||||||
While static mode focuses on host-based routing, you can achieve path-based routing using redirects:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- port: 80
|
|
||||||
redirect:
|
|
||||||
api.example.com/v1: https://api-v1.internal:8080
|
|
||||||
api.example.com/v2: https://api-v2.internal:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use HAProxy ACLs via custom templates (advanced).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Plugins with Static Configuration
|
|
||||||
|
|
||||||
Enable plugins globally or per-host in static mode:
|
|
||||||
|
|
||||||
### Global Plugin Configuration
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
plugins:
|
|
||||||
enabled: [cleanup]
|
|
||||||
config:
|
|
||||||
cleanup:
|
|
||||||
max_idle_time: 600
|
|
||||||
```
|
|
||||||
|
|
||||||
### Per-Host Plugin Configuration (via env vars)
|
|
||||||
|
|
||||||
Since static mode doesn't support per-host plugin configuration directly, use environment variables for domain-specific plugins:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
EASYHAPROXY_PLUGINS_ENABLED: cloudflare,deny_pages
|
|
||||||
EASYHAPROXY_PLUGIN_CLOUDFLARE_IP_LIST_PATH: /etc/haproxy/cloudflare_ips.lst
|
|
||||||
EASYHAPROXY_PLUGIN_DENY_PAGES_PATHS: /admin,/private
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Using Plugins](../../docs/plugins.md) for more details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comparison: Static vs. Dynamic Discovery
|
|
||||||
|
|
||||||
| Feature | Static Mode | Docker/Swarm/K8s Mode |
|
|
||||||
|---------------|-----------------------------|----------------------------------------------|
|
|
||||||
| Configuration | YAML file | Container labels / Ingress annotations |
|
|
||||||
| Backend types | Any (containers, VMs, IPs) | Containers only |
|
|
||||||
| Updates | Manual config edit + reload | Automatic discovery |
|
|
||||||
| Use case | Fixed infrastructure | Dynamic container environments |
|
|
||||||
| Plugin config | Global via YAML/env | Per-container/ingress via labels/annotations |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tips
|
|
||||||
|
|
||||||
1. **Reloading Configuration:**
|
|
||||||
```bash
|
|
||||||
# EasyHAProxy watches config.yml for changes
|
|
||||||
# Edit conf/config.yml, changes auto-reload
|
|
||||||
|
|
||||||
# Or manually restart:
|
|
||||||
docker compose restart haproxy
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Validate Configuration:**
|
|
||||||
```bash
|
|
||||||
# Check HAProxy config is valid
|
|
||||||
docker compose exec haproxy haproxy -c -f /etc/haproxy/haproxy.cfg
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **View Generated Config:**
|
|
||||||
```bash
|
|
||||||
docker compose exec haproxy cat /etc/haproxy/haproxy.cfg
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Debugging:**
|
|
||||||
```bash
|
|
||||||
# Enable debug mode
|
|
||||||
docker compose up
|
|
||||||
# Watch logs in real-time
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **SSL Certificate Management:**
|
|
||||||
```bash
|
|
||||||
# Generate self-signed cert
|
|
||||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
|
||||||
-keyout host.key -out host.crt \
|
|
||||||
-subj "/CN=host1.local"
|
|
||||||
|
|
||||||
# Combine into PEM
|
|
||||||
cat host.crt host.key > host1.local.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Backend Unreachable
|
### FileNotFoundError: config.yml
|
||||||
|
|
||||||
**Error:** `503 Service Unavailable`
|
|
||||||
|
|
||||||
**Causes:**
|
|
||||||
- Backend container not running
|
|
||||||
- Wrong container name in config
|
|
||||||
- Wrong port number
|
|
||||||
- Network connectivity issues
|
|
||||||
|
|
||||||
**Debug:**
|
|
||||||
```bash
|
```bash
|
||||||
# Check backend container is running
|
# Make sure config.yml exists
|
||||||
docker ps | grep container_name
|
ls conf/config.yml
|
||||||
|
|
||||||
# Test backend directly
|
# If missing, copy from an example:
|
||||||
curl http://container_name:port
|
cp conf/config-basic.yml conf/config.yml
|
||||||
|
|
||||||
# Check HAProxy logs
|
|
||||||
docker compose logs haproxy
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration Not Reloading
|
### 503 Service Unavailable
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
```bash
|
||||||
# Restart HAProxy
|
# Check backend is running
|
||||||
docker compose restart haproxy
|
docker ps | grep container
|
||||||
|
curl http://localhost:8080
|
||||||
# Check file is mounted correctly
|
|
||||||
docker compose exec haproxy cat /etc/haproxy/static/config.yml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### SSL Certificate Not Found
|
### SSL Certificate Not Found
|
||||||
|
|
||||||
**Error:** Certificate errors in logs
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
```bash
|
||||||
# Verify certificate is mounted
|
# Verify certificate exists
|
||||||
docker compose exec haproxy ls -la /certs/haproxy/
|
ls -la host1.local.pem
|
||||||
|
|
||||||
# Check certificate format (must be PEM with cert + key)
|
# Regenerate if needed
|
||||||
openssl x509 -in host.pem -text -noout
|
cd ../.. && ./examples/generate-keys.sh
|
||||||
openssl rsa -in host.pem -check
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### Changes Not Applied
|
||||||
|
|
||||||
## Migration from Dynamic to Static
|
```bash
|
||||||
|
# Restart to reload config
|
||||||
If you have Docker labels and want to convert to static config:
|
docker compose restart
|
||||||
|
|
||||||
**Docker label:**
|
|
||||||
```yaml
|
|
||||||
labels:
|
|
||||||
easyhaproxy.http.host: api.example.com
|
|
||||||
easyhaproxy.http.port: 80
|
|
||||||
easyhaproxy.http.localport: 8080
|
|
||||||
easyhaproxy.http.redirect_ssl: true
|
|
||||||
```
|
|
||||||
|
|
||||||
**Static config equivalent:**
|
|
||||||
```yaml
|
|
||||||
easymapping:
|
|
||||||
- port: 80
|
|
||||||
redirect:
|
|
||||||
api.example.com: https://api.example.com
|
|
||||||
|
|
||||||
- port: 443
|
|
||||||
ssl: true
|
|
||||||
hosts:
|
|
||||||
api.example.com:
|
|
||||||
containers:
|
|
||||||
- container_name:8080
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -471,6 +420,5 @@ easymapping:
|
||||||
## Further Reading
|
## Further Reading
|
||||||
|
|
||||||
- [Static Configuration Guide](../../docs/static.md)
|
- [Static Configuration Guide](../../docs/static.md)
|
||||||
- [Environment Variables](../../docs/environment-variable.md)
|
|
||||||
- [Using Plugins](../../docs/plugins.md)
|
- [Using Plugins](../../docs/plugins.md)
|
||||||
- [SSL Configuration](../../docs/ssl.md)
|
- [Environment Variables](../../docs/environment-variable.md)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from functions import loggerEasyHaproxy
|
||||||
|
|
||||||
|
|
||||||
class DockerLabelHandler:
|
class DockerLabelHandler:
|
||||||
|
|
@ -32,7 +33,16 @@ class DockerLabelHandler:
|
||||||
|
|
||||||
def get_json(self, label, default_value={}):
|
def get_json(self, label, default_value={}):
|
||||||
if self.has_label(label):
|
if self.has_label(label):
|
||||||
return json.loads(self.__data[label])
|
value = self.__data[label]
|
||||||
|
if not value: # Handle empty strings
|
||||||
|
return default_value
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
loggerEasyHaproxy.error(
|
||||||
|
f"Invalid JSON in label '{label}': {value}. Error: {e}. Using default value."
|
||||||
|
)
|
||||||
|
return default_value
|
||||||
return default_value
|
return default_value
|
||||||
|
|
||||||
def set_data(self, data):
|
def set_data(self, data):
|
||||||
|
|
@ -67,8 +77,7 @@ class HaproxyConfigGenerator:
|
||||||
self.global_plugin_configs = []
|
self.global_plugin_configs = []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If plugin system fails to initialize, log but continue
|
# If plugin system fails to initialize, log but continue
|
||||||
import logging
|
loggerEasyHaproxy.warning(f"Failed to initialize plugin system: {e}")
|
||||||
logging.warning(f"Failed to initialize plugin system: {e}")
|
|
||||||
self.plugin_manager = None
|
self.plugin_manager = None
|
||||||
self.global_plugin_configs = []
|
self.global_plugin_configs = []
|
||||||
|
|
||||||
|
|
@ -102,8 +111,7 @@ class HaproxyConfigGenerator:
|
||||||
global_configs = [r.haproxy_config for r in global_results if r.haproxy_config]
|
global_configs = [r.haproxy_config for r in global_results if r.haproxy_config]
|
||||||
self.global_plugin_configs.extend(global_configs)
|
self.global_plugin_configs.extend(global_configs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import logging
|
loggerEasyHaproxy.warning(f"Failed to execute global plugins: {e}")
|
||||||
logging.warning(f"Failed to execute global plugins: {e}")
|
|
||||||
|
|
||||||
templates_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'templates')
|
templates_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'templates')
|
||||||
file_loader = FileSystemLoader(templates_dir)
|
file_loader = FileSystemLoader(templates_dir)
|
||||||
|
|
@ -274,8 +282,7 @@ class HaproxyConfigGenerator:
|
||||||
if result.metadata["fcgi_app_definition"] not in self.global_plugin_configs:
|
if result.metadata["fcgi_app_definition"] not in self.global_plugin_configs:
|
||||||
self.global_plugin_configs.append(result.metadata["fcgi_app_definition"])
|
self.global_plugin_configs.append(result.metadata["fcgi_app_definition"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import logging
|
loggerEasyHaproxy.warning(f"Failed to execute domain plugins for {hostname}: {e}")
|
||||||
logging.warning(f"Failed to execute domain plugins for {hostname}: {e}")
|
|
||||||
easymapping[port]["hosts"][hostname]["plugin_configs"] = []
|
easymapping[port]["hosts"][hostname]["plugin_configs"] = []
|
||||||
else:
|
else:
|
||||||
easymapping[port]["hosts"][hostname]["plugin_configs"] = []
|
easymapping[port]["hosts"][hostname]["plugin_configs"] = []
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import os
|
import os
|
||||||
import logging
|
|
||||||
|
|
||||||
from deepdiff import DeepDiff
|
from deepdiff import DeepDiff
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ class PluginManager:
|
||||||
elif plugin.plugin_type == PluginType.DOMAIN:
|
elif plugin.plugin_type == PluginType.DOMAIN:
|
||||||
self.domain_plugins.append(plugin)
|
self.domain_plugins.append(plugin)
|
||||||
|
|
||||||
self.logger.info(f"Loaded {source} plugin: {plugin.name} ({plugin.plugin_type.value})")
|
self.logger.debug(f"Loaded {source} plugin: {plugin.name} ({plugin.plugin_type.value})")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._handle_error(f"Failed to load plugin from {filepath}: {str(e)}")
|
self._handle_error(f"Failed to load plugin from {filepath}: {str(e)}")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue