Files
vpengops 17a3e8e9c0 fix(api-gateway): rewrite APISIX package and deploy to production
The 09-01 package referenced a nonexistent Cloudron etcd addon and an
unpinned base image. Rewritten: digest-pinned apisix 3.18.0 with an
embedded single-node etcd, loopback-only Admin API, and read-only
rootfs support. Deployed live at apigw.knownelement.com (healthz 200).

Umbrella: https://projects.knownelement.com/issues/632
2026-09-07 10:02:09 -05:00

149 lines
5.8 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# APISIX runtime setup for Cloudron:
# 1. persist the Admin API key (rotating it would break automation clients)
# 2. relocate the APISIX runtime tree to /app/data - Cloudron mounts the
# container rootfs READ-ONLY (writable: /app/data, /tmp, /run), so the
# generated config.yaml, nginx.conf, logs and sockets must live there
# 3. start the embedded single-node etcd (state under /app/data/etcd,
# survives restarts and backups)
# 4. regenerate config.yaml on EVERY start, then apisix init + init_etcd
# 5. seed the /healthz route once (cosmetic - Cloudron accepts 4xx)
# 6. exec openresty in the foreground with the /app/data prefix
#
# The Admin API listens on 127.0.0.1:9180 ONLY and is never published;
# reach it via `cloudron exec` with the key in /app/data/.admin_key.
DATA_DIR="/app/data"
ETCD_DIR="${DATA_DIR}/etcd"
ADMIN_KEY_FILE="${DATA_DIR}/.admin_key"
ETCD_ENDPOINT="http://127.0.0.1:2379"
PROXY_PORT="${CLOUDRON_APP_PORT:-9080}"
# writable runtime tree (container rootfs is read-only on Cloudron).
# The apisix CLI derives its install dir from the script path and writes
# nginx.conf there, so the lua CLI tree + deps are copied to /app/data and
# invoked from the copy (~62 MB, once per start).
export APISIX_PREFIX="${DATA_DIR}/apisix"
CONF="${APISIX_PREFIX}/conf/config.yaml"
mkdir -p "${APISIX_PREFIX}/conf" "${APISIX_PREFIX}/logs"
# re-copy on every start so upgrades never run a stale tree
rm -rf "${APISIX_PREFIX}/apisix" "${APISIX_PREFIX}/deps"
cp -a /usr/local/apisix/apisix "${APISIX_PREFIX}/"
cp -a /usr/local/apisix/deps "${APISIX_PREFIX}/"
# the CLI hardcodes apisix_home = /usr/local/apisix (read-only); repoint the
# copy at the writable prefix so nginx.conf and deps resolve under /app/data
grep -rl '/usr/local/apisix' "${APISIX_PREFIX}/apisix" \
| xargs sed -i "s|/usr/local/apisix|${APISIX_PREFIX}|g"
# the CLI looks for its config templates under $APISIX_PREFIX/conf
cp -a /usr/local/apisix/conf/. "${APISIX_PREFIX}/conf/"
APISIX_CLI="/usr/local/openresty/luajit/bin/luajit ${APISIX_PREFIX}/apisix/cli/apisix.lua"
# --- 1. persistent Admin API key ---------------------------------------------
if [[ ! -s "${ADMIN_KEY_FILE}" ]]; then
( umask 077; head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > "${ADMIN_KEY_FILE}" )
echo "Generated new Admin API key at ${ADMIN_KEY_FILE}"
fi
ADMIN_KEY="$(cat "${ADMIN_KEY_FILE}")"
# --- 2. embedded etcd ----------------------------------------------------------
mkdir -p "${ETCD_DIR}"
# --initial-cluster is ignored on restarts from an existing data-dir.
etcd \
--name default \
--data-dir "${ETCD_DIR}" \
--listen-client-urls "http://127.0.0.1:2379" \
--advertise-client-urls "http://127.0.0.1:2379" \
--listen-peer-urls "http://127.0.0.1:2380" \
--initial-advertise-peer-urls "http://127.0.0.1:2380" \
--initial-cluster "default=http://127.0.0.1:2380" \
--auto-compaction-retention=1h &
echo "Waiting for embedded etcd at ${ETCD_ENDPOINT} ..."
for i in $(seq 1 30); do
if etcdctl --endpoints="${ETCD_ENDPOINT}" endpoint health >/dev/null 2>&1; then
echo "etcd is up"
break
fi
if [[ "$i" -eq 30 ]]; then
echo "etcd failed to become healthy" >&2
exit 1
fi
sleep 1
done
# --- 3. generated config (rewritten on every start) -----------------------------
cat > "${CONF}" <<EOF
# Generated by start.sh on every boot - manual edits will be lost.
# Data-plane routes live in the embedded etcd (persisted under
# /app/data/etcd) and are managed through the Admin API.
deployment:
role: traditional
role_traditional:
config_provider: etcd
etcd:
host:
- "${ETCD_ENDPOINT}"
prefix: /apisix
timeout: 30
admin:
admin_listen:
ip: 127.0.0.1
port: 9180
admin_key:
- name: admin
key: ${ADMIN_KEY}
role: admin
apisix:
node_listen:
- ${PROXY_PORT}
enable_ipv6: false
ssl:
# TLS terminates at the Cloudron platform proxy
enable: false
nginx_config:
error_log_level: warn
worker_processes: auto
http:
# restore real client IPs from the Cloudron proxy's X-Forwarded-For
real_ip_header: X-Forwarded-For
real_ip_from:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8
EOF
# --- 4. init + schema bootstrap -------------------------------------------------
${APISIX_CLI} init
${APISIX_CLI} init_etcd
# apisix init rewrites config.yaml (deployment timestamp). nginx runs workers
# as nobody (no `user` directive in the generated nginx.conf), so the config
# must stay world-readable within the container - acceptable because the
# Admin API itself listens on 127.0.0.1 only.
chmod 644 "${CONF}"
echo "Generated APISIX config at ${CONF} (proxy port ${PROXY_PORT})"
# --- 5. seed the health route once ----------------------------------------------
# Cloudron's healthcheck accepts 4xx, so it is not required for the platform -
# it gives https://<app-domain>/healthz a friendly answer instead of a bare 404.
if [[ -z "$(etcdctl --endpoints="${ETCD_ENDPOINT}" get /apisix/routes/healthz --print-value-only 2>/dev/null)" ]]; then
# ngx.say is unavailable in the rewrite phase - a bare ngx.exit(200) is the
# error-proof direct response (200, empty body).
etcdctl --endpoints="${ETCD_ENDPOINT}" put /apisix/routes/healthz \
'{"uri":"/healthz","id":"healthz","priority":100,"plugins":{"serverless-pre-function":{"phase":"rewrite","functions":["return function() ngx.exit(200) end"]}}}' > /dev/null \
|| echo "WARNING: seeding /healthz failed (cosmetic only, continuing)" >&2
echo "Seeded /healthz route"
fi
# --- 6. run (foreground) ----------------------------------------------------------
echo "Starting APISIX ..."
rm -f "${APISIX_PREFIX}/conf/config_listen.sock" "${APISIX_PREFIX}/logs/worker_events.sock"
exec /usr/local/openresty/bin/openresty -p "${APISIX_PREFIX}" -g 'daemon off;'