The OAP Init Job
The chart creates the storage schema with a one-shot *-oap-init-* Job, and the main OAP
Deployment refuses to serve until that Job has finished. This page explains that handshake, why
the Job is deliberately not a Helm hook, and how to watch, rerun, or clean it up.
The handshake
Two OAP containers from the same image, started with different modes:
| Workload | JAVA_OPTS |
Behaviour |
|---|---|---|
*-oap-init-* Job |
<oap.javaOpts> -Dmode=init |
Creates the storage schema (Elasticsearch indices / SQL tables / BanyanDB groups), then exits. restartPolicy: Never. |
*-oap Deployment |
-Dmode=no-init <oap.javaOpts> |
Never touches the schema. Blocks with port 12800 closed until the schema exists, so the pod is not Ready. |
Order of events on helm install:
- Both the Job pod and the OAP pods run the same
wait-for-storageinit container and block until the storage backend answers. For Elasticsearch and BanyanDB it gives up after 60 attempts 5s apart (at least 5 minutes) and fails; for PostgreSQL it retriespg_isreadyevery 3s indefinitely. - The Job’s OAP boots in
initmode, writes the schema and exitsCompleted. - The OAP pods, still blocked in
no-initmode, see the schema, open12800, and pass their startup/readiness probes. - Helm’s
--waitresolves, because the Job and the Deployment run in the same phase.
Because the OAP pods’ readiness depends on the Job, a failing Job shows up as OAP pods stuck in
0/1 Running — see Watching and debugging below.
Why it is not a Helm hook
The Job is a normal release resource, not a post-install/post-upgrade hook. A hook would
deadlock helm upgrade --install --wait:
- Helm waits for every release resource to become Ready before it runs
post-*hooks. - The OAP Deployment never becomes Ready until the schema exists.
- The schema is created by the hook, which never runs. Helm waits until timeout.
As a main-phase resource the Job runs alongside the Deployment, and the two unblock each other.
To have Helm surface init-Job failures directly — instead of only reporting that OAP never became
Ready — add --wait-for-jobs alongside --wait:
helm upgrade --install skywalking oci://registry-1.docker.io/apache/skywalking-helm \
--version 5.0.0 -n skywalking --create-namespace \
--set oap.image.tag=11.0.0 \
--set oap.storageType=banyandb \
--set ui.image.tag=horizon-1.0.0 \
--set elasticsearch.enabled=false \
--set banyandb.enabled=true \
--set banyandb.image.tag=0.11.0 \
--wait --wait-for-jobs
The value-hashed Job name
A Job’s spec.template is immutable. With a stable name, any helm upgrade that changes the pod
template (a new image tag, a new env var) would fail with field is immutable. So the name carries
a short hash of the rendered values:
{{ printf "%s-init-%s" (include "skywalking.oap.fullname" . | trunc 40 | trimSuffix "-")
(.Values | toYaml | sha256sum | trunc 8) }}
For a release named skywalking that renders to, for example:
skywalking-skywalking-helm-oap-init-39aeb14b
Consequences worth knowing:
- Any value change produces a new Job name, so
helm upgradecreates a fresh Job and re-runs init; Helm prunes the previous one because it is no longer in the manifest. - The hash covers the whole values tree, including subchart values — an unrelated change (a UI setting, a BanyanDB replica count) also re-runs init.
- The OAP name is truncated to 40 characters before the suffix, so the Job name stays inside the 63-character DNS limit.
Configuring the Job
The Job reuses the OAP values for everything that must match the server: oap.image.*,
oap.javaOpts, oap.env, oap.resources, oap.securityContext, the OAP service account, the
storage env vars derived from oap.storageType, and the same oap.config / oap.secretMounts
volumes. Only these oapInit.* keys are Job-specific:
| Value | Default | Purpose |
|---|---|---|
oapInit.nodeAffinity |
{} |
Node affinity for the Job pod |
oapInit.nodeSelector |
{} |
Node selector for the Job pod |
oapInit.tolerations |
[] |
Tolerations for the Job pod |
oapInit.extraPodLabels |
{} |
Extra pod labels, e.g. sidecar.istio.io/inject: "false" |
oapInit.ttlSecondsAfterFinished |
"" |
Auto-delete the finished Job via the TTL-after-finished controller |
A service mesh sidecar is the common reason to set extraPodLabels — an injected sidecar keeps the
pod alive after the OAP container exits, so the Job never reports Complete:
oapInit:
extraPodLabels:
sidecar.istio.io/inject: "false"
ttlSecondsAfterFinished and the GitOps caveat
oapInit.ttlSecondsAfterFinished is empty by default, so completed Jobs and their pods stay in the
namespace where you can still read their logs. Set it to tidy them up:
--set oapInit.ttlSecondsAfterFinished=600
Leave it empty when the release is managed by a GitOps tool (Argo CD, Flux). The TTL controller deletes the Job, the GitOps reconcile sees a resource missing from the desired state, recreates it, and init runs again on every sync loop. The value-hashed name already makes upgrades work without TTL — the setting is only cosmetic cleanup.
The startup probe budget
Because no-init OAP keeps 12800 closed while it waits, the chart’s default startup probe is
generous — from chart/skywalking/values.yaml:
oap:
startupProbe: {}
# Boot budget defaults to 30 (failureThreshold) * 10 (periodSeconds) = 300 seconds.
# In no-init mode OAP keeps port 12800 closed until the OAP init Job has created the storage
# schema, so the budget must be large enough to cover storage startup + schema creation;
# otherwise the pod is restarted while it is legitimately waiting for the init Job.
That 300-second budget has to cover storage startup plus schema creation. A cold Elasticsearch cluster, a slow PVC, or a first-time BanyanDB group creation can exceed it, and the pod is then restarted while it was legitimately waiting. Raise it rather than reducing the wait:
oap:
startupProbe:
tcpSocket:
port: 12800
failureThreshold: 90 # 90 * 10s = 15 minutes
periodSeconds: 10
Setting oap.startupProbe replaces the default block entirely, so repeat tcpSocket.port: 12800.
The liveness and readiness probes default to the same TCP check on 12800 with
initialDelaySeconds: 5, periodSeconds: 10.
Watching and debugging
export NS=skywalking
export RELEASE=skywalking
# The chart renders exactly one Job, whatever the storage type
kubectl get jobs -n "$NS" -l release="$RELEASE"
JOB=$(kubectl get jobs -n "$NS" -l release="$RELEASE" -o jsonpath='{.items[0].metadata.name}')
# Logs (while running, and after completion until the TTL removes it)
kubectl logs -n "$NS" "job/$JOB" --tail=200
# Block until it finishes
kubectl wait --for=condition=complete -n "$NS" "job/$JOB" --timeout=10m
Common outcomes:
| Symptom | Cause |
|---|---|
Job pod stuck in Init:0/1 |
wait-for-storage cannot reach the backend — wrong host/port, backend not up |
Job pod Error / BackoffLimitExceeded |
OAP could not create the schema: bad credentials, an incompatible or half-started backend. Read the logs |
OAP container exited, pod still Running, Job never Complete |
An injected mesh sidecar keeps the pod alive; set oapInit.extraPodLabels as above |
OAP pods 0/1 Running, restarting every ~5 min |
Init never completed, and the startup probe budget expired |
Forcing a rerun
Every helm upgrade with a changed value already reruns init. To rerun it without changing any
value, delete the Job and upgrade — Helm recreates the now-missing resource:
kubectl delete job -n skywalking -l release=skywalking
helm upgrade skywalking oci://registry-1.docker.io/apache/skywalking-helm \
--version 5.0.0 -n skywalking --reuse-values
See also
- Configure OAP — the
oap.env,oap.javaOptsand config-override values the Job inherits - Pick a Backend — what “the schema” means per storage type
- Install and Startup troubleshooting
- Upgrade
- skywalking chart values