Zaroz
Deploy from CI/CD

Deploy from any shell

zaroz-deploy.sh is everything the CI-specific guides call. It needs curl, jq and a POSIX shell, so it runs in Jenkins, Drone, Woodpecker, Buildkite, a Makefile or a cron job.

TerminalCode
IMAGE=ghcr.io/acme/api:3f2a1c9d8e7b \ ZAROZ_SERVICE=2d1a0c1e-0000-0000-0000-000000000000 \ ZAROZ_TOKEN=zaroz_... \ sh deploy/zaroz-deploy.sh
VariableRequiredMeaning
IMAGEyeshost/repository:tag to run
ZAROZ_SERVICEyesthe service instance UUID
ZAROZ_TOKENyesthe deploy token
REGISTRY_USERNAME, REGISTRY_PASSWORDnoa pull-only login for a private registry
ROLLBACKno1 (default) puts the previous image back when the rollout fails; 0 disables
DEPLOY_TIMEOUT_SECONDSnohow long to wait, default 600
ZAROZ_APInodefaults to https://kernel.zaroz.cloud/api/v2

Exit code 0 means the new image is serving. Anything else means it is not; if rollback ran, the previous image is serving again.

The script

TerminalCode
#!/usr/bin/env sh # Deploy an image to a Zaroz Workload and fail the job if the rollout fails. # # IMAGE=ghcr.io/acme/api:3f2a1c ZAROZ_SERVICE=<uuid> ZAROZ_TOKEN=zaroz_... ./zaroz-deploy.sh # # Steps: remember what runs now, point the Workload's source at IMAGE, start a # deployment, wait for it, check the result, and (ROLLBACK=1, the default) put # the previous image back when the new one did not roll out. # # Optional: REGISTRY_USERNAME / REGISTRY_PASSWORD when the image is in a private # registry (stored by Zaroz, never returned by the API), DEPLOY_TIMEOUT_SECONDS, # ROLLBACK=0 to disable the automatic rollback, ZAROZ_API. # # Needs: curl, jq. POSIX sh. No other dependency. set -eu : "${ZAROZ_TOKEN:?set ZAROZ_TOKEN to the deploy token}" : "${ZAROZ_SERVICE:?set ZAROZ_SERVICE to the service instance uuid}" : "${IMAGE:?set IMAGE to host/repository:tag}" API="${ZAROZ_API:-https://kernel.zaroz.cloud/api/v2}" TIMEOUT="${DEPLOY_TIMEOUT_SECONDS:-600}" ROLLBACK="${ROLLBACK:-1}" BASE="$API/service-instances/$ZAROZ_SERVICE" # Secrets never go in argv (visible to `ps` for every user on a shared runner): # the bearer header travels in curl's config on stdin, the body in a 0600 temp file. # The registry login is read by jq from the environment, not from its arguments. umask 077 TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT export REGISTRY_USERNAME="${REGISTRY_USERNAME:-}" REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}" call() { # method path [json body]; prints the body, fails on a non-2xx _out="$TMP/out"; : > "$_out" if [ -n "${3:-}" ]; then printf '%s' "$3" > "$TMP/body"; set -- "$1" "$2" --data "@$TMP/body"; fi _method="$1"; _path="$2"; shift 2 _code="$(printf 'header = "Authorization: Bearer %s"\n' "$ZAROZ_TOKEN" \ | curl -sS -K - -o "$_out" -w '%{http_code}' -X "$_method" "$BASE$_path" \ -H 'Content-Type: application/json' "$@")" rm -f "$TMP/body" case "$_code" in 2??) cat "$_out" ;; *) echo "zaroz: $_method $_path answered $_code: $(cat "$_out")" >&2; return 1 ;; esac } # host/repository:tag -> three parts (a tag after the last colon of the last path segment) split_image() { _host="${1%%/*}"; _rest="${1#*/}" case "$_rest" in *:*) _repo="${_rest%:*}"; _tag="${_rest##*:}" ;; *) _repo="$_rest"; _tag=latest ;; esac printf '%s %s %s' "$_host" "$_repo" "$_tag" } set_source() { # image set -- $(split_image "$1") # Registry login is only sent when given; absent leaves the stored login alone. call PUT /source "$(jq -n --arg h "$1" --arg r "$2" --arg t "$3" \ '{kind:"registry",host:$h,repository:$r,tag:$t} + (if (env.REGISTRY_USERNAME // "") != "" then {credentials:{username:env.REGISTRY_USERNAME,password:env.REGISTRY_PASSWORD}} else {} end)')" >/dev/null } deploy_and_wait() { # prints the deployment json; exit 0 when it rolled out _dep="$(call POST /deployments)" _id="$(printf '%s' "$_dep" | jq -r .id)" echo "zaroz: deployment #$(printf '%s' "$_dep" | jq -r .number) started" >&2 _deadline=$(( $(date +%s) + TIMEOUT )) while :; do _list="$(call GET '/deployments?page_size=5')" _active="$(printf '%s' "$_list" | jq -r '.active_deployment_id // empty')" if [ -z "$_active" ]; then break; fi [ "$(date +%s)" -lt "$_deadline" ] || { echo "zaroz: timed out after ${TIMEOUT}s" >&2; return 1; } sleep 5 done _outcome="$(printf '%s' "$_list" | jq -r --arg id "$_id" '[.data[]|select(.id==$id)][0].outcome // "unknown"')" _reason="$(printf '%s' "$_list" | jq -r --arg id "$_id" '[.data[]|select(.id==$id)][0].failure_reason // ""')" # "succeeded" only says the rollout was applied; the workloads say whether it is serving. _bad="$(printf '%s' "$_list" | jq -r '[.workloads[]|select(.status!="running")|.role+"="+.status]|join(",")')" if [ "$_outcome" != succeeded ]; then echo "zaroz: rollout $_outcome ${_reason:+($_reason)}" >&2; return 1; fi if [ -n "$_bad" ]; then echo "zaroz: not serving: $_bad" >&2; return 1; fi } PREVIOUS="$(call GET /source | jq -r '.running_image // empty')" echo "zaroz: running now: ${PREVIOUS:-nothing}; deploying $IMAGE" >&2 set_source "$IMAGE" if deploy_and_wait; then echo "zaroz: $IMAGE is serving" >&2 exit 0 fi if [ "$ROLLBACK" = 1 ] && [ -n "$PREVIOUS" ] && [ "$PREVIOUS" != "$IMAGE" ]; then echo "zaroz: rolling back to $PREVIOUS" >&2 set_source "$PREVIOUS" deploy_and_wait && echo "zaroz: rolled back to $PREVIOUS" >&2 || echo "zaroz: rollback failed too" >&2 fi exit 1

Why the token is never on the command line

Arguments of a process are readable by every user on the machine (ps, /proc/<pid>/cmdline), and CI runners are often shared. So the script never writes the token or the registry password in an argument: the Authorization header goes to curl -K - as configuration on standard input (printf is a shell builtin, so it appears in no process list), the request body goes through a private temporary file, and jq reads the registry login from the environment. Copy the same habit into your own scripts: curl -H "Authorization: Bearer $TOKEN" is the pattern to avoid.

What each request does

The script is five requests. You can run them yourself:

TerminalCode
B="$ZAROZ_API/service-instances/$ZAROZ_SERVICE" zcurl() { printf 'header = "Authorization: Bearer %s"\n' "$ZAROZ_TOKEN" | curl -sS -K - "$@"; } zcurl "$B/source" | jq '{running_image, pending}' # what runs now, and is a change pending zcurl -X PUT "$B/source" -H 'Content-Type: application/json' \ -d '{"kind":"registry","host":"ghcr.io","repository":"acme/api","tag":"3f2a1c9d8e7b"}' zcurl -X POST "$B/deployments" # deploys what was saved; no body zcurl "$B/deployments?page_size=5" \ | jq '{active_deployment_id, newest: .data[0] | {number, status, outcome, failure_reason}, workloads}'

failure_reason is one of not_configured, product_not_deployable, subscription_not_active, hostname_taken, invalid_hostname, network_unavailable, cluster_rejected, unknown. Zaroz never repeats raw cluster messages, because they quote internal hostnames; the closed vocabulary is what you get.

Testing the script without a Workload

The repository unit-examples ships _shared/test/run.sh, which runs this script against a small mock of those routes and checks the success path and the rollback path.

Last modified on