Skip to content

Commit 293a808

Browse files
committed
Add basic CDN to serve up firmware images
1 parent 6d15e8a commit 293a808

10 files changed

Lines changed: 363 additions & 0 deletions

File tree

apps/appsets/project-understack.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ spec:
1515
server: '*'
1616
- namespace: 'argo-events'
1717
server: '*'
18+
- namespace: 'cdn'
19+
server: '*'
1820
- namespace: 'cert-manager'
1921
server: '*'
2022
- namespace: 'dex'

apps/site/cdn.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
component: cdn
3+
sources:
4+
- ref: understack
5+
path: 'components/cdn'
6+
- ref: deploy
7+
path: '{{.name}}/manifests/cdn'

components/cdn/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Poor-man's CDN for serving firmware images
2+
3+
Images are stored in Object Store
4+
5+
Cacheing reverse-proxies at each fabric will fetch the images from Object Store
6+
and make them available via HTTPS.
7+
8+
This allows a device to access firmware images via an HTTPS request to a
9+
cluster-local tendot IP address.
10+
11+
## Proxy configuration
12+
13+
The proxy edge service caches files locally on a persistent volume.
14+
15+
Nginx configuration contains:
16+
- the service address for rook-ceph
17+
- the name of our bucket
18+
19+
All files are proxied to that object bucket. Anonymous credentials are used,
20+
therefore we need to make the files in our bucket readable by anonymous if they
21+
are to be accessible via HTTP.
22+
23+
## Uploading file to object storage
24+
25+
Our credentials and bucket info is in a secret and a configmap both named after
26+
the bucketclaim:
27+
28+
``` sh
29+
KEY_ID=`kubectl -n cdn get secrets firmware-images -o jsonpath='{.data.AWS_ACCESS_KEY_ID}' | base64 -d`
30+
KEY=`kubectl -n cdn get secrets firmware-images -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d`
31+
```
32+
33+
I was able to manage the bucket using the minio CLI client called "mc".
34+
35+
I was testing this without direct access to the object store because there was
36+
no ingress for it at the time of writing. Therefore I configured a port forward
37+
so I could upload files from my laptop. I also had to mess with DNS resolution
38+
because RGW is looking at the "host" header:
39+
40+
``` sh
41+
kubectl -n rook-ceph port-forward svc/rook-ceph-rgw-ceph-objectstore 8081:80 &
42+
echo "127.0.0.1 rook-ceph-rgw-ceph-objectstore.rook-ceph.svc" | sudo tee -a /etc/hosts
43+
mc alias set myrook http://rook-ceph-rgw-ceph-objectstore.rook-ceph.svc:8081 $KEY_ID $KEY
44+
mc anonymous set download myrook/firmware-images
45+
mc cp DELL/R7615/BIOS_H3TGJ_WN64_1.15.3.EXE myrook/firmware-images/DELL/R7615/
46+
mc anonymous set download myrook/firmware-images/DELL/R7615/BIOS_H3TGJ_WN64_1.15.3.EXE
47+
```
48+
49+
## Testing with curl
50+
51+
curl https://cdn.dev.undercloud.rackspace.net/DELL/R7615/BIOS_H3TGJ_WN64_1.15.3.EXE | sha256sum
52+
53+
## See nginx logs to check that it is Cacheing
54+
55+
``` sh
56+
⇒ kubectl -n cdn logs deployments/cdn-edge
57+
Defaulted container "nginx" out of: nginx, cache-dir-init (init)
58+
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
59+
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
60+
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
61+
10-listen-on-ipv6-by-default.sh: info: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)
62+
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
63+
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
64+
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
65+
/docker-entrypoint.sh: Configuration complete; ready for start up
66+
10.64.49.118 - - [26/Feb/2026:12:36:47 +0000] "GET /DELL/R7615/BIOS_H3TGJ_WN64_1.15.3.EXE HTTP/1.1" 200 2523429 "-" "curl/8.14.1" "10.64.50.136" cache=EXPIRED
67+
10.64.49.118 - - [26/Feb/2026:12:36:56 +0000] "GET /DELL/R7615/BIOS_H3TGJ_WN64_1.15.3.EXE HTTP/1.1" 200 32591328 "-" "curl/8.14.1" "10.64.50.136" cache=HIT
68+
10.64.49.118 - - [26/Feb/2026:12:45:18 +0000] "GET /DELL/R7615/BIOS_H3TGJ_WN64_1.15.3.EXE HTTP/1.1" 200 32591328 "-" "curl/8.14.1" "10.64.50.136" cache=HIT
69+
```

