Skip to content

Commit 5c2bc63

Browse files
docs: add keycloak database migration guide
1 parent 6e10803 commit 5c2bc63

4 files changed

Lines changed: 265 additions & 0 deletions

File tree

cspell.config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,9 @@ words:
299299
- toastify
300300
- Toastify
301301
- unpublishing
302+
- pguser
303+
- dbname
304+
- jdbc
302305
# Ignore RegExp patterns (for things like URLs, version numbers, etc.)
303306
ignoreRegExpList:
304307
# Ignore Cyrillic characters (non-English content in FAQ files)

docs/admin/update/index.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ Keep your AI/Run CodeMie platform up-to-date with the latest features, security
4141
description="Upgrade Keycloak identity and access management with version-specific migration guides and database updates."
4242
link="/admin/update/keycloak-upgrade/"
4343
/>
44+
<FeatureCard
45+
icon="/img/tools/keycloak.svg"
46+
iconType="image"
47+
invertInDarkTheme={false}
48+
title="Keycloak Database Migration"
49+
description="Migrate Keycloak from in-cluster PostgreSQL (PGO) to a cloud-managed PostgreSQL instance (dedicated or shared with CodeMie)."
50+
link="/admin/update/keycloak-database-migration/"
51+
/>
4452
<FeatureCard
4553
icon="/img/tools/elasticsearch.svg"
4654
iconType="image"
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
---
2+
id: keycloak-database-migration
3+
title: Keycloak Database Migration
4+
sidebar_label: Keycloak Database Migration
5+
description: Migrate Keycloak from in-cluster PostgreSQL (PGO) to a cloud-managed PostgreSQL instance.
6+
pagination_prev: admin/update/update-overview
7+
pagination_next: null
8+
---
9+
10+
import Tabs from '@theme/Tabs';
11+
import TabItem from '@theme/TabItem';
12+
13+
# Keycloak Database Migration
14+
15+
This guide covers migrating Keycloak's database from the in-cluster PostgreSQL (Crunchy PGO) to a cloud-managed PostgreSQL instance. Two target options are supported:
16+
17+
- **Shared database** — Keycloak reuses the existing CodeMie PostgreSQL instance. No infrastructure changes required.
18+
- **Dedicated database** — Keycloak gets its own PostgreSQL instance. Update the Terraform repository and apply with `TF_VAR_keycloak_shared_database_instance=false`.
19+
20+
:::warning Plan Downtime
21+
Keycloak will be unavailable during the migration.
22+
:::
23+
24+
## What Changes
25+
26+
| Component | Before (PGO) | After (Cloud-Managed) |
27+
| ------------------- | --------------------------------------------------------------- | -------------------------------------------- |
28+
| Database host | `keycloak-primary.security.svc` | Cloud PostgreSQL endpoint |
29+
| Database user | `admin` | `keycloak_admin` |
30+
| Kubernetes secret | `keycloak-pguser-admin` (auto-created by PGO) | `keycloak-postgresql` (manually created) |
31+
| Secret keys | `password`, `host`, `port`, `dbname`, `user`, `uri`, `jdbc-uri` | `password` only |
32+
| Helm values section | `pgo.enabled: true` | `sharedDatabaseInstance.enabled: true/false` |
33+
| Operator | Postgres Operator in `postgres-operator` namespace | Not required |
34+
35+
## Prerequisites
36+
37+
- `kubectl` access to the cluster (`security` and `postgres-operator` namespaces)
38+
- Updated `codemie-helm-charts` repository with the new Keycloak configuration
39+
- `deployment_outputs.env` with target database connection details
40+
41+
**Dedicated database only**: Update the Terraform repository, set `TF_VAR_keycloak_shared_database_instance=false` in `deployment.conf`, and apply Terraform to provision a dedicated PostgreSQL instance. After apply, copy `deployment_outputs.env`.
42+
43+
## Step 1: Stop Keycloak
44+
45+
```bash
46+
kubectl scale statefulset keycloakx -n security --replicas=0
47+
kubectl get pods -n security -l app.kubernetes.io/name=keycloakx
48+
```
49+
50+
## Step 2: Dump the PGO Database
51+
52+
Launch a temporary pod and dump the database. The pod will be reused in Step 5 for restoring.
53+
54+
```bash
55+
KC_PGO_PASSWORD=$(kubectl get secret keycloak-pguser-admin -n security \
56+
-o jsonpath='{.data.password}' | base64 -d)
57+
```
58+
59+
```bash
60+
kubectl run pg-tmp \
61+
--image=postgres:17-alpine \
62+
--restart=Never \
63+
-n security \
64+
-- sleep 3600
65+
```
66+
67+
```bash
68+
kubectl wait pod/pg-tmp -n security \
69+
--for=condition=Ready --timeout=60s
70+
```
71+
72+
```bash
73+
kubectl exec pg-tmp -n security -- sh -c \
74+
"PGPASSWORD='${KC_PGO_PASSWORD}' pg_dump \
75+
--host=keycloak-primary.security.svc \
76+
--port=5432 \
77+
--username=admin \
78+
--dbname=keycloak \
79+
--no-owner \
80+
--no-privileges \
81+
--format=plain \
82+
--file=/tmp/keycloak-dump.sql"
83+
```
84+
85+
```bash
86+
# Save a local backup copy
87+
kubectl cp security/pg-tmp:/tmp/keycloak-dump.sql ./keycloak-dump.sql
88+
```
89+
90+
## Step 3: Create Kubernetes Secrets
91+
92+
<Tabs groupId="keycloak-db-option">
93+
<TabItem value="dedicated" label="Dedicated Database" default>
94+
95+
```bash
96+
source deployment_outputs.env
97+
```
98+
99+
```bash
100+
# Password for the keycloak_admin database user
101+
kubectl create secret generic keycloak-postgresql \
102+
--from-literal=password="${KEYCLOAK_POSTGRES_DATABASE_PASSWORD}" \
103+
--namespace security
104+
```
105+
106+
</TabItem>
107+
<TabItem value="shared" label="Shared Database">
108+
109+
```bash
110+
source deployment_outputs.env
111+
```
112+
113+
```bash
114+
KEYCLOAK_DB_PASSWORD=$(openssl rand -base64 16 | tr -d '=+/' | head -c 16)
115+
```
116+
117+
```bash
118+
# Password for the keycloak_admin database user
119+
kubectl create secret generic keycloak-postgresql \
120+
--from-literal=password="${KEYCLOAK_DB_PASSWORD}" \
121+
--namespace security
122+
```
123+
124+
```bash
125+
# CodeMie RDS admin credentials (used by the Helm hook Job to create the keycloak DB and user)
126+
kubectl create secret generic codemie-postgresql \
127+
--from-literal=PG_USER="${CODEMIE_POSTGRES_DATABASE_USER}" \
128+
--from-literal=PG_PASS="${CODEMIE_POSTGRES_DATABASE_PASSWORD}" \
129+
--namespace security
130+
```
131+
132+
</TabItem>
133+
</Tabs>
134+
135+
## Step 4: Prepare the Target Database
136+
137+
<Tabs groupId="keycloak-db-option">
138+
<TabItem value="dedicated" label="Dedicated Database" default>
139+
140+
Edit `keycloak-helm/values-<cloud_name>.yaml`:
141+
142+
```yaml
143+
database:
144+
hostname: "<KEYCLOAK_POSTGRES_DATABASE_HOST value>"
145+
# ...
146+
sharedDatabaseInstance:
147+
enabled: false
148+
```
149+
150+
</TabItem>
151+
<TabItem value="shared" label="Shared Database">
152+
153+
Edit `keycloak-helm/values-<cloud_name>.yaml` with the CodeMie database hostname:
154+
155+
```yaml
156+
database:
157+
hostname: "<CODEMIE_POSTGRES_DATABASE_HOST value>"
158+
# ...
159+
sharedDatabaseInstance:
160+
enabled: true
161+
```
162+
163+
Deploy the Keycloak chart with `replicas=0` to trigger the init Job that creates the `keycloak` database and `keycloak_admin` user:
164+
165+
```bash
166+
helm upgrade --install keycloak keycloak-helm/. \
167+
-n security \
168+
--values keycloak-helm/values-<cloud_name>.yaml \
169+
--set keycloakx.replicas=0 \
170+
--wait \
171+
--timeout 900s \
172+
--dependency-update
173+
```
174+
175+
The init Job is automatically deleted after successful completion.
176+
177+
</TabItem>
178+
</Tabs>
179+
180+
## Step 5: Restore the Dump
181+
182+
Restore from the dump file that is already inside `pg-tmp` (created in Step 2).
183+
184+
<Tabs groupId="keycloak-db-option">
185+
<TabItem value="dedicated" label="Dedicated Database" default>
186+
187+
```bash
188+
kubectl exec pg-tmp -n security -- sh -c \
189+
"PGPASSWORD='${KEYCLOAK_POSTGRES_DATABASE_PASSWORD}' psql \
190+
--host='${KEYCLOAK_POSTGRES_DATABASE_HOST}' \
191+
--port=5432 \
192+
--username=keycloak_admin \
193+
--dbname=keycloak \
194+
--file=/tmp/keycloak-dump.sql"
195+
```
196+
197+
</TabItem>
198+
<TabItem value="shared" label="Shared Database">
199+
200+
```bash
201+
KEYCLOAK_DB_PASSWORD=$(kubectl get secret keycloak-postgresql -n security \
202+
-o jsonpath='{.data.password}' | base64 -d)
203+
204+
kubectl exec pg-tmp -n security -- sh -c \
205+
"PGPASSWORD='${KEYCLOAK_DB_PASSWORD}' psql \
206+
--host='${CODEMIE_POSTGRES_DATABASE_HOST}' \
207+
--port=5432 \
208+
--username=keycloak_admin \
209+
--dbname=keycloak \
210+
--file=/tmp/keycloak-dump.sql"
211+
```
212+
213+
</TabItem>
214+
</Tabs>
215+
216+
Clean up the temporary pod:
217+
218+
```bash
219+
kubectl delete pod pg-tmp -n security
220+
```
221+
222+
## Step 6: Deploy Updated Keycloak
223+
224+
```bash
225+
helm upgrade --install keycloak keycloak-helm/. \
226+
-n security \
227+
--values keycloak-helm/values-<cloud_name>.yaml \
228+
--wait \
229+
--timeout 900s \
230+
--dependency-update
231+
```
232+
233+
## Step 7: Verify
234+
235+
```bash
236+
kubectl get pods -n security -l app.kubernetes.io/name=keycloakx
237+
kubectl logs -n security -l app.kubernetes.io/name=keycloakx --tail=20
238+
```
239+
240+
Then verify in the browser:
241+
242+
1. Open the Keycloak admin console
243+
2. Check that realms, clients, and users are present
244+
3. Test an SSO login flow
245+
246+
## Step 8: Clean Up PGO Resources
247+
248+
The PGO PostgreSQL pods and secrets in the `security` namespace are removed automatically by `helm upgrade` (the `postgres-cluster.yaml` template no longer exists in the chart). Only the Postgres Operator itself remains in a separate namespace:
249+
250+
```bash
251+
helm uninstall postgres-operator -n postgres-operator
252+
kubectl delete namespace postgres-operator
253+
```

sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,7 @@ const sidebars: SidebarsConfig = {
736736
items: ['admin/update/keycloak-upgrade/keycloak-24-to-26'],
737737
},
738738
'admin/update/keycloak-operator-upgrade',
739+
'admin/update/keycloak-database-migration/keycloak-database-migration',
739740
'admin/update/terraform-upgrade/terraform-upgrade',
740741
'admin/update/oauth2-proxy-upgrade',
741742
'admin/update/elasticsearch-kibana-upgrade',

0 commit comments

Comments
 (0)