Adding a Service¶
Adding a new service to AKKO involves touching several files to ensure the service is routed, secured, monitored, and visible in the cockpit portal. This page provides the full checklist and a concrete example.
Checklist¶
| Step | File | Purpose |
|---|---|---|
| 1 | helm/akko/charts/<sub-chart>/ |
Helm sub-chart (templates, values, helpers) |
| 2 | keycloak/realm-akko.json |
OAuth2 client (if the service needs SSO) |
| 3 | scripts/generate-secrets.sh |
Add any new secrets to .env generation |
| 4 | branding/cockpit/index.html |
Add a service card to the portal |
| 5 | branding/cockpit/app.js |
Register the health check endpoint |
| 6 | branding/cockpit/nginx.conf |
Add a health proxy route (avoids CORS) |
| 7 | scripts/start.sh |
Echo the service URL at startup |
| 8 | Documentation | Update architecture docs, README, etc. |
Each step is detailed below, followed by a complete worked example.
Step 1 -- Helm Sub-Chart¶
Create a Helm sub-chart in helm/akko/charts/<service-name>/ and add the dependency to helm/akko/Chart.yaml. Every AKKO service follows a consistent pattern:
- Pinned image tag (never
latest) - Container name prefixed with
akko- - Traefik IngressRoute for HTTPS routing
- Healthcheck with liveness and readiness probes
- Resource limits appropriate for the service
- Security context (
runAsNonRoot,drop: [ALL])
Traefik Labels Pattern¶
All services exposed through Traefik use these four labels:
labels:
traefik.enable: "true"
traefik.http.routers.<name>.rule: "Host(`<subdomain>.{{ .Values.global.domain }}`)"
traefik.http.routers.<name>.entrypoints: "websecure"
traefik.http.routers.<name>.tls: "true"
If the service listens on a non-standard port (anything other than 80), add:
To protect the service behind Keycloak SSO via oauth2-proxy, add the middleware:
traefik.http.routers.<name>.middlewares: "oauth2-auth-chain@file"
# Plus the oauth2 callback router:
traefik.http.routers.<name>-oauth2.rule: "Host(`<subdomain>.{{ .Values.global.domain }}`) && PathPrefix(`/oauth2/`)"
traefik.http.routers.<name>-oauth2.entrypoints: "websecure"
traefik.http.routers.<name>-oauth2.tls: "true"
traefik.http.routers.<name>-oauth2.service: "oauth2-proxy"
Healthcheck Pattern¶
livenessProbe:
httpGet:
path: /<health-path>
port: <port>
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /<health-path>
port: <port>
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
Healthcheck tips
- Use
httpGetprobes for HTTP health endpoints. - Use
execprobes with the service's native healthcheck command when available (e.g.,["traefik", "healthcheck"]or["mc", "ready", "local"]). - Some minimal images (like Ollama's) lack
curlandwget. UsetcpSocketprobes as a fallback.
Step 2 -- Keycloak OAuth Client¶
If the service supports OpenID Connect, add a client to keycloak/realm-akko.json
in the clients array. The client ID should match the service name. Set
publicClient: false for server-side (confidential) flows.
Step 3 -- Secret Generation¶
If the service needs credentials, add them to scripts/generate-secrets.sh inside
the heredoc that writes .env:
After editing, delete .env and re-run to pick up the new variables:
Step 4 -- Cockpit Card¶
Add a card in branding/cockpit/index.html inside the <div class="grid"> section.
Every card follows this structure:
<a class="card"
data-service="<service-id>"
data-subdomain="<subdomain>"
data-health="<service-id>"
data-category="<category>"
href="#" target="_blank" rel="noopener">
<div class="card__header">
<div class="card__icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<!-- SVG icon paths -->
</svg>
</div>
<div class="card__status">
<svg class="uptime-ring" width="28" height="28" viewBox="0 0 28 28" aria-hidden="true">
<circle cx="14" cy="14" r="11"/>
<circle class="ring-fill" cx="14" cy="14" r="11"/>
</svg>
<span class="status-dot status-dot--checking"></span>
</div>
</div>
<h4 class="card__name">Service Name</h4>
<p class="card__desc">Short description</p>
<button class="fav-btn" title="Pin" aria-label="Pin Service Name">☆</button>
</a>
Key attributes:
data-service-- unique service identifier (used for favorites, filtering)data-subdomain-- the subdomain used in the URL (e.g.,jupyterforlab.akko.local, configurable viaglobal.domain)data-health-- must match the key in theSERVICESobject inapp.jsdata-category-- used for category filtering (ui,data,monitoring,infra)
Step 5 -- Health Check in app.js¶
Add an entry to the SERVICES object in branding/cockpit/app.js:
const SERVICES = {
// ... existing services ...
myservice: { name: 'MyService', endpoint: '/api/health/myservice' },
};
The cockpit polls these endpoints every 15 seconds and updates the status dot and uptime ring on each card.
Step 6 -- Nginx Health Proxy¶
Add a reverse proxy block in branding/cockpit/nginx.conf to forward the cockpit's
health check request to the actual service (avoids CORS issues since the cockpit
runs on a different subdomain):
location /api/health/myservice {
set $upstream_myservice http://myservice:8080;
proxy_pass $upstream_myservice/health;
proxy_connect_timeout 3s;
proxy_read_timeout 3s;
}
Dynamic resolution
Always use set $upstream_xxx with a variable before proxy_pass. This
leverages the resolver 127.0.0.11 directive so nginx starts even if
the target service is down. Without this, nginx crashes on startup when
a service is unavailable.
Step 7 -- Startup URL¶
Add the service URL to the startup banner in scripts/start.sh:
Step 8 -- Documentation¶
Update architecture documentation and any relevant guides to reflect the new service.
Worked Example: Adding "DataHub"¶
Here is a complete, concrete example of adding a hypothetical DataHub metadata catalog service to AKKO.
1. Helm Sub-Chart¶
Create helm/akko/charts/akko-datahub/ with the standard sub-chart structure:
helm/akko/charts/akko-datahub/
├── Chart.yaml
├── values.yaml
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ └── _helpers.tpl
Add the dependency in helm/akko/Chart.yaml:
Example values.yaml:
enabled: true
image:
repository: acryldata/datahub-gms
tag: "v0.13.0"
pullPolicy: IfNotPresent
resources:
requests:
memory: 512Mi
cpu: 250m
limits:
memory: 1Gi
cpu: 500m
2. keycloak/realm-akko.json¶
Add a confidential client in the clients array:
{
"clientId": "datahub",
"enabled": true,
"publicClient": false,
"secret": "${KC_CLIENT_SECRET_DATAHUB}",
"redirectUris": ["https://datahub.akko.local/*"],
"webOrigins": ["https://datahub.akko.local"],
"protocol": "openid-connect",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false
}
3. scripts/generate-secrets.sh¶
Add to the heredoc:
4. branding/cockpit/index.html¶
<a class="card"
data-service="datahub"
data-subdomain="datahub"
data-health="datahub"
data-category="data"
href="#" target="_blank" rel="noopener">
<div class="card__header">
<div class="card__icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<ellipse cx="12" cy="5" rx="9" ry="3"/>
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
</svg>
</div>
<div class="card__status">
<svg class="uptime-ring" width="28" height="28" viewBox="0 0 28 28" aria-hidden="true">
<circle cx="14" cy="14" r="11"/>
<circle class="ring-fill" cx="14" cy="14" r="11"/>
</svg>
<span class="status-dot status-dot--checking"></span>
</div>
</div>
<h4 class="card__name">DataHub</h4>
<p class="card__desc">Metadata Catalog</p>
<button class="fav-btn" title="Pin" aria-label="Pin DataHub">☆</button>
</a>
5. branding/cockpit/app.js¶
const SERVICES = {
// ... existing ...
datahub: { name: 'DataHub', endpoint: '/api/health/datahub' },
};
6. branding/cockpit/nginx.conf¶
location /api/health/datahub {
set $upstream_datahub http://datahub:8080;
proxy_pass $upstream_datahub/health;
proxy_connect_timeout 3s;
proxy_read_timeout 3s;
}
7. scripts/start.sh¶
Verification¶
After completing all steps:
# Regenerate secrets
rm .env && ./scripts/generate-secrets.sh
# Deploy with Helm
helm upgrade akko helm/akko/ -n akko -f helm/examples/values-dev.yaml \
--set-file akko-keycloak.realm.data=helm/examples/realm-akko-k3d.json
# Verify DataHub is healthy
kubectl get pods -n akko | grep datahub
kubectl logs -f deploy/akko-datahub -n akko
# Check cockpit health proxy
curl -sf https://cockpit.akko.local/api/health/datahub
# Open in browser
open https://datahub.akko.local