components/cdn/deployment.yaml

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: cdn-edge
5+
namespace: cdn
6+
labels:
7+
app: cdn-edge
8+
spec:
9+
replicas: 1
10+
selector:
11+
matchLabels:
12+
app: cdn-edge
13+
strategy:
14+
type: RollingUpdate
15+
rollingUpdate:
16+
maxSurge: 1
17+
maxUnavailable: 0
18+
template:
19+
metadata:
20+
labels:
21+
app: cdn-edge
22+
spec:
23+
# Init container to set correct permissions on cache dir
24+
initContainers:
25+
- name: cache-dir-init
26+
image: busybox:1.36
27+
command: ["sh", "-c", "mkdir -p /var/cache/nginx/cdn && chown -R 101:101 /var/cache/nginx"]
28+
volumeMounts:
29+
- name: nginx-cache
30+
mountPath: /var/cache/nginx
31+
32+
containers:
33+
- name: nginx
34+
image: nginx:1.27-alpine
35+
ports:
36+
- containerPort: 8080
37+
name: http
38+
39+
resources:
40+
requests:
41+
cpu: "250m"
42+
memory: "256Mi"
43+
limits:
44+
cpu: "2"
45+
memory: "1Gi"
46+
47+
volumeMounts:
48+
- name: nginx-config
49+
mountPath: /etc/nginx/nginx.conf
50+
subPath: nginx.conf
51+
- name: nginx-config
52+
mountPath: /etc/nginx/conf.d/default.conf
53+
subPath: default.conf
54+
- name: nginx-cache
55+
mountPath: /var/cache/nginx
56+
57+
livenessProbe:
58+
httpGet:
59+
path: /healthz
60+
port: 8080
61+
initialDelaySeconds: 5
62+
periodSeconds: 10
63+
64+
readinessProbe:
65+
httpGet:
66+
path: /healthz
67+
port: 8080
68+
initialDelaySeconds: 3
69+
periodSeconds: 5
70+
71+
# Graceful shutdown — let in-flight transfers complete
72+
lifecycle:
73+
preStop:
74+
exec:
75+
command: ["/bin/sh", "-c", "sleep 5 && nginx -s quit"]
76+
77+
securityContext:
78+
runAsNonRoot: true
79+
runAsUser: 101 # nginx user in nginx:alpine
80+
allowPrivilegeEscalation: false
81+
readOnlyRootFilesystem: true
82+
83+
volumes:
84+
- name: nginx-config
85+
configMap:
86+
name: nginx-config
87+
- name: nginx-cache
88+
persistentVolumeClaim:
89+
claimName: nginx-cache
90+
91+
terminationGracePeriodSeconds: 30

components/cdn/kustomization.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
resources:
4+
- namespace.yaml
5+
- deployment.yaml
6+
- service.yaml
7+
- nginx-config.yaml
8+
- pvc.yaml
9+
- object-bucket-claim.yaml

components/cdn/namespace.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
apiVersion: v1
2+
kind: Namespace
3+
metadata:
4+
name: cdn

