Skip to main content

Kubernetes Deployment

Deploy Keyline on Kubernetes with Helm charts, manifests, and best practices for production environments.

Overview

Keyline can be deployed on Kubernetes using raw manifests, Kustomize, or Helm charts. This guide covers all three approaches.

Prerequisites

  • Kubernetes 1.20+
  • kubectl configured
  • Helm 3.0+ (for Helm deployment)
  • Redis cluster (for production)
  • Elasticsearch cluster with Security API enabled

Quick Start with Helm

Add Helm Repository

helm repo add keyline https://wasilak.github.io/keyline-helm-charts
helm repo update

Install Keyline

helm install keyline ghcr.io/wasilak/keyline \
--namespace auth \
--create-namespace \
--set session.secret=$(openssl rand -base64 32) \
--set cache.encryptionKey=$(openssl rand -base64 32) \
--set elasticsearch.adminPassword=${ES_ADMIN_PASSWORD}

Verify Installation

# Check pods
kubectl get pods -n auth

# Check services
kubectl get svc -n auth

# Check health
kubectl port-forward svc/keyline 9000:9000 -n auth
curl http://localhost:9000/healthz

Manual Deployment

Namespace

apiVersion: v1
kind: Namespace
metadata:
name: auth
labels:
name: auth

ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
name: keyline-config
namespace: auth
data:
config.yaml: |
server:
port: 9000
mode: forward_auth
read_timeout: 30s
write_timeout: 30s

oidc:
enabled: true
issuer_url: ${OIDC_ISSUER_URL}
client_id: ${OIDC_CLIENT_ID}
client_secret: ${OIDC_CLIENT_SECRET}
redirect_url: https://auth.example.com/auth/callback

session:
ttl: 24h
cookie_name: keyline_session
cookie_domain: .example.com

cache:
backend: redis
redis_url: redis://redis.auth.svc.cluster.local:6379
credential_ttl: 1h

user_management:
enabled: true
password_length: 32
credential_ttl: 1h

role_mappings:
- claim: groups
pattern: "admin"
es_roles:
- superuser

default_es_roles:
- viewer

elasticsearch:
admin_user: keyline_admin
admin_password: ${ES_ADMIN_PASSWORD}
url: https://elasticsearch:9200

Secrets

apiVersion: v1
kind: Secret
metadata:
name: keyline-secrets
namespace: auth
type: Opaque
stringData:
SESSION_SECRET: <generate-with-openssl-rand-base64-32>
CACHE_ENCRYPTION_KEY: <generate-with-openssl-rand-base64-32>
ES_ADMIN_PASSWORD: <your-es-admin-password>
OIDC_CLIENT_SECRET: <your-oidc-client-secret>

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: keyline
namespace: auth
labels:
app: keyline
spec:
replicas: 2
selector:
matchLabels:
app: keyline
template:
metadata:
labels:
app: keyline
spec:
containers:
- name: keyline
image: ghcr.io/wasilak/keyline:latest
imagePullPolicy: Always
ports:
- containerPort: 9000
name: http
env:
- name: SESSION_SECRET
valueFrom:
secretKeyRef:
name: keyline-secrets
key: SESSION_SECRET
- name: CACHE_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: keyline-secrets
key: CACHE_ENCRYPTION_KEY
- name: ES_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keyline-secrets
key: ES_ADMIN_PASSWORD
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: keyline-secrets
key: OIDC_CLIENT_SECRET
- name: REDIS_URL
value: "redis://redis.auth.svc.cluster.local:6379"
volumeMounts:
- name: config
mountPath: /etc/keyline
readOnly: true
livenessProbe:
httpGet:
path: /healthz
port: 9000
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: 9000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumes:
- name: config
configMap:
name: keyline-config
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: keyline
topologyKey: kubernetes.io/hostname

Service

apiVersion: v1
kind: Service
metadata:
name: keyline
namespace: auth
labels:
app: keyline
spec:
type: ClusterIP
ports:
- port: 9000
targetPort: 9000
protocol: TCP
name: http
selector:
app: keyline

Ingress (Optional)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keyline
namespace: auth
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- auth.example.com
secretName: keyline-tls
rules:
- host: auth.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: keyline
port:
number: 9000

Kustomize Deployment

kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: auth

resources:
- namespace.yaml
- configmap.yaml
- secret.yaml
- deployment.yaml
- service.yaml
- ingress.yaml

configMapGenerator:
- name: keyline-config
files:
- config.yaml

secretGenerator:
- name: keyline-secrets
envs:
- secrets.env

generatorOptions:
disableNameSuffixHash: true

secrets.env

SESSION_SECRET=your-session-secret
CACHE_ENCRYPTION_KEY=your-encryption-key
ES_ADMIN_PASSWORD=your-es-admin-password
OIDC_CLIENT_SECRET=your-oidc-client-secret

Deploy with Kustomize

kubectl apply -k overlays/production

Horizontal Pod Autoscaler

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: keyline-hpa
namespace: auth
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: keyline
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

Pod Disruption Budget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: keyline-pdb
namespace: auth
spec:
minAvailable: 1
selector:
matchLabels:
app: keyline

Network Policy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: keyline-network-policy
namespace: auth
spec:
podSelector:
matchLabels:
app: keyline
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 9000
egress:
- to:
- namespaceSelector:
matchLabels:
name: auth
ports:
- protocol: TCP
port: 6379 # Redis
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 9200 # Elasticsearch
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 443 # OIDC providers

Monitoring

Prometheus ServiceMonitor

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: keyline
namespace: auth
labels:
app: keyline
spec:
selector:
matchLabels:
app: keyline
endpoints:
- port: http
path: /_metrics
interval: 30s

Logs with Loki

apiVersion: v1
kind: ConfigMap
metadata:
name: keyline-promtail-config
namespace: auth
data:
config.yaml: |
scrape_configs:
- job_name: keyline
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: keyline

Troubleshooting

Pod Won't Start

# Check pod status
kubectl get pods -n auth

# Describe pod
kubectl describe pod <pod-name> -n auth

# Check logs
kubectl logs <pod-name> -n auth

Configuration Issues

# Validate ConfigMap
kubectl get configmap keyline-config -n auth -o yaml

# Validate Secret
kubectl get secret keyline-secrets -n auth -o yaml

Health Check Fails

# Port forward
kubectl port-forward svc/keyline 9000:9000 -n auth

# Test health endpoint
curl http://localhost:9000/healthz

Next Steps