How to Migrate Schemas to Apicurio v3
This guide shows you how to freeze schema registration in Apicurio Registry v2, export the schemas, import them into v3, verify the import, switch the production hostname to v3 and confirm that production traffic reaches v3.
Type |
How-to guide |
Goal |
Serve every existing schema from Apicurio Registry v3 on the production hostname, with v2 kept on a legacy hostname for rollback. |
Audience |
Platform Operator with Helm access to the Axual Streaming release in the |
When to use |
Use this guide as the second stage of Upgrade Apicurio Registry from v2 to v3, after v3 runs alongside v2. |
Prerequisites
Confirm the following before you begin.
Access and permissions required
You need the following access and permissions:
-
Helm access to the Axual Streaming release in the
kafkanamespace. -
The client secret of the
apicurio-migrationKeycloak client. -
The client secret of the platform’s
apicurio-apiKeycloak client.
Resources that must exist before starting
The following resources must already exist before you start:
-
Apicurio v3 running alongside v2 on a temporary hostname, and empty. See How to Deploy Apicurio v3 Alongside v2.
-
The
apicurio-migrationKeycloak client with themigration-adminrole. See How to Create the Apicurio Migration Client. -
Applications that use the Confluent-compatible API pointing at
/apis/ccompat/v7, because v3 no longer serves/apis/ccompat/v6. See Confluent-compatible (ccompat) endpoint.
|
Run Steps 2 to 6 in one sitting. Schema registration is blocked from Step 2 until the ingress switch in Step 6, typically for 30 to 45 minutes; reads keep working throughout. The upgrade can’t run incrementally: if a schema is added to v2 after you export, empty v3 with Restore the Apicurio v3 topics and start again from Step 3. |
Replace every <VALUE> placeholder and every example.com hostname with your actual value before running a command. [same as configured for your apicurio-registry] means copy the value from the matching key of your existing apicurio-registry block.
|
Step 1: Set shell variables
The CLI commands in this guide, and the script, reuse the shell variables below. 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 (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>"
Step 2: Block schema registration in v2
Apicurio v2 has no global read-only switch, so freeze registration with two changes to the v2 deployment that keep reads working:
-
Remap the developer roles (in
apicurio-registry.env) so no token resolves to a developer role:sr-developerandsr-adminmatch nothing, and only the migration client (holdingmigration-admin) can still register subjects. -
Disable
admin-override(inapicurio-registry.config). The chart enables it with rolesr-admin, and Apicurio evaluates it before role-based access control (RBAC), so anysr-admintoken, including the platform’sapicurio-api, would otherwise bypass the freeze.
axual-streaming:
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 the `migration-admin` role) can still register subjects.
- name: REGISTRY_AUTH_ROLES_DEVELOPER
value: "sr-developer-frozen" # no token carries this name, so apps registering subjects are 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, using the same command as in Step 2: Deploy Apicurio v3. Only the v2 registry pod restarts, and reads resume within seconds. Every client, including the platform’s apicurio-api (sr-admin), is now read-only, and only the migration-admin client can register or export.
The freeze doesn’t affect anonymous read access, which only REGISTRY_AUTH_ANONYMOUS_READS_ENABLED (registry.auth.anonymous-read-access.enabled) controls. Apicurio evaluates anonymous read before RBAC, so the role remap can’t disable it. With anonymous read enabled (as in production), both anonymous and authenticated reads keep working throughout; only subject registration is blocked.
|
Step 3: Export schemas from v2
Use the dedicated migration client ($MIGRATION_CLIENT_ID), not the platform’s apicurio-api client, which must stay frozen. Get a token for it, record how many artifacts v2 holds so you can match the count on v3 in Step 5, then export all schemas and metadata:
# Get an access token from the v2 Keycloak.
# Use the migration client: it holds migration-admin, 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 and raise it, for example to 600 seconds, so the connection isn’t dropped mid-transfer. For nginx, the annotations are nginx.ingress.kubernetes.io/proxy-read-timeout and proxy-send-timeout.
|
Verify the export file isn’t empty:
ls -lh apicurio-export.zip # should be > 0 bytes
Step 4: Import schemas into v3
Import the export file into v3 with the platform’s apicurio-api client.
|
The v3 registry must be empty for the import to succeed. If a previous attempt left data behind, or you need to start over, empty v3 first with Restore the Apicurio v3 topics. |
Import is an admin operation on v3, which isn’t frozen, so use the platform’s apicurio-api client ($APICURIO_API_CLIENT_ID and $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. Fetch the token from the existing v2 Keycloak, which v3 still uses 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 only their metadata, and that the count matches the v2 artifact count you recorded in Step 3:
# Count imported artifacts; must match the v2 count from Step 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
If the counts differ or an artifact has no versions, see How to Troubleshoot the Apicurio v3 Upgrade before you switch ingress.
Step 6: Switch ingress to v3
Move the production hostname from v2 to v3, and v2 to a legacy hostname.
| Don’t disable v2 yet. v3 depends on the v2 Keycloak for authentication until you complete How to Remove Apicurio v2. |
Update v3 to the production URL, the EXISTING_APICURIO_HOST your v2 deployment used until now. Re-point registryApiUrl, webRedirectUrl, both ingress hosts and their tls.hosts away from the temporary NEW_APICURIO_HOST:
axual-streaming:
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 for a rollback. Re-point its webRedirectUrl, ingress host and tls.hosts too; otherwise the v2 UI redirect and certificate still reference the production hostname that v3 now serves:
axual-streaming:
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: "apicurio-legacy.example.com"
tls:
- secretName: [same as configured for your apicurio-registry]
hosts:
- "apicurio-legacy.example.com"
If you use the Auth Proxy, 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 a rollback.
axual-streaming:
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 with the same authProxy ingress key you configured in your v2 values (for example apicurio-auth-proxy-legacy.example.com), mirroring the v2 registry move above. Skip this if you don’t run the Auth Proxy.
|
Apply all changes with a helm upgrade, using the same command as in Step 2: Deploy Apicurio v3. Schema registration resumes as soon as the production hostname reaches v3, because the freeze applies only to v2.
Your Instance Clusters need no change to their Schema Registry URL. It keeps the same hostname and the /apis/registry/v2 path; this step repoints that hostname from v2 to v3, and Apicurio v3 keeps serving the v2 API. The Axual Platform switches to the v3 API in 2026.4.
|
Step 7: Verify production traffic reaches v3
Check that the production hostname now reaches v3 on both API paths, and that it holds the same number of artifacts as v2 did. Only v3 serves the /apis/registry/v3 path, so an answer there proves the cutover; the /apis/registry/v2 path is the one your Instance Clusters and applications keep using.
# Get a fresh token for the platform apicurio-api client.
TOKEN=$(curl -skS --fail-with-body -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')
# v3 API on the production hostname; must match the v2 count from Step 3.
curl -skS --fail-with-body \
"https://${EXISTING_APICURIO_HOST}/apis/registry/v3/search/artifacts?limit=1" \
-H "Authorization: Bearer $TOKEN" | jq '.count'
# v2 API on the production hostname, now served by v3; must print the same count.
curl -skS --fail-with-body \
"https://${EXISTING_APICURIO_HOST}/apis/registry/v2/search/artifacts?limit=1" \
-H "Authorization: Bearer $TOKEN" | jq '.count'
Both commands print the artifact count you recorded in Step 3. Then open https://apicurio.example.com in a browser and confirm the v3 web console lists your artifacts. If either check fails, see How to Roll Back the Apicurio v3 Upgrade.
Run steps 3 to 5 with a script
The script below runs the CLI part of this guide in one pass: the export from v2 (Step 3), the import into v3 (Step 4) and the count comparison from Step 5, with the same variables as Step 1. It doesn’t perform the Helm changes: apply the registration freeze (Step 2) before running it, and the ingress switch (Step 6) after.
The registration freeze must already be in place when you run this. The script compares $V2_COUNT and $V3_COUNT and warns if they differ, but it can’t detect schemas registered in v2 after the export; the freeze prevents that.
|
#!/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"
Restore the Apicurio v3 topics
To discard a failed import and run Step 4 again, the v3 store must be empty. 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 (Access Control Lists), empty. -
Re-run the import from Step 4.
| This restores only the v3 registry store and doesn’t touch v2. v2 keeps serving reads throughout. |
Next steps
Keep v2 on its legacy hostname for your rollback window. Once you’re confident in v3, continue with How to Remove Apicurio v2.