From 848eb83f835064cd0bc76accb6173c0935c1fc4f Mon Sep 17 00:00:00 2001 From: "Andrew M. Stoltz" <1578013+astoltz@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:02:08 -0500 Subject: [PATCH] Deploy FlowerCore.Telephony: Blazor+REST+Twilio IVR - Local container image (fc-telephony-web:latest) on all 3 RKE2 nodes - 1Password OnePasswordItem for Twilio credentials (optional: true) - Cloudflare origin cert for telephony.iamwork.in - Piper TTS egress to edge1:8500 - SQLite with 5Gi Longhorn PVC - NetworkPolicy: Traefik ingress, DNS, TTS, Twilio API egress --- apps/telephony/telephony.yaml | 241 ++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 apps/telephony/telephony.yaml diff --git a/apps/telephony/telephony.yaml b/apps/telephony/telephony.yaml new file mode 100644 index 0000000..53da5f5 --- /dev/null +++ b/apps/telephony/telephony.yaml @@ -0,0 +1,241 @@ +# FlowerCore.Telephony - Blazor Server + REST API + Twilio IVR +# ArgoCD managed - BlueJay Lab +# Credentials: 1Password → OnePasswordItem CRD → K8s Secret (twilio-credentials) +# TTS: Piper on edge1 (10.0.57.15:8500) +# Public: telephony.iamwork.in via Cloudflare origin cert +--- +apiVersion: v1 +kind: Namespace +metadata: + name: telephony + labels: + app.kubernetes.io/part-of: bluejay-infra +--- +# Cloudflare Origin Certificate for *.iamwork.in (15-year RSA) +apiVersion: v1 +kind: Secret +metadata: + name: cf-origin-iamwork-in + namespace: telephony +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVvRENDQTRpZ0F3SUJBZ0lVSXN4c1NKV1VRL0tqZ09ldk81YnNuVi9rZVE4d0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZc3hDekFKQmdOVkJBWVRBbFZUTVJrd0Z3WURWUVFLRXhCRGJHOTFaRVpzWVhKbExDQkpibU11TVRRdwpNZ1lEVlFRTEV5dERiRzkxWkVac1lYSmxJRTl5YVdkcGJpQlRVMHdnUTJWeWRHbG1hV05oZEdVZ1FYVjBhRzl5CmFYUjVNUll3RkFZRFZRUUhFdzFUWVc0Z1JuSmhibU5wYzJOdk1STXdFUVlEVlFRSUV3cERZV3hwWm05eWJtbGgKTUI0WERUSTJNRE14TURFMk16TXdNRm9YRFRReE1ETXdOakUyTXpNd01Gb3dZakVaTUJjR0ExVUVDaE1RUTJ4dgpkV1JHYkdGeVpTd2dTVzVqTGpFZE1Cc0dBMVVFQ3hNVVEyeHZkV1JHYkdGeVpTQlBjbWxuYVc0Z1EwRXhKakFrCkJnTlZCQU1USFVOc2IzVmtSbXhoY21VZ1QzSnBaMmx1SUVObGNuUnBabWxqWVhSbE1JSUJJakFOQmdrcWhraUcKOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXV0QmpkQ0xEdHdMQlZCU0Y1ZU1OMkt3ckIxTmZmRVhRMjlRRAo1aVR0dzJFcEZXNVJJSllkMjNrYUpCMU5jZXpHWlg4a0Q0cGEyWHpFZW1MVEtJNWw0MU11b3FoWjczNVE3U3RWCkVjRFFTT2ZYTkZQdFMwb0hqb0pRdGF2QjM0ZmJNR3l4Mmx0MU9HUzRNMGtLUWpBNWR6OTJQYjNyZ1RKR0JhOW4KeTZtVThncjRuUHRSdklxZ3NxdjRtMFA3dVU1YjE3NzU1Y2JLSDVoMzIxWHVjMDU4Tzl4M2JHQ0NuRUJXWDdqeApjRGhkUEs1Ri9XRjVBQnl5cFhIQ0ZxUUd4M1NVbmtCQ0ZQSmRabnMra3BHVUZWZGhud3B6NjBtNnlJSzQ0eVR4CjZqR3JOTFEyM1dOK2gwU1lCZU5vb2JBWThydkpiVlZEaGJqSVhBTWtFNGQzVll1TlhRSURBUUFCbzRJQklqQ0MKQVI0d0RnWURWUjBQQVFIL0JBUURBZ1dnTUIwR0ExVWRKUVFXTUJRR0NDc0dBUVVGQndNQ0JnZ3JCZ0VGQlFjRApBVEFNQmdOVkhSTUJBZjhFQWpBQU1CMEdBMVVkRGdRV0JCUkt1NkJVUDZ0N2dpbFRPay9FdEdKQ3R6N3dTREFmCkJnTlZIU01FR0RBV2dCUWs2Rk5YWFh3MFFJZXA2NVRidXVFV2VQd3BwREJBQmdnckJnRUZCUWNCQVFRME1ESXcKTUFZSUt3WUJCUVVITUFHR0pHaDBkSEE2THk5dlkzTndMbU5zYjNWa1pteGhjbVV1WTI5dEwyOXlhV2RwYmw5agpZVEFqQmdOVkhSRUVIREFhZ2d3cUxtbGhiWGR2Y21zdWFXNkNDbWxoYlhkdmNtc3VhVzR3T0FZRFZSMGZCREV3Ckx6QXRvQ3VnS1lZbmFIUjBjRG92TDJOeWJDNWpiRzkxWkdac1lYSmxMbU52YlM5dmNtbG5hVzVmWTJFdVkzSnMKTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDSjMvTGNleE5pb0lWdUxoemhmbTZCeDV2SWk3T25CaHF1WUlDdwplNnArZ0prdE16ZFJQcDV0bk03dllBWmxMajVJOTByWDRuczhJc3dEbzJBN2wwYTRGZVJFclFmRklsZXQzbjIyCjUxVTZYVElCSks5c1FZT0FkU3pJUzV1OUNKSFpBUTF5WmxSd3BBR3RVWnhxL1dpcGFWUTRwNXhrcEJNMVlZSlAKNW1jQ09HcFErSnpORlpQc2daYUJncDBYL1BBZkNJRkkyZld5QWE2elBqRm0rdDVXUXIrZlBaT2VUS2VIbWVzVgo3UlZxUUdEb3Q0eTY1NklEdmdmU2ZLRnFIRW9XNDJVbDBxQ05hMS9keEJld3NIS1VWWE1ETkdiQlNVQjM4TG9YCm1OQ3hJQlVOUjR0TG1CQUxZT3hVMnZhSWRCd0xBc2YrcndnVnVjUGpCUTc2VWMwUQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzYwR04wSXNPM0FzRlUKRklYbDR3M1lyQ3NIVTE5OFJkRGIxQVBtSk8zRFlTa1ZibEVnbGgzYmVSb2tIVTF4N01abGZ5UVBpbHJaZk1SNgpZdE1vam1YalV5NmlxRm52ZmxEdEsxVVJ3TkJJNTljMFUrMUxTZ2VPZ2xDMXE4SGZoOXN3YkxIYVczVTRaTGd6ClNRcENNRGwzUDNZOXZldUJNa1lGcjJmTHFaVHlDdmljKzFHOGlxQ3lxL2liUS91NVRsdlh2dm5seHNvZm1IZmIKVmU1elRudzczSGRzWUlLY1FGWmZ1UEZ3T0YwOHJrWDlZWGtBSExLbGNjSVdwQWJIZEpTZVFFSVU4bDFtZXo2UwprWlFWVjJHZkNuUHJTYnJJZ3JqakpQSHFNYXMwdERiZFkzNkhSSmdGNDJpaHNCanl1OGx0VlVPRnVNaGNBeVFUCmgzZFZpNDFkQWdNQkFBRUNnZ0VBTGlseXZkNmVTcEYvZUxtV2lhTVV4NUxwa2dhWHpITkxCQnNNZUpqcytLL0EKVVdlZ1crTkVUdmlLalZ5QlI5SzRocG1IYldDa2lPUDBBQUwrQnlKQ3lvekNOQmJTSEdRejlwc1R5dzZBV1ZlUwpuYjlVWGx1VmFQRktKTTRqbXNydERuYjVic25WT2lGblErTDdTalkwNlFMUlFybjBvUWp0ZFJldUdBMFlQVU90CkhSYzNsMFg2ZHJqdkJYY2prWTQwWm9ZYkRrelJnU1JWbWVOUGFIbjZPR0NtYUVUMXVyK01qYVZ2ME9lbEdIWncKVzljSEIxaHNxRzUvMWU3V0RQN0l0cjkwTmg4ay81NVhiK3lQUnhsRFd5bWtZMzIvdFBtZzdESTRKV2tRRWt3cgpIZUtwODVTcE5ta1liRnVpVFppeU8zZDZ0aXZHNHhFZW8rSzFVVFU4c1FLQmdRRFRNSEU1RDFYVC9HbGR5VHNsCllrODRVL1N0NXUrK2RIUEt1Wmw2dVB0UGgxV1lrdnFRcmdrL05YanVud2xGN0Y3b2tWOGdPeWxreTYwYTZkcXIKeXZwN1ZJdXYzekVlc2h2NjNWMlpaVkMzcXZYSzFheit3Zmx3NitCZmVuRlY5S2NENHN0dTdwOFRPWmFGN01CUgo3YXZzaXVXbWtqdmM1TlVLRmVDRTY0SnZFUUtCZ1FEaWMrbWlNLzBodDN1ajhuOXgyMDFQZFNqbEpVaUc1NjNNCnRYZlBCdDJRT0NhaVluUFNFdTdXdm5pQWRFL2xrMm91cFRWam9LYmZPbDFyQjd6UzVhc2kxdVdDZDhlUy9UWGIKdU5iRmlNMDB4L3JxalMydCtQbTd4MVhrYTB4TFNSRDNmZ0tSQldSN3pscStkYWZ1WE1qelUxRnh5dTIycGphRgpIMEl3NEpCUmpRS0JnUUNOaWhMb0Rob1V5RCtKNXJzb00vb3FJMEtDWnB0WlJzendHbkg5cVFwdFk2Ti9iVXBYCk92emhpeUh3czAvUXVEbG5uejVrNktHMmR6Y2VLWXN2eGdzWUt6S3ZmV043VWgya2hVWWM3NlVvWTREMkh6MGgKUkxtNzc2cGg4enNRUTdiSHlQRlUrTUpPYlRNdnNOdTRUUlVEcEplRGl0QnFIRWVYeWMrKzVlUjJNUUtCZ0h2UgptVHVoWlpVYitEVEtrVGkyQ20yWnlBU1RBRGNUVW9xTjVyYUNNSDk4MUZNUnRmWjFkN1pmYXhBQmlQWWtSbmkrCnlKUnk4UXM1cEg2ek9tR3VSb2JFTGJYS3ZJcjRmSXhwWXJXYmVXaVV0L09yd2dCUUZHekNMNHEzeUgyWnMvYy8KSlRRYVdMa0JPY2pPR0VaUzRXVjZkeHZiTTJNZE9zNUxLeXdDZmFhNUFvR0FIQUE1eEN0dndOZE4xeExndkZ3RApPK2lyMDl1bXMxOFBzSVpmK1ZrWGtpcHF4MWNUT0hEanpPR01yWXV0M2FFeE00Zjd2ckFHRFMyY2pwZjM0T1JxCit4Y2gwWlNaQ2FDZmlnZG9OelNkcDFLcmo0cnFKdG5ZdS9CNDlDQlVoSDBNaCtSRWswQ0hHOVE4b3FOWFk0V0wKbVVOVTZMYUkwQWtvSzNVb2tWQVJEYXM9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K +--- +# 1Password → K8s Secret sync for Twilio credentials +# Creates secret "twilio-credentials" with fields: AccountSid, AuthToken, DefaultFromNumber +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: twilio-credentials + namespace: telephony +spec: + itemPath: "vaults/IAmWorkin/items/Twilio Account" +--- +# Application configuration overlay +apiVersion: v1 +kind: ConfigMap +metadata: + name: telephony-config + namespace: telephony +data: + appsettings.Production.json: | + { + "Telephony": { + "Provider": "twilio", + "Twilio": { + "VoiceUrl": "https://telephony.iamwork.in/api/twilio/webhooks/voice/incoming", + "StatusCallbackUrl": "https://telephony.iamwork.in/api/twilio/webhooks/voice/status" + } + }, + "Tts": { + "PiperUrl": "http://10.0.57.15:8500", + "DefaultEngine": "piper", + "SampleRate": 8000 + }, + "DatabaseProvider": "Sqlite", + "ConnectionStrings": { + "DefaultConnection": "Data Source=/data/telephony.db" + }, + "Kestrel": { + "Endpoints": { + "Http": { "Url": "http://0.0.0.0:5100" } + } + } + } +--- +# Persistent volume for SQLite database +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: telephony-data + namespace: telephony +spec: + accessModes: [ReadWriteOnce] + resources: + requests: + storage: 5Gi +--- +# Telephony web application +apiVersion: apps/v1 +kind: Deployment +metadata: + name: telephony-web + namespace: telephony + labels: + app: telephony-web +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: telephony-web + template: + metadata: + labels: + app: telephony-web + spec: + containers: + - name: telephony-web + image: localhost/fc-telephony-web:latest + imagePullPolicy: Never + ports: + - containerPort: 5100 + name: http + env: + - name: Telephony__Twilio__AccountSid + valueFrom: + secretKeyRef: + name: twilio-credentials + key: AccountSid + optional: true + - name: Telephony__Twilio__AuthToken + valueFrom: + secretKeyRef: + name: twilio-credentials + key: AuthToken + optional: true + - name: Telephony__Twilio__DefaultFromNumber + valueFrom: + secretKeyRef: + name: twilio-credentials + key: DefaultFromNumber + optional: true + volumeMounts: + - name: telephony-config + mountPath: /app/appsettings.Production.json + subPath: appsettings.Production.json + readOnly: true + - name: telephony-data + mountPath: /data + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 1Gi + cpu: "1" + livenessProbe: + httpGet: + path: /health + port: 5100 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 5100 + initialDelaySeconds: 10 + periodSeconds: 5 + volumes: + - name: telephony-config + configMap: + name: telephony-config + - name: telephony-data + persistentVolumeClaim: + claimName: telephony-data +--- +# ClusterIP service +apiVersion: v1 +kind: Service +metadata: + name: telephony-web + namespace: telephony +spec: + selector: + app: telephony-web + ports: + - port: 5100 + targetPort: 5100 + name: http +--- +# Traefik IngressRoute — public via Cloudflare +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: telephony-web + namespace: telephony +spec: + entryPoints: + - websecure + routes: + - kind: Rule + match: Host(`telephony.iamwork.in`) + services: + - name: telephony-web + port: 5100 + tls: + secretName: cf-origin-iamwork-in +--- +# NetworkPolicy: deny-all baseline + Traefik ingress + DNS egress + edge1 TTS egress +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: telephony-netpol + namespace: telephony +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress + ingress: + # Allow Traefik ingress controller + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik-system + egress: + # Allow DNS resolution (CoreDNS in kube-system) + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + # Allow Piper TTS on edge1 (10.0.57.15:8500) + - to: + - ipBlock: + cidr: 10.0.57.15/32 + ports: + - port: 8500 + protocol: TCP + # Allow Twilio API outbound (HTTPS) + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + ports: + - port: 443 + protocol: TCP + # Allow 1Password Connect for secret sync + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: onepassword-system