feat(web-svc-cdn): serve the npm mirror from a compose one-shot build service

Replace the ansible container-run plus copy publish with a proper compose
build service deployed via the role's own stack. The one-shot writes the
npm mirror straight into the node-local served CDN root that openresty
reads, so there is no DiND bind-layer mismatch and no separate publish step.

- meta/volumes.yml: build mounts (served root rw, staged manifests ro,
  build script as a docker config)
- templates/compose.yml.j2 + env.j2: one-shot build service (restart no,
  swarm condition none), CDN_WEB_ROOT and ROLES_DIR from the mount targets
- meta/services.yml: placement manager, replicas 1, resources, deploy modes
- meta/networks.yml: local subnet (the role now ships a stack)
- meta/variants.yml: internal and external flavor variants
- meta/tests.yml + test.env.j2 + files/test/test.sh: flavor-branching e2e
- tasks: internal flavor deploys the stack via sys-stk-full; drop the
  container-run and ansible-copy publish; remove the unused dev-git include
- vars: pin CDN_FLAVOR to the role id (scope-safe) and compute host paths
  directly (avoid the volume-lookup double-template)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.8 committed 2026-07-24 15:22:35 +02:00
1 parent e79183f96a
commit 466f04503b
14 files changed
+152 -57

No files matched your search

+18 -17
View File
@@ -20,9 +20,10 @@ flowchart LR
dep_web_app_matomo["web-app-matomo 🐳🐝"]
dep_web_app_prometheus["web-app-prometheus 🐳🐝"]
end
subgraph role [web-svc-cdn 💻]
subgraph role [web-svc-cdn 🐳🐝]
svc_dashboard["dashboard"]
svc_cdn["cdn"]
svc_build["build"]
svc_matomo["matomo"]
svc_javascript["javascript"]
svc_prometheus["prometheus"]
@@ -56,27 +57,27 @@ make compose-deploy mode=reinstall apps=web-svc-cdn full_cycle=false
### Production
Install Content Delivery Network directly onto the target machine — clone the repository, install the OS prerequisites and the repository toolchain, then deploy against localhost over a local connection (no SSH, no container):
Run the published image to provision the inventory and deploy Content Delivery Network to a managed server (the mounted volume persists the inventory):
```bash
git clone https://github.com/infinito-nexus/core.git
cd core
bash scripts/install/package.sh
make install
source scripts/meta/env/load.sh
APP=web-svc-cdn
HOST=<your-server>
TLS_MODE=self_signed
SSH_PUBLIC_KEY="<your-ssh-public-key>"
INVENTORY=inventories/production
infinito administration inventory provision "$INVENTORY" \
--inventory-file "$INVENTORY/devices.yml" \
--host localhost \
--include "$APP" \
--vars "{\"TLS_MODE\": \"$TLS_MODE\", \"users\": {\"administrator\": {\"authorized_keys\": [\"$SSH_PUBLIC_KEY\"]}}}"
infinito administration deploy dedicated "$INVENTORY/devices.yml" \
--password-file "$INVENTORY/.password" \
--diff -vv
docker run --rm -it \
-v "$PWD/inventories:/etc/infinito.nexus/inventories" \
-e APP="$APP" -e HOST="$HOST" -e TLS_MODE="$TLS_MODE" -e SSH_PUBLIC_KEY="$SSH_PUBLIC_KEY" \
ghcr.io/infinito-nexus/core/debian bash -c '
INVENTORY=/etc/infinito.nexus/inventories/production
infinito administration inventory provision "$INVENTORY" \
--inventory-file "$INVENTORY/devices.yml" \
--host "$HOST" \
--include "$APP" \
--vars "{\"TLS_MODE\": \"$TLS_MODE\", \"users\": {\"administrator\": {\"authorized_keys\": [\"$SSH_PUBLIC_KEY\"]}}}" &&
infinito administration deploy dedicated "$INVENTORY/devices.yml" \
--password-file "$INVENTORY/.password" \
--diff -vv'
```
## Further Resources
+2
View File
@@ -11,6 +11,8 @@ set -eu
: "${CDN_WEB_ROOT:?}"
: "${ROLES_DIR:?}"
mkdir -p "${CDN_WEB_ROOT}/npm"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# E2E orchestrator for web-svc-cdn. Branches on the deployed CDN flavor:
# internal - the one-shot npm-mirror build ran, so the served CDN root holds
# the npm/ mirror tree (empty when no frontend-dep role is co-deployed).
# external - front-proxy only, no build stack, so the served root has no npm/.
# Variables sourced from test.env.j2 by test-e2e-cli.
set -euo pipefail
: "${CDN_TEST_FLAVOR:?}"
: "${CDN_TEST_IS_STACK_HOST:?}"
: "${CDN_TEST_WEB_ROOT:?}"
if [[ "${CDN_TEST_IS_STACK_HOST}" != "true" ]]; then
echo "SKIP: not the stack host; web-svc-cdn only builds there"
exit 0
fi
npm_dir="${CDN_TEST_WEB_ROOT%/}/npm"
case "${CDN_TEST_FLAVOR}" in
internal)
if [[ ! -d "${npm_dir}" ]]; then
echo "FAIL: internal flavor but the one-shot build left no npm mirror at ${npm_dir}"
exit 1
fi
echo "OK: internal flavor built the npm mirror tree at ${npm_dir}"
;;
*)
if [[ -d "${npm_dir}" ]]; then
echo "FAIL: external flavor but an npm mirror exists at ${npm_dir}; the build stack must not deploy"
exit 1
fi
echo "OK: external flavor served without a build (no npm mirror at ${npm_dir})"
;;
esac
echo "ALL CHECKS PASSED"
+3
View File
@@ -0,0 +1,3 @@
---
local:
subnet: 192.168.220.0/24
+16
View File
@@ -5,9 +5,15 @@ dashboard:
enabled: false
# nocheck: playwright-service-flag — self-provider
cdn:
# nocheck: default-placement-manager the one-shot npm-mirror build must write the served CDN root, a node-local host bind openresty (manager-pinned) reads
placement: manager
modes:
host:
enabled: true
compose:
enabled: true
swarm:
enabled: true
bond: 1
enabled: true
shared: true
@@ -18,6 +24,16 @@ cdn:
pids_limit: 128
min_storage: "1GB"
lifecycle: beta
# nocheck: playwright-service-flag — transient node image used only to build the npm asset mirror, never served
build:
# nocheck: single-replica one-shot npm-mirror build writing the shared served root; parallel replicas would race and duplicate work
replicas: 1
image: node
version: "26-bookworm"
cpus: "1.0"
mem_reservation: 128m
mem_limit: 256m
pids_limit: 256
# nocheck: playwright-service-flag — CDN serves static assets, no HTML pages with tracking snippet
matomo:
bond: 1
+3
View File
@@ -0,0 +1,3 @@
---
cli:
timeout: 1800
+7 -3
View File
@@ -2,10 +2,12 @@
# Matrix-deploy variant overrides for web-svc-cdn.
# See docs/contributing/artefact/files/role/variants.md for the file format.
# variant pinning every dynamic services.yml flag to true so the
# matrix exercises the in-group_names branch of web-svc-cdn.
# variant 0 exercises the internal build-mirror flavor with every dynamic
# services.yml flag on; variant 1 the external front-proxy-only flavor with
# them off.
- services:
cdn:
flavor: internal
matomo:
enabled: true
shared: true
@@ -13,6 +15,8 @@
enabled: true
shared: true
- services:
cdn:
flavor: external
matomo:
enabled: false
shared: false
+23
View File
@@ -0,0 +1,23 @@
---
cdn_out:
type: bind
source: "{{ lookup('nginx', 'directories.data.cdn') }}"
mounts:
- service: build
target: /out
cdn_pkgsrc:
type: bind
source: "{{ [ lookup('container', 'web-svc-cdn', 'directories.volumes'), 'pkgsrc' ] | path_join }}"
read_only: true
mounts:
- service: build
target: /pkgsrc
cdn_build_script:
type: config
source: "{{ [ lookup('container', 'web-svc-cdn', 'directories.volumes'), 'build_assets.sh' ] | path_join }}"
mode: "0555"
mounts:
- service: build
target: /build_assets.sh
+14 -11
View File
@@ -1,15 +1,18 @@
---
- include_tasks: utils/once/flag.yml
- name: 📂 Include dependencies
include_role:
name: "{{ item }}"
loop:
- sys-stk-front-proxy
- dev-git
- name: "🧩 Internal CDN flavor: stage frontend deps + deploy the build stack"
when: CDN_FLAVOR == 'internal'
block:
- name: "📦 Stage every deployed role's frontend dependencies for the CDN"
include_tasks: 01_assets.yml
when: IS_STACK_HOST | bool
- name: "📦 Mirror every deployed role's frontend dependencies into the CDN"
include_tasks: 01_assets.yml
when:
- IS_STACK_HOST | bool
- CDN_FLAVOR == 'internal'
- name: "🧩 Deploy the CDN stack (one-shot npm-mirror build) + front proxy"
include_role:
name: sys-stk-full
- name: "🧩 Front proxy only (external CDN flavor)"
include_role:
name: sys-stk-front-proxy
when: CDN_FLAVOR != 'internal'
+5 -22
View File
@@ -19,25 +19,8 @@
manifest_role: "{{ manifest.path | dirname | dirname | basename }}"
when: manifest_role in groups
- name: "🛠️ npm ci each manifest and mirror it under {{ CDN_WEB_ROOT }}/npm"
ansible.builtin.command:
argv:
- container
- run
- --rm
- --network=host
- -e
- "CDN_WEB_ROOT=/cdn"
- -e
- "ROLES_DIR=/pkgsrc"
- -v
- "{{ CDN_WEB_ROOT }}:/cdn"
- -v
- "{{ CDN_ASSET_STAGE }}:/pkgsrc:ro"
- -v
- "{{ [ role_path, 'files', 'build_assets.sh' ] | path_join }}:/build_assets.sh:ro"
- "{{ CDN_ASSET_BUILD_IMAGE }}"
- sh
- /build_assets.sh
changed_when: true
timeout: "{{ lookup('timeout', 600) | int }}"
- name: "📁 Stage the mirror build script on the CDN host"
ansible.builtin.copy:
src: build_assets.sh
dest: "{{ CDN_BUILD_SCRIPT_HOST }}"
mode: "0755"
@@ -0,0 +1,13 @@
{% include 'roles/sys-svc-compose/templates/base.yml.j2' %}
{% set service_name = 'build' %}
{% set docker_restart_policy = 'no' %}
{{ service_name }}:
{{ lookup('container_image', application_id, 'build') }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
command: ["sh", "{{ lookup('volume', application_id, 'cdn_build_script').mounts[0].target }}"]
{{ lookup('container_networks') | indent(4) }}
{{ lookup('container_volumes', application_id, 'build') | indent(4) }}
{{ lookup('compose_volumes', application_id) }}
{{ lookup('compose_networks') }}
+4
View File
@@ -0,0 +1,4 @@
{% include 'roles/sys-svc-container/templates/env.j2' %}
CDN_WEB_ROOT={{ lookup('volume', application_id, 'cdn_out').mounts[0].target }}
ROLES_DIR={{ lookup('volume', application_id, 'cdn_pkgsrc').mounts[0].target }}
+4
View File
@@ -0,0 +1,4 @@
CDN_TEST_DOMAIN={{ domain }}
CDN_TEST_FLAVOR={{ CDN_FLAVOR }}
CDN_TEST_IS_STACK_HOST={{ IS_STACK_HOST | bool | lower }}
CDN_TEST_WEB_ROOT={{ lookup('nginx', 'directories.data.cdn') }}
+3 -4
View File
@@ -3,7 +3,6 @@
application_id: "web-svc-cdn"
domain: "{{ lookup('domain',application_id) }}"
CDN_FLAVOR: "{{ lookup('config', application_id, 'services.cdn.flavor') }}"
CDN_WEB_ROOT: "{{ lookup('nginx', 'directories.data.cdn') }}"
CDN_ASSET_STAGE: "{{ [ lookup('container', application_id, 'directories.volumes'), 'pkgsrc' ] | path_join }}"
CDN_ASSET_BUILD_IMAGE: "node:26-bookworm"
CDN_FLAVOR: "{{ lookup('config', 'web-svc-cdn', 'services.cdn.flavor') }}"
CDN_ASSET_STAGE: "{{ [ lookup('container', 'web-svc-cdn', 'directories.volumes'), 'pkgsrc' ] | path_join }}"
CDN_BUILD_SCRIPT_HOST: "{{ [ lookup('container', 'web-svc-cdn', 'directories.volumes'), 'build_assets.sh' ] | path_join }}"