484 lines
16 KiB
YAML
484 lines
16 KiB
YAML
---
|
|
# Deploy Supabase on OpenShift via the supabase-community Helm chart.
|
|
#
|
|
# Prerequisites:
|
|
# - KUBECONFIG set in environment (or ocp_kubeconfig host var)
|
|
# - cert-manager installed with a ClusterIssuer matching supabase_tls_cluster_issuer
|
|
# - A ReadWriteOnce-capable StorageClass set via supabase_storage_class
|
|
# - Vault variables defined (see meta/argument_specs.yml for the full list)
|
|
#
|
|
# Architecture:
|
|
# - Kong API gateway is the single ingress point (Route: supabase_studio_host)
|
|
# - Studio dashboard is served through Kong
|
|
# - DB runs as root — anyuid SCC granted to the db ServiceAccount
|
|
# - Vector reads /var/log/pods — privileged SCC granted to vector SA
|
|
# - All secrets are created as K8s Secrets and referenced via secretRef
|
|
#
|
|
# OpenShift SCC notes:
|
|
# - supabase-db ServiceAccount → anyuid SCC (postgres runs as root)
|
|
# - supabase-vector ServiceAccount → privileged SCC (hostPath to /var/log/pods)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Step 0: Resolve secrets from Vault (generate and store if missing)
|
|
# ------------------------------------------------------------------
|
|
- name: Resolve Supabase secrets from Vault
|
|
ansible.builtin.include_tasks: vault_secrets.yml
|
|
|
|
# ------------------------------------------------------------------
|
|
# Step 1: Namespace
|
|
# ------------------------------------------------------------------
|
|
- name: Create Supabase namespace
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: "{{ supabase_namespace }}"
|
|
|
|
# ------------------------------------------------------------------
|
|
# Step 2: SCC grants (ClusterRoleBindings before helm so pods start clean)
|
|
# ------------------------------------------------------------------
|
|
- name: Create ClusterRole granting anyuid SCC (supabase db)
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRole
|
|
metadata:
|
|
name: supabase-db-anyuid-scc
|
|
rules:
|
|
- apiGroups: [security.openshift.io]
|
|
resources: [securitycontextconstraints]
|
|
verbs: [use]
|
|
resourceNames: [anyuid]
|
|
|
|
- name: Bind anyuid SCC to supabase db ServiceAccount
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRoleBinding
|
|
metadata:
|
|
name: supabase-db-anyuid-scc
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: "{{ supabase_db_fullname }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
roleRef:
|
|
kind: ClusterRole
|
|
name: supabase-db-anyuid-scc
|
|
apiGroup: rbac.authorization.k8s.io
|
|
|
|
- name: Create ClusterRole granting anyuid SCC (supabase kong)
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRole
|
|
metadata:
|
|
name: supabase-kong-anyuid-scc
|
|
rules:
|
|
- apiGroups: [security.openshift.io]
|
|
resources: [securitycontextconstraints]
|
|
verbs: [use]
|
|
resourceNames: [anyuid]
|
|
|
|
- name: Bind anyuid SCC to supabase kong ServiceAccount
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRoleBinding
|
|
metadata:
|
|
name: supabase-kong-anyuid-scc
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: "{{ supabase_kong_fullname }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
roleRef:
|
|
kind: ClusterRole
|
|
name: supabase-kong-anyuid-scc
|
|
apiGroup: rbac.authorization.k8s.io
|
|
|
|
- name: Create ClusterRole granting anyuid SCC (supabase functions)
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRole
|
|
metadata:
|
|
name: supabase-functions-anyuid-scc
|
|
rules:
|
|
- apiGroups: [security.openshift.io]
|
|
resources: [securitycontextconstraints]
|
|
verbs: [use]
|
|
resourceNames: [anyuid]
|
|
|
|
- name: Bind anyuid SCC to supabase functions ServiceAccount
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRoleBinding
|
|
metadata:
|
|
name: supabase-functions-anyuid-scc
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: "{{ supabase_functions_fullname }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
roleRef:
|
|
kind: ClusterRole
|
|
name: supabase-functions-anyuid-scc
|
|
apiGroup: rbac.authorization.k8s.io
|
|
|
|
- name: Create ClusterRole granting privileged SCC (supabase vector)
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRole
|
|
metadata:
|
|
name: supabase-vector-privileged-scc
|
|
rules:
|
|
- apiGroups: [security.openshift.io]
|
|
resources: [securitycontextconstraints]
|
|
verbs: [use]
|
|
resourceNames: [privileged]
|
|
|
|
- name: Bind privileged SCC to supabase vector ServiceAccount
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: ClusterRoleBinding
|
|
metadata:
|
|
name: supabase-vector-privileged-scc
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: "{{ supabase_vector_fullname }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
roleRef:
|
|
kind: ClusterRole
|
|
name: supabase-vector-privileged-scc
|
|
apiGroup: rbac.authorization.k8s.io
|
|
|
|
# ------------------------------------------------------------------
|
|
# Step 3: Kubernetes Secrets (no_log — referenced by helm via secretRef)
|
|
# ------------------------------------------------------------------
|
|
- name: Create JWT secret
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: "{{ __supabase_secret_jwt }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
type: Opaque
|
|
stringData:
|
|
anonKey: "{{ __supabase_anon_key }}"
|
|
serviceKey: "{{ __supabase_service_key }}"
|
|
secret: "{{ __supabase_jwt_secret }}"
|
|
no_log: "{{ supabase_no_log }}"
|
|
|
|
- name: Create DB secret
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: "{{ __supabase_secret_db }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
type: Opaque
|
|
stringData:
|
|
password: "{{ __supabase_db_password }}"
|
|
database: "{{ supabase_db_name }}"
|
|
no_log: "{{ supabase_no_log }}"
|
|
|
|
- name: Create dashboard secret
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: "{{ __supabase_secret_dashboard }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
type: Opaque
|
|
stringData:
|
|
username: "{{ __supabase_dashboard_username }}"
|
|
password: "{{ __supabase_dashboard_password }}"
|
|
openAiApiKey: "{{ __supabase_openai_api_key }}"
|
|
no_log: "{{ supabase_no_log }}"
|
|
|
|
- name: Create analytics secret
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: "{{ __supabase_secret_analytics }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
type: Opaque
|
|
stringData:
|
|
publicAccessToken: "{{ __supabase_analytics_public_token }}"
|
|
privateAccessToken: "{{ __supabase_analytics_private_token }}"
|
|
no_log: "{{ supabase_no_log }}"
|
|
|
|
- name: Create realtime secret
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: "{{ __supabase_secret_realtime }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
type: Opaque
|
|
stringData:
|
|
secretKeyBase: "{{ __supabase_realtime_secret_key_base }}"
|
|
no_log: "{{ supabase_no_log }}"
|
|
|
|
- name: Create meta secret
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: "{{ __supabase_secret_meta }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
type: Opaque
|
|
stringData:
|
|
cryptoKey: "{{ __supabase_meta_crypto_key }}"
|
|
no_log: "{{ supabase_no_log }}"
|
|
|
|
- name: Create SMTP secret
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: "{{ __supabase_secret_smtp }}"
|
|
namespace: "{{ supabase_namespace }}"
|
|
type: Opaque
|
|
stringData:
|
|
username: "{{ vault_supabase_smtp_username | default('') }}" # set in host_vars
|
|
password: "{{ vault_supabase_smtp_password | default('') }}" # set in host_vars
|
|
no_log: "{{ supabase_no_log }}"
|
|
|
|
# ------------------------------------------------------------------
|
|
# Step 4: Optional Docker Hub pull secret
|
|
# Avoids anonymous rate limiting for docker.io images (kong, vector, etc.)
|
|
# ------------------------------------------------------------------
|
|
- name: Create Docker Hub pull secret
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: supabase-dockerhub
|
|
namespace: "{{ supabase_namespace }}"
|
|
type: kubernetes.io/dockerconfigjson
|
|
data:
|
|
.dockerconfigjson: >-
|
|
{{ {'auths': {'https://index.docker.io/v1/': {
|
|
'username': supabase_dockerhub_username,
|
|
'password': vault_supabase_dockerhub_token,
|
|
'auth': (supabase_dockerhub_username + ':' + vault_supabase_dockerhub_token) | b64encode
|
|
}}} | to_json | b64encode }}
|
|
no_log: "{{ supabase_no_log }}"
|
|
when: supabase_dockerhub_enabled | bool
|
|
|
|
# ------------------------------------------------------------------
|
|
# Step 5: Helm install / upgrade
|
|
# ------------------------------------------------------------------
|
|
- name: Deploy Supabase via Helm
|
|
kubernetes.core.helm:
|
|
release_name: "{{ supabase_release_name }}"
|
|
chart_ref: supabase
|
|
chart_repo_url: "{{ supabase_chart_repo_url }}"
|
|
chart_version: "{{ supabase_chart_version | default(omit, true) }}"
|
|
release_namespace: "{{ supabase_namespace }}"
|
|
create_namespace: false
|
|
wait: false
|
|
values:
|
|
# Use predictable names so SCC bindings and Route target can reference them
|
|
deployment:
|
|
db:
|
|
fullnameOverride: "{{ supabase_db_fullname }}"
|
|
kong:
|
|
fullnameOverride: "{{ supabase_kong_fullname }}"
|
|
functions:
|
|
fullnameOverride: "{{ supabase_functions_fullname }}"
|
|
vector:
|
|
fullnameOverride: "{{ supabase_vector_fullname }}"
|
|
|
|
# Secrets via secretRef — keep secret material out of Helm values
|
|
secret:
|
|
jwt:
|
|
secretRef: "{{ __supabase_secret_jwt }}"
|
|
db:
|
|
secretRef: "{{ __supabase_secret_db }}"
|
|
dashboard:
|
|
secretRef: "{{ __supabase_secret_dashboard }}"
|
|
analytics:
|
|
secretRef: "{{ __supabase_secret_analytics }}"
|
|
realtime:
|
|
secretRef: "{{ __supabase_secret_realtime }}"
|
|
meta:
|
|
secretRef: "{{ __supabase_secret_meta }}"
|
|
smtp:
|
|
secretRef: "{{ __supabase_secret_smtp }}"
|
|
|
|
# Storage — all PVCs use the configured StorageClass
|
|
persistence:
|
|
db:
|
|
storageClassName: "{{ supabase_storage_class }}"
|
|
size: "{{ supabase_db_storage_size }}"
|
|
functions:
|
|
storageClassName: "{{ supabase_storage_class }}"
|
|
size: "{{ supabase_functions_storage_size }}"
|
|
imgproxy:
|
|
storageClassName: "{{ supabase_storage_class }}"
|
|
size: "{{ supabase_imgproxy_storage_size }}"
|
|
minio:
|
|
storageClassName: "{{ supabase_storage_class }}"
|
|
size: "{{ supabase_minio_storage_size }}"
|
|
|
|
# Explicit docker.io registry prefix on all docker.io images.
|
|
# OpenShift ImageContentSourcePolicy rewrites bare image refs (e.g. kong/kong)
|
|
# to the configured mirror; supplying the full docker.io/ hostname forces the
|
|
# pull against the real registry and lets the pull secret take effect.
|
|
image:
|
|
analytics:
|
|
repository: docker.io/supabase/logflare
|
|
auth:
|
|
repository: docker.io/supabase/gotrue
|
|
db:
|
|
repository: docker.io/supabase/postgres
|
|
functions:
|
|
repository: docker.io/supabase/edge-runtime
|
|
imgproxy:
|
|
repository: docker.io/darthsim/imgproxy
|
|
kong:
|
|
repository: docker.io/kong/kong
|
|
meta:
|
|
repository: docker.io/supabase/postgres-meta
|
|
realtime:
|
|
repository: docker.io/supabase/realtime
|
|
rest:
|
|
repository: docker.io/postgrest/postgrest
|
|
storage:
|
|
repository: docker.io/supabase/storage-api
|
|
studio:
|
|
repository: docker.io/supabase/studio
|
|
vector:
|
|
repository: docker.io/timberio/vector
|
|
|
|
# Docker Hub pull secret (only wired in when enabled)
|
|
imagePullSecrets: "{{ [{'name': 'supabase-dockerhub'}] if supabase_dockerhub_enabled | bool else [] }}"
|
|
|
|
# Disable chart's nginx ingress — we create an OCP Route below
|
|
ingress:
|
|
enabled: false
|
|
|
|
# Studio public URL (used by auth callbacks and the browser)
|
|
environment:
|
|
studio:
|
|
SUPABASE_PUBLIC_URL: "https://{{ supabase_studio_host }}"
|
|
STUDIO_DEFAULT_ORGANIZATION: "{{ supabase_org_name }}"
|
|
STUDIO_DEFAULT_PROJECT: "{{ supabase_project_name }}"
|
|
NEXT_PUBLIC_ENABLE_LOGS: "true"
|
|
|
|
# SMTP
|
|
auth:
|
|
environment:
|
|
GOTRUE_SMTP_HOST: "{{ supabase_smtp_host }}"
|
|
GOTRUE_SMTP_PORT: "{{ supabase_smtp_port | string }}"
|
|
GOTRUE_SMTP_ADMIN_EMAIL: "{{ supabase_smtp_admin_email }}"
|
|
GOTRUE_SMTP_SENDER_NAME: "{{ supabase_smtp_sender_name }}"
|
|
GOTRUE_MAILER_AUTOCONFIRM: "{{ 'false' if supabase_smtp_enabled else 'true' }}"
|
|
|
|
# ------------------------------------------------------------------
|
|
# Step 6: OpenShift Route (edge TLS, cert-manager certificate)
|
|
# ------------------------------------------------------------------
|
|
- name: Create TLS Certificate for Supabase
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: cert-manager.io/v1
|
|
kind: Certificate
|
|
metadata:
|
|
name: supabase-tls
|
|
namespace: "{{ supabase_namespace }}"
|
|
spec:
|
|
secretName: supabase-tls
|
|
issuerRef:
|
|
name: "{{ supabase_tls_cluster_issuer }}"
|
|
kind: ClusterIssuer
|
|
dnsNames:
|
|
- "{{ supabase_studio_host }}"
|
|
|
|
- name: Create OpenShift Route for Supabase (Kong gateway)
|
|
kubernetes.core.k8s:
|
|
state: present
|
|
definition:
|
|
apiVersion: route.openshift.io/v1
|
|
kind: Route
|
|
metadata:
|
|
name: supabase
|
|
namespace: "{{ supabase_namespace }}"
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: "{{ supabase_tls_cluster_issuer }}"
|
|
spec:
|
|
host: "{{ supabase_studio_host }}"
|
|
to:
|
|
kind: Service
|
|
name: "{{ supabase_kong_fullname }}"
|
|
weight: 100
|
|
port:
|
|
targetPort: http
|
|
tls:
|
|
termination: edge
|
|
insecureEdgeTerminationPolicy: Redirect
|
|
certificate: ""
|
|
key: ""
|
|
caCertificate: ""
|
|
|
|
# ------------------------------------------------------------------
|
|
# Step 7: Wait for Kong deployment to be healthy
|
|
# ------------------------------------------------------------------
|
|
- name: Wait for Kong deployment to be ready
|
|
kubernetes.core.k8s_info:
|
|
api_version: apps/v1
|
|
kind: Deployment
|
|
namespace: "{{ supabase_namespace }}"
|
|
name: "{{ supabase_kong_fullname }}"
|
|
register: __supabase_kong_deploy
|
|
until: >-
|
|
__supabase_kong_deploy.resources | length > 0 and
|
|
(__supabase_kong_deploy.resources[0].status.readyReplicas | default(0)) >= 1
|
|
retries: "{{ __supabase_wait_retries }}"
|
|
delay: 10
|
|
|
|
# ------------------------------------------------------------------
|
|
# Step 8: Summary
|
|
# ------------------------------------------------------------------
|
|
- name: Display Supabase deployment summary
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "Supabase deployment complete!"
|
|
- " Namespace : {{ supabase_namespace }}"
|
|
- " Studio URL : https://{{ supabase_studio_host }}"
|
|
- " Kong service : {{ supabase_kong_fullname }}:8000"
|
|
- " DB PVC : {{ supabase_db_storage_size }} ({{ supabase_storage_class }})"
|
|
- " SMTP enabled : {{ supabase_smtp_enabled }}"
|
|
- ""
|
|
- " Login with vault_supabase_dashboard_username / vault_supabase_dashboard_password"
|