Skip to content

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:

  traefik.http.services.<name>.loadbalancer.server.port: "<port>"

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 httpGet probes for HTTP health endpoints.
  • Use exec probes with the service's native healthcheck command when available (e.g., ["traefik", "healthcheck"] or ["mc", "ready", "local"]).
  • Some minimal images (like Ollama's) lack curl and wget. Use tcpSocket probes 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:

# --- MyService ---
MYSERVICE_ADMIN_PASSWORD=$(gen_password)
KC_CLIENT_SECRET_MYSERVICE=$(gen_hex)

After editing, delete .env and re-run to pick up the new variables:

rm .env && ./scripts/generate-secrets.sh

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">&#9734;</button>
</a>

Key attributes:

  • data-service -- unique service identifier (used for favorites, filtering)
  • data-subdomain -- the subdomain used in the URL (e.g., jupyter for lab.akko.local, configurable via global.domain)
  • data-health -- must match the key in the SERVICES object in app.js
  • data-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:

echo "  MyService:     https://myservice.$AKKO_DOMAIN"

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:

- name: akko-datahub
  version: 0.1.0
  condition: akko-datahub.enabled

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:

# --- DataHub ---
DATAHUB_ADMIN_PASSWORD=$(gen_password)
KC_CLIENT_SECRET_DATAHUB=$(gen_hex)

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">&#9734;</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

echo "  DataHub:       https://datahub.$AKKO_DOMAIN"

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