diff --git a/go/Dockerfile b/go/Dockerfile index 3c05a3c6b4..c81e2c117f 100644 --- a/go/Dockerfile +++ b/go/Dockerfile @@ -1,3 +1,5 @@ +ARG BASE_IMAGE=gcr.io/distroless/static-debian12:latest + FROM golang:1.25 AS builder WORKDIR /go/src/github.com/unkeyed/unkey/go @@ -9,7 +11,8 @@ ARG VERSION ENV CGO_ENABLED=0 RUN go build -o bin/unkey -ldflags="-X 'github.com/unkeyed/unkey/go/pkg/version.Version=${VERSION}'" ./main.go -FROM gcr.io/distroless/static-debian12 +FROM ${BASE_IMAGE} + COPY --from=builder /go/src/github.com/unkeyed/unkey/go/bin/unkey / LABEL org.opencontainers.image.source=https://github.com/unkeyed/unkey/go diff --git a/go/Dockerfile.tilt b/go/Dockerfile.tilt new file mode 100644 index 0000000000..f6d07e1421 --- /dev/null +++ b/go/Dockerfile.tilt @@ -0,0 +1,9 @@ +FROM busybox:latest + +# Copy the pre-built binary +COPY bin/unkey /unkey + +LABEL org.opencontainers.image.source=https://github.com/unkeyed/unkey/go +LABEL org.opencontainers.image.description="Unkey API" + +ENTRYPOINT ["/unkey"] \ No newline at end of file diff --git a/go/K8S_DEVELOPMENT.md b/go/K8S_DEVELOPMENT.md new file mode 100644 index 0000000000..e0251f0c25 --- /dev/null +++ b/go/K8S_DEVELOPMENT.md @@ -0,0 +1,65 @@ +# Kubernetes Development Setup + +Run Unkey services locally using Kubernetes instead of Docker Compose. + +## Prerequisites + +- Docker Desktop with Kubernetes enabled OR OrbStack with Kubernetes enabled +- kubectl + +Check requirements: +```bash +make k8s-check +``` + +## Quick Start + +Start everything: +```bash +make k8s-up +``` + +Start with hot reloading (requires Tilt): +```bash +make dev +``` + +## Individual Services + +```bash +make start-mysql +make start-clickhouse +make start-redis +make start-s3 +make start-api +make start-gw +make start-ctrl +``` + +## Management + +```bash +# Stop everything +make k8s-down + +# Reset environment +make k8s-reset + +# View services +kubectl get pods -n unkey +kubectl get services -n unkey +``` + +## Tilt (Optional) + +Start specific services: +```bash +tilt up -- --services=mysql --services=clickhouse +tilt up -- --services=api --services=gw --services=ctrl +tilt up -- --services=all +``` + +Stop Tilt: +```bash +tilt down +``` \ No newline at end of file diff --git a/go/Makefile b/go/Makefile index 3a6d89a19e..4749db6658 100644 --- a/go/Makefile +++ b/go/Makefile @@ -1,4 +1,4 @@ -.PHONY: install tools fmt test-unit test-integration test-integration-long test-stress test build generate pull up clean +.PHONY: install tools fmt test-unit test-integration test-integration-long test-stress test build generate pull up clean k8s-check k8s-up k8s-down k8s-reset k8s-status start-mysql start-ctrl start-all dev # Detect OS and set GOMAXPROCS accordingly UNAME_S := $(shell uname -s) @@ -68,3 +68,106 @@ test-integration: export SIMULATION_TEST=false test-integration: up @echo "Running integration tests w/$(PARALLEL_PROCS) parallel test processes" @go test -tags=integration -json -failfast -timeout=15m -parallel=$(PARALLEL_PROCS) ./... | tparse -all -progress -smallscreen + +# ============================================================================ +# Kubernetes Development Commands +# ============================================================================ + +k8s-check: ## Check if kubectl is available and cluster is running + @command -v kubectl >/dev/null 2>&1 || { echo "ERROR: kubectl not found. Install from: https://kubernetes.io/docs/tasks/tools/"; exit 1; } + @kubectl cluster-info >/dev/null 2>&1 || { echo "ERROR: Kubernetes cluster not available. Enable Kubernetes in Docker Desktop/OrbStack"; exit 1; } + @echo "Kubernetes cluster is available" + +k8s-up: k8s-check ## Deploy all services to current Kubernetes cluster + @echo "Building Docker images..." + @docker build -t unkey/mysql:latest -f ../deployment/Dockerfile.mysql ../ + @docker build -t unkey/clickhouse:latest -f ../deployment/Dockerfile.clickhouse ../ + @docker build -t unkey:latest . + @echo "Creating namespace..." + @kubectl apply -f k8s/manifests/namespace.yaml + @echo "Applying Kubernetes manifests..." + @kubectl apply -f k8s/manifests/ + @echo "Waiting for services to be ready..." + @kubectl wait --for=condition=ready pod -l app=mysql -n unkey --timeout=180s + @kubectl wait --for=condition=ready pod -l app=clickhouse -n unkey --timeout=180s + @kubectl wait --for=condition=ready pod -l app=s3 -n unkey --timeout=180s + @kubectl wait --for=condition=ready pod -l app=api -n unkey --timeout=180s + @kubectl wait --for=condition=ready pod -l app=gw -n unkey --timeout=180s + @kubectl wait --for=condition=ready pod -l app=ctrl -n unkey --timeout=180s + @kubectl wait --for=condition=ready pod -l app=dashboard -n unkey --timeout=180s + @echo "Kubernetes environment is ready!" + @echo "" + @echo "Services accessible via NodePort on localhost - check actual ports with:" + @echo "" + @echo "Check NodePort assignments: make k8s-ports" + @make k8s-status + +k8s-stop: ## Stop all pods (scale deployments to 0) + @echo "Stopping all pods..." + @kubectl scale deployment --all --replicas=0 -n unkey + @echo "All pods stopped" + +k8s-down: ## Delete all services from current Kubernetes cluster + @echo "Deleting all services..." + @kubectl delete namespace unkey --ignore-not-found=true + @echo "Services deleted" + +k8s-reset: k8s-down k8s-up ## Reset the entire Kubernetes environment + +# Helper function to deploy a service +define deploy-service + @echo "Starting $(2)..." + $(if $(3),@docker build -t $(3) $(4)) + @kubectl apply -f k8s/manifests/namespace.yaml + @kubectl apply -f k8s/manifests/$(1).yaml + @kubectl wait --for=condition=ready pod -l app=$(1) -n unkey --timeout=180s + $(if $(5),@kubectl wait --for=condition=ready pod -l app=$(5) -n unkey --timeout=180s) + @echo "$(2) is ready!" +endef + +start-mysql: k8s-check ## Deploy only MySQL + $(call deploy-service,mysql,MySQL,unkey/mysql:latest,-f ../deployment/Dockerfile.mysql ../) + +start-clickhouse: k8s-check ## Deploy only ClickHouse + @docker build -t unkey/clickhouse:latest -f ../deployment/Dockerfile.clickhouse ../ + $(call deploy-service,clickhouse,ClickHouse) + +start-redis: k8s-check ## Deploy only Redis + $(call deploy-service,redis,Redis) + +start-s3: k8s-check ## Deploy only S3 (MinIO) + $(call deploy-service,s3,S3 (MinIO)) + +start-planetscale: k8s-check ## Deploy only PlanetScale HTTP driver + $(call deploy-service,planetscale,PlanetScale HTTP driver) + +start-observability: k8s-check ## Deploy only Observability stack + $(call deploy-service,observability,Observability stack,,,otel-collector) + +start-api: k8s-check ## Deploy only API service (3 replicas) + $(call deploy-service,api,API,unkey:latest,.) + +start-gw: k8s-check ## Deploy only Gateway service + $(call deploy-service,gw,Gateway,unkey:latest,.) + +start-ctrl: k8s-check ## Deploy only ctrl service + $(call deploy-service,ctrl,Control Plane,unkey:latest,.) + +start-dashboard: k8s-check ## Deploy only dashboard service + $(call deploy-service,dashboard,Dashboard,unkey/dashboard:latest,-f ../apps/dashboard/Dockerfile ../) + +start-unkey-services: start-api start-gw start-ctrl start-dashboard ## Deploy all Unkey services + +start-dependencies: start-mysql start-clickhouse start-redis start-s3 start-planetscale start-observability ## Deploy all dependency services + +start-all: start-dependencies start-unkey-services ## Deploy all services individually + +dev: ## Start with Tilt (if available) or fallback to k8s-up + @if command -v tilt >/dev/null 2>&1; then \ + echo "Starting with Tilt..."; \ + tilt up; \ + else \ + echo "Tilt not found, using k8s-up instead"; \ + echo "Install Tilt from: https://docs.tilt.dev/install.html"; \ + make k8s-up; \ + fi diff --git a/go/Tiltfile b/go/Tiltfile new file mode 100644 index 0000000000..63d3fc0ccf --- /dev/null +++ b/go/Tiltfile @@ -0,0 +1,311 @@ +# Tiltfile for Unkey development +# This provides an enhanced development experience with hot reloading and unified logs + +# Load Tilt extensions +load('ext://namespace', 'namespace_create') +load('ext://restart_process', 'docker_build_with_restart') + +# Tilt configuration +config.define_string_list("services", args=False, usage="Services to run (mysql,ctrl,clickhouse,s3,observability,planetscale or all)") +config.define_bool("debug", args=False, usage="Enable debug mode") + +cfg = config.parse() +services = cfg.get('services', ['all']) +debug_mode = cfg.get('debug', False) + +print("Tilt starting with services: %s" % services) + +# Create namespace using the extension with allow_duplicates +namespace_create('unkey', allow_duplicates=True) + +# Define which services to start +start_mysql = 'all' in services or 'mysql' in services +start_clickhouse = 'all' in services or 'clickhouse' in services +start_s3 = 'all' in services or 's3' in services +start_observability = 'all' in services or 'observability' in services +start_planetscale = 'all' in services or 'planetscale' in services +start_api = 'all' in services or 'api' in services +start_gw = 'all' in services or 'gateway' in services or 'gw' in services +start_ctrl = 'all' in services or 'ctrl' in services +start_dashboard = 'all' in services or 'dashboard' in services + +# Apply RBAC +k8s_yaml('k8s/manifests/rbac.yaml') + +# Redis service +redis_started = False +if start_api: # Redis is needed by API service + print("Setting up Redis...") + k8s_yaml('k8s/manifests/redis.yaml') + k8s_resource( + 'redis', + port_forwards='6379:6379', + resource_deps=[], + labels=['database'] + ) + redis_started = True + +# MySQL service +if start_mysql: + print("Setting up MySQL...") + # Only build if image doesn't exist + if not local('docker images -q unkey/mysql:latest 2>/dev/null', quiet=True): + print("Building MySQL image...") + docker_build( + 'unkey/mysql:latest', + '../', # Build context at repo root + dockerfile='../deployment/Dockerfile.mysql' + ) + else: + print("Using existing MySQL image") + k8s_yaml('k8s/manifests/mysql.yaml') + k8s_resource( + 'mysql', + port_forwards='3306:3306', + resource_deps=[], + labels=['database'] + ) + +# ClickHouse service +if start_clickhouse: + print("Setting up ClickHouse...") + # Only build if image doesn't exist + if not local('docker images -q unkey/clickhouse:latest 2>/dev/null', quiet=True): + print("Building ClickHouse image...") + docker_build( + 'unkey/clickhouse:latest', + '../', # Build context at repo root + dockerfile='../deployment/Dockerfile.clickhouse' + ) + else: + print("Using existing ClickHouse image") + k8s_yaml('k8s/manifests/clickhouse.yaml') + k8s_resource( + 'clickhouse', + port_forwards='8123:8123', + resource_deps=[], + labels=['database'] + ) + +# S3 (MinIO) service +if start_s3: + print("Setting up S3 (MinIO)...") + k8s_yaml('k8s/manifests/s3.yaml') + k8s_resource( + 's3', + port_forwards=['9000:9000', '9001:9001'], + resource_deps=[], + labels=['storage'] + ) + +# PlanetScale HTTP driver +if start_planetscale: + print("Setting up PlanetScale HTTP driver...") + k8s_yaml('k8s/manifests/planetscale.yaml') + deps = ['mysql'] if start_mysql else [] + k8s_resource( + 'planetscale', + resource_deps=deps, + labels=['database'] + ) + +# Observability stack +if start_observability: + print("Setting up observability stack...") + k8s_yaml('k8s/manifests/observability.yaml') + k8s_resource( + 'prometheus', + port_forwards='9090:9090', + resource_deps=[], + labels=['observability'] + ) + k8s_resource( + 'otel-collector', + port_forwards=['4317:4317', '4318:4318'], + resource_deps=[], + labels=['observability'] + ) + +# Build Unkey binary locally (independent of infrastructure) +if start_api or start_gw or start_ctrl: + print("Building Unkey binary...") + # Build locally first for faster updates + local_resource( + 'unkey-compile', + 'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./bin/unkey ./main.go', + deps=['./main.go', './pkg', './cmd', './apps'], + ) + +# API service (3 replicas) +if start_api: + print("Setting up API service...") + + docker_build_with_restart( + 'unkey-api:latest', + '.', + dockerfile='Dockerfile.tilt', + entrypoint=['/unkey', 'run', 'api'], + only=['./bin'], + live_update=[ + sync('./bin/unkey', '/unkey'), + ], + ignore=['./cmd/gw', './cmd/ctrl', './apps/gw', './apps/ctrl'] + ) + + k8s_yaml('k8s/manifests/api.yaml') + + # Build dependency list + api_deps = [] + if start_mysql: api_deps.append('mysql') + if start_clickhouse: api_deps.append('clickhouse') + if redis_started: api_deps.append('redis') + + # Add compilation dependency for Unkey services + api_deps.append('unkey-compile') + + k8s_resource( + 'api', + port_forwards='7070:7070', + resource_deps=api_deps, + labels=['unkey'], + auto_init=True, + trigger_mode=TRIGGER_MODE_AUTO if debug_mode else TRIGGER_MODE_MANUAL + ) + +# Gateway service (1 replica) +if start_gw: + print("Setting up Gateway service...") + + docker_build_with_restart( + 'unkey-gw:latest', + '.', + dockerfile='Dockerfile.tilt', + entrypoint=['/unkey', 'run', 'gw'], + only=['./bin'], + live_update=[ + sync('./bin/unkey', '/unkey'), + ], + ignore=['./cmd/api', './cmd/ctrl', './apps/api', './apps/ctrl'] + ) + + k8s_yaml('k8s/manifests/gw.yaml') + + # Build dependency list + gw_deps = [] + if start_mysql: gw_deps.append('mysql') + # Add compilation dependency for Unkey services + gw_deps.append('unkey-compile') + + k8s_resource( + 'gw', + port_forwards=['8080:8080', '8443:8443'], + resource_deps=gw_deps, + labels=['unkey'], + auto_init=True, + trigger_mode=TRIGGER_MODE_AUTO if debug_mode else TRIGGER_MODE_MANUAL + ) + +# Ctrl service (1 replica) +if start_ctrl: + print("Setting up Ctrl service...") + + docker_build_with_restart( + 'unkey-ctrl:latest', + '.', + dockerfile='Dockerfile.tilt', + entrypoint=['/unkey', 'run', 'ctrl'], + only=['./bin'], + live_update=[ + sync('./bin/unkey', '/unkey'), + ], + ignore=['./cmd/api', './cmd/gw', './apps/api', './apps/gw'] + ) + + k8s_yaml('k8s/manifests/ctrl.yaml') + + # Build dependency list + ctrl_deps = [] + if start_mysql: ctrl_deps.append('mysql') + if start_s3: ctrl_deps.append('s3') + # Add compilation dependency for Unkey services + ctrl_deps.append('unkey-compile') + + k8s_resource( + 'ctrl', + port_forwards='7091:7091', + resource_deps=ctrl_deps, + labels=['unkey'], + auto_init=True, + trigger_mode=TRIGGER_MODE_AUTO if debug_mode else TRIGGER_MODE_MANUAL + ) + +# Dashboard service +if start_dashboard: + print("Setting up Dashboard service...") + # Build the dashboard image + docker_build( + 'unkey/dashboard:latest', + '../', + dockerfile='../apps/dashboard/Dockerfile', + live_update=[ + sync('../apps/dashboard', '/unkey/apps/dashboard'), + run('cd /unkey/apps/dashboard && pnpm build', trigger=['**/*.tsx', '**/*.ts', '**/*.css']), + ] + ) + k8s_yaml('k8s/manifests/dashboard.yaml') + + # Build dependency list + dashboard_deps = [] + if start_planetscale: dashboard_deps.append('planetscale') + if start_clickhouse: dashboard_deps.append('clickhouse') + + k8s_resource( + 'dashboard', + port_forwards='3000:3000', + resource_deps=dashboard_deps, + labels=['unkey'], + auto_init=True, + trigger_mode=TRIGGER_MODE_AUTO if debug_mode else TRIGGER_MODE_MANUAL + ) + +# Tilt UI customization +active_services = [] +if redis_started: active_services.append('redis') +if start_mysql: active_services.append('mysql') +if start_clickhouse: active_services.append('clickhouse') +if start_s3: active_services.append('s3') +if start_planetscale: active_services.append('planetscale') +if start_observability: active_services.extend(['prometheus', 'otel-collector']) +if start_api: active_services.append('api') +if start_gw: active_services.append('gw') +if start_ctrl: active_services.append('ctrl') +if start_dashboard: active_services.append('dashboard') + +print(""" +Tilt is ready! + +Services running: %s + +Web UI: http://localhost:10350 + +Services available via Tilt port forwards: +Dashboard: http://localhost:3000 +API: http://localhost:7070 +Gateway: http://localhost:6060 +Ctrl: http://localhost:7091 +Prometheus: http://localhost:9090 +S3 Console: http://localhost:9000 +ClickHouse: http://localhost:8123 + +Tips: +- Press 's' to open a shell in running containers +- Press 'r' to manually trigger rebuilds +- Use 'tilt down' to stop all services +- Use 'tilt logs ' to view specific service logs + +Development workflow: +- Edit Go files -> automatic rebuild & restart +- View logs in unified Tilt UI +- Use ingress for service access + +""" % ', '.join(active_services)) diff --git a/go/k8s/manifests/api.yaml b/go/k8s/manifests/api.yaml new file mode 100644 index 0000000000..6d7f945516 --- /dev/null +++ b/go/k8s/manifests/api.yaml @@ -0,0 +1,130 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api + namespace: unkey + labels: + app: api +spec: + replicas: 3 + selector: + matchLabels: + app: api + template: + metadata: + labels: + app: api + spec: + containers: + - name: api + image: unkey-api:latest + imagePullPolicy: Never # Use local images + ports: + - containerPort: 7070 + env: + # Server Configuration + - name: UNKEY_HTTP_PORT + value: "7070" + - name: UNKEY_LOGS_COLOR + value: "true" + - name: UNKEY_TEST_MODE + value: "false" + # Instance Identification + - name: UNKEY_PLATFORM + value: "kubernetes" + - name: UNKEY_IMAGE + value: "unkey:latest" + - name: UNKEY_REGION + value: "local" + - name: UNKEY_INSTANCE_ID + value: "api-dev" + # Database Configuration + - name: UNKEY_DATABASE_PRIMARY + value: "unkey:password@tcp(mysql:3306)/unkey?parseTime=true&interpolateParams=true" + # Caching and Storage + - name: UNKEY_REDIS_URL + value: "redis://redis:6379" + - name: UNKEY_CLICKHOUSE_URL + value: "clickhouse://default:password@clickhouse:9000?secure=false&skip_verify=true" + # Observability - DISABLED for development + - name: UNKEY_OTEL + value: "false" + - name: UNKEY_PROMETHEUS_PORT + value: "0" + # Vault Configuration + - name: UNKEY_VAULT_MASTER_KEYS + value: "Ch9rZWtfMmdqMFBJdVhac1NSa0ZhNE5mOWlLSnBHenFPENTt7an5MRogENt9Si6wms4pQ2XIvqNSIgNpaBenJmXgcInhu6Nfv2U=" + - name: UNKEY_VAULT_S3_URL + value: "http://s3:3902" + - name: UNKEY_VAULT_S3_BUCKET + value: "vault" + - name: UNKEY_VAULT_S3_ACCESS_KEY_ID + value: "minio_root_user" + - name: UNKEY_VAULT_S3_ACCESS_KEY_SECRET + value: "minio_root_password" + # ClickHouse Proxy Service Configuration + - name: UNKEY_CHPROXY_AUTH_TOKEN + value: "chproxy-test-token-123" + # Request Body Configuration + - name: UNKEY_MAX_REQUEST_BODY_SIZE + value: "10485760" + command: ["/unkey", "run", "api"] + readinessProbe: + httpGet: + path: /v2/liveness + port: 7070 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /v2/liveness + port: 7070 + initialDelaySeconds: 30 + periodSeconds: 10 + initContainers: + - name: wait-for-dependencies + image: busybox:1.36 + command: + [ + "sh", + "-c", + "until nc -z mysql 3306 && nc -z clickhouse 8123; do echo waiting for dependencies; sleep 2; done;", + ] + +--- +apiVersion: v1 +kind: Service +metadata: + name: api + namespace: unkey + labels: + app: api +spec: + selector: + app: api + ports: + - name: http + port: 7070 + targetPort: 7070 + protocol: TCP + type: LoadBalancer + +--- +apiVersion: v1 +kind: Service +metadata: + name: api-lb + namespace: unkey + labels: + app: api + service: loadbalancer +spec: + selector: + app: api + ports: + - name: http + port: 80 + targetPort: 7070 + protocol: TCP + type: LoadBalancer diff --git a/go/k8s/manifests/clickhouse.yaml b/go/k8s/manifests/clickhouse.yaml new file mode 100644 index 0000000000..fd9fa919d5 --- /dev/null +++ b/go/k8s/manifests/clickhouse.yaml @@ -0,0 +1,105 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: clickhouse-pvc + namespace: unkey +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: clickhouse + namespace: unkey + labels: + app: clickhouse +spec: + replicas: 1 + selector: + matchLabels: + app: clickhouse + template: + metadata: + labels: + app: clickhouse + spec: + containers: + - name: clickhouse + image: unkey/clickhouse:latest + ports: + - containerPort: 8123 + name: http + - containerPort: 9000 + name: native + env: + - name: CLICKHOUSE_ADMIN_USER + value: "default" + - name: CLICKHOUSE_ADMIN_PASSWORD + value: "password" + - name: ALLOW_EMPTY_PASSWORD + value: "no" + volumeMounts: + - name: clickhouse-storage + mountPath: /bitnami/clickhouse + readinessProbe: + exec: + command: + - clickhouse-client + - --host + - localhost + - --user + - default + - --password + - password + - --query + - "SELECT 1" + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + exec: + command: + - clickhouse-client + - --host + - localhost + - --user + - default + - --password + - password + - --query + - "SELECT 1" + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 5 + volumes: + - name: clickhouse-storage + persistentVolumeClaim: + claimName: clickhouse-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: clickhouse + namespace: unkey + labels: + app: clickhouse +spec: + selector: + app: clickhouse + ports: + - name: http + port: 8123 + targetPort: 8123 + protocol: TCP + - name: native + port: 9000 + targetPort: 9000 + protocol: TCP + type: LoadBalancer diff --git a/go/k8s/manifests/ctrl.yaml b/go/k8s/manifests/ctrl.yaml new file mode 100644 index 0000000000..f0d9a6e5e9 --- /dev/null +++ b/go/k8s/manifests/ctrl.yaml @@ -0,0 +1,101 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ctrl + namespace: unkey + labels: + app: ctrl +spec: + replicas: 1 + selector: + matchLabels: + app: ctrl + template: + metadata: + labels: + app: ctrl + spec: + serviceAccountName: ctrl-service-account + containers: + - name: ctrl + image: unkey-ctrl:latest + imagePullPolicy: Never # Use local images + ports: + - containerPort: 7091 + env: + # Server Configuration + - name: UNKEY_HTTP_PORT + value: "7091" + - name: UNKEY_LOGS_COLOR + value: "true" + # Instance Identification + - name: UNKEY_PLATFORM + value: "kubernetes" + - name: UNKEY_IMAGE + value: "unkey:latest" + - name: UNKEY_REGION + value: "local" + - name: UNKEY_INSTANCE_ID + value: "ctrl-dev" + # Database Configuration + - name: UNKEY_DATABASE_PRIMARY + value: "unkey:password@tcp(mysql:3306)/unkey?parseTime=true&interpolateParams=true" + - name: UNKEY_DATABASE_PARTITION + value: "unkey:password@tcp(mysql:3306)/partition_001?parseTime=true&interpolateParams=true" + - name: UNKEY_DATABASE_HYDRA + value: "unkey:password@tcp(mysql:3306)/hydra?parseTime=true&interpolateParams=true" + # Observability - DISABLED for development + - name: UNKEY_OTEL + value: "false" + # Control Plane Specific + - name: UNKEY_AUTH_TOKEN + value: "dev-auth-token" + - name: UNKEY_METALD_ADDRESS + value: "http://metald-placeholder:8080" + - name: UNKEY_SPIFFE_SOCKET_PATH + value: "/var/lib/spire/agent/agent.sock" + # Vault Configuration (required) + - name: UNKEY_VAULT_MASTER_KEYS + value: "Ch9rZWtfMmdqMFBJdVhac1NSa0ZhNE5mOWlLSnBHenFPENTt7an5MRogENt9Si6wms4pQ2XIvqNSIgNpaBenJmXgcInhu6Nfv2U=" + - name: UNKEY_VAULT_S3_URL + value: "http://s3:3902" + - name: UNKEY_VAULT_S3_BUCKET + value: "vault" + - name: UNKEY_VAULT_S3_ACCESS_KEY_ID + value: "minio_root_user" + - name: UNKEY_VAULT_S3_ACCESS_KEY_SECRET + value: "minio_root_password" + # Additional Configuration + - name: UNKEY_ACME_ENABLED + value: "false" + - name: UNKEY_METALD_BACKEND + value: "k8s" + command: ["/unkey", "run", "ctrl"] + initContainers: + - name: wait-for-dependencies + image: busybox:1.36 + command: + [ + "sh", + "-c", + "until nc -z mysql 3306 && nc -z s3 3902; do echo waiting for dependencies; sleep 2; done;", + ] + +--- +apiVersion: v1 +kind: Service +metadata: + name: ctrl + namespace: unkey + labels: + app: ctrl +spec: + selector: + app: ctrl + ports: + - name: http + port: 7091 + targetPort: 7091 + protocol: TCP + type: LoadBalancer diff --git a/go/k8s/manifests/dashboard.yaml b/go/k8s/manifests/dashboard.yaml new file mode 100644 index 0000000000..ec59e7a046 --- /dev/null +++ b/go/k8s/manifests/dashboard.yaml @@ -0,0 +1,80 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dashboard + namespace: unkey + labels: + app: dashboard +spec: + replicas: 1 + selector: + matchLabels: + app: dashboard + template: + metadata: + labels: + app: dashboard + spec: + initContainers: + - name: wait-for-dependencies + image: busybox:1.36 + command: + - sh + - -c + - | + until nc -z planetscale 3900; do + echo waiting for planetscale + sleep 2 + done + containers: + - name: dashboard + image: unkey/dashboard:latest + imagePullPolicy: Never + ports: + - containerPort: 3000 + env: + # Database configuration + - name: DATABASE_HOST + value: "planetscale:3900" + # ClickHouse configuration + - name: CLICKHOUSE_URL + value: "http://unkey:password@clickhouse:8123" + # Environment + - name: NODE_ENV + value: "production" + # Instance identification + - name: UNKEY_PLATFORM + value: "kubernetes" + - name: UNKEY_REGION + value: "local" + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + +--- +apiVersion: v1 +kind: Service +metadata: + name: dashboard + namespace: unkey + labels: + app: dashboard +spec: + selector: + app: dashboard + ports: + - name: http + port: 3000 + targetPort: 3000 + protocol: TCP + type: LoadBalancer diff --git a/go/k8s/manifests/gw.yaml b/go/k8s/manifests/gw.yaml new file mode 100644 index 0000000000..1abd03dfea --- /dev/null +++ b/go/k8s/manifests/gw.yaml @@ -0,0 +1,120 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gw + namespace: unkey + labels: + app: gw +spec: + replicas: 1 + selector: + matchLabels: + app: gw + template: + metadata: + labels: + app: gw + spec: + containers: + - name: gw + image: unkey-gw:latest + imagePullPolicy: Never # Use local images + ports: + - containerPort: 8080 + - containerPort: 8443 + env: + # Server Configuration + - name: UNKEY_HTTP_PORT + value: "8080" + - name: UNKEY_HTTPS_PORT + value: "8443" + - name: UNKEY_TLS_ENABLED + value: "false" + # Instance Identification + - name: UNKEY_PLATFORM + value: "kubernetes" + - name: UNKEY_IMAGE + value: "unkey:latest" + - name: UNKEY_REGION + value: "local" + - name: UNKEY_GATEWAY_ID + value: "gw-dev" + - name: UNKEY_DEFAULT_CERT_DOMAIN + value: "" + - name: UNKEY_MAIN_DOMAIN + value: "" + - name: UNKEY_CTRL_ADDR + value: "ctrl:7091" + # Database Configuration - Partitioned (for gateway operations) + - name: UNKEY_DATABASE_PRIMARY + value: "unkey:password@tcp(mysql:3306)/partition_001?parseTime=true&interpolateParams=true" + # Database Configuration - Keys Service + - name: UNKEY_KEYS_DATABASE_PRIMARY + value: "unkey:password@tcp(mysql:3306)/unkey?parseTime=true&interpolateParams=true" + # ClickHouse Configuration + - name: UNKEY_CLICKHOUSE_URL + value: "clickhouse://default:password@clickhouse:9000?secure=false&skip_verify=true" + # Redis Configuration + - name: UNKEY_REDIS_URL + value: "redis://redis:6379" + # Observability - DISABLED for development + - name: UNKEY_OTEL + value: "false" + - name: UNKEY_PROMETHEUS_PORT + value: "0" + # Vault Configuration + - name: UNKEY_VAULT_MASTER_KEYS + value: "Ch9rZWtfMmdqMFBJdVhac1NSa0ZhNE5mOWlLSnBHenFPENTt7an5MRogENt9Si6wms4pQ2XIvqNSIgNpaBenJmXgcInhu6Nfv2U=" + - name: UNKEY_VAULT_S3_URL + value: "http://s3:3902" + - name: UNKEY_VAULT_S3_BUCKET + value: "vault" + - name: UNKEY_VAULT_S3_ACCESS_KEY_ID + value: "minio_root_user" + - name: UNKEY_VAULT_S3_ACCESS_KEY_SECRET + value: "minio_root_password" + command: ["/unkey", "run", "gw"] + readinessProbe: + httpGet: + path: /unkey/_internal/liveness + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /unkey/_internal/liveness + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + initContainers: + - name: wait-for-dependencies + image: busybox:1.36 + command: + [ + "sh", + "-c", + "until nc -z mysql 3306; do echo waiting for dependencies; sleep 2; done;", + ] + +--- +apiVersion: v1 +kind: Service +metadata: + name: gw + namespace: unkey + labels: + app: gw +spec: + selector: + app: gw + ports: + - name: http + port: 8080 + targetPort: 8080 + protocol: TCP + - name: https + port: 8443 + targetPort: 8443 + protocol: TCP + type: ClusterIP diff --git a/go/k8s/manifests/mysql.yaml b/go/k8s/manifests/mysql.yaml new file mode 100644 index 0000000000..25366af105 --- /dev/null +++ b/go/k8s/manifests/mysql.yaml @@ -0,0 +1,90 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc + namespace: unkey +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + namespace: unkey + labels: + app: mysql +spec: + replicas: 1 + selector: + matchLabels: + app: mysql + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: unkey/mysql:latest + imagePullPolicy: Never + ports: + - containerPort: 3306 + env: + - name: MYSQL_ROOT_PASSWORD + value: "root" + - name: MYSQL_DATABASE + value: "unkey" + - name: MYSQL_USER + value: "unkey" + - name: MYSQL_PASSWORD + value: "password" + args: + - "--max_connections=1000" + volumeMounts: + - name: mysql-storage + mountPath: /var/lib/mysql + readinessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 30 + periodSeconds: 10 + livenessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 60 + periodSeconds: 30 + volumes: + - name: mysql-storage + persistentVolumeClaim: + claimName: mysql-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql + namespace: unkey + labels: + app: mysql +spec: + selector: + app: mysql + ports: + - port: 3306 + targetPort: 3306 + protocol: TCP + type: LoadBalancer diff --git a/go/k8s/manifests/namespace.yaml b/go/k8s/manifests/namespace.yaml new file mode 100644 index 0000000000..cdc6e8d819 --- /dev/null +++ b/go/k8s/manifests/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: unkey + labels: + app.kubernetes.io/name: unkey + app.kubernetes.io/instance: dev diff --git a/go/k8s/manifests/observability.yaml b/go/k8s/manifests/observability.yaml new file mode 100644 index 0000000000..ec7034a488 --- /dev/null +++ b/go/k8s/manifests/observability.yaml @@ -0,0 +1,183 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-config + namespace: unkey +data: + prometheus.yml: | + global: + scrape_interval: 15s + scrape_configs: + - job_name: 'unkey-services' + static_configs: + - targets: ['ctrl:8084'] + metrics_path: /metrics + scrape_interval: 5s + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus + namespace: unkey + labels: + app: prometheus +spec: + replicas: 1 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + containers: + - name: prometheus + image: prom/prometheus:v2.45.0 + ports: + - containerPort: 9090 + args: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus/" + - "--web.console.libraries=/etc/prometheus/console_libraries" + - "--web.console.templates=/etc/prometheus/consoles" + - "--storage.tsdb.retention.time=200h" + - "--web.enable-lifecycle" + volumeMounts: + - name: prometheus-config + mountPath: /etc/prometheus/ + volumes: + - name: prometheus-config + configMap: + name: prometheus-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus + namespace: unkey + labels: + app: prometheus +spec: + selector: + app: prometheus + ports: + - port: 9090 + targetPort: 9090 + protocol: TCP + type: NodePort + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: otel-collector + namespace: unkey + labels: + app: otel-collector +spec: + replicas: 1 + selector: + matchLabels: + app: otel-collector + template: + metadata: + labels: + app: otel-collector + spec: + containers: + - name: otel-collector + image: otel/opentelemetry-collector-contrib:0.92.0 + ports: + - containerPort: 4317 # OTLP gRPC + - containerPort: 4318 # OTLP HTTP + - containerPort: 8889 # Prometheus metrics + command: + - /otelcol-contrib + - --config=/etc/otelcol-contrib/otel-collector.yml + env: + - name: GOMEMLIMIT + value: "160MiB" + volumeMounts: + - name: otel-collector-config + mountPath: /etc/otelcol-contrib/ + volumes: + - name: otel-collector-config + configMap: + name: otel-collector-config + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: otel-collector-config + namespace: unkey +data: + otel-collector.yml: | + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + prometheus: + config: + scrape_configs: + - job_name: 'otel-collector' + scrape_interval: 10s + static_configs: + - targets: ['0.0.0.0:8889'] + + processors: + batch: + + exporters: + prometheus: + endpoint: "0.0.0.0:8889" + logging: + loglevel: info + + service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [logging] + metrics: + receivers: [otlp, prometheus] + processors: [batch] + exporters: [prometheus, logging] + logs: + receivers: [otlp] + processors: [batch] + exporters: [logging] + +--- +apiVersion: v1 +kind: Service +metadata: + name: otel-collector + namespace: unkey + labels: + app: otel-collector +spec: + selector: + app: otel-collector + ports: + - name: otlp-grpc + port: 4317 + targetPort: 4317 + protocol: TCP + - name: otlp-http + port: 4318 + targetPort: 4318 + protocol: TCP + - name: prometheus + port: 8889 + targetPort: 8889 + protocol: TCP + type: NodePort diff --git a/go/k8s/manifests/planetscale.yaml b/go/k8s/manifests/planetscale.yaml new file mode 100644 index 0000000000..612ae65134 --- /dev/null +++ b/go/k8s/manifests/planetscale.yaml @@ -0,0 +1,67 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: planetscale + namespace: unkey + labels: + app: planetscale +spec: + replicas: 1 + selector: + matchLabels: + app: planetscale + template: + metadata: + labels: + app: planetscale + spec: + containers: + - name: planetscale-http + image: ghcr.io/mattrobenolt/ps-http-sim:v0.0.12 + ports: + - containerPort: 3900 + args: + [ + "-listen-port=3900", + "-mysql-dbname=unkey", + "-mysql-addr=mysql", + "-mysql-max-rows=100000", + "-mysql-idle-timeout=1s", + ] + env: + - name: DATABASE_HOST + value: "mysql" + - name: DATABASE_NAME + value: "unkey" + - name: DATABASE_USERNAME + value: "unkey" + - name: DATABASE_PASSWORD + value: "password" + readinessProbe: + tcpSocket: + port: 3900 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + tcpSocket: + port: 3900 + initialDelaySeconds: 30 + periodSeconds: 10 + +--- +apiVersion: v1 +kind: Service +metadata: + name: planetscale + namespace: unkey + labels: + app: planetscale +spec: + selector: + app: planetscale + ports: + - port: 3900 + targetPort: 3900 + protocol: TCP + type: LoadBalancer diff --git a/go/k8s/manifests/rbac.yaml b/go/k8s/manifests/rbac.yaml new file mode 100644 index 0000000000..9523d7a690 --- /dev/null +++ b/go/k8s/manifests/rbac.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ctrl-service-account + namespace: unkey + labels: + app: ctrl + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ctrl-deployment-manager + labels: + app: ctrl +rules: + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["pods", "services"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["configmaps", "secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ctrl-deployment-manager-binding + labels: + app: ctrl +subjects: + - kind: ServiceAccount + name: ctrl-service-account + namespace: unkey +roleRef: + kind: ClusterRole + name: ctrl-deployment-manager + apiGroup: rbac.authorization.k8s.io diff --git a/go/k8s/manifests/redis.yaml b/go/k8s/manifests/redis.yaml new file mode 100644 index 0000000000..523d806e18 --- /dev/null +++ b/go/k8s/manifests/redis.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: unkey + labels: + app: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:8.0 + ports: + - containerPort: 6379 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 10 + periodSeconds: 30 + +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: unkey + labels: + app: redis +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 + protocol: TCP + type: LoadBalancer diff --git a/go/k8s/manifests/s3.yaml b/go/k8s/manifests/s3.yaml new file mode 100644 index 0000000000..96314dad6c --- /dev/null +++ b/go/k8s/manifests/s3.yaml @@ -0,0 +1,106 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: s3-pvc + namespace: unkey +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 50Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: s3 + namespace: unkey + labels: + app: s3 +spec: + replicas: 1 + selector: + matchLabels: + app: s3 + template: + metadata: + labels: + app: s3 + spec: + containers: + - name: minio + image: bitnami/minio:2025.7.23 + ports: + - containerPort: 9000 + name: api + - containerPort: 9001 + name: console + env: + - name: MINIO_ROOT_USER + value: "minio_root_user" + - name: MINIO_ROOT_PASSWORD + value: "minio_root_password" + - name: MINIO_API_PORT_NUMBER + value: "9000" + - name: MINIO_CONSOLE_PORT_NUMBER + value: "9001" + - name: MINIO_DEFAULT_BUCKETS + value: "vault" + volumeMounts: + - name: s3-storage + mountPath: /data + readinessProbe: + httpGet: + path: /minio/health/ready + port: 9000 + initialDelaySeconds: 30 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /minio/health/live + port: 9000 + initialDelaySeconds: 60 + periodSeconds: 30 + volumes: + - name: s3-storage + persistentVolumeClaim: + claimName: s3-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: s3 + namespace: unkey + labels: + app: s3 +spec: + selector: + app: s3 + ports: + - name: api + port: 3902 + targetPort: 9000 + protocol: TCP + type: LoadBalancer + +--- +apiVersion: v1 +kind: Service +metadata: + name: s3-console + namespace: unkey + labels: + app: s3 + service: console +spec: + selector: + app: s3 + ports: + - name: console + port: 3903 + targetPort: 9001 + protocol: TCP + type: LoadBalancer