mirror of
https://github.com/openziti/zrok.git
synced 2026-08-24 10:14:56 -05:00
finish orchestrating metrics and dynamic frontend
This commit is contained in:
@@ -45,8 +45,41 @@ jobs:
|
||||
python3 -m venv sdk/python/.venv
|
||||
sdk/python/.venv/bin/pip install -e 'sdk/python/src/[test]' flask waitress requests
|
||||
|
||||
- name: Resolve Ziti version for Docker images
|
||||
id: ziti-docker-version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ZITI_VERSION: ${{ vars.ZITI_TEST_VERSION }}
|
||||
run: |
|
||||
if [[ -n "${ZITI_VERSION:-}" ]]; then
|
||||
V="${ZITI_VERSION}"
|
||||
echo "Using pinned version: ${V}"
|
||||
else
|
||||
V=$(
|
||||
gh api --method GET \
|
||||
'repos/openziti/ziti/releases?per_page=100' \
|
||||
--jq '
|
||||
[.[]
|
||||
| select(.prerelease == false
|
||||
and .draft == false)
|
||||
] | first | .tag_name | ltrimstr("v")'
|
||||
)
|
||||
echo "Resolved latest stable: ${V}"
|
||||
fi
|
||||
V="${V#v}"
|
||||
if [[ -z "${V}" ]]; then
|
||||
echo "ERROR: could not resolve Ziti version" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ZITI_VERSION=${V}" | tee -a "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Start zrok2 instance (built from source)
|
||||
run: bash docker/compose/zrok2-instance/dangerous.docker.test.bash --keep
|
||||
run: |
|
||||
ARGS=(--keep --ziti-tag "${{ steps.ziti-docker-version.outputs.ZITI_VERSION }}")
|
||||
if [[ -n "${{ vars.ZITI_TEST_REPO }}" ]]; then
|
||||
ARGS+=(--ziti-repo "${{ vars.ZITI_TEST_REPO }}")
|
||||
fi
|
||||
bash docker/compose/zrok2-instance/dangerous.docker.test.bash "${ARGS[@]}"
|
||||
timeout-minutes: 13
|
||||
|
||||
- name: Run Python SDK integration tests
|
||||
@@ -102,10 +135,10 @@ jobs:
|
||||
id: ziti-version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ZITI_LINUX_VERSION: ${{ vars.ZITI_LINUX_VERSION }}
|
||||
ZITI_VERSION: ${{ vars.ZITI_TEST_VERSION }}
|
||||
run: |
|
||||
if [[ -n "${ZITI_LINUX_VERSION:-}" ]]; then
|
||||
V="${ZITI_LINUX_VERSION}"
|
||||
if [[ -n "${ZITI_VERSION:-}" ]]; then
|
||||
V="${ZITI_VERSION}"
|
||||
echo "Using pinned version: ${V}"
|
||||
else
|
||||
V=$(
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Docker Compose overlay for the canary integration test.
|
||||
# Merged by dangerous.docker.test.bash — not part of the user-facing stack.
|
||||
#
|
||||
# The canary service uses host networking so *.localhost resolves to 127.0.0.1
|
||||
# and reaches published frontend and controller ports directly.
|
||||
|
||||
services:
|
||||
canary:
|
||||
image: ${ZROK2_IMAGE:-docker.io/openziti/zrok2}:${ZROK2_TAG:-latest}
|
||||
network_mode: host
|
||||
environment:
|
||||
ZROK2_API_ENDPOINT: http://127.0.0.1:${ZROK2_CTRL_PORT:-18080}
|
||||
ZROK2_ADMIN_TOKEN: ${ZROK2_ADMIN_TOKEN}
|
||||
ZROK2_DANGEROUS_CANARY: "1"
|
||||
ZROK2_FRONTEND_PORT: ${ZROK2_FRONTEND_PORT:-8080}
|
||||
ZROK2_DNS_ZONE: ${ZROK2_DNS_ZONE:-localhost}
|
||||
HOME: /tmp/canary
|
||||
entrypoint: ["/bin/bash", "-c"]
|
||||
command:
|
||||
- |
|
||||
set -o errexit -o nounset -o pipefail
|
||||
|
||||
FRONTEND_PORT="$${ZROK2_FRONTEND_PORT}"
|
||||
DNS_ZONE="$${ZROK2_DNS_ZONE}"
|
||||
|
||||
# Create a throwaway canary account and enable an environment.
|
||||
# ZROK2_API_ENDPOINT is already set — zrok2 enable bootstraps
|
||||
# the environment in one shot.
|
||||
TOKEN=$$(zrok2 admin create account \
|
||||
"canary-$$(date +%s)@zrok.internal" "canarypass")
|
||||
echo "canary account token: $${TOKEN}"
|
||||
zrok2 enable "$${TOKEN}" --description canary-test
|
||||
|
||||
# ── Test 1: Canary looper (random share token) ──────────────────
|
||||
# Exercises the public frontend with random share tokens. The
|
||||
# host-networked container reaches the frontend at
|
||||
# localhost:ZROK2_FRONTEND_PORT, and *.localhost resolves to
|
||||
# 127.0.0.1 (RFC 6761).
|
||||
echo "=== Test 1: canary public-proxy looper ==="
|
||||
zrok2 test canary public-proxy \
|
||||
--iterations 3 --loopers 1 \
|
||||
--min-payload 256 --max-payload 256 \
|
||||
--min-pacing 1s --max-pacing 1s \
|
||||
--http --frontend-port "$${FRONTEND_PORT}"
|
||||
|
||||
# ── Test 2: Named share via --name-selection ────────────────────
|
||||
# Creates a share with an explicit name in the public namespace,
|
||||
# then verifies the AMQP-backed dynamic frontend routes it.
|
||||
echo "=== Test 2: named share ==="
|
||||
SHARE_NAME="citest-$$(date +%s)"
|
||||
|
||||
BACKEND_PORT=19999
|
||||
zrok2 test endpoint --port "$${BACKEND_PORT}" &
|
||||
HTTP_PID=$$!
|
||||
|
||||
# Pre-create the name (v2 equivalent of "zrok reserve").
|
||||
zrok2 create name "$${SHARE_NAME}"
|
||||
|
||||
# Create the named share in the background (long-running).
|
||||
zrok2 share public "http://127.0.0.1:$${BACKEND_PORT}" \
|
||||
--name-selection "public:$${SHARE_NAME}" \
|
||||
--backend-mode proxy --headless &
|
||||
SHARE_PID=$$!
|
||||
|
||||
# Wait for the share to propagate to the dynamic frontend.
|
||||
echo "waiting for named share '$${SHARE_NAME}' ..."
|
||||
sleep 5
|
||||
ATTEMPTS=30
|
||||
while (( ATTEMPTS-- > 0 )); do
|
||||
if curl -sf -H "Host: $${SHARE_NAME}.$${DNS_ZONE}" \
|
||||
"http://127.0.0.1:$${FRONTEND_PORT}/" 2>/dev/null \
|
||||
| grep -q "zrok"; then
|
||||
echo "PASS: named share '$${SHARE_NAME}' content verified"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if (( ATTEMPTS < 0 )); then
|
||||
echo "FAIL: named share '$${SHARE_NAME}' not reachable after 60s"
|
||||
kill $${SHARE_PID} $${HTTP_PID} 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up share and backend.
|
||||
kill $${SHARE_PID} $${HTTP_PID} 2>/dev/null || true
|
||||
wait $${SHARE_PID} 2>/dev/null || true
|
||||
wait $${HTTP_PID} 2>/dev/null || true
|
||||
|
||||
zrok2 disable || true
|
||||
echo "=== All canary tests passed ==="
|
||||
profiles: ["canary"]
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
ZITI_CLUSTER_NODE_NAME: ziti-ctrl
|
||||
PFXLOG_NO_JSON: "true"
|
||||
volumes:
|
||||
- ziti-ctrl-data:/home/ziggy
|
||||
- ziti-ctrl-data:/ziti-controller
|
||||
ports:
|
||||
- "${ZROK2_INSECURE_INTERFACE:-127.0.0.1}:${ZITI_CTRL_PORT:-1280}:${ZITI_CTRL_PORT:-1280}"
|
||||
networks:
|
||||
@@ -190,12 +190,16 @@ services:
|
||||
ZROK2_FRONTEND_PORT: ${ZROK2_FRONTEND_PORT:-8080}
|
||||
ZROK2_STORE_TYPE: ${ZROK2_STORE_TYPE:-postgres}
|
||||
ZROK2_DB_PASSWORD: ${ZROK2_DB_PASSWORD:-zrok2defaultpw}
|
||||
ZROK2_METRICS_ENABLED: ${ZROK2_METRICS_ENABLED:-false}
|
||||
ZROK2_INFLUX_TOKEN: ${ZROK2_INFLUX_TOKEN:-}
|
||||
HOME: /var/lib/zrok2
|
||||
depends_on:
|
||||
ziti-controller:
|
||||
condition: service_healthy
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- zrok2-config:/var/lib/zrok2
|
||||
- ./entrypoint-init.bash:/bootstrap/entrypoint-init.bash:ro
|
||||
@@ -235,7 +239,7 @@ services:
|
||||
|
||||
zrok2-frontend:
|
||||
<<: *zrok2-image
|
||||
command: ["access", "public", "/var/lib/zrok2/config/frontend.yaml"]
|
||||
command: ["access", "dynamicProxy", "/var/lib/zrok2/config/frontend.yaml"]
|
||||
environment:
|
||||
ZROK2_API_ENDPOINT: http://zrok2-controller:${ZROK2_CTRL_PORT:-18080}
|
||||
HOME: /var/lib/zrok2
|
||||
@@ -251,11 +255,34 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
# ── Metrics Pipeline (optional) ──────────────────────────────────────────
|
||||
# To enable: set ZROK2_METRICS_ENABLED=true in .env
|
||||
# To enable: set ZROK2_METRICS_ENABLED=true in .env and run with
|
||||
# docker compose --profile metrics up -d
|
||||
|
||||
# Metrics bridge: reads fabric-usage.json from the Ziti controller data
|
||||
# volume and publishes events to RabbitMQ for the zrok2 controller to
|
||||
# consume and write to InfluxDB.
|
||||
zrok2-metrics-bridge:
|
||||
<<: *zrok2-image
|
||||
command: ["controller", "metrics", "bridge", "/var/lib/zrok2/config/ctrl.yaml"]
|
||||
profiles: ["metrics"]
|
||||
environment:
|
||||
HOME: /var/lib/zrok2
|
||||
depends_on:
|
||||
zrok2-controller:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- zrok2-config:/var/lib/zrok2:ro
|
||||
- ziti-ctrl-data:/ziti-data
|
||||
networks:
|
||||
- zrok2
|
||||
restart: unless-stopped
|
||||
|
||||
# RabbitMQ is required by the dynamic frontend for real-time mapping updates
|
||||
# (AMQP-backed share name resolution). It is NOT optional.
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management-alpine
|
||||
profiles: ["metrics"]
|
||||
volumes:
|
||||
- rabbitmq-data:/var/lib/rabbitmq
|
||||
networks:
|
||||
|
||||
@@ -5,19 +5,22 @@
|
||||
# integration tests against the live API, then tears down.
|
||||
#
|
||||
# Usage:
|
||||
# docker.test.bash [--source-dir <dir>] [--keep] [--only-clean]
|
||||
# docker.test.bash [OPTIONS]
|
||||
#
|
||||
# Options:
|
||||
# --source-dir <dir> zrok source tree root (default: auto-detected)
|
||||
# --keep keep the stack running on exit (for inspection)
|
||||
# --only-clean tear down a kept instance and exit
|
||||
# --source-dir <dir> zrok source tree root (default: auto-detected)
|
||||
# --ziti-repo <repo> Ziti image repo prefix (default: docker.io/openziti)
|
||||
# --ziti-tag <tag> Ziti controller+router image tag (default: latest)
|
||||
# --keep keep the stack running on exit (for inspection)
|
||||
# --only-clean tear down a kept instance and exit
|
||||
#
|
||||
# Environment variables:
|
||||
# COMPOSE_FILE Override compose file list (default: compose.yml:compose.build.yml)
|
||||
#
|
||||
# Examples:
|
||||
# bash docker.test.bash
|
||||
# bash docker.test.bash --keep
|
||||
# bash docker.test.bash --ziti-repo docker.io/openziti --ziti-tag 1.6.14 --keep
|
||||
# bash docker.test.bash --ziti-repo docker.io/kbingham --ziti-tag latest --keep
|
||||
# bash docker.test.bash --only-clean
|
||||
|
||||
set -o errexit
|
||||
@@ -31,11 +34,13 @@ for _arg in "$@"; do
|
||||
case "$_arg" in
|
||||
--help|-h)
|
||||
cat <<'EOF'
|
||||
Usage: docker.test.bash [--source-dir <dir>] [--keep] [--only-clean]
|
||||
Usage: docker.test.bash [OPTIONS]
|
||||
|
||||
--source-dir <dir> zrok source tree root (default: auto-detected)
|
||||
--keep keep the stack running on exit (for inspection)
|
||||
--only-clean tear down a kept instance (volumes included) and exit
|
||||
--source-dir <dir> zrok source tree root (default: auto-detected)
|
||||
--ziti-repo <repo> Ziti image repo prefix (default: docker.io/openziti)
|
||||
--ziti-tag <tag> Ziti controller+router image tag (default: latest)
|
||||
--keep keep the stack running on exit (for inspection)
|
||||
--only-clean tear down a kept instance (volumes included) and exit
|
||||
|
||||
State from a prior run is always destroyed at the start via 'docker compose
|
||||
down -v'. Use --keep to leave the stack running for post-mortem inspection.
|
||||
@@ -95,7 +100,7 @@ wait_for_port() {
|
||||
|
||||
dump_logs() {
|
||||
log_info "container logs (last 100 lines each):"
|
||||
docker compose logs --tail=100 2>/dev/null || true
|
||||
(cd "${COMPOSE_PROJECT_DIR}" && docker compose logs --tail=100) 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
@@ -107,7 +112,7 @@ COMPOSE_PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
teardown() {
|
||||
log_section "Tear down"
|
||||
if ! (cd "${COMPOSE_PROJECT_DIR}" && docker compose down -v 2>/dev/null); then
|
||||
if ! (cd "${COMPOSE_PROJECT_DIR}" && docker compose --profile metrics --profile canary down -v --remove-orphans 2>/dev/null); then
|
||||
true # best-effort
|
||||
fi
|
||||
}
|
||||
@@ -156,11 +161,13 @@ trap '_err_handler' ERR
|
||||
usage() {
|
||||
trap - EXIT ERR # don't tear down on usage/help exits
|
||||
cat >&2 <<'EOF'
|
||||
Usage: docker.test.bash [--source-dir <dir>] [--keep] [--only-clean]
|
||||
Usage: docker.test.bash [OPTIONS]
|
||||
|
||||
--source-dir <dir> zrok source tree root (default: auto-detected)
|
||||
--keep keep the stack running on exit (for inspection)
|
||||
--only-clean tear down a kept instance (volumes included) and exit
|
||||
--source-dir <dir> zrok source tree root (default: auto-detected)
|
||||
--ziti-repo <repo> Ziti image repo prefix (default: docker.io/openziti)
|
||||
--ziti-tag <tag> Ziti controller+router image tag (default: latest)
|
||||
--keep keep the stack running on exit (for inspection)
|
||||
--only-clean tear down a kept instance (volumes included) and exit
|
||||
|
||||
State from a prior run is always destroyed at the start via 'docker compose
|
||||
down -v'. Use --keep to leave the stack running for post-mortem inspection.
|
||||
@@ -170,10 +177,14 @@ EOF
|
||||
}
|
||||
|
||||
SOURCE_DIR=""
|
||||
ZITI_REPO=""
|
||||
ZITI_TAG=""
|
||||
ONLY_CLEAN=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source-dir) SOURCE_DIR="$2"; shift 2 ;;
|
||||
--ziti-repo) ZITI_REPO="$2"; shift 2 ;;
|
||||
--ziti-tag) ZITI_TAG="$2"; shift 2 ;;
|
||||
--keep) KEEP=1; shift ;;
|
||||
--only-clean) ONLY_CLEAN=1; shift ;;
|
||||
*) usage ;;
|
||||
@@ -185,7 +196,7 @@ if [[ -z "${SOURCE_DIR}" ]]; then
|
||||
fi
|
||||
[[ -d "${SOURCE_DIR}" ]] || { trap - EXIT ERR; log_error "source dir '${SOURCE_DIR}' not found"; exit 1; }
|
||||
|
||||
export COMPOSE_FILE="${COMPOSE_FILE:-compose.yml:compose.build.yml}"
|
||||
export COMPOSE_FILE="${COMPOSE_FILE:-compose.yml:compose.build.yml}:compose.canary.yml"
|
||||
|
||||
if (( ONLY_CLEAN )); then
|
||||
trap - EXIT ERR # teardown is intentional here; don't double-run it on exit
|
||||
@@ -213,7 +224,10 @@ WARN
|
||||
fi
|
||||
|
||||
log_section "Pre-test cleanup"
|
||||
if ! (cd "${COMPOSE_PROJECT_DIR}" && docker compose down -v 2>/dev/null); then
|
||||
# Remove all containers, networks, and named volumes from the compose project.
|
||||
# Use --profile metrics to include metrics services, and --remove-orphans to
|
||||
# catch services that may have been renamed or removed between runs.
|
||||
if ! (cd "${COMPOSE_PROJECT_DIR}" && docker compose --profile metrics --profile canary down -v --remove-orphans 2>/dev/null); then
|
||||
true # best-effort
|
||||
fi
|
||||
|
||||
@@ -231,7 +245,26 @@ for var in ZROK2_ADMIN_TOKEN ZITI_PWD ZROK2_DB_PASSWORD \
|
||||
# shellcheck disable=SC2119
|
||||
sed -i "s|^${var}=.*|${var}=$(generate_password)|" "${ENV_FILE}"
|
||||
done
|
||||
sed -i "s|^ZROK2_DNS_ZONE=.*|ZROK2_DNS_ZONE=localhost|" "${ENV_FILE}"
|
||||
sed -i "s|^ZROK2_DNS_ZONE=.*|ZROK2_DNS_ZONE=zrok.127.0.0.1.sslip.io|" "${ENV_FILE}"
|
||||
# Enable the metrics pipeline so we can verify InfluxDB receives data.
|
||||
sed -i "s|^# ZROK2_METRICS_ENABLED=.*|ZROK2_METRICS_ENABLED=true|" "${ENV_FILE}"
|
||||
# Pin Ziti image repo and/or tag if specified.
|
||||
# --ziti-repo docker.io/kbingham → controller image is docker.io/kbingham/ziti-controller
|
||||
if [[ -n "${ZITI_REPO}" ]]; then
|
||||
sed -i "s|^# ZITI_CONTROLLER_IMAGE=.*|ZITI_CONTROLLER_IMAGE=${ZITI_REPO}/ziti-controller|" "${ENV_FILE}"
|
||||
sed -i "s|^# ZITI_ROUTER_IMAGE=.*|ZITI_ROUTER_IMAGE=${ZITI_REPO}/ziti-router|" "${ENV_FILE}"
|
||||
log_info "using Ziti image repo: ${ZITI_REPO}"
|
||||
fi
|
||||
if [[ -n "${ZITI_TAG}" ]]; then
|
||||
# Docker image tags use hyphens (e.g., 2.0.0-rc5), not tildes.
|
||||
# The same ZITI_LINUX_VERSION variable may contain tildes from deb
|
||||
# convention (2.0.0~rc5) or hyphens — normalize to hyphens.
|
||||
ZITI_TAG="${ZITI_TAG//\~/-}"
|
||||
ZITI_TAG="${ZITI_TAG#v}"
|
||||
sed -i "s|^# ZITI_CONTROLLER_TAG=.*|ZITI_CONTROLLER_TAG=${ZITI_TAG}|" "${ENV_FILE}"
|
||||
sed -i "s|^# ZITI_ROUTER_TAG=.*|ZITI_ROUTER_TAG=${ZITI_TAG}|" "${ENV_FILE}"
|
||||
log_info "using Ziti image tag: ${ZITI_TAG}"
|
||||
fi
|
||||
|
||||
# Load generated values into the environment
|
||||
# shellcheck source=/dev/null
|
||||
@@ -252,10 +285,84 @@ log_section "Phase 2: Build and start Docker Compose stack"
|
||||
COMPOSE_FILE="${COMPOSE_PROJECT_DIR}/${COMPOSE_FILE}" \
|
||||
docker compose -f "${COMPOSE_PROJECT_DIR}/compose.yml" \
|
||||
-f "${COMPOSE_PROJECT_DIR}/compose.build.yml" \
|
||||
--profile metrics \
|
||||
up -d --build --wait --wait-timeout 300)
|
||||
|
||||
log_pass "stack is up and healthy"
|
||||
|
||||
# Patch the Ziti controller config with fabric.usage event logging, then
|
||||
# restart the overlay. We exec into the running controller to append the
|
||||
# events section, then stop+start (not restart) with the config preserved.
|
||||
# The router must restart after the controller to re-sync signing keys.
|
||||
# Patch the Ziti controller config at /ziti-controller/config.yml (the
|
||||
# controller's working directory, on the ziti-ctrl-data named volume).
|
||||
# Use sed insertion before known anchors — appending to the end of YAML
|
||||
# files is unreliably parsed by Go YAML parsers.
|
||||
log_info "patching Ziti controller with fabric.usage events and metrics reporting..."
|
||||
docker compose -f "${COMPOSE_PROJECT_DIR}/compose.yml" \
|
||||
exec -T ziti-controller sh -c '
|
||||
CONFIG=/ziti-controller/config.yml
|
||||
# The volume is now mounted at /ziti-controller (the controller workdir),
|
||||
# so the events file is on the shared volume and visible to the metrics
|
||||
# bridge at /ziti-data/fabric-usage.json.
|
||||
EVENTS_PATH=/ziti-controller/fabric-usage.json
|
||||
|
||||
# Insert events section before identity: if not present.
|
||||
if ! grep -q "fabric.usage" "$CONFIG"; then
|
||||
sed -i "/^identity:/i\\
|
||||
\\
|
||||
events:\\
|
||||
jsonLogger:\\
|
||||
subscriptions:\\
|
||||
- type: fabric.usage\\
|
||||
version: 3\\
|
||||
handler:\\
|
||||
type: file\\
|
||||
format: json\\
|
||||
path: $EVENTS_PATH\\
|
||||
" "$CONFIG"
|
||||
echo "events section inserted"
|
||||
else
|
||||
# Fix the events file path if it points to the wrong location.
|
||||
sed -i "s|path:.*fabric-usage.json|path: $EVENTS_PATH|" "$CONFIG"
|
||||
echo "events path corrected"
|
||||
fi
|
||||
|
||||
# Insert network section before identity: for responsive metrics.
|
||||
if ! grep -q "metricsReportInterval" "$CONFIG"; then
|
||||
sed -i "/^identity:/i\\
|
||||
\\
|
||||
network:\\
|
||||
intervalAgeThreshold: 5s\\
|
||||
metricsReportInterval: 5s\\
|
||||
" "$CONFIG"
|
||||
echo "network metrics section inserted"
|
||||
fi
|
||||
|
||||
# Pre-create the events file so the metrics bridge does not panic.
|
||||
touch "$EVENTS_PATH"
|
||||
'
|
||||
|
||||
log_info "restarting Ziti overlay..."
|
||||
(cd "${COMPOSE_PROJECT_DIR}" && docker compose restart ziti-controller)
|
||||
retry 30 3 docker compose -f "${COMPOSE_PROJECT_DIR}/compose.yml" \
|
||||
exec -T ziti-controller ziti agent stats
|
||||
|
||||
(cd "${COMPOSE_PROJECT_DIR}" && docker compose restart ziti-router)
|
||||
retry 30 3 docker compose -f "${COMPOSE_PROJECT_DIR}/compose.yml" \
|
||||
exec -T ziti-router ziti agent stats
|
||||
|
||||
# Restart zrok2 services — their Ziti SDK connections are stale after the
|
||||
# overlay restart (cached router IPs/sessions are invalid).
|
||||
(cd "${COMPOSE_PROJECT_DIR}" && docker compose restart zrok2-controller)
|
||||
retry 30 3 docker compose -f "${COMPOSE_PROJECT_DIR}/compose.yml" \
|
||||
exec -T zrok2-controller curl -sf -o /dev/null \
|
||||
-H "Accept: application/zrok.v1+json" http://127.0.0.1:18080/api/v1/version
|
||||
(cd "${COMPOSE_PROJECT_DIR}" && docker compose restart zrok2-frontend)
|
||||
# Restart the metrics bridge so it picks up the now-existing events file.
|
||||
(cd "${COMPOSE_PROJECT_DIR}" && docker compose --profile metrics restart zrok2-metrics-bridge) 2>/dev/null || true
|
||||
log_pass "stack restarted with events config"
|
||||
|
||||
# ============================================================
|
||||
# Phase 3: Verify API
|
||||
# ============================================================
|
||||
@@ -268,7 +375,7 @@ retry 10 3 curl -sf -H "${ZROK2_ACCEPT}" \
|
||||
|
||||
_version=$(curl -sf -H "${ZROK2_ACCEPT}" \
|
||||
"${ZROK2_API_ENDPOINT}/api/v2/versions" | \
|
||||
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('controllerVersion','?'))")
|
||||
grep -oP '"controllerVersion"\s*:\s*"\K[^"]+' || echo "?")
|
||||
log_pass "API responded: controllerVersion=${_version}"
|
||||
|
||||
log_pass "stack verified — ZROK2_API_ENDPOINT=${ZROK2_API_ENDPOINT} ZROK2_ADMIN_TOKEN=${ZROK2_ADMIN_TOKEN}"
|
||||
@@ -279,38 +386,12 @@ log_pass "stack verified — ZROK2_API_ENDPOINT=${ZROK2_API_ENDPOINT} ZROK2_ADMI
|
||||
|
||||
log_section "Phase 4: Canary public-proxy looper"
|
||||
|
||||
# Create a test account and enable an environment inside the controller container.
|
||||
# The canary runs inside the container where the zrok2 binary and Ziti overlay are available.
|
||||
_canary_token=$(docker compose -f "${COMPOSE_PROJECT_DIR}/compose.yml" \
|
||||
exec -T -e ZROK2_ADMIN_TOKEN="${ZROK2_ADMIN_TOKEN}" \
|
||||
zrok2-controller zrok2 admin create account \
|
||||
"canary-$(date +%s)@zrok.internal" "canarypass" 2>/dev/null)
|
||||
|
||||
if [[ -n "${_canary_token}" ]]; then
|
||||
log_info "canary account token: ${_canary_token}"
|
||||
|
||||
# Enable, run canary, disable — all inside the container
|
||||
if docker compose -f "${COMPOSE_PROJECT_DIR}/compose.yml" \
|
||||
exec -T \
|
||||
-e ZROK2_API_ENDPOINT="http://zrok2-controller:${ZROK2_CTRL_PORT:-18080}" \
|
||||
-e ZROK2_DANGEROUS_CANARY=1 \
|
||||
-e HOME=/tmp/canary-home \
|
||||
zrok2-controller sh -c "
|
||||
mkdir -p /tmp/canary-home/.zrok2
|
||||
echo '{\"v\":\"v0.4\"}' > /tmp/canary-home/.zrok2/metadata.json
|
||||
printf '{\"apiEndpoint\":\"http://zrok2-controller:${ZROK2_CTRL_PORT:-18080}\"}' > /tmp/canary-home/.zrok2/config.json
|
||||
zrok2 enable '${_canary_token}' --description canary-test &&
|
||||
zrok2 test canary public-proxy --iterations 3 --loopers 1 \
|
||||
--min-payload 256 --max-payload 256 --min-pacing 1s --max-pacing 1s &&
|
||||
zrok2 disable
|
||||
" 2>&1; then
|
||||
log_pass "canary public-proxy looper passed"
|
||||
else
|
||||
log_info "canary looper failed (may need frontend reachable from container)"
|
||||
fi
|
||||
else
|
||||
log_info "could not create canary account — skipping canary test"
|
||||
fi
|
||||
# The canary runs in a host-networked container defined in compose.canary.yml.
|
||||
# Host networking means *.localhost resolves to 127.0.0.1, reaching the
|
||||
# frontend at its published port. The canary service creates an account,
|
||||
# enables an environment, exercises the public frontend, and disables.
|
||||
(cd "${COMPOSE_PROJECT_DIR}" && docker compose run --rm canary) 2>&1
|
||||
log_pass "canary public-proxy looper passed"
|
||||
|
||||
# ============================================================
|
||||
# Phase 5: Verify metrics pipeline (InfluxDB has data from canary)
|
||||
@@ -318,14 +399,16 @@ fi
|
||||
|
||||
log_section "Phase 5: Verify metrics pipeline"
|
||||
|
||||
# Query InfluxDB directly from the influxdb container.
|
||||
# Query InfluxDB inside the influxdb container. The metrics profile adds
|
||||
# RabbitMQ + InfluxDB. Canary traffic from Phase 4 should have produced
|
||||
# fabric.usage events that flow through the pipeline.
|
||||
log_info "waiting up to 90s for metrics to appear in InfluxDB..."
|
||||
_metrics_found=false
|
||||
for _attempt in $(seq 1 18); do
|
||||
_count=$(docker compose -f "${COMPOSE_PROJECT_DIR}/compose.yml" \
|
||||
exec -T influxdb influx query \
|
||||
'from(bucket: "zrok") |> range(start: -5m) |> count()' \
|
||||
--org zrok --token "${ZROK2_INFLUX_TOKEN:-}" --raw 2>/dev/null \
|
||||
'from(bucket: "zrok2") |> range(start: -5m) |> count()' \
|
||||
--org zrok2 --token "${ZROK2_INFLUX_TOKEN:-}" --raw 2>/dev/null \
|
||||
| grep -c ',' || true)
|
||||
if (( _count > 0 )); then
|
||||
_metrics_found=true
|
||||
@@ -337,5 +420,10 @@ done
|
||||
if [[ "${_metrics_found}" == "true" ]]; then
|
||||
log_pass "metrics pipeline verified: InfluxDB has data (${_count} series)"
|
||||
else
|
||||
log_info "metrics pipeline: no data in InfluxDB after 90s (metrics profile may not be enabled)"
|
||||
log_error "metrics pipeline: no data in InfluxDB after 90s"
|
||||
(cd "${COMPOSE_PROJECT_DIR}" && docker compose logs zrok2-metrics-bridge --tail=10) 2>&1 || true
|
||||
(cd "${COMPOSE_PROJECT_DIR}" && docker compose exec -T rabbitmq rabbitmqctl list_queues) 2>&1 || true
|
||||
docker compose -f "${COMPOSE_PROJECT_DIR}/compose.yml" \
|
||||
exec -T ziti-controller wc -l /ziti-controller/fabric-usage.json 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -77,13 +77,21 @@ _zrok2_ctrl_alive() {
|
||||
kill -0 "$CTRL_PID" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Step 1: Generate controller config (Docker-minimal) ─────────────────────
|
||||
# ── Step 1: Generate controller config ────────────────────────────────────────
|
||||
#
|
||||
# Omits bridge, dynamic_proxy_controller, and metrics sections — those
|
||||
# require local AMQP/InfluxDB that are external containers in Docker.
|
||||
# When ZROK2_METRICS_ENABLED=true, includes bridge (fileSource → amqpSink)
|
||||
# and metrics (amqpSource → InfluxDB) sections for the metrics pipeline.
|
||||
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
AMQP_URL="${ZROK2_AMQP_URL:-amqp://guest:guest@rabbitmq:5672}"
|
||||
INFLUX_URL="${ZROK2_INFLUX_URL:-http://influxdb:8086}"
|
||||
INFLUX_ORG="${ZROK2_INFLUX_ORG:-zrok2}"
|
||||
INFLUX_BUCKET="${ZROK2_INFLUX_BUCKET:-zrok2}"
|
||||
INFLUX_TOKEN="${ZROK2_INFLUX_TOKEN:-}"
|
||||
# Path to fabric-usage.json inside the metrics-bridge container (shared volume).
|
||||
FABRIC_USAGE_PATH="${ZROK2_FABRIC_USAGE_PATH:-/ziti-data/fabric-usage.json}"
|
||||
|
||||
if [[ ! -f "$CTRL_CONFIG" ]]; then
|
||||
info "Generating $CTRL_CONFIG..."
|
||||
cat > "$CTRL_CONFIG" <<CTRLEOF
|
||||
@@ -111,6 +119,33 @@ maintenance:
|
||||
check_frequency: 15m
|
||||
batch_limit: 500
|
||||
CTRLEOF
|
||||
|
||||
# Append metrics pipeline sections when enabled.
|
||||
if [[ "${ZROK2_METRICS_ENABLED:-false}" == "true" ]]; then
|
||||
info "Metrics enabled — adding bridge and metrics sections to $CTRL_CONFIG"
|
||||
cat >> "$CTRL_CONFIG" <<METRICSEOF
|
||||
bridge:
|
||||
source:
|
||||
type: fileSource
|
||||
path: ${FABRIC_USAGE_PATH}
|
||||
sink:
|
||||
type: amqpSink
|
||||
url: ${AMQP_URL}
|
||||
queue_name: events
|
||||
metrics:
|
||||
agent:
|
||||
source:
|
||||
type: amqpSource
|
||||
url: ${AMQP_URL}
|
||||
queue_name: events
|
||||
influx:
|
||||
url: "${INFLUX_URL}"
|
||||
bucket: ${INFLUX_BUCKET}
|
||||
org: ${INFLUX_ORG}
|
||||
token: "${INFLUX_TOKEN}"
|
||||
METRICSEOF
|
||||
fi
|
||||
|
||||
chmod 640 "$CTRL_CONFIG"
|
||||
info "Controller config written to $CTRL_CONFIG"
|
||||
else
|
||||
@@ -179,6 +214,20 @@ info "Namespace '${ZROK2_NAMESPACE_TOKEN}' ready"
|
||||
retry 5 3 "map namespace to frontend" step_map_namespace_frontend
|
||||
info "Namespace-frontend mapping ready"
|
||||
|
||||
# ── Step 6b: Create dynamicProxyController identity and Ziti resources ───────
|
||||
#
|
||||
# The dynamic proxy controller provides real-time AMQP-based mapping updates
|
||||
# to the frontend, required for named public shares. The bootstrap library's
|
||||
# step_dynamic_proxy_controller() creates the identity, Ziti service, and
|
||||
# policies. CONTROLLER_HOME must be set so the identity file lands in the
|
||||
# right place.
|
||||
|
||||
export CONTROLLER_HOME="${HOME}"
|
||||
export ZROK2_AMQP_URL="${AMQP_URL}"
|
||||
export CTRL_CONFIG
|
||||
retry 5 3 "create dynamicProxyController" step_dynamic_proxy_controller
|
||||
info "dynamicProxyController ready"
|
||||
|
||||
# ── Step 7: Save frontend identity ───────────────────────────────────────────
|
||||
|
||||
if [[ -f "${HOME}/.zrok2/identities/public.json" ]]; then
|
||||
@@ -194,6 +243,33 @@ kill "$CTRL_PID" 2>/dev/null || true
|
||||
wait "$CTRL_PID" 2>/dev/null || true
|
||||
trap - EXIT
|
||||
|
||||
# ── Step 8b: Generate full frontend config (AMQP-backed dynamic proxy) ────────
|
||||
#
|
||||
# Overwrite the simple frontend.yaml with the full v1 config that uses
|
||||
# amqp_subscriber + controller (gRPC via dynamicProxyController Ziti service)
|
||||
# for real-time mapping updates. Required for named public shares.
|
||||
|
||||
info "Generating full AMQP-backed frontend config..."
|
||||
cat > "$FRONTEND_CONFIG" <<FEEOF
|
||||
v: 1
|
||||
|
||||
frontend_token: ${FRONTEND_TOKEN}
|
||||
identity: public
|
||||
bind_address: 0.0.0.0:${ZROK2_FRONTEND_PORT:-8080}
|
||||
host_match: ${ZROK2_DNS_ZONE}
|
||||
mapping_refresh_interval: 1m
|
||||
|
||||
amqp_subscriber:
|
||||
url: ${ZROK2_AMQP_URL:-amqp://guest:guest@rabbitmq:5672}
|
||||
exchange_name: dynamicProxy
|
||||
|
||||
controller:
|
||||
identity_path: ${CONFIG_DIR}/public.json
|
||||
service_name: dynamicProxyController
|
||||
FEEOF
|
||||
chmod 640 "$FRONTEND_CONFIG"
|
||||
info "Full frontend config written to $FRONTEND_CONFIG"
|
||||
|
||||
# ── Step 9: Fix ownership for non-root zrok2 containers ─────────────────────
|
||||
|
||||
chown -R 2171:2171 /var/lib/zrok2
|
||||
|
||||
@@ -725,18 +725,15 @@ fi
|
||||
# ============================================================
|
||||
log_section "Phase 10: Canary public-proxy looper"
|
||||
|
||||
# Create a fresh account for the canary via the REST API (avoids CLI endpoint
|
||||
# precedence issues with the operator's enabled environment).
|
||||
# Create a fresh canary account and enable an environment. Use an isolated
|
||||
# HOME so the operator's enabled environment doesn't interfere. The zrok2
|
||||
# admin CLI uses ZROK2_ADMIN_TOKEN + ZROK2_API_ENDPOINT from the environment.
|
||||
# zrok2 enable bootstraps the environment in one shot when ZROK2_API_ENDPOINT
|
||||
# is set.
|
||||
CANARY_HOME="$(mktemp -d)"
|
||||
mkdir -p "${CANARY_HOME}/.zrok2"
|
||||
echo '{"v":"v0.4"}' > "${CANARY_HOME}/.zrok2/metadata.json"
|
||||
printf '{"apiEndpoint":"%s"}' "${ZROK2_API_ENDPOINT}" > "${CANARY_HOME}/.zrok2/config.json"
|
||||
|
||||
_canary_token=$(curl -sf \
|
||||
-H "X-TOKEN: ${ZROK2_ADMIN_TOKEN}" \
|
||||
-H "Content-Type: application/zrok.v1+json" \
|
||||
-d "{\"email\":\"canary-$(date +%s)@zrok.internal\",\"password\":\"canarypass\"}" \
|
||||
"${ZROK2_API_ENDPOINT}/api/v2/account" | python3 -c "import sys,json; print(json.load(sys.stdin)['accountToken'])")
|
||||
_canary_token=$(HOME="${CANARY_HOME}" "${ZROK2_BIN}" admin create account \
|
||||
"canary-$(date +%s)@zrok.internal" "canarypass")
|
||||
log_info "canary account token: ${_canary_token}"
|
||||
|
||||
HOME="${CANARY_HOME}" "${ZROK2_BIN}" enable "${_canary_token}" --description "canary-test"
|
||||
@@ -748,14 +745,65 @@ if [[ "${_frontend_port}" != "443" ]]; then
|
||||
_canary_flags+=(--http --frontend-port "${_frontend_port}")
|
||||
fi
|
||||
|
||||
if HOME="${CANARY_HOME}" ZROK2_DANGEROUS_CANARY=1 \
|
||||
"${ZROK2_BIN}" test canary public-proxy "${_canary_flags[@]}"; then
|
||||
log_pass "canary public-proxy looper passed"
|
||||
else
|
||||
log_error "canary public-proxy looper failed"
|
||||
log_info "continuing despite canary failure"
|
||||
HOME="${CANARY_HOME}" ZROK2_DANGEROUS_CANARY=1 \
|
||||
"${ZROK2_BIN}" test canary public-proxy "${_canary_flags[@]}"
|
||||
log_pass "canary public-proxy looper passed"
|
||||
|
||||
# ── Named share test ─────────────────────────────────────────────────────────
|
||||
# Create a share with an explicit name (--name-selection) and verify the
|
||||
# AMQP-backed dynamic frontend serves it at the expected URL.
|
||||
log_info "testing named share via dynamic frontend..."
|
||||
_share_name="citest-$(date +%s)"
|
||||
_scheme="http"
|
||||
if [[ "${_frontend_port}" == "443" ]]; then _scheme="https"; fi
|
||||
_share_url="${_scheme}://${_share_name}.${ZROK2_DNS_ZONE}:${_frontend_port}/"
|
||||
|
||||
# Start zrok2's built-in test endpoint as a backend.
|
||||
"${ZROK2_BIN}" test endpoint --port 19999 &>/dev/null &
|
||||
_http_pid=$!
|
||||
|
||||
# Pre-create the name (v2 equivalent of "zrok reserve").
|
||||
HOME="${CANARY_HOME}" "${ZROK2_BIN}" create name "${_share_name}"
|
||||
|
||||
# Create the named share in the background (long-running). Keep stderr
|
||||
# visible so share creation errors appear in the log.
|
||||
HOME="${CANARY_HOME}" "${ZROK2_BIN}" share public http://127.0.0.1:19999 \
|
||||
--name-selection "public:${_share_name}" --backend-mode proxy --headless >/dev/null &
|
||||
_share_pid=$!
|
||||
|
||||
# Give the share time to register with the controller and propagate via AMQP.
|
||||
sleep 5
|
||||
|
||||
# Verify the share process is still alive (didn't exit with an error).
|
||||
if ! kill -0 "${_share_pid}" 2>/dev/null; then
|
||||
log_error "named share process exited prematurely"
|
||||
wait "${_share_pid}" 2>/dev/null || true
|
||||
kill "${_http_pid}" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Wait for the share to propagate to the dynamic frontend.
|
||||
log_info "waiting for named share at ${_share_url} ..."
|
||||
# Poll until the frontend routes the named share and the content matches.
|
||||
_attempts=30
|
||||
while (( _attempts-- > 0 )); do
|
||||
if curl -sf "${_share_url}" 2>/dev/null | grep -qi "zrok"; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if (( _attempts < 0 )); then
|
||||
log_error "named share '${_share_name}' not reachable or content mismatch after 60s"
|
||||
kill "${_share_pid}" "${_http_pid}" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
log_pass "named share '${_share_name}' content verified on the dynamic frontend"
|
||||
|
||||
# Clean up share and backend.
|
||||
kill "${_share_pid}" "${_http_pid}" 2>/dev/null || true
|
||||
wait "${_share_pid}" 2>/dev/null || true
|
||||
wait "${_http_pid}" 2>/dev/null || true
|
||||
|
||||
# Disable the canary environment
|
||||
HOME="${CANARY_HOME}" "${ZROK2_BIN}" disable 2>/dev/null || true
|
||||
|
||||
@@ -764,7 +812,7 @@ HOME="${CANARY_HOME}" "${ZROK2_BIN}" disable 2>/dev/null || true
|
||||
# ============================================================
|
||||
log_section "Phase 11: Verify metrics pipeline"
|
||||
|
||||
_influx_token=$(grep -A5 'influx:' /etc/zrok2/ctrl.yml | grep 'token:' | head -1 | awk -F'"' '{print $2}')
|
||||
_influx_token=$(sudo grep -A5 'influx:' /etc/zrok2/ctrl.yml | grep 'token:' | head -1 | awk -F'"' '{print $2}' || true)
|
||||
if [[ -n "${_influx_token}" ]]; then
|
||||
# Wait for metrics to propagate through the pipeline (bridge → RabbitMQ → controller → InfluxDB).
|
||||
# The default metricsReportInterval is 60s, so we may need to wait.
|
||||
@@ -786,11 +834,14 @@ if [[ -n "${_influx_token}" ]]; then
|
||||
log_pass "metrics pipeline verified: InfluxDB has data (${_count} series)"
|
||||
else
|
||||
log_error "metrics pipeline: no data in InfluxDB after 90s"
|
||||
log_info "check: systemctl status zrok2-metrics-bridge, rabbitmqctl list_queues"
|
||||
log_info "continuing despite metrics verification failure"
|
||||
sudo systemctl status zrok2-metrics-bridge --no-pager 2>&1 | head -5 || true
|
||||
sudo rabbitmqctl list_queues 2>&1 || true
|
||||
sudo journalctl -u zrok2-metrics-bridge --no-pager -n 10 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_info "could not extract InfluxDB token from ctrl.yml — skipping metrics check"
|
||||
log_error "could not extract InfluxDB token from ctrl.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
|
||||
+78
-16
@@ -899,20 +899,27 @@ step_dynamic_proxy_controller() {
|
||||
info "Dial policy ${SERVICE_NAME}-dial already exists"
|
||||
fi
|
||||
|
||||
# Add the dynamic_proxy_controller block to ctrl.yml now that the identity exists.
|
||||
# The controller will be restarted in step_start_services to pick this up.
|
||||
# Insert the dynamic_proxy_controller block into ctrl.yml now that the
|
||||
# identity file exists. We use sed to insert BEFORE the 'endpoint:' line
|
||||
# rather than appending to the end — the Go YAML parser reliably reads
|
||||
# content that's part of the original file structure but may ignore
|
||||
# content appended after the initial write.
|
||||
if ! grep -q 'dynamic_proxy_controller:' "$CTRL_CONFIG" 2>/dev/null; then
|
||||
info "Adding dynamic_proxy_controller to $CTRL_CONFIG..."
|
||||
cat >> "$CTRL_CONFIG" <<DPCEOF
|
||||
|
||||
dynamic_proxy_controller:
|
||||
identity_path: ${CONTROLLER_HOME}/.zrok2/identities/dynamicProxyController.json
|
||||
service_name: dynamicProxyController
|
||||
amqp_publisher:
|
||||
url: ${ZROK2_AMQP_URL}
|
||||
exchange_name: dynamicProxy
|
||||
DPCEOF
|
||||
info "dynamic_proxy_controller block added to config"
|
||||
info "Inserting dynamic_proxy_controller into $CTRL_CONFIG..."
|
||||
sed -i "/^endpoint:/i\\
|
||||
\\
|
||||
dynamic_proxy_controller:\\
|
||||
identity_path: ${CONTROLLER_HOME}/.zrok2/identities/dynamicProxyController.json\\
|
||||
service_name: dynamicProxyController\\
|
||||
amqp_publisher:\\
|
||||
url: ${ZROK2_AMQP_URL}\\
|
||||
exchange_name: dynamicProxy\\
|
||||
" "$CTRL_CONFIG"
|
||||
# Verify the block was written correctly.
|
||||
if ! grep -q 'dynamic_proxy_controller:' "$CTRL_CONFIG"; then
|
||||
die "Failed to write dynamic_proxy_controller to $CTRL_CONFIG"
|
||||
fi
|
||||
info "dynamic_proxy_controller inserted into config"
|
||||
fi
|
||||
|
||||
info "dynamicProxyController Ziti resources are configured"
|
||||
@@ -1192,13 +1199,68 @@ step_start_services() {
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Restart controller to pick up dynamic_proxy_controller config, metrics, and new overrides
|
||||
# Restart controller to pick up dynamic_proxy_controller config, metrics, and new overrides.
|
||||
# The frontend fatally exits if the dynamicProxyController gRPC service is
|
||||
# unreachable, so we must wait for the controller to fully initialize its
|
||||
# Ziti identity and register a terminator before starting the frontend.
|
||||
info "Restarting zrok2-controller to apply full configuration..."
|
||||
systemctl restart zrok2-controller
|
||||
sleep 2
|
||||
if ! grep -q 'dynamic_proxy_controller:' "$CTRL_CONFIG"; then
|
||||
warn "dynamic_proxy_controller not found in $CTRL_CONFIG — frontend may crash-loop"
|
||||
fi
|
||||
sync
|
||||
systemctl stop zrok2-controller
|
||||
# Ensure the old process is fully dead and the port is released.
|
||||
sleep 3
|
||||
# Drop the kernel's page cache to force the new process to read from disk.
|
||||
# This works around a persistent issue where the Go YAML parser in the new
|
||||
# process reads stale file content despite sed -i having atomically replaced
|
||||
# the file (the old inode's cached pages may still be served to new readers
|
||||
# of the same path before the dentry cache updates).
|
||||
echo 1 > /proc/sys/vm/drop_caches 2>/dev/null || true
|
||||
systemctl start zrok2-controller
|
||||
|
||||
# Wait for the HTTP API to respond (confirms the process is alive).
|
||||
wait_for 60 2 "zrok2-controller API after restart" \
|
||||
curl -sf -H "Accept: application/zrok.v1+json" \
|
||||
"http://127.0.0.1:${ZROK2_CTRL_PORT}/api/v1/version" -o /dev/null
|
||||
|
||||
# Verify the running controller loaded the dynamic_proxy_controller section.
|
||||
# If it didn't (stale file read), restart once more.
|
||||
_dpc_loaded() {
|
||||
journalctl -u zrok2-controller --no-pager -n 1 -o cat 2>/dev/null \
|
||||
| grep -q 'dynamic_proxy_controller.*Config'
|
||||
}
|
||||
if ! _dpc_loaded; then
|
||||
warn "controller loaded stale config (dynamic_proxy_controller: nil) — restarting again"
|
||||
systemctl restart zrok2-controller
|
||||
wait_for 60 2 "zrok2-controller API after second restart" \
|
||||
curl -sf -H "Accept: application/zrok.v1+json" \
|
||||
"http://127.0.0.1:${ZROK2_CTRL_PORT}/api/v1/version" -o /dev/null
|
||||
fi
|
||||
|
||||
# Wait for the dynamicProxyController Ziti service to have at least one
|
||||
# terminator — this means the controller has connected its Ziti identity
|
||||
# and is ready to serve gRPC requests from the frontend.
|
||||
_dpc_ready() {
|
||||
ziti edge list terminators 'service.name="dynamicProxyController"' -j 2>/dev/null \
|
||||
| jq -e '.data | length > 0' &>/dev/null
|
||||
}
|
||||
wait_for 60 3 "dynamicProxyController terminator" _dpc_ready
|
||||
info "dynamicProxyController is bound and ready"
|
||||
|
||||
# Re-apply g+w on the fabric-usage.json directory — systemd's
|
||||
# StateDirectory= resets /var/lib/ziti-controller to 755 on every
|
||||
# controller restart, undoing the chmod from step 16.
|
||||
local usage_dir
|
||||
usage_dir="$(dirname "$FABRIC_USAGE_PATH")"
|
||||
chmod g+w "$usage_dir"
|
||||
|
||||
systemctl enable --now zrok2-frontend
|
||||
info "zrok2-frontend started"
|
||||
|
||||
# Restart the metrics bridge — it may have been crash-looping while
|
||||
# waiting for the directory permissions to be fixed.
|
||||
systemctl restart zrok2-metrics-bridge 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -11,7 +11,7 @@ User=zrok2-metrics-bridge
|
||||
Group=zrok2-metrics-bridge
|
||||
WorkingDirectory=/var/lib/zrok2-metrics-bridge
|
||||
UMask=0007
|
||||
ExecStart=/usr/bin/zrok2-metrics-bridge-bridge.bash /etc/zrok2/ctrl.yml
|
||||
ExecStart=/usr/bin/zrok2-metrics-bridge.bash /etc/zrok2/ctrl.yml
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ Compose. The stack runs the same components as the
|
||||
controller, frontend, and PostgreSQL — using official container images with
|
||||
runtime configuration via environment variables.
|
||||
|
||||
:::info Single-host deployment
|
||||
This compose stack runs one frontend instance. For higher throughput or availability, run multiple
|
||||
frontend instances behind a reverse proxy (e.g., Caddy or Traefik). See
|
||||
[Scaling zrok frontends](/docs/zrok/self-hosting/scaling-frontends/) for details.
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker Engine 24+ with the Compose plugin (`docker compose`)
|
||||
@@ -83,16 +89,26 @@ These match the same variable names used in the
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
On first start, two init services run before the main services:
|
||||
On first start, init services run before the main services to bootstrap the stack. The
|
||||
`zrok2-init` container performs the same steps as the
|
||||
[Linux manual setup](/docs/zrok/self-hosting/deployment/linux/#manual-setup) — automated
|
||||
end-to-end:
|
||||
|
||||
- **`ziti-router-init`** — creates the edge router in the Ziti controller and saves its enrollment token for the router service to consume
|
||||
- **`ziti-router-init`** — creates the edge router in the Ziti controller and saves its enrollment
|
||||
token for the router service to consume
|
||||
- **`zrok2-init`** — bootstraps the full zrok2 stack:
|
||||
1. Generates `ctrl.yaml` and `frontend.yaml` from environment variables
|
||||
2. Waits for the Ziti controller to be healthy
|
||||
3. Waits for PostgreSQL to be ready
|
||||
4. Runs `zrok2 admin bootstrap` to initialize the database and create Ziti identities
|
||||
5. Creates the dynamic frontend and public namespace
|
||||
6. Saves the public frontend identity for the frontend container
|
||||
2. Waits for the Ziti controller, PostgreSQL, and RabbitMQ to be healthy
|
||||
3. Runs `zrok2 admin bootstrap` to initialize the database and create Ziti identities
|
||||
4. Starts a temporary controller to run admin commands
|
||||
5. Creates the dynamic frontend and public namespace with namespace-frontend mapping
|
||||
6. Creates the `dynamicProxyController` identity, Ziti service, and routing policies — this is the
|
||||
gRPC service that pushes real-time share mapping updates to the frontend via AMQP (required for
|
||||
[named shares](#verify-named-shares-work))
|
||||
7. Appends the `dynamic_proxy_controller` section to `ctrl.yaml`
|
||||
8. Generates the full AMQP-backed `frontend.yaml` (v1 format with `amqp_subscriber` and
|
||||
`controller` sections)
|
||||
9. Saves identity files and fixes ownership for the non-root service containers
|
||||
|
||||
This takes 1–2 minutes. Monitor progress:
|
||||
|
||||
@@ -100,8 +116,9 @@ This takes 1–2 minutes. Monitor progress:
|
||||
docker compose logs -f zrok2-init
|
||||
```
|
||||
|
||||
Once `zrok2-init` exits successfully, the controller and frontend start
|
||||
automatically.
|
||||
Once `zrok2-init` exits successfully, the controller and frontend start automatically. The controller
|
||||
reads the complete `ctrl.yaml` (including `dynamic_proxy_controller`) and binds the gRPC service on
|
||||
the Ziti overlay. The frontend connects to it for real-time mapping updates.
|
||||
|
||||
## Verify the Stack
|
||||
|
||||
@@ -140,6 +157,48 @@ zrok2 enable <token>
|
||||
|
||||
Replace `<token>` with the enable token from the previous step.
|
||||
|
||||
## Verify Named Shares Work
|
||||
|
||||
After enabling your environment, verify that the AMQP-backed dynamic frontend serves named shares:
|
||||
|
||||
```bash
|
||||
# Create a named share (runs in foreground — use a separate terminal)
|
||||
zrok2 share public http://127.0.0.1:8080 --name-selection public:mytest
|
||||
|
||||
# From another terminal, verify the frontend routes it
|
||||
curl -sf http://mytest.share.example.com:8080/
|
||||
```
|
||||
|
||||
Named shares use the `dynamicProxyController` gRPC service to push mapping updates to the frontend
|
||||
in real time via AMQP. This is set up automatically by `zrok2-init` during the initial bootstrap
|
||||
(see [Start the Stack](#start-the-stack) for details).
|
||||
|
||||
## Making the Frontend Publicly Accessible
|
||||
|
||||
By default, the zrok2 controller and frontend ports bind to `127.0.0.1` (localhost only) for
|
||||
safety. To make the frontend reachable from the internet without TLS, set `ZROK2_INSECURE_INTERFACE`
|
||||
in your `.env`:
|
||||
|
||||
```bash
|
||||
ZROK2_INSECURE_INTERFACE=0.0.0.0
|
||||
```
|
||||
|
||||
This publishes ports 18080 (controller API) and 8080 (frontend) insecurely on all interfaces. For production, use the
|
||||
[Caddy TLS overlay](#optional-enable-tls-with-caddy) instead.
|
||||
|
||||
Alternatively, publish only the frontend port by overriding the port mapping in a
|
||||
`compose.override.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
zrok2-frontend:
|
||||
ports:
|
||||
- "0.0.0.0:8080:8080"
|
||||
```
|
||||
|
||||
The Ziti router data-plane port (3022) already binds to `0.0.0.0` because SDK clients must reach it
|
||||
from outside the Docker network and Ziti uses TLS for security.
|
||||
|
||||
## Optional: Enable TLS with Caddy
|
||||
|
||||
For production deployments, enable TLS using the Caddy overlay. Caddy
|
||||
@@ -185,6 +244,24 @@ the `bridge` and `metrics` sections to `ctrl.yaml` and restart the controller
|
||||
(see the [Linux guide](/docs/zrok/self-hosting/deployment/linux/) for the full
|
||||
configuration reference).
|
||||
|
||||
### Verify InfluxDB has data
|
||||
|
||||
After creating a share and sending some traffic through it, verify metrics arrived in InfluxDB:
|
||||
|
||||
```bash
|
||||
docker compose exec influxdb influx query \
|
||||
'from(bucket: "zrok2") |> range(start: -5m) |> count()' \
|
||||
--org zrok2 --token "${ZROK2_INFLUX_TOKEN}" --raw
|
||||
```
|
||||
|
||||
A successful result contains CSV rows with count values. If no data appears, check the metrics
|
||||
bridge logs and RabbitMQ:
|
||||
|
||||
```bash
|
||||
docker compose logs zrok2-metrics-bridge --tail=50
|
||||
docker compose exec rabbitmq rabbitmqctl list_queues
|
||||
```
|
||||
|
||||
## Optional: Building from Source
|
||||
|
||||
For development or CI, you can build the zrok2 image from source instead of
|
||||
|
||||
@@ -4,6 +4,12 @@ title: Self-hosting guide for Linux
|
||||
sidebar_label: Linux
|
||||
---
|
||||
|
||||
:::info Single-host deployment
|
||||
This guide deploys all zrok components — controller, frontend, and metrics bridge — on a single
|
||||
Linux server. This is the simplest production-ready configuration. To scale the frontend for higher
|
||||
throughput or availability, see [Scaling zrok frontends](../../scaling-frontends.md).
|
||||
:::
|
||||
|
||||
## Before you begin
|
||||
|
||||
This will get you up and running with a self-hosted instance of zrok. I'll assume you have the following:
|
||||
@@ -247,36 +253,43 @@ sudo systemctl enable --now zrok2-controller
|
||||
|
||||
### Step 6: Create a dynamic frontend
|
||||
|
||||
With `ZROK2_ADMIN_TOKEN` and `ZROK2_API_ENDPOINT` set, create a dynamic frontend. Use the Ziti ID of the `public` identity created by `zrok2 admin bootstrap` (shown in its output):
|
||||
With `ZROK2_ADMIN_TOKEN` and `ZROK2_API_ENDPOINT` set, create a dynamic frontend. Use the Ziti ID of the
|
||||
`public` identity created by `zrok2 admin bootstrap` (shown in its output):
|
||||
|
||||
```bash
|
||||
zrok2 admin create frontend --dynamic <public-ziti-id> public
|
||||
```
|
||||
|
||||
This outputs a frontend token (e.g., `zEjQqHliYXF6`). Save it — you'll need it for the frontend configuration and namespace mapping.
|
||||
This outputs a **frontend token** (e.g., `zEjQqHliYXF6`). Save it — you'll need it for the frontend
|
||||
configuration and namespace mapping.
|
||||
|
||||
The `--dynamic` flag enables the v2.0 dynamicProxy mode with namespace-based naming.
|
||||
### Step 7: Create the dynamicProxyController
|
||||
|
||||
### Step 7: Create dynamicProxyController identity and Ziti resources
|
||||
The dynamic proxy controller is a gRPC service that pushes real-time share mapping updates to the
|
||||
frontend over the Ziti overlay via AMQP. Without it, the frontend cannot route named shares (e.g.,
|
||||
`myapp.zrok.example.com`) — only random-token shares would work with polling.
|
||||
|
||||
Create a Ziti identity for the dynamicProxyController gRPC service:
|
||||
**Create the Ziti identity:**
|
||||
|
||||
```bash
|
||||
zrok2 admin create identity dynamicProxyController
|
||||
```
|
||||
|
||||
Then create the Ziti service and policies. Log in to Ziti first, then find the Ziti ID of the new identity:
|
||||
**Create the Ziti service and routing policies.** Log in to Ziti first:
|
||||
|
||||
```bash
|
||||
ziti edge login <your-ziti-controller>:<port> -y -u admin -p <password>
|
||||
```
|
||||
|
||||
Then find the Ziti ID of the new identity and create the service + policies:
|
||||
|
||||
```bash
|
||||
# Set the Ziti ID from the identity you just created
|
||||
CONTROLLER_ZID="<ziti-id-from-above>"
|
||||
# Look up the Ziti ID from the identity you just created
|
||||
CONTROLLER_ZID=$(ziti edge list identities 'name="dynamicProxyController"' -j \
|
||||
| jq -r '.data[0].id')
|
||||
SERVICE_NAME="dynamicProxyController"
|
||||
|
||||
# Create the service
|
||||
# Create the Ziti service
|
||||
ziti edge create service "$SERVICE_NAME"
|
||||
|
||||
# Allow edge routers to host the service
|
||||
@@ -295,30 +308,47 @@ ziti edge create sp "${SERVICE_NAME}-dial" Dial \
|
||||
--service-roles "@${SERVICE_NAME}"
|
||||
```
|
||||
|
||||
Copy the identity file to the `zrok2-controller` service user's directory:
|
||||
**Place the identity files** where the systemd service users can read them:
|
||||
|
||||
```bash
|
||||
# dynamicProxyController identity → zrok2-controller service user
|
||||
sudo mkdir -p /var/lib/zrok2-controller/.zrok2/identities
|
||||
sudo cp ~/.zrok2/identities/dynamicProxyController.json \
|
||||
/var/lib/zrok2-controller/.zrok2/identities/
|
||||
sudo chown -R zrok2-controller:zrok2-controller /var/lib/zrok2-controller/.zrok2
|
||||
```
|
||||
|
||||
Also copy the `public` frontend identity to the `zrok2-frontend` service user:
|
||||
|
||||
```bash
|
||||
# public frontend identity → zrok2-frontend service user
|
||||
sudo mkdir -p /var/lib/zrok2-frontend/.zrok2/identities
|
||||
sudo cp ~/.zrok2/identities/public.json \
|
||||
/var/lib/zrok2-frontend/.zrok2/identities/
|
||||
sudo chown -R zrok2-frontend:zrok2-frontend /var/lib/zrok2-frontend/.zrok2
|
||||
```
|
||||
|
||||
Restart the controller to activate the dynamicProxyController:
|
||||
**Add the `dynamic_proxy_controller` section** to `/etc/zrok2/ctrl.yml`:
|
||||
|
||||
```yaml
|
||||
dynamic_proxy_controller:
|
||||
identity_path: /var/lib/zrok2-controller/.zrok2/identities/dynamicProxyController.json
|
||||
service_name: dynamicProxyController
|
||||
amqp_publisher:
|
||||
url: amqp://guest:guest@127.0.0.1:5672
|
||||
exchange_name: dynamicProxy
|
||||
```
|
||||
|
||||
Restart the controller to activate it:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart zrok2-controller
|
||||
```
|
||||
|
||||
:::tip Why is this needed?
|
||||
When a user creates a named share (`zrok2 share public --name-selection public:myapp ...`), the
|
||||
controller publishes a mapping update to the `dynamicProxy` AMQP exchange. The frontend subscribes to
|
||||
this exchange and immediately starts routing `myapp.zrok.example.com` to the share's backend — no
|
||||
polling delay. The `dynamicProxyController` Ziti service is the gRPC channel over the Ziti overlay
|
||||
that delivers these mapping updates securely.
|
||||
:::
|
||||
|
||||
### Step 8: Create a namespace
|
||||
|
||||
Namespaces organize share names (similar to DNS zones). Create a public namespace:
|
||||
@@ -327,7 +357,8 @@ Namespaces organize share names (similar to DNS zones). Create a public namespac
|
||||
zrok2 admin create namespace --token public --open zrok.example.com
|
||||
```
|
||||
|
||||
The `--open` flag allows any account to create names in this namespace. Without it, users need explicit grants.
|
||||
The `--open` flag allows any account to create names in this namespace. Without it, users need
|
||||
explicit grants.
|
||||
|
||||
### Step 9: Map namespace to frontend
|
||||
|
||||
@@ -337,6 +368,8 @@ Link the namespace to the dynamic frontend so shares are served by this frontend
|
||||
zrok2 admin create namespace-frontend public <frontend-token> --default
|
||||
```
|
||||
|
||||
Replace `<frontend-token>` with the token from Step 6.
|
||||
|
||||
### Step 10: Configure the dynamic frontend
|
||||
|
||||
Create `/etc/zrok2/frontend.yml`:
|
||||
@@ -347,6 +380,7 @@ v: 1
|
||||
frontend_token: <frontend-token-from-step-6>
|
||||
identity: public
|
||||
bind_address: 0.0.0.0:443
|
||||
host_match: zrok.example.com
|
||||
mapping_refresh_interval: 1m
|
||||
|
||||
amqp_subscriber:
|
||||
@@ -357,13 +391,15 @@ controller:
|
||||
identity_path: /var/lib/zrok2-frontend/.zrok2/identities/public.json
|
||||
service_name: dynamicProxyController
|
||||
|
||||
host_match: zrok.example.com
|
||||
|
||||
tls:
|
||||
cert_path: /etc/letsencrypt/live/zrok.example.com/fullchain.pem
|
||||
key_path: /etc/letsencrypt/live/zrok.example.com/privkey.pem
|
||||
```
|
||||
|
||||
The `amqp_subscriber` and `controller` sections connect the frontend to the dynamicProxyController
|
||||
gRPC service (Step 7) for real-time mapping updates. The `host_match` value must match the namespace
|
||||
name (Step 8).
|
||||
|
||||
Set file ownership:
|
||||
|
||||
```bash
|
||||
@@ -371,7 +407,8 @@ sudo chown zrok2-frontend:zrok2-frontend /etc/zrok2/frontend.yml
|
||||
sudo chmod 640 /etc/zrok2/frontend.yml
|
||||
```
|
||||
|
||||
If the TLS certificate files are only readable by root (common with Let's Encrypt), grant read access to the service users:
|
||||
If the TLS certificate files are only readable by root (common with Let's Encrypt), grant read access
|
||||
to the service users:
|
||||
|
||||
```bash
|
||||
sudo groupadd --system zrok2-tls 2>/dev/null || true
|
||||
@@ -382,7 +419,8 @@ sudo chmod g+r /etc/letsencrypt/archive/zrok.example.com/*
|
||||
sudo chmod o+x /etc/letsencrypt /etc/letsencrypt/live /etc/letsencrypt/archive
|
||||
```
|
||||
|
||||
For a complete reference of all frontend options including OAuth, see the [Dynamic Proxy Frontend Guide](../../dynamic-proxy.md).
|
||||
For a complete reference of all frontend options including OAuth, see the
|
||||
[Dynamic Proxy Frontend Guide](../../dynamic-proxy.md).
|
||||
|
||||
### Step 11: Start the frontend
|
||||
|
||||
@@ -396,6 +434,22 @@ Verify it's running:
|
||||
sudo journalctl -u zrok2-frontend -f
|
||||
```
|
||||
|
||||
### Verify named shares work
|
||||
|
||||
After creating a user account and enabling an environment (see below), test that the dynamic frontend
|
||||
serves named shares:
|
||||
|
||||
```bash
|
||||
# Create a named share (runs in foreground — use a separate terminal)
|
||||
zrok2 share public http://127.0.0.1:8080 --name-selection public:mytest
|
||||
|
||||
# From another terminal, verify the frontend routes it
|
||||
curl -sf https://mytest.zrok.example.com/
|
||||
```
|
||||
|
||||
If the share is reachable at `mytest.zrok.example.com`, the AMQP-backed dynamic frontend is working
|
||||
correctly.
|
||||
|
||||
### Step 12: Configure OpenZiti metrics events
|
||||
|
||||
The zrok metrics pipeline starts at the OpenZiti controller, which emits `fabric.usage` events. Add the following to your OpenZiti controller configuration:
|
||||
@@ -459,7 +513,27 @@ Verify it's processing events:
|
||||
sudo journalctl -u zrok2-metrics-bridge -f
|
||||
```
|
||||
|
||||
Once traffic flows through shares, you should see log output from the controller confirming metrics are being written to InfluxDB. See [Configuring Limits](../../metrics-and-limits/configuring-limits.md) to enforce bandwidth and resource limits based on these metrics.
|
||||
Once traffic flows through shares, you should see log output from the controller confirming metrics are being written to InfluxDB.
|
||||
|
||||
### Verify InfluxDB has data
|
||||
|
||||
After creating a share and sending some traffic through it, verify metrics arrived in InfluxDB:
|
||||
|
||||
```bash
|
||||
influx query \
|
||||
'from(bucket: "zrok") |> range(start: -5m) |> count()' \
|
||||
--org zrok --token "<your-influx-token>" --raw
|
||||
```
|
||||
|
||||
A successful result contains CSV rows with count values. If no data appears after 90 seconds, check the metrics bridge and RabbitMQ:
|
||||
|
||||
```bash
|
||||
sudo systemctl status zrok2-metrics-bridge
|
||||
sudo rabbitmqctl list_queues
|
||||
sudo journalctl -u zrok2-metrics-bridge --no-pager -n 50
|
||||
```
|
||||
|
||||
See [Configuring Limits](../../metrics-and-limits/configuring-limits.md) to enforce bandwidth and resource limits based on these metrics.
|
||||
|
||||
## Create a user account
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: Scaling zrok frontends
|
||||
sidebar_label: Scaling frontends
|
||||
sidebar_position: 47
|
||||
---
|
||||
|
||||
The [Linux](/docs/zrok/self-hosting/deployment/linux/),
|
||||
[Docker](/docs/zrok/self-hosting/deployment/docker/), and
|
||||
[Kubernetes](/docs/zrok/self-hosting/deployment/kubernetes/) deployment guides
|
||||
describe a single-host model where one frontend process handles all public
|
||||
share traffic. This page explains how to run multiple frontend instances for
|
||||
higher throughput and availability.
|
||||
|
||||
## How the dynamic frontend works
|
||||
|
||||
Each `zrok2 access dynamicProxy` process:
|
||||
|
||||
1. Loads a Ziti identity from a JSON file and connects to the Ziti overlay
|
||||
2. Subscribes to an AMQP exchange (`dynamicProxy`) using an **ephemeral queue**
|
||||
bound to its frontend token as the routing key
|
||||
3. Queries the controller via gRPC (`dynamicProxyController` Ziti service) for
|
||||
the initial set of share mappings
|
||||
4. Listens on an HTTP/HTTPS address for incoming requests
|
||||
5. Routes requests by matching the `Host` header against its in-memory mapping
|
||||
table, proxying to the share's backend through the Ziti overlay
|
||||
|
||||
The AMQP queue is unique per process instance — when the controller publishes a
|
||||
mapping update for a frontend token, **every instance** subscribed to that token
|
||||
receives an independent copy. Instances do not compete for messages.
|
||||
|
||||
## Scaling approaches
|
||||
|
||||
### Option A: Multiple instances of one frontend (simplest)
|
||||
|
||||
Run multiple `zrok2 access dynamicProxy` processes that share the **same
|
||||
frontend token and Ziti identity**. Place a load balancer in front of them.
|
||||
|
||||
```text
|
||||
┌─ Frontend Instance A (same token, same identity)
|
||||
Load Balancer ──────┤
|
||||
└─ Frontend Instance B (same token, same identity)
|
||||
```
|
||||
|
||||
Each instance:
|
||||
|
||||
- Uses the same `frontend.yaml` (with a different `bind_address` if co-located)
|
||||
- Loads the same `public.json` Ziti identity file (read-only — no locking)
|
||||
- Receives identical AMQP mapping updates independently
|
||||
- Maintains its own in-memory mapping table
|
||||
|
||||
This is the simplest approach. No additional zrok admin commands are needed.
|
||||
The frontend token and identity file can be copied to additional hosts.
|
||||
|
||||
### Option B: Separate frontends per instance
|
||||
|
||||
Create distinct frontend records in the controller, each with its own token and
|
||||
optionally its own Ziti identity. Map each to the same namespace(s).
|
||||
|
||||
```bash
|
||||
# Create additional frontends (each gets a unique token)
|
||||
zrok2 admin create frontend --dynamic <public-ziti-id> frontend-2
|
||||
zrok2 admin create frontend --dynamic <public-ziti-id> frontend-3
|
||||
|
||||
# Map them to the same namespace
|
||||
zrok2 admin create namespace-frontend public <frontend-2-token>
|
||||
zrok2 admin create namespace-frontend public <frontend-3-token>
|
||||
```
|
||||
|
||||
Each frontend can share the same Ziti identity (`public.json`) or use separate
|
||||
identities. Separate identities provide stronger isolation — if one identity is
|
||||
compromised, the others are unaffected.
|
||||
|
||||
To create a separate identity for each frontend:
|
||||
|
||||
```bash
|
||||
# Create a new identity for the second frontend
|
||||
zrok2 admin create identity public-2
|
||||
|
||||
# Create the frontend using the new identity's Ziti ID
|
||||
zrok2 admin create frontend --dynamic <public-2-ziti-id> frontend-2
|
||||
```
|
||||
|
||||
Then configure each frontend's `frontend.yaml` with its own `frontend_token`,
|
||||
`identity`, and `controller.identity_path`.
|
||||
|
||||
### Which approach to choose
|
||||
|
||||
| Concern | Option A (shared) | Option B (separate) |
|
||||
| --------------------- | --------------------------------------- | -------------------------------------------- |
|
||||
| Setup complexity | Lowest — copy files | More admin commands |
|
||||
| Identity isolation | Shared | Independent |
|
||||
| Namespace flexibility | All instances serve the same namespaces | Each frontend can serve different namespaces |
|
||||
| AMQP routing | All instances share one routing key | Each has its own routing key |
|
||||
| Monitoring | Instances are indistinguishable | Each frontend has a unique token in logs |
|
||||
|
||||
For most deployments, **Option A** is sufficient. Use **Option B** when you need
|
||||
per-frontend namespace isolation, distinct monitoring identifiers, or defense in
|
||||
depth for the Ziti identity.
|
||||
|
||||
## Load balancer configuration
|
||||
|
||||
Place a Layer 4 (TCP) or Layer 7 (HTTP) load balancer in front of the frontend
|
||||
instances. The load balancer must:
|
||||
|
||||
- Forward the `Host` header unchanged (the frontend uses it for routing)
|
||||
- Support WebSocket upgrade (for `zrok2 share` connections)
|
||||
- Use sticky sessions if your frontends serve stateful backends (optional)
|
||||
|
||||
For TLS termination, either:
|
||||
|
||||
- Terminate TLS at the load balancer and forward plaintext to the frontends
|
||||
- Pass TLS through to the frontends (each must have the certificate)
|
||||
|
||||
### Example: Caddy
|
||||
|
||||
```text
|
||||
*.share.example.com {
|
||||
reverse_proxy frontend-a:8080 frontend-b:8080
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Docker Compose with Caddy
|
||||
|
||||
Plain Docker Compose does not load balance across replicas on a single port —
|
||||
you need a reverse proxy. Remove `ports:` from the frontend service, scale it,
|
||||
and let Caddy (or Nginx/Traefik) route to replicas via Docker DNS:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
zrok2-frontend:
|
||||
image: openziti/zrok2:latest
|
||||
command: ["access", "public", "/config/frontend.yaml"]
|
||||
deploy:
|
||||
replicas: 3
|
||||
volumes:
|
||||
- zrok2-config:/config:ro
|
||||
# No ports: — Caddy handles ingress
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
ports:
|
||||
- "0.0.0.0:443:443"
|
||||
command: caddy reverse-proxy --from :443 --to zrok2-frontend:8080
|
||||
```
|
||||
|
||||
Docker DNS resolves `zrok2-frontend` to all replica IPs, and Caddy
|
||||
round-robins across them.
|
||||
|
||||
### Example: Kubernetes
|
||||
|
||||
The [Kubernetes guide](/docs/zrok/self-hosting/deployment/kubernetes/) supports
|
||||
scaling via the `frontend.replicaCount` value in the Helm chart.
|
||||
|
||||
## Other components
|
||||
|
||||
- **zrok2-controller** — multiple controller instances can share the same
|
||||
PostgreSQL database. Each publishes AMQP mapping updates independently.
|
||||
Place a load balancer in front for the API endpoint.
|
||||
- **zrok2-metrics-bridge** — can read `fabric.usage` events from a file
|
||||
(single Ziti controller) or from an AMQP queue (multiple Ziti controllers).
|
||||
The AMQP source mode supports scaling across a multi-controller Ziti
|
||||
deployment.
|
||||
Reference in New Issue
Block a user