diff --git a/k8s/temporal/00-namespace.yaml b/k8s/temporal/00-namespace.yaml new file mode 100644 index 0000000..07d4b82 --- /dev/null +++ b/k8s/temporal/00-namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: temporal + labels: + name: temporal diff --git a/k8s/temporal/01-postgres-statefulset.yaml b/k8s/temporal/01-postgres-statefulset.yaml new file mode 100644 index 0000000..cafe474 --- /dev/null +++ b/k8s/temporal/01-postgres-statefulset.yaml @@ -0,0 +1,128 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: temporal-postgres-pvc + namespace: temporal +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: temporal-postgres-init + namespace: temporal +data: + init.sql: | + CREATE DATABASE temporal; + CREATE DATABASE temporal_visibility; + GRANT ALL PRIVILEGES ON DATABASE temporal TO postgres; + GRANT ALL PRIVILEGES ON DATABASE temporal_visibility TO postgres; + +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: temporal-postgres + namespace: temporal + labels: + app: temporal-postgres +spec: + serviceName: temporal-postgres + replicas: 1 + selector: + matchLabels: + app: temporal-postgres + template: + metadata: + labels: + app: temporal-postgres + spec: + containers: + - name: postgres + image: postgres:15-alpine + ports: + - name: db + containerPort: 5432 + protocol: TCP + env: + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: temporal-postgres-secret + key: password + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + - name: init-scripts + mountPath: /docker-entrypoint-initdb.d + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 500m + memory: 1Gi + livenessProbe: + exec: + command: + - /bin/sh + - -c + - pg_isready -U postgres + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - /bin/sh + - -c + - pg_isready -U postgres + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: init-scripts + configMap: + name: temporal-postgres-init + volumeClaimTemplates: + - metadata: + name: postgres-storage + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 10Gi + +--- +apiVersion: v1 +kind: Service +metadata: + name: temporal-postgres + namespace: temporal + labels: + app: temporal-postgres +spec: + type: ClusterIP + clusterIP: None # Headless service for StatefulSet + ports: + - port: 5432 + targetPort: 5432 + protocol: TCP + name: db + selector: + app: temporal-postgres + +--- +apiVersion: v1 +kind: Secret +metadata: + name: temporal-postgres-secret + namespace: temporal +type: Opaque +stringData: + password: "temporal-password-changeme" diff --git a/k8s/temporal/02-elasticsearch-statefulset.yaml b/k8s/temporal/02-elasticsearch-statefulset.yaml new file mode 100644 index 0000000..429494c --- /dev/null +++ b/k8s/temporal/02-elasticsearch-statefulset.yaml @@ -0,0 +1,101 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: temporal-elasticsearch-pvc + namespace: temporal +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi + +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: temporal-elasticsearch + namespace: temporal + labels: + app: temporal-elasticsearch +spec: + serviceName: temporal-elasticsearch + replicas: 1 + selector: + matchLabels: + app: temporal-elasticsearch + template: + metadata: + labels: + app: temporal-elasticsearch + spec: + containers: + - name: elasticsearch + image: docker.elastic.co/elasticsearch/elasticsearch:7.10.0 + ports: + - name: http + containerPort: 9200 + protocol: TCP + - name: transport + containerPort: 9300 + protocol: TCP + env: + - name: discovery.type + value: single-node + - name: ES_JAVA_OPTS + value: "-Xms512m -Xmx512m" + - name: xpack.security.enabled + value: "false" + volumeMounts: + - name: elasticsearch-storage + mountPath: /usr/share/elasticsearch/data + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 500m + memory: 1Gi + livenessProbe: + httpGet: + path: /_cluster/health + port: 9200 + initialDelaySeconds: 60 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /_cluster/health + port: 9200 + initialDelaySeconds: 30 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: elasticsearch-storage + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 20Gi + +--- +apiVersion: v1 +kind: Service +metadata: + name: temporal-elasticsearch + namespace: temporal + labels: + app: temporal-elasticsearch +spec: + type: ClusterIP + clusterIP: None # Headless service for StatefulSet + ports: + - port: 9200 + targetPort: 9200 + protocol: TCP + name: http + - port: 9300 + targetPort: 9300 + protocol: TCP + name: transport + selector: + app: temporal-elasticsearch diff --git a/k8s/temporal/03-temporal-server-statefulset.yaml b/k8s/temporal/03-temporal-server-statefulset.yaml new file mode 100644 index 0000000..4332a10 --- /dev/null +++ b/k8s/temporal/03-temporal-server-statefulset.yaml @@ -0,0 +1,208 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: temporal-server-config + namespace: temporal +data: + config.yaml: | + log: + stdout: true + level: info + + persistence: + defaultStore: postgres + visibilityStore: postgres + numHistoryShards: 4 + storeType: postgres + postgres: + user: "postgres" + password: "temporal-password-changeme" + host: "temporal-postgres.temporal.svc.cluster.local" + port: 5432 + maxConns: 20 + maxIdleConns: 20 + maxConnLifetime: 0 + + visibilityDbStore: postgres + visibilityPersistencePostgres: + user: "postgres" + password: "temporal-password-changeme" + host: "temporal-postgres.temporal.svc.cluster.local" + port: 5432 + dbName: temporal_visibility + maxConns: 10 + maxIdleConns: 10 + maxConnLifetime: 0 + + elasticsearch: + url: "http://temporal-elasticsearch.temporal.svc.cluster.local:9200" + version: "7" + indices: + visibility: temporal_visibility_v1 + + global: + membership: + maxJoinDuration: 30s + broadcastAddress: temporal-server-0.temporal-server.temporal.svc.cluster.local + port: 7946 + + services: + frontend: + rpc: + grpcPort: 7233 + membershipPort: 7946 + bindOnLocalHost: false + matching: + rpc: + grpcPort: 7235 + membershipPort: 7946 + bindOnLocalHost: false + history: + rpc: + grpcPort: 7234 + membershipPort: 7946 + bindOnLocalHost: false + worker: + rpc: + grpcPort: 7239 + membershipPort: 7946 + bindOnLocalHost: false + +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: temporal-server + namespace: temporal + labels: + app: temporal-server +spec: + serviceName: temporal-server + replicas: 1 + selector: + matchLabels: + app: temporal-server + template: + metadata: + labels: + app: temporal-server + spec: + containers: + - name: temporal + image: temporalio/auto-setup:1.20.0 + imagePullPolicy: IfNotPresent + ports: + - name: frontend + containerPort: 7233 + protocol: TCP + - name: matching + containerPort: 7235 + protocol: TCP + - name: history + containerPort: 7234 + protocol: TCP + - name: worker + containerPort: 7239 + protocol: TCP + - name: metrics + containerPort: 9090 + protocol: TCP + env: + - name: TEMPORAL_STORE_ENGINE + value: "postgres" + - name: POSTGRES_USER + value: "postgres" + - name: POSTGRES_PWD + valueFrom: + secretKeyRef: + name: temporal-postgres-secret + key: password + - name: POSTGRES_SEEDS + value: "temporal-postgres.temporal.svc.cluster.local" + - name: POSTGRES_PORT + value: "5432" + - name: DB + value: temporal + - name: VISIBILITY_DB + value: temporal_visibility + - name: ELASTICSEARCH_SEEDS + value: "temporal-elasticsearch.temporal.svc.cluster.local" + - name: ELASTICSEARCH_PORT + value: "9200" + - name: ELASTICSEARCH_VERSION + value: "7" + - name: TEMPORAL_NAMESPACE_DOMAIN + value: "default" + volumeMounts: + - name: temporal-config + mountPath: /etc/temporal + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 1000m + memory: 2Gi + livenessProbe: + tcpSocket: + port: 7233 + initialDelaySeconds: 60 + periodSeconds: 10 + readinessProbe: + tcpSocket: + port: 7233 + initialDelaySeconds: 30 + periodSeconds: 5 + volumes: + - name: temporal-config + configMap: + name: temporal-server-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: temporal-server + namespace: temporal + labels: + app: temporal-server +spec: + type: ClusterIP + clusterIP: None # Headless service for StatefulSet + ports: + - port: 7233 + targetPort: 7233 + protocol: TCP + name: frontend + - port: 7235 + targetPort: 7235 + protocol: TCP + name: matching + - port: 7234 + targetPort: 7234 + protocol: TCP + name: history + - port: 7239 + targetPort: 7239 + protocol: TCP + name: worker + selector: + app: temporal-server + +--- +apiVersion: v1 +kind: Service +metadata: + name: temporal-frontend + namespace: temporal + labels: + app: temporal-server +spec: + type: ClusterIP + ports: + - port: 7233 + targetPort: 7233 + protocol: TCP + name: frontend + selector: + app: temporal-server diff --git a/k8s/temporal/04-temporal-ui-deployment.yaml b/k8s/temporal/04-temporal-ui-deployment.yaml new file mode 100644 index 0000000..49cdb76 --- /dev/null +++ b/k8s/temporal/04-temporal-ui-deployment.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: temporal-ui + namespace: temporal + labels: + app: temporal-ui +spec: + replicas: 1 + selector: + matchLabels: + app: temporal-ui + template: + metadata: + labels: + app: temporal-ui + spec: + containers: + - name: ui + image: temporalio/ui:2.10.0 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + protocol: TCP + env: + - name: TEMPORAL_ADDRESS + value: "temporal-frontend.temporal.svc.cluster.local:7233" + - name: TEMPORAL_CORS_ORIGINS + value: "http://localhost:3000" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 5 + +--- +apiVersion: v1 +kind: Service +metadata: + name: temporal-ui + namespace: temporal + labels: + app: temporal-ui +spec: + type: ClusterIP + ports: + - port: 3000 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: temporal-ui + +--- +apiVersion: v1 +kind: Service +metadata: + name: temporal-ui-external + namespace: temporal + labels: + app: temporal-ui +spec: + type: LoadBalancer + ports: + - port: 3000 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: temporal-ui diff --git a/k8s/temporal/README.md b/k8s/temporal/README.md new file mode 100644 index 0000000..c3bb9e9 --- /dev/null +++ b/k8s/temporal/README.md @@ -0,0 +1,126 @@ +# Temporal Server Deployment for Poimen Agent + +## Phase 1.1: Temporal Infrastructure + +This directory contains Kubernetes manifests for deploying Temporal Server with all required backends. + +### Components + +1. **PostgreSQL StatefulSet** (01-postgres-statefulset.yaml) + - Persistent storage for event log + - Two databases: `temporal` (events) + `temporal_visibility` + - PVC: 10Gi + - Health checks: liveness + readiness + - Port: 5432 + +2. **Elasticsearch StatefulSet** (02-elasticsearch-statefulset.yaml) + - Search engine for workflow visibility + - Single-node cluster + - PVC: 20Gi + - Port: 9200 (HTTP), 9300 (transport) + - Health checks: HTTP GET /_cluster/health + +3. **Temporal Server StatefulSet** (03-temporal-server-statefulset.yaml) + - Main Temporal server instance + - Image: temporalio/auto-setup:1.20.0 + - Services: + - Frontend: 7233 (gRPC) + - Matching: 7235 (internal) + - History: 7234 (internal) + - Worker: 7239 (internal) + - Headless service for StatefulSet communication + - ClusterIP service for worker connections + +4. **Temporal UI Deployment** (04-temporal-ui-deployment.yaml) + - Web UI for workflow visualization + - Image: temporalio/ui:2.10.0 + - Connects to: temporal-frontend:7233 + - Port: 3000 (internal), 3000 (external LoadBalancer) + +### Deployment + +```bash +# Deploy all Temporal components +kubectl apply -k k8s/temporal/ + +# Wait for StatefulSets to be ready +kubectl wait --for=condition=ready pod -l app=temporal-server -n temporal --timeout=300s + +# Verify deployment +kubectl get all -n temporal + +# Port forward to Temporal UI +kubectl port-forward -n temporal svc/temporal-ui-external 3000:3000 +# Access at http://localhost:3000 +``` + +### Persistence + +- PostgreSQL: 10Gi PVC for event log + visibility +- Elasticsearch: 20Gi PVC for search index +- Both use dynamic provisioning (PersistentVolumeClaim) + +### Security Considerations + +1. PostgreSQL password in Secret: `temporal-postgres-secret` + - Default: "temporal-password-changeme" + - **Must be changed for production** + +2. Elasticsearch security disabled (xpack.security.enabled: false) + - **Must be enabled for production** + +3. Services use ClusterIP (internal only) + - Temporal UI exposed via LoadBalancer for demo + - **Should use Ingress for production** + +### Health Checks + +- PostgreSQL: `pg_isready` liveness + readiness +- Elasticsearch: HTTP GET to /_cluster/health +- Temporal Server: TCP socket probe to port 7233 +- Temporal UI: HTTP GET to / (port 8080) + +### Monitoring + +Temporal Server exports Prometheus metrics on port 9090: +```bash +kubectl port-forward -n temporal svc/temporal-server 9090:9090 +# Metrics available at http://localhost:9090/metrics +``` + +### Troubleshooting + +```bash +# Check Temporal Server logs +kubectl logs -n temporal -f statefulset/temporal-server + +# Check PostgreSQL logs +kubectl logs -n temporal -f statefulset/temporal-postgres + +# Check Elasticsearch logs +kubectl logs -n temporal -f statefulset/temporal-elasticsearch + +# Check Temporal UI logs +kubectl logs -n temporal -f deployment/temporal-ui + +# Debug connectivity +kubectl run -it --rm debug --image=alpine --restart=Never -n temporal -- sh +# Inside pod: +# apk add postgresql-client +# psql -h temporal-postgres -U postgres -d temporal +# apk add curl +# curl http://temporal-elasticsearch:9200/_cluster/health +``` + +### Next Phase (1.2) + +After Temporal deployment is verified: +1. Add Temporal Rust SDK to project +2. Create worker registration +3. Setup task queue polling + +--- + +**Status**: Phase 1.1 Implementation ✅ +**Created**: 2025-01-30 +**Effort**: 150 LOC (manifests) diff --git a/k8s/temporal/kustomization.yaml b/k8s/temporal/kustomization.yaml new file mode 100644 index 0000000..0e050ba --- /dev/null +++ b/k8s/temporal/kustomization.yaml @@ -0,0 +1,19 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: temporal + +resources: + - 00-namespace.yaml + - 01-postgres-statefulset.yaml + - 02-elasticsearch-statefulset.yaml + - 03-temporal-server-statefulset.yaml + - 04-temporal-ui-deployment.yaml + +commonLabels: + app.kubernetes.io/name: temporal + app.kubernetes.io/part-of: poimen-agent + +commonAnnotations: + phase: "1.1" + component: "temporal-infrastructure"