Terraster - Uncomplicated Load Balancer/Reverse Proxy
Project Status: Early Stages 🚧
This project is currently in its early development stages. While the core functionality is in place and working as intended, further improvements and features are actively being developed. Expect updates as the project evolves.
A high-performance, feature-rich Layer 7 (L7) load balancer with a robust and user-friendly admin API.
Features
Quick Start
- Build Terraster:
go build -o terraster cmd/main.go
- Create a configuration file or use provided in repo (config.yaml):
port: 443
admin_port: 4433
services:
- name: backend-api # service name
host: internal-api1.local.com # service listener hostname
port: 8455 # service listener port
tls: # service tls configuration
cert_file: "/path/to/api-cert.pem"
key_file: "/path/to/api-key.pem"
health_check: # service health check configuration - will be used by each location
type: "http"
path: "/"
interval: "5s"
timeout: "3s"
thresholds:
healthy: 2
unhealthy: 3
locations:
- path: "/api/" # served path suffix so "https://internal-api1.local.com/api/"
lb_policy: round-robin # load balancing policy
http_redirect: true # http to https redirect
redirect: "/" # redirect e.q. from "/" to "/api/"
backends:
- url: http://internal-api1.local.com:8455
weight: 5
max_connections: 1000
health_check: # or have separate health check for each backend and override service health check
type: "http"
path: "/api_health"
interval: "4s"
timeout: "3s"
thresholds:
healthy: 1
unhealthy: 2
- url: http://internal-api2.local.com:8455
weight: 3
max_connections: 800
- name: frontend
host: frontend.local.com
locations:
- path: "/"
lb_policy: least_connections
http_redirect: false
rewrite: "/frontend/" # rewrite e.q. from "/" to "/frontend/" in the backend service
backends:
- url: http://frontend-1.local.com:3000
weight: 5
max_connections: 1000
- url: http://frontend-2.local.com:3000
weight: 3
max_connections: 800
# global health check will be used by every service that don't have health_check configuration
health_check:
interval: 10s
timeout: 2s
path: /health
# api authentication
auth:
jwt_secret: mySecretKey
db_path: ./auth.db
password_expiry_days: 7
password_history_size: 5
admin_api:
rate_limit:
requests_per_second: 10
burst: 20
rate_limit: # global rate limit for each service if not defined in the service
requests_per_second: 100
burst: 150
connection_pool:
max_idle: 100
max_open: 1000
idle_timeout: 90s
- Run the load balancer:
./terraster --config config.yaml
Configuration Examples
Basic Configuration
port: 8080
algorithm: round-robin
backends:
- url: http://localhost:8081
- url: http://localhost:8082
Advanced Configuration
port: 8080
admin_port: 8081
algorithm: adaptive
tls:
enabled: true
domains:
- example.com
cert_dir: /etc/certs
auto_cert: true
rate_limit:
requests_per_second: 1000
burst: 50
circuit_breaker:
threshold: 5
timeout: 60s
connection_pool:
max_idle: 100
max_open: 1000
idle_timeout: 90s
cors:
allowed_origins:
- https://example.com
allowed_methods:
- GET
- POST
allowed_headers:
- Content-Type
max_age: 3600
security:
hsts: true
hsts_max_age: 31536000
frame_options: DENY
content_type_options: true
xss_protection: true
API Examples
Admin API
- Get Backend Status:
curl http://localhost:8081/api/backends \
-H "Authorization: Bearer eyJhbGciOiJIUzI1..." \
-H "Content-Type: application/json"
- Add Backend to service:
curl -X POST http://localhost:8081/api/backends?service_name=backend-api \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1..." \
-d '{
"url": "http://newbackend:8080",
"weight": 5
}'
// tools/benchmark/main.go
package main
import (
"flag"
"fmt"
"net/http"
"sync"
"time"
)
func main() {
url := flag.String("url", "http://localhost:8080", "URL to benchmark")
concurrency := flag.Int("c", 10, "Number of concurrent requests")
requests := flag.Int("n", 1000, "Total number of requests")
duration := flag.Duration("d", 0, "Duration of the test")
flag.Parse()
results := make(chan time.Duration, *requests)
errors := make(chan error, *requests)
var wg sync.WaitGroup
start := time.Now()
client := &http.Client{
Timeout: time.Second * 10,
}
if *duration > 0 {
timer := time.NewTimer(*duration)
go func() {
<-timer.C
fmt.Println("Duration reached, stopping...")
*requests = 0
}()
}
// Start workers
for i := 0; i < *concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < *requests / *concurrency; i++ {
requestStart := time.Now()
resp, err := client.Get(*url)
if err != nil {
errors <- err
continue
}
resp.Body.Close()
results <- time.Since(requestStart)
}
}()
}
// Wait for completion
wg.Wait()
close(results)
close(errors)
// Process results
var total time.Duration
var count int
var min, max time.Duration
errCount := 0
for d := range results {
if min == 0 || d < min {
min = d
}
if d > max {
max = d
}
total += d
count++
}
for range errors {
errCount++
}
// Print results
fmt.Printf("\nBenchmark Results:\n")
fmt.Printf("URL: %s\n", *url)
fmt.Printf("Concurrency Level: %d\n", *concurrency)
fmt.Printf("Time taken: %v\n", time.Since(start))
fmt.Printf("Complete requests: %d\n", count)
fmt.Printf("Failed requests: %d\n", errCount)
fmt.Printf("Requests per second: %.2f\n", float64(count)/time.Since(start).Seconds())
fmt.Printf("Mean latency: %v\n", total/time.Duration(count))
fmt.Printf("Min latency: %v\n", min)
fmt.Printf("Max latency: %v\n", max)
}
Docker Deployment
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o terraster cmd/main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/terraster .
COPY config.yaml .
EXPOSE 8080 8081 9090
CMD ["./terraster", "--config", "config.yaml"]
# docker-compose.yml
version: '3.8'
services:
terraster:
build: .
ports:
- "8080:8080"
- "8081:8081"
- "9090:9090"
volumes:
- ./config.yaml:/root/config.yaml
- ./certs:/etc/certs
restart: unless-stopped
License
MIT License