components/cdn/nginx-config.yaml

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: nginx-config
5+
namespace: cdn
6+
data:
7+
nginx.conf: |
8+
worker_processes auto;
9+
error_log /var/log/nginx/error.log warn;
10+
pid /var/cache/nginx/nginx.pid;
11+
12+
# Tune for large file serving
13+
worker_rlimit_nofile 65535;
14+
15+
events {
16+
worker_connections 4096;
17+
use epoll;
18+
multi_accept on;
19+
}
20+
21+
http {
22+
include /etc/nginx/mime.types;
23+
default_type application/octet-stream;
24+
25+
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
26+
'$status $body_bytes_sent "$http_referer" '
27+
'"$http_user_agent" "$http_x_forwarded_for" '
28+
'cache=$upstream_cache_status';
29+
30+
access_log /var/log/nginx/access.log main;
31+
32+
# Large file optimisations
33+
sendfile on;
34+
tcp_nopush on;
35+
tcp_nodelay on;
36+
keepalive_timeout 65;
37+
38+
# Proxy cache zone configuration:
39+
# keys_zone=cdn_cache:50m — 50MB for cache keys/metadata (~400k keys)
40+
# max_size=50g — on-disk cache (adjust to your PVC size)
41+
# inactive=30d — evict if not accessed in this time
42+
# use_temp_path=off — write directly to cache dir (avoids extra copy)
43+
proxy_cache_path /var/cache/nginx/cdn
44+
levels=1:2
45+
keys_zone=cdn_cache:50m
46+
max_size=5g
47+
inactive=7d
48+
use_temp_path=off;
49+
50+
# Don't buffer large files to disk before sending — stream them
51+
proxy_buffering on;
52+
proxy_request_buffering off;
53+
54+
# Increase timeouts for large file transfers
55+
proxy_connect_timeout 10s;
56+
proxy_send_timeout 300s;
57+
proxy_read_timeout 300s;
58+
send_timeout 300s;
59+
60+
# Hide upstream headers we don't want to leak
61+
proxy_hide_header x-amz-request-id;
62+
proxy_hide_header x-amz-id-2;
63+
64+
include /etc/nginx/conf.d/*.conf;
65+
}
66+
default.conf: |
67+
# Upstream: your S3 S3 origin
68+
# In production this points at your S3 service endpoint.
69+
# Can also point at R2/S3 by changing the server and Host header.
70+
upstream s3_origin {
71+
server rook-ceph-rgw-ceph-objectstore.rook-ceph.svc:80;
72+
keepalive 32;
73+
}
74+
75+
server {
76+
listen 8080;
77+
server_name _;
78+
79+
# TLS — cert mounted from a k8s secret via ingress or directly
80+
#ssl_certificate /etc/nginx/tls/tls.crt;
81+
#ssl_certificate_key /etc/nginx/tls/tls.key;
82+
#ssl_protocols TLSv1.2 TLSv1.3;
83+
#ssl_ciphers HIGH:!aNULL:!MD5;
84+
85+
proxy_cache cdn_cache;
86+
proxy_cache_valid 200 206 7d; # Cache 200 and partial content
87+
proxy_cache_valid 404 1m; # Don't cache 404s for long
88+
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
89+
proxy_cache_lock on; # Collapse simultaneous requests for the same file
90+
proxy_cache_lock_timeout 10s;
91+
92+
proxy_cache_key "$scheme$proxy_host$uri";
93+
94+
add_header X-Cache-Status $upstream_cache_status always;
95+
add_header X-Served-By $hostname always;
96+
97+
add_header X-Content-Type-Options nosniff always;
98+
add_header X-Frame-Options DENY always;
99+
100+
proxy_cache_revalidate on;
101+
proxy_cache_bypass 0;
102+
proxy_no_cache 0;
103+
proxy_ignore_headers Cache-Control Expires Set-Cookie;
104+
105+
location / {
106+
# Forward to Object Storage.
107+
# S3-compatible API expects requests in the form: /bucket/key
108+
proxy_pass http://s3_origin/firmware-images$request_uri;
109+
110+
proxy_http_version 1.1;
111+
proxy_set_header Connection ""; # keepalive to upstream
112+
proxy_set_header Host rook-ceph-rgw-ceph-objectstore.rook-ceph.svc;
113+
proxy_set_header X-Real-IP $remote_addr;
114+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
115+
116+
# Don't forward auth headers downstream
117+
proxy_set_header Authorization "";
118+
119+
# Tell clients files are immutable — they should cache forever
120+
add_header Cache-Control "public, max-age=31536000, immutable" always;
121+
122+
# Support resumable downloads
123+
proxy_force_ranges on;
124+
125+
# Stream large files rather than buffering defeats the cache, so keep buffering on:
126+
proxy_buffering on;
127+
}
128+
129+
# Health check endpoint (used by k8s liveness/readiness probes)
130+
location /healthz {
131+
access_log off;
132+
return 200 "ok\n";
133+
add_header Content-Type text/plain;
134+
}
135+
136+
# Expose basic cache stats (restrict to internal)
137+
location /nginx_status {
138+
stub_status;
139+
allow 10.0.0.0/8;
140+
allow 172.16.0.0/12;
141+
allow 192.168.0.0/16;
142+
deny all;
143+
}
144+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: objectbucket.io/v1alpha1
2+
kind: ObjectBucketClaim
3+
metadata:
4+
name: firmware-images
5+
namespace: cdn
6+
spec:
7+
bucketName: firmware-images
8+
storageClassName: ceph-bucket
9+
additionalConfig:
10+
maxObjects: "1000"
11+
maxSize: "5G"

components/cdn/pvc.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Persistent volume for the Nginx cache.
2+
apiVersion: v1
3+
kind: PersistentVolumeClaim
4+
metadata:
5+
name: nginx-cache
6+
namespace: cdn
7+
spec:
8+
accessModes:
9+
- ReadWriteOnce
10+
storageClassName: openebs-lvm
11+
resources:
12+
requests:
13+
storage: 5Gi

components/cdn/service.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: cdn-edge
5+
namespace: cdn
6+
spec:
7+
selector:
8+
app: cdn-edge
9+
ports:
10+
- name: http
11+
port: 80
12+
targetPort: 8080
13+
type: ClusterIP

0 commit comments

Comments
 (0)