Upgrade to Apicurio Registry v3
- Version compatibility across releases
- What changed in Apicurio v3
- Prerequisites
- Step 1 — Prepare the Apicurio v3 values
- Step 2 — Deploy Apicurio v3 alongside v2
- Step 3 — Block schema registration in v2 and export schemas
- Step 4 — Import schemas into v3
- Step 5 — Verify the import
- Step 6 — Switch ingress to v3
- Step 7 — Keycloak handoff and disable v2
- Rollback
- Troubleshooting
- End-to-end upgrade script
This page describes how to upgrade the Apicurio Registry server from v2 to v3 on Axual Streaming 2.0.0 (Axual Platform 2026.2). It assumes you have already upgraded Axual Streaming to 2.0.0 (your existing Apicurio v2 keeps running unchanged) — see Upgrade Axual Streaming to 2.0.0.
Axual Streaming 2.0.0 ships both Apicurio Registry versions side by side:
-
Apicurio Registry v2 — the
apicurio-registry3.1.11chart (Apicurio Registry2.6.13.Ax1), keyed asapicurio-registry, toggled withglobal.apicurio.enabled(enabled by default). -
Apicurio Registry v3 — the
apicurio-registry-v30.1.0chart (Apicurio Registry3.3.0.Ax1), keyed asapicurio-registry-v3, toggled withglobal.apicurio-registry-v3.enabled(enable it to upgrade).
Both versions ship together so you can run them side by side, move your schemas, switch traffic to v3 and then retire v2 — with no impact on client apps.
|
Apicurio v2 is fully supported in
Apicurio v3 is a major version with breaking changes. The data Apicurio stores in Kafka is not compatible between v2 and v3, so this upgrade is performed as an export from v2 / import into v3 — not as an in-place version bump. |
-
Reads: near-zero.
-
Writes: schema registration to v2 must be blocked during export and import — typically 30–45 minutes, until ingress is switched to v3.
|
This upgrade changes only the Apicurio Registry server. Your applications do not need to change: the v2 Apicurio Registry client libraries (for example, the Java client and SerDes, |
Version compatibility across releases
Apicurio v3 keeps serving the v2 API alongside its native v3 API. As a result, the registry server, the Axual governance plane (Platform Manager and Topic Browse), and your applications each move from the v2 API to the v3 API independently, on different schedules:
| Platform version | Apicurio server | Platform Manager and Topic Browse API | Apps API |
|---|---|---|---|
|
v2 |
v2 |
v2 |
|
v3 |
v2 |
v2 (stable) or v3 (beta) |
|
v3 |
v3 |
v2 or v3 (both stable) |
|
v3 |
v3 |
v2 or v3 (both stable) |
|
v3 |
v3 |
v2 or v3 (both stable) |
|
v3 |
v3 |
v2 (deprecated) or v3 (stable) |
In 2026.2 you run the v3 server, but Platform Manager and Topic Browse keep calling its v2 API (keep the /apis/registry/v2 path in the Schema Registry URL), and your applications keep using their v2 clients.
Confluent-compatible (ccompat) endpoint
Applications that use the Confluent-compatible API are the one client-side exception. Apicurio v3 removes the /apis/ccompat/v6 endpoint and serves only /apis/ccompat/v7, so these clients must change their Schema Registry URL from /apis/ccompat/v6 to /apis/ccompat/v7.
Apicurio v2 already serves /apis/ccompat/v7 too, so make this change before the ingress cutover (Step 6): the v7 URL works against both versions, so moving the server to v3 is then seamless for them. This is unrelated to the native registry API, which v3 keeps serving unchanged at /apis/registry/v2.
What changed in Apicurio v3
| Area | Apicurio v2 | Apicurio v3 |
|---|---|---|
Configuration property prefix |
|
|
Environment variables |
|
|
Container image |
|
|
Web UI |
Bundled in the registry container |
Separate container ( |
Ingress |
Single ingress block |
Split into |
Kafka storage |
A single compacted topic |
Three separate topics (journal, snapshots, events) |
Consumer group prefix |
|
|
Kafka topics
Where v2 used a single compacted topic, v3 uses three topics. All three are created automatically by the chart’s init container — together with the ACLs for the Apicurio principal — and their names are configurable:
| Values key | Purpose |
|---|---|
|
Schema data journal |
|
Periodic data snapshots |
|
Change-event notifications |
The default consumer group prefix also changes from apicurio-registry- to apicurio-; this is handled automatically by the init container.
Prerequisites
-
You have upgraded Axual Streaming to
2.0.0(see Upgrade Axual Streaming to 2.0.0) and your existing Apicurio v2 keeps running unchanged under itsapicurio-registrykey. -
You have created a dedicated migration client in Keycloak (
apicurio-migration) with themigration-adminrole — see Create the migration client in Keycloak below.
Create the migration client in Keycloak
The apicurio-migration client is the only principal that retains register and export access on v2 during the registration freeze (Step 3). It needs the migration-admin role, which becomes the sole admin role on v2 while every other client — including the platform’s apicurio-api — is read-only.
Log in to the Keycloak Admin Console for the realm defined in your Apicurio values under security.authentication.keycloak.realm.
-
Create the
migration-adminrealm role: Navigate to Realm roles → Create role, entermigration-adminas the role name, and save. -
Create the
apicurio-migrationclient: Navigate to Clients → Create client. Set the Client ID toapicurio-migration, then on the Capability config step apply these settings:Setting Value Client authentication
On
Authorization
Off
Standard flow
unchecked
Implicit flow
unchecked
Direct access grants
unchecked
Service accounts roles
checked
OAuth 2.0 Device Authorization Grant
unchecked
OIDC CIBA Grant
unchecked
Save the client and copy the client secret from the Credentials tab — you will need it in Step 3.
-
Assign the role to the service account: Open the
apicurio-migrationclient, go to the Service account roles tab and click Assign role. In the filter dropdown (top-left of the dialog), switch from Filter by clients to Filter by realm roles, then selectmigration-adminand click Assign.
Step 1 — Prepare the Apicurio v3 values
Your Apicurio v2 keeps running unchanged under its apicurio-registry: block. Add a new apicurio-registry-v3: configuration block alongside it by working through the sections below.
Do not pin image.tag in any of the blocks below — the chart already pins the correct Apicurio version.
|
1.1 — Enable Apicurio v3 and disable bundled Keycloak
Enable Apicurio v3 in the global block. Disable the bundled Keycloak instances — v3 reuses the existing v2 Keycloak throughout the upgrade.
global:
apicurio:
enabled: true # your existing Apicurio v2, still running
apicurio-registry-v3:
enabled: true # enable Apicurio v3 alongside v2
axual-streaming:
apicurio-registry-v3:
imagePullSecrets:
- name: [same as configured for your apicurio-registry]
apicurioKeycloak:
enabled: false # reuse the v2 Keycloak — do not deploy a new one
apicurioKeycloakMysql:
enabled: false
1.2 — UI URL
Set the public HTTPS URL the browser uses to reach the v3 API. Use a temporary hostname for now — you will switch to the production hostname in Step 6.
axual-streaming:
apicurio-registry-v3:
ui:
config:
registryApiUrl: "https://apicurio-v3.example.com/apis/registry/v3"
registryApiUrl must be publicly reachable from the browser — do not use localhost.
|
1.3 — Kafka
Replace <tenant> and <instance> with the values for your environment.
axual-streaming:
apicurio-registry-v3:
kafka:
bootstrapServers: [same as configured for your apicurio-registry]
journalTopic: "_<tenant>-<instance>-kafkasql-journal"
snapshotsTopic: "_<tenant>-<instance>-kafkasql-snapshots"
eventsTopic: "_<tenant>-<instance>-registry-events"
1.4 — TLS
Reuse the same TLS secret names as your existing Apicurio v2 deployment.
axual-streaming:
apicurio-registry-v3:
tls:
clientKeypairSecretName: [same as configured for your apicurio-registry]
serverKeypairSecretName: [same as configured for your apicurio-registry]
truststoreCaSecretName: [same as configured for your apicurio-registry]
1.5 — Kafka init container
The init container creates the three Kafka topics and the ACLs for the Apicurio principal. Reuse the same broker TLS secret names as your Apicurio v2 deployment.
axual-streaming:
apicurio-registry-v3:
kafkaInitContainer:
apicurioPrincipal: [same as configured for your apicurio-registry]
replicationFactor: [same as configured for your apicurio-registry]
minIsr: [same as configured for your apicurio-registry]
tls:
keypairSecretName: [same as configured for your apicurio-registry]
keypairSecretKeyName: [same as configured for your apicurio-registry]
keypairSecretCertName: [same as configured for your apicurio-registry]
truststoreCaSecretName: [same as configured for your apicurio-registry]
truststoreCaSecretCertName: [same as configured for your apicurio-registry]
1.6 — Security
Copy the security block from your v2 values as-is. The only value that must differ is webRedirectUrl — point it to the temporary v3 hostname for now; you will update it to the production hostname in Step 6.
axual-streaming:
apicurio-registry-v3:
security:
authentication:
enabled: true
basicAuthEnabled: true
keycloak:
authUrl: [same as configured for your apicurio-registry]
realm: [same as configured for your apicurio-registry]
webClientId: [same as configured for your apicurio-registry]
webRedirectUrl: "https://apicurio-v3.example.com" # point to v3, not v2
1.7 — Ingress
Where v2 had a single ingress block with a hosts array, v3 splits it into ingress.backend (the API) and ingress.ui (the web console). Both blocks accept the same className, annotations, and tls fields you already have on the v2 ingress, and both route to the same Apicurio Service — typically <release-name>-apicurio-registry-v3 — on different ports and paths:
| Ingress (values key) | Service name | Service port | Path |
|---|---|---|---|
|
|
|
|
|
|
|
|
If you front Apicurio with an external ingress or gateway rather than the chart’s ingress resources, see the Ingress routing reference for the full service/port/path mapping (including the Auth Proxy).
Use a temporary hostname here — you will cut over to the production hostname in Step 6.
axual-streaming:
apicurio-registry-v3:
ingress:
backend:
enabled: true
className: [same as configured for your apicurio-registry]
annotations: [same as configured for your apicurio-registry]
host: "apicurio-v3.example.com"
tls:
- secretName: [same as configured for your apicurio-registry]
hosts:
- "apicurio-v3.example.com"
ui:
enabled: true
className: [same as configured for your apicurio-registry]
annotations: [same as configured for your apicurio-registry]
host: "apicurio-v3.example.com"
tls:
- secretName: [same as configured for your apicurio-registry]
hosts:
- "apicurio-v3.example.com"
The external-dns annotations belong on backend only — both backend and UI share the same hostname, so a single DNS registration is enough. The UI does not need to re-register the record.
|
1.8 — Auth Proxy (optional)
Skip this section if you do not use the Auth Proxy sidecar.
The Auth Proxy is disabled by default (authProxy.enabled: false). When enabled, it runs as a sidecar and validates JWT tokens before forwarding requests to the Apicurio backend.
If you are adapting your v2 authProxy values, update these three fields — in v2 the Apicurio backend was on port 8080, so the proxy could sit at 8081; in v3 the backend moved to 8081, so these must change:
| Value | v2 | v3 |
|---|---|---|
|
|
|
|
|
|
|
|
The Auth Proxy ingress cannot share a hostname and path with |
axual-streaming:
apicurio-registry-v3:
authProxy:
enabled: true
secrets:
auth-proxy:
client-secret-salt: not-too-salty
clientSecretAlgorithm: HmacSHA256
config:
auth-proxy:
valid-issuer-uri: [same as configured for your apicurio-registry]
jwks-endpoint-uri: [same as configured for your apicurio-registry]
client-id: [same as configured for your apicurio-registry]
check-audience: [same as configured for your apicurio-registry]
ingress:
enabled: true
className: [same as configured for your apicurio-registry]
annotations: [same as configured for your apicurio-registry]
hosts:
- host: "apicurio-auth-proxy.example.com"
paths:
- path: "/apis"
pathType: "Prefix"
tls:
- secretName: "<same TLS secret as your other ingresses>"
hosts:
- "apicurio-auth-proxy.example.com"
Step 2 — Deploy Apicurio v3 alongside v2
Apply the updated values with a Helm upgrade of your existing Streaming release:
helm upgrade --install streaming oci://registry.axual.io/axual-charts/axual-streaming --version=2.0.0 -n kafka -f values.yaml
Wait for all apicurio-registry-v3 pods to become ready before continuing.
Step 3 — Block schema registration in v2 and export schemas
|
Block new schema registrations in v2 for the duration of the export and import. The upgrade cannot be run incrementally: if a schema is added to v2 after you export, you must delete the v3 journal topic and start the upgrade over (see Troubleshooting). |
3.1 — Set shell variables
The CLI commands in the rest of this page reuse the shell variables below — the same names are used consistently throughout, and in the end-to-end script at the end. Set them once in your shell:
# --- Hosts ---
EXISTING_APICURIO_HOST="apicurio.example.com" # current production Apicurio v2 host
NEW_APICURIO_HOST="apicurio-v3.example.com" # temporary v3 host, until the Step 6 cutover
LEGACY_APICURIO_HOST="apicurio-legacy.example.com" # where v2 moves after cutover (kept for rollback)
APICURIO_KEYCLOAK_HOST="apicurio.example.com" # Keycloak host (often the same host, under /auth)
APICURIO_REALM="apicurio"
# --- Migration client (created in the prerequisites; holds the migration-admin role) ---
MIGRATION_CLIENT_ID="apicurio-migration"
MIGRATION_CLIENT_SECRET="<migration client secret from the Credentials tab>"
# --- Platform client (apicurio-api; used for the v3 import — keeps sr-admin throughout) ---
APICURIO_API_CLIENT_ID="apicurio-api"
APICURIO_API_CLIENT_SECRET="<apicurio-api client secret>"
3.2 — Block schema registration in v2
Apicurio v2 has no global way to make the Registry read-only.
We need two changes on the Apicurio v2 deployment to freeze all subjects registration while keeping subjects read:
-
Remap the developer roles (in
apicurio-registry.env) so no token resolves to a developer role —sr-developerandsr-adminmatch nothing, and only your dedicated migration client (holdingmigration-admin) keeps developer role and rights to register new subjects. -
Disable
admin-override(inapicurio-registry.config) — the chart enables it with rolesr-admin, and because it is evaluated before RBAC, anysr-admintoken (including the platform’sapicurio-api) would otherwise bypass the freeze.
apicurio-registry:
config:
# The chart sets registry.auth.admin-override.enabled=true (role: sr-admin); turn it off here.
registry.auth.admin-override.enabled: "false"
env:
# No regular client resolves to a developer role any more (sr-developer / sr-admin match nothing);
# only the dedicated `apicurio-migration` client (holding `migration-admin` role) keeps reading/registering subjects access.
- name: REGISTRY_AUTH_ROLES_DEVELOPER
value: "sr-developer-frozen" # no token carries this name → apps registering subjects denied
- name: REGISTRY_AUTH_ROLES_ADMIN
value: "migration-admin" # the admin role during the freeze; held ONLY by the migration client
# Keep authenticated readers (Platform Manager, Topic Browse, your apps) able to read subjects during the freeze.
- name: REGISTRY_AUTH_AUTHENTICATED_READS_ENABLED
value: "true"
|
Disable |
Apply both with a helm upgrade of the Streaming release; only the v2 registry pod restarts, and reads resume within seconds. With these in place, every client — including the platform’s apicurio-api (sr-admin) — is read-only, and only the migration-admin client can register or export.
|
This freeze does not affect anonymous read access. Anonymous read is governed only by |
3.3 — Export schemas from v2
Use the dedicated migration client created in the prerequisites ($MIGRATION_CLIENT_ID) — not the platform’s apicurio-api client, which must stay frozen. Obtain a token for it, record how many artifacts v2 holds (so you can match it on v3 in Step 5), then export all schemas and metadata:
# Get an access token from the v2 Keycloak.
# Use the dedicated migration client that holds the migration-admin role — now the only admin role —
# so it keeps register/export access, while every other client is frozen to read-only.
TOKEN=$(curl -skv -X POST \
"https://${APICURIO_KEYCLOAK_HOST}/auth/realms/${APICURIO_REALM}/protocol/openid-connect/token" \
-d "grant_type=client_credentials&client_id=${MIGRATION_CLIENT_ID}&client_secret=${MIGRATION_CLIENT_SECRET}" \
| jq -r '.access_token')
# Count the artifacts currently in v2 — record this number to verify the v3 import in Step 5.
curl -skv \
"https://${EXISTING_APICURIO_HOST}/apis/registry/v2/search/artifacts" \
-H "Authorization: Bearer $TOKEN" | jq '.count'
# Export all schemas and metadata.
curl -skv \
"https://${EXISTING_APICURIO_HOST}/apis/registry/v2/admin/export" \
-H "Authorization: Bearer $TOKEN" \
-o apicurio-export.zip
The export (and the Step 4 import) can take several minutes for large registries. Check the request timeout on your Ingress (or OpenShift Route) first — for nginx, the relevant annotations are nginx.ingress.kubernetes.io/proxy-read-timeout and proxy-send-timeout — and raise it (for example to 600 seconds) so the connection is not dropped mid-transfer.
|
Verify the export file is not empty:
ls -lh apicurio-export.zip # should be > 0 bytes
Step 4 — Import schemas into v3
|
The v3 registry must be empty for the import to succeed. If a previous attempt left data behind — or you need to start over — restore the v3 Kafka topics first: see Restore Apicurio v3 topics in the Rollback section. |
Import is an admin operation on v3 — which is not frozen — so use the platform’s apicurio-api client ($APICURIO_API_CLIENT_ID / $APICURIO_API_CLIENT_SECRET), which keeps its normal sr-admin role throughout. This is a different client from the $MIGRATION_CLIENT_ID used for the export in Step 3.3. The token is still fetched from the existing v2 Keycloak, since v3 reuses it at this stage.
# Get an access token for the platform apicurio-api client (from the existing v2 Keycloak).
TOKEN=$(curl -skv -X POST \
"https://${APICURIO_KEYCLOAK_HOST}/auth/realms/${APICURIO_REALM}/protocol/openid-connect/token" \
-d "grant_type=client_credentials&client_id=${APICURIO_API_CLIENT_ID}&client_secret=${APICURIO_API_CLIENT_SECRET}" \
| jq -r '.access_token')
# Import into v3.
curl -skv \
"https://${NEW_APICURIO_HOST}/apis/registry/v3/admin/import" \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/zip" \
--data-binary @apicurio-export.zip
Step 5 — Verify the import
Check that artifacts were imported with their versions (not just metadata), and that the count matches the v2 artifact count you recorded in Step 3.3:
# Count imported artifacts — must match the v2 count from Step 3.3.
curl -skv \
"https://${NEW_APICURIO_HOST}/apis/registry/v3/search/artifacts?limit=5" \
-H "Authorization: Bearer $TOKEN" | jq '.count'
# Pick one artifact and confirm it has versions.
ARTIFACT_ID=$(curl -skv \
"https://${NEW_APICURIO_HOST}/apis/registry/v3/search/artifacts?limit=1" \
-H "Authorization: Bearer $TOKEN" | jq -r '.artifacts[0].artifactId')
curl -skv \
"https://${NEW_APICURIO_HOST}/apis/registry/v3/groups/default/artifacts/${ARTIFACT_ID}/versions" \
-H "Authorization: Bearer $TOKEN" | jq '.count' # must be > 0
Step 6 — Switch ingress to v3
| Do not disable v2 yet. v3 still depends on the v2 Keycloak for authentication until Step 7 is complete. |
Update v3 to the production URL — this is the EXISTING_APICURIO_HOST, the hostname your v2 deployment used until now. Re-point registryApiUrl, webRedirectUrl, both ingress hosts and their tls.hosts away from the temporary NEW_APICURIO_HOST:
apicurio-registry-v3:
ui:
config:
# From NEW_APICURIO_HOST to EXISTING_APICURIO_HOST
registryApiUrl: "https://apicurio.example.com/apis/registry/v3"
security:
authentication:
keycloak:
# From NEW_APICURIO_HOST to EXISTING_APICURIO_HOST
webRedirectUrl: "https://apicurio.example.com"
ingress:
backend:
host: "apicurio.example.com"
tls:
- secretName: [same as configured for your apicurio-registry]
hosts:
- "apicurio.example.com"
ui:
host: "apicurio.example.com"
tls:
- secretName: [same as configured for your apicurio-registry]
hosts:
- "apicurio.example.com"
Move v2 to a legacy hostname (LEGACY_APICURIO_HOST) so it stays reachable in case you need to roll back. Re-point its webRedirectUrl, ingress host and tls.hosts too — otherwise the v2 UI redirect and certificate still reference the production hostname now served by v3:
apicurio-registry:
security:
authentication:
keycloak:
# Point v2's redirect at its new legacy hostname (LEGACY_APICURIO_HOST)
webRedirectUrl: "https://apicurio-legacy.example.com"
ingress:
hosts:
- host: [ LEGACY_APICURIO_HOST ]
tls:
- secretName: [same as configured for your apicurio-registry]
hosts:
- [ LEGACY_APICURIO_HOST ]
If you use the Auth Proxy (section 1.8), cut its ingress over at the same time: give the v3 Auth Proxy the production auth-proxy hostname, and move the v2 Auth Proxy ingress (the apicurio-auth ingress) to a legacy hostname so it stays reachable for rollback.
apicurio-registry-v3:
authProxy:
ingress:
hosts:
- host: "apicurio-auth-proxy.example.com"
paths:
- path: "/apis"
pathType: "Prefix"
tls:
- secretName: [same as configured for your apicurio-registry]
hosts:
- "apicurio-auth-proxy.example.com"
Move the v2 Auth Proxy ingress to a legacy hostname using the same authProxy ingress key you configured in your v2 values (for example apicurio-auth-proxy-legacy.example.com), mirroring the v2 registry legacy move above. Skip this if you do not run the Auth Proxy.
|
Apply all changes with a helm upgrade (same command as Step 2).
|
Your Instance Clusters need no Schema Registry reconfiguration. The Schema Registry URL keeps the same hostname and the |
Step 7 — Keycloak handoff and disable v2
Before you disable v2, v3 must take over authentication — disabling v2 removes everything the v2 release owns. The chart provides Keycloak as two independent pieces: its database (apicurioKeycloakMysql) and the Keycloak server (apicurioKeycloak). Move each piece from v2 to v3 only if it is the bundled one; if a piece is externalized, v3 points at the same external instance, and you skip that move.
|
Whatever the v2 release owns is deleted when you disable v2 — including the bundled |
Move the Keycloak MySQL from v2 to v3 (optional — skip if the database is externalized)
| If your Keycloak database is externalized (for example Azure Database for MySQL), skip this sub-step. It is not owned by either release, and v3’s Keycloak will point at the same database in the next sub-step. |
The bundled apicurioKeycloakMysql belongs to the v2 release, so stand up a v3-owned MySQL and copy the data into it (do not disable v2 yet):
-
Enable a v3-owned MySQL on the v3 release:
apicurio-registry-v3: apicurioKeycloakMysql: enabled: true auth: database: "apicurio-kc-db" username: "keycloak" # use the same passwords as your existing v2 Keycloak database -
Copy the Keycloak data from the v2 database into the v3 database — for example with
mysqldump, while both MySQL pods are running:# Dump the v2 Keycloak database kubectl exec -n kafka <v2-mysql-pod> -- \ mysqldump -u root -p"<root-password>" apicurio-kc-db > apicurio-kc-db.sql # Restore it into the v3 Keycloak database kubectl exec -i -n kafka <v3-mysql-pod> -- \ mysql -u root -p"<root-password>" apicurio-kc-db < apicurio-kc-db.sql
Move Keycloak from v2 to v3 (optional — skip if Keycloak is externalized)
If Keycloak is externalized — deployed by a separate chart/release with apicurioKeycloak.enabled: false in the Apicurio chart — skip this sub-step. Apicurio v2 and v3 both point at the same external Keycloak, so there is nothing to move; go straight to Disable Apicurio v2.
|
The bundled apicurioKeycloak belongs to the v2 release, so enable it on the v3 release, pointed at the v3-owned MySQL from the previous sub-step (or at your external database if only the MySQL is externalized):
apicurio-registry-v3:
apicurioKeycloak:
enabled: true
realm: "apicurio"
args: ['start']
database:
vendor: "mysql"
hostname: "<keycloak-db-hostname>" # the v3-owned MySQL from the previous sub-step, or your external database
database: "apicurio-kc-db"
username: "keycloak"
Restart the v3 Keycloak pod and verify you can log in with an sr-admin user and that the realm, clients and role grants are present.
Rollback
If something goes wrong after the ingress switch, roll back by pointing ingress back to v2:
apicurio-registry-v3:
ingress:
backend:
enabled: false
ui:
enabled: false
apicurio-registry:
ingress:
hosts:
- host: "apicurio.example.com"
Also undo the Step 3 registration freeze on the v2 release so it accepts schema registrations again: remove the REGISTRY_AUTH_ROLES_DEVELOPER / REGISTRY_AUTH_ROLES_ADMIN remaps and REGISTRY_AUTH_AUTHENTICATED_READS_ENABLED from apicurio-registry.env, and remove the registry.auth.admin-override.enabled: "false" line from apicurio-registry.config (restoring the chart default), then helm upgrade. This restores your normal v2 RBAC configuration.
|
Any schemas registered in v3 after the cutover will not be present in v2 — they are lost on rollback and must be re-added to v2 manually. Moving data from v3 back to v2 is not supported. |
Restore Apicurio v3 topics
If you need to discard a failed import and start the import (Step 4) from scratch, the v3 store must be empty again. Restore the v3 Kafka topics:
-
Delete the three v3 Kafka topics (
kafka.journalTopic,kafka.snapshotsTopic,kafka.eventsTopic). -
Restart the v3 pod (for example
kubectl rollout restart deploy/<apicurio-registry-v3-deployment> -n kafka). On startup the chart’s init container recreates the three topics — and their ACLs — empty. -
Re-run the import from Step 4.
| This restores only the v3 registry store; it does not touch v2. v2 keeps serving reads throughout. |
Troubleshooting
| Symptom | Cause | Solution |
|---|---|---|
Registry is not empty on import |
Data from a previous import exists |
Restore the v3 topics (delete the three v3 topics and restart the v3 pod), then re-run the import |
Artifacts imported but show 0 versions |
Events topic missing, or no ACLs |
Verify the events topic exists and ACLs are set for the Apicurio principal |
v3 pod stuck in |
Kafka topics missing or wrong cleanup policy |
Check the init container logs |
UI shows "Unable to connect" |
|
Set |
Auth proxy or backend crashes on startup |
Port |
Set |
-
Official Apicurio guide: Migrating from Apicurio Registry 2.6 to 3.x — see the Apicurio Registry documentation.
End-to-end upgrade script
The script below runs the CLI portion of the upgrade — the export from v2 (Step 3.3), the import into v3 (Step 4) and the verification (Step 5) — in one pass, reusing the same variables as the rest of this page. It does not perform the Helm changes: apply the registration freeze (Step 3.2) before running it, and the ingress cutover (Step 6) after.
|
The registration freeze must already be in place when you run this. The script reads |
#!/usr/bin/env bash
set -euo pipefail
# 1. Configure variables
EXISTING_APICURIO_HOST="apicurio.example.com" # current production Apicurio v2 host
NEW_APICURIO_HOST="apicurio-v3.example.com" # temporary v3 host, until the Step 6 cutover
APICURIO_KEYCLOAK_HOST="apicurio.example.com" # Keycloak host (often the same host, under /auth)
APICURIO_REALM="apicurio"
MIGRATION_CLIENT_ID="apicurio-migration" # holds migration-admin; keeps export access during the freeze
MIGRATION_CLIENT_SECRET="<migration client secret>"
APICURIO_API_CLIENT_ID="apicurio-api" # platform client; keeps sr-admin for the v3 import
APICURIO_API_CLIENT_SECRET="<apicurio-api client secret>"
# Helper: fetch a client_credentials token from the v2 Keycloak.
get_token() { # get_token <client_id> <client_secret>
curl -skv -X POST \
"https://${APICURIO_KEYCLOAK_HOST}/auth/realms/${APICURIO_REALM}/protocol/openid-connect/token" \
-d "grant_type=client_credentials&client_id=$1&client_secret=$2" \
| jq -r '.access_token'
}
# 2. Get an access token for v2 (migration client — keeps register/export during the freeze)
TOKEN=$(get_token "$MIGRATION_CLIENT_ID" "$MIGRATION_CLIENT_SECRET")
# 3. Export schemas from v2
curl -skv "https://${EXISTING_APICURIO_HOST}/apis/registry/v2/admin/export" \
-H "Authorization: Bearer $TOKEN" -o apicurio-export.zip
ls -lh apicurio-export.zip # should be > 0 bytes
# 4. Count the artifacts in v2
V2_COUNT=$(curl -skv "https://${EXISTING_APICURIO_HOST}/apis/registry/v2/search/artifacts" \
-H "Authorization: Bearer $TOKEN" | jq '.count')
echo "v2 artifacts: $V2_COUNT"
# 5. Import schemas into v3 (apicurio-api client — v3 is not frozen)
TOKEN=$(get_token "$APICURIO_API_CLIENT_ID" "$APICURIO_API_CLIENT_SECRET")
curl -skv "https://${NEW_APICURIO_HOST}/apis/registry/v3/admin/import" \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/zip" \
--data-binary @apicurio-export.zip
# 6. Count the artifacts in v3 and compare against v2
V3_COUNT=$(curl -skv "https://${NEW_APICURIO_HOST}/apis/registry/v3/search/artifacts" \
-H "Authorization: Bearer $TOKEN" | jq '.count')
echo "v2 artifacts: $V2_COUNT, v3 artifacts: $V3_COUNT"
[ "$V2_COUNT" = "$V3_COUNT" ] && echo "OK: counts match" || echo "WARNING: counts differ"