feat: add supabase deployer

This commit is contained in:
2026-04-12 20:07:05 -04:00
parent df1dd39197
commit 5fe7b4edeb
8 changed files with 973 additions and 26 deletions

View File

@@ -0,0 +1,483 @@
---
# 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"