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

@@ -333,29 +333,86 @@
- never - never
- sno_deploy_service_accounts - sno_deploy_service_accounts
roles: tasks:
- role: ocp_service_account # ---- supabase-deployer ----
ocp_service_account_name: aap-deployer - name: Provision supabase-deployer ServiceAccount
ocp_service_account_namespace: aap ansible.builtin.include_role:
ocp_service_account_cluster_role_rules: name: ocp_service_account
- apiGroups: [""] vars:
resources: ["namespaces"] ocp_service_account_name: supabase-deployer
verbs: ["get", "list", "create", "patch"] ocp_service_account_namespace: supabase
- apiGroups: [""] ocp_service_account_cluster_role_rules:
resources: ["secrets"] - apiGroups: [""]
verbs: ["get", "list", "watch", "create", "patch"] resources: ["namespaces"]
- apiGroups: [""] verbs: ["get", "list", "create", "patch", "delete"]
resources: ["serviceaccounts"] - apiGroups: [""]
verbs: ["get", "list", "watch"] resources: ["secrets", "configmaps", "services", "serviceaccounts",
- apiGroups: ["apps"] "persistentvolumeclaims", "pods", "endpoints"]
resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
verbs: ["get", "list", "watch"] - apiGroups: ["apps"]
- apiGroups: ["operators.coreos.com"] resources: ["deployments", "statefulsets", "replicasets"]
resources: ["operatorgroups", "subscriptions", "clusterserviceversions"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
verbs: ["get", "list", "create", "patch", "watch"] - apiGroups: ["rbac.authorization.k8s.io"]
- apiGroups: ["apiextensions.k8s.io"] resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
resources: ["customresourcedefinitions"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "escalate", "bind"]
verbs: ["get", "list", "watch"] - apiGroups: ["security.openshift.io"]
- apiGroups: ["aap.ansible.com"] resources: ["securitycontextconstraints"]
resources: ["ansibleautomationplatforms"] verbs: ["get", "list", "use"]
verbs: ["get", "list", "create", "patch", "watch"] - apiGroups: ["route.openshift.io"]
resources: ["routes", "routes/custom-host"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["cert-manager.io"]
resources: ["certificates", "clusterissuers", "issuers"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- name: Store supabase-deployer token in Vault
community.hashi_vault.vault_kv2_write:
path: toallab/supabase-deployer
engine_mount_point: kv
data:
token: "{{ __ocp_service_account_token }}"
no_log: true
# ---- aap-deployer ----
- name: Provision aap-deployer ServiceAccount
ansible.builtin.include_role:
name: ocp_service_account
vars:
ocp_service_account_name: aap-deployer
ocp_service_account_namespace: aap
ocp_service_account_cluster_role_rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "create", "patch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create", "patch"]
- apiGroups: [""]
resources: ["serviceaccounts"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]
- apiGroups: ["operators.coreos.com"]
resources: ["operatorgroups", "subscriptions", "clusterserviceversions"]
verbs: ["get", "list", "create", "patch", "watch"]
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch"]
- apiGroups: ["aap.ansible.com"]
resources: ["ansibleautomationplatforms"]
verbs: ["get", "list", "create", "patch", "watch"]
- name: Store aap-deployer token in Vault
community.hashi_vault.vault_kv2_write:
path: toallab/aap-deployer
engine_mount_point: kv
data:
token: "{{ __ocp_service_account_token }}"
no_log: true

View File

@@ -0,0 +1,44 @@
---
# Deploy Supabase on OpenShift (SNO) via the supabase-community Helm chart.
#
# Chart: https://github.com/supabase-community/supabase-kubernetes
#
# Prerequisites:
# - cert-manager installed (deploy_openshift.yml --tags sno_deploy_certmanager)
# - NFS StorageClass available (deploy_openshift.yml --tags sno_deploy_nfs)
# - supabase-deployer ServiceAccount provisioned and token saved to 1Password:
# ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
# - vault_supabase_deployer_token set in host_vars (see hashivault_secrets.yml)
#
# All Supabase secrets (JWT, DB password, etc.) are auto-generated on first run
# and stored in Vault at supabase_vault_path. No manual secret setup required.
#
# Usage:
# ansible-navigator run playbooks/deploy_supabase.yml --mode stdout
# ansible-navigator run playbooks/deploy_supabase.yml --mode stdout -e supabase_chart_version=0.0.9
# ansible-navigator run playbooks/deploy_supabase.yml --mode stdout -e supabase_db_storage_size=20Gi
- name: Deploy Supabase on OpenShift
hosts: sno.openshift.toal.ca
gather_facts: false
connection: local
environment:
K8S_AUTH_HOST: "{{ supabase_k8s_api_url }}"
K8S_AUTH_API_KEY: "{{ vault_supabase_deployer_token }}"
K8S_AUTH_VERIFY_SSL: "false"
pre_tasks:
- name: Verify supabase-deployer token is available
ansible.builtin.assert:
that:
- vault_supabase_deployer_token is defined
- vault_supabase_deployer_token | length > 0
fail_msg: >-
vault_supabase_deployer_token is not set.
Provision the ServiceAccount with:
ansible-navigator run playbooks/deploy_openshift.yml --tags sno_deploy_service_accounts
Then save the displayed token to 1Password and reference it as vault_supabase_deployer_token.
roles:
- role: supabase

View File

@@ -0,0 +1,65 @@
---
# --- Debugging ---
# Set to false to disable no_log and expose secret values in task output.
# Never set false in production or shared environments.
supabase_no_log: true
# --- Vault ---
# KV2 path where Supabase secrets are stored and retrieved.
# Run with: op run --env-file=~/.ansible.zshenv -- ansible-navigator ...
# so that VAULT_ADDR / VAULT_TOKEN are injected from 1Password.
supabase_vault_mount: kv
supabase_vault_path: supabase
# --- Helm release ---
supabase_release_name: supabase
supabase_namespace: supabase
supabase_chart_repo_url: https://supabase-community.github.io/supabase-kubernetes
supabase_chart_version: "" # blank = latest
# --- External access ---
# Studio and API gateway are both served through Kong on this hostname.
# No generic default — must be set in host_vars or group_vars.
supabase_studio_host: ""
supabase_tls_cluster_issuer: ""
# --- Storage ---
# Empty string uses the cluster's default StorageClass.
supabase_storage_class: ""
supabase_db_storage_size: 10Gi
supabase_functions_storage_size: 1Gi
supabase_imgproxy_storage_size: 1Gi
supabase_minio_storage_size: 20Gi
# --- Database ---
supabase_db_name: postgres
# --- Studio branding ---
supabase_org_name: ""
supabase_project_name: ""
# --- SMTP (set supabase_smtp_enabled: true to configure email auth) ---
supabase_smtp_enabled: false
supabase_smtp_host: ""
supabase_smtp_port: 587
supabase_smtp_admin_email: ""
supabase_smtp_sender_name: Supabase
# --- Internal component name overrides ---
# These become predictable Kubernetes resource names (service, SA, etc.)
# Must match deployment.*.fullnameOverride values passed to the chart
supabase_db_fullname: supabase-db
supabase_kong_fullname: supabase-kong
supabase_functions_fullname: supabase-functions
supabase_vector_fullname: supabase-vector
# --- Docker Hub pull secret (optional) ---
# Set to avoid anonymous rate limiting on docker.io images (kong, timberio/vector, etc.)
# vault_supabase_dockerhub_token must be set in host_vars when enabled.
supabase_dockerhub_enabled: false
supabase_dockerhub_username: ""
# --- Wait ---
# helm wait is disabled — image pulls from docker.io can take longer than any
# reasonable helm timeout. Pod readiness is checked separately below.
supabase_wait_timeout: 600

View File

@@ -0,0 +1,120 @@
---
argument_specs:
main:
short_description: Deploy Supabase on OpenShift via the supabase-community Helm chart
description:
- Deploys the full Supabase stack (Studio, Kong, Auth, REST, Realtime, Storage,
Meta, Analytics, Functions, ImgProxy, Vector) to an OpenShift cluster.
- All secrets are created as Kubernetes Secrets and referenced via secretRef
so secret material never appears in Helm values.
- OpenShift SCCs are granted before the Helm install so pods start with
correct permissions (anyuid for postgres, privileged for vector).
- An OCP Route with edge TLS termination is created, backed by a cert-manager
Certificate against the configured ClusterIssuer.
- >-
Vault variables (vault_supabase_*) must be defined in host_vars before running.
Generate JWT values at https://supabase.com/docs/guides/self-hosting/docker
Generate secrets with openssl rand -base64 64
options:
supabase_release_name:
description: Helm release name.
type: str
default: supabase
supabase_namespace:
description: Kubernetes namespace for all Supabase resources.
type: str
default: supabase
supabase_chart_repo_url:
description: URL of the supabase-community Helm chart repository.
type: str
default: https://supabase-community.github.io/supabase-kubernetes
supabase_chart_version:
description: Chart version to install. Empty string means latest.
type: str
default: ""
supabase_studio_host:
description: >-
FQDN for the Studio dashboard and Kong API gateway Route.
Defaults to supabase.apps.<cluster>.<domain>.
type: str
supabase_tls_cluster_issuer:
description: cert-manager ClusterIssuer name for TLS certificate provisioning.
type: str
default: ""
supabase_storage_class:
description: StorageClass name for all PersistentVolumeClaims (must support ReadWriteOnce).
type: str
default: ""
supabase_db_storage_size:
description: Size of the PostgreSQL PVC.
type: str
default: 10Gi
supabase_functions_storage_size:
description: Size of the Edge Functions PVC.
type: str
default: 1Gi
supabase_imgproxy_storage_size:
description: Size of the ImgProxy PVC.
type: str
default: 1Gi
supabase_minio_storage_size:
description: Size of the MinIO (object storage) PVC.
type: str
default: 20Gi
supabase_db_name:
description: PostgreSQL database name.
type: str
default: postgres
supabase_org_name:
description: Default organisation name shown in Supabase Studio.
type: str
default: ""
supabase_project_name:
description: Default project name shown in Supabase Studio.
type: str
default: ""
supabase_smtp_enabled:
description: >-
Enable SMTP for auth email. When false, auto-confirm is enabled so
sign-ups do not require email verification.
type: bool
default: false
supabase_smtp_host:
description: SMTP server hostname.
type: str
default: ""
supabase_smtp_port:
description: SMTP server port.
type: int
default: 587
supabase_smtp_admin_email:
description: Admin email address used as From header.
type: str
default: ""
supabase_smtp_sender_name:
description: Display name used in outgoing email.
type: str
default: Supabase
supabase_db_fullname:
description: >-
fullnameOverride for the db component. Determines the ServiceAccount
name that receives the anyuid SCC — must match deployment.db.fullnameOverride
passed to the chart.
type: str
default: supabase-db
supabase_kong_fullname:
description: >-
fullnameOverride for the kong component. Determines the Service name
that the OCP Route targets.
type: str
default: supabase-kong
supabase_vector_fullname:
description: >-
fullnameOverride for the vector component. Determines the ServiceAccount
name that receives the privileged SCC.
type: str
default: supabase-vector
supabase_wait_timeout:
description: Seconds to wait for deployments to become ready.
type: int
default: 600

View File

@@ -0,0 +1,13 @@
---
galaxy_info:
role_name: supabase
author: ptoal
description: Deploy Supabase on OpenShift via the supabase-community Helm chart
license: MIT
min_ansible_version: "2.14"
platforms:
- name: EL
versions:
- "9"
dependencies: []

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"

View File

@@ -0,0 +1,152 @@
---
# Read Supabase secrets from Vault; generate and store any that are missing.
#
# All __supabase_* facts set here are consumed by the K8s secret tasks in main.yml.
#
# JWT note: if jwt_secret is absent OR either JWT token is absent, all three are
# regenerated together — a partial JWT state (e.g. tokens signed by a different
# secret) would break auth across all services.
# ------------------------------------------------------------------
# Read existing secrets (may fail if path does not exist yet)
# ------------------------------------------------------------------
- name: Read supabase secrets from Vault
community.hashi_vault.vault_kv2_get:
path: "{{ supabase_vault_path }}"
engine_mount_point: "{{ supabase_vault_mount }}"
register: __supabase_vault_read
failed_when: false
no_log: "{{ supabase_no_log }}"
- name: Set existing vault data fact
ansible.builtin.set_fact:
__sv: "{{ __supabase_vault_read.secret | default({}) }}"
no_log: "{{ supabase_no_log }}"
# ------------------------------------------------------------------
# Resolve non-JWT secrets: use existing or generate random values
# ------------------------------------------------------------------
- name: Resolve non-JWT secrets (generate any that are missing)
ansible.builtin.set_fact:
__supabase_db_password: >-
{{ __sv.db_password
| default(lookup('community.general.random_string', length=32, special=false)) }}
__supabase_dashboard_username: >-
{{ __sv.dashboard_username | default('supabase') }}
__supabase_dashboard_password: >-
{{ __sv.dashboard_password
| default(lookup('community.general.random_string', length=24, special=false)) }}
__supabase_analytics_public_token: >-
{{ __sv.analytics_public_token
| default(lookup('community.general.random_string', length=32, special=false)) }}
__supabase_analytics_private_token: >-
{{ __sv.analytics_private_token
| default(lookup('community.general.random_string', length=32, special=false)) }}
__supabase_realtime_secret_key_base: >-
{{ __sv.realtime_secret_key_base
| default(lookup('community.general.random_string', length=64, special=false)) }}
__supabase_meta_crypto_key: >-
{{ __sv.meta_crypto_key
| default(lookup('community.general.random_string', length=32, special=false)) }}
__supabase_openai_api_key: >-
{{ __sv.openai_api_key | default('') }}
no_log: "{{ supabase_no_log }}"
# ------------------------------------------------------------------
# JWT secret + signed tokens (must be generated as a coherent set)
# ------------------------------------------------------------------
- name: Check whether JWT values need to be (re)generated
ansible.builtin.set_fact:
__supabase_needs_jwt_gen: >-
{{ not (__sv.jwt_secret | default('') | string | length > 0)
or not (__sv.anon_key | default('') | string | length > 0)
or not (__sv.service_key | default('') | string | length > 0) }}
- name: Generate JWT signing secret
ansible.builtin.set_fact:
__supabase_jwt_secret: >-
{{ lookup('community.general.random_string', length=64, special=false) }}
no_log: "{{ supabase_no_log }}"
when: __supabase_needs_jwt_gen | bool
- name: Use existing JWT signing secret
ansible.builtin.set_fact:
__supabase_jwt_secret: "{{ __sv.jwt_secret }}"
no_log: "{{ supabase_no_log }}"
when: not __supabase_needs_jwt_gen | bool
- name: Generate anon and service_role JWT tokens
ansible.builtin.command:
argv:
- python3
- -c
- |
import hmac, hashlib, base64, json, time, sys
def b64url(data):
if isinstance(data, str):
data = data.encode()
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
def make_jwt(secret, role):
now = int(time.time())
header = b64url(json.dumps({'alg': 'HS256', 'typ': 'JWT'}, separators=(',', ':')))
payload = b64url(json.dumps(
{'role': role, 'iss': 'supabase', 'iat': now, 'exp': now + 157680000},
separators=(',', ':')
))
msg = '{}.{}'.format(header, payload)
sig = b64url(hmac.new(secret.encode(), msg.encode(), hashlib.sha256).digest())
return '{}.{}'.format(msg, sig)
secret = sys.argv[1]
print(make_jwt(secret, 'anon'))
print(make_jwt(secret, 'service_role'))
- "{{ __supabase_jwt_secret }}"
register: __supabase_jwt_output
changed_when: false
no_log: "{{ supabase_no_log }}"
when: __supabase_needs_jwt_gen | bool
- name: Set generated JWT token facts
ansible.builtin.set_fact:
__supabase_anon_key: "{{ __supabase_jwt_output.stdout_lines[0] }}"
__supabase_service_key: "{{ __supabase_jwt_output.stdout_lines[1] }}"
no_log: "{{ supabase_no_log }}"
when: __supabase_needs_jwt_gen | bool
- name: Use existing JWT tokens from Vault
ansible.builtin.set_fact:
__supabase_anon_key: "{{ __sv.anon_key }}"
__supabase_service_key: "{{ __sv.service_key }}"
no_log: "{{ supabase_no_log }}"
when: not __supabase_needs_jwt_gen | bool
# ------------------------------------------------------------------
# Write back to Vault (always — ensures all fields are present and
# any newly generated values are persisted before K8s secrets are written)
# ------------------------------------------------------------------
- name: Write supabase secrets to Vault
community.hashi_vault.vault_kv2_write:
path: "{{ supabase_vault_path }}"
engine_mount_point: "{{ supabase_vault_mount }}"
data:
jwt_secret: "{{ __supabase_jwt_secret }}"
anon_key: "{{ __supabase_anon_key }}"
service_key: "{{ __supabase_service_key }}"
db_password: "{{ __supabase_db_password }}"
dashboard_username: "{{ __supabase_dashboard_username }}"
dashboard_password: "{{ __supabase_dashboard_password }}"
analytics_public_token: "{{ __supabase_analytics_public_token }}"
analytics_private_token: "{{ __supabase_analytics_private_token }}"
realtime_secret_key_base: "{{ __supabase_realtime_secret_key_base }}"
meta_crypto_key: "{{ __supabase_meta_crypto_key }}"
openai_api_key: "{{ __supabase_openai_api_key }}"
no_log: "{{ supabase_no_log }}"
- name: Report vault secret status
ansible.builtin.debug:
msg: >-
{{ 'Generated and stored new Supabase secrets in Vault'
if __supabase_needs_jwt_gen | bool
else 'Using existing Supabase secrets from Vault' }}

View File

@@ -0,0 +1,13 @@
---
# Computed internal vars — not intended for override
__supabase_wait_retries: "{{ (supabase_wait_timeout | int / 10) | int }}"
# Kubernetes secret names created before helm install; referenced via secretRef
__supabase_secret_jwt: supabase-jwt
__supabase_secret_db: supabase-db
__supabase_secret_dashboard: supabase-dashboard
__supabase_secret_analytics: supabase-analytics
__supabase_secret_realtime: supabase-realtime
__supabase_secret_meta: supabase-meta
__supabase_secret_smtp: supabase-smtp