Skip to content

Local development deployment

utilities/deploy_sites.sh builds the platform images from this checkout, loads them into a local cluster, and installs the sites Helm release from helm/ecmwf-sites with utilities/values-local-dev.yaml. Use the sys-admin guides for shared platforms.

Requirements

Docker, kubectl, Helm, and one local Kubernetes runtime: kind, k3d, or a single-node k3s (run the script on the k3s node). All cluster nodes must run the same supported image platform: linux/amd64 or linux/arm64. The script enforces this: it reads the platform of every node, stops on a mixed-platform cluster or an unsupported platform, and stops when a --platform override differs from the node platform. For k3s, it also stops when the cluster has more than one node, because the image import only reaches the local node.

Deploy

make deploy-dev                          # or select:
make deploy-dev RUNTIME=kind TAG=local-dev
./utilities/deploy_sites.sh --runtime kind --tag local-dev
flowchart LR
    A["detect runtime and node platform"] --> B["build 11 images"]
    B --> C["load images into the cluster nodes (kind load / k3d import / k3s ctr)"]
    C --> D["verify loaded images on every node"]
    D --> E["ensure TLS Secret (self-signed when missing)"]
    E --> F["helm upgrade --install sites (post-rendered, values-local-dev.yaml)"]
    F --> G["delete platform pods so they use the loaded images"]

The script never pushes images to a registry. Site images are loaded under both the short name and the full registry/project/name reference, because the operator renders full references into Site Pod image settings. Do not run the script while another operator changes the same release.

The 11 built images are core, hub, operator, site-api, site-proxy, site-admin, site-oauth2, site-s3, site-webdav, site-mirror, and site-initializer (matrix in set_image_matrix()). core, hub, and operator use the Dockerfiles under utilities/docker/local-dev/ (source builds from this checkout); the site images reuse the production site/*.Dockerfile files, the same files make publish-images builds. Each build passes TAG=<local tag> as a build argument, and the script inspects each built image and stops when its platform does not match the requested one. Every site-* image is then re-tagged with the full registry reference (build_images).

Runtime detection and cluster access

Without --runtime, the script reads the current kubectl context and matches, in order: kind-*, then any context containing k3s, then k3d-* (or any context containing k3d), then a kubectl version probe for a +k3s server build. When nothing matches, it stops and tells you to pass --runtime or set SITES_CLUSTER_RUNTIME.

With a runtime selected, the script prepares access (configure_cluster_access):

  • kind: takes the cluster name from the kind-<name> context, or from the first kind get clusters entry, and runs kind export kubeconfig.
  • k3d: takes the cluster name from the k3d-<name> context, or from the first k3d cluster list entry.
  • k3s: probes for a usable containerd import command, in order: k3s ctr, sudo -n k3s ctr, ctr -n k8s.io, sudo -n ctr -n k8s.io. It stops with guidance when none works.

Build, load, and verify

Loading (load_image): kind load docker-image per image, k3d image import per image, or docker save <image> | k3s ctr images import -. Each site-* image is loaded twice, under both its tags (load_images).

Verification (verify_loaded_images) runs before Helm. It lists the cluster nodes (kind get nodes; k3d node containers by label; k3s: the local node), reads the containerd image list on each node, and stops when any image is missing. Short names are checked as docker.io/library/<image>; site-* images are additionally checked under the full registry reference.

Helm upgrade

deploy_sites.sh then (deploy_sites):

  1. applies the namespace (default sites-dev) idempotently;
  2. ensures the TLS Secret (see below);
  3. runs helm upgrade --install sites helm/ecmwf-sites with the local values file and these overrides:
  4. images.tag and sitesConfig.hub.version set to the local tag, so the release references the images it just loaded;
  5. images.pullPolicy and sitesConfig.kubernetes.image_pull_policy set to IfNotPresent, so neither platform pods nor Site pods try to pull the local tags;
  6. sitesConfig.kubernetes.private_registry, private_registry_project, and private_registry_image_pull_secrets_name from the registry settings below;
  7. one --set-file sitesConfig.branding.<asset> per branding file present (see Local branding assets);
  8. deletes all pods labelled sites.ecmwf.int/app in the platform namespace (forced, grace period 0) so they restart on the refreshed images. Site pods are not restarted; recreate a Site to apply new Site images.

Post-renderer

The Helm release is post-processed by utilities/local-dev/post-renderer/render.sh: it writes the rendered chart plus kustomization.yaml to a temporary directory and runs kustomize build (or kubectl kustomize). The kustomization applies the local changes the chart does not offer: it runs the Hub Deployment and every CronJob as user and group 0, so they can write to local (non-NFS) volumes. With Helm 4, which takes a post-render plugin instead of a direct command, the script re-installs the same renderer as the Helm plugin sites-local-dev on every run and passes the plugin name.

Options

Setting Effect
--runtime kind\|k3d\|k3s / RUNTIME (make) / SITES_CLUSTER_RUNTIME Override runtime auto-detection
--tag / TAG (make) / SITES_LOCAL_IMAGE_TAG Local image tag (default local-dev)
--platform / SITES_LOCAL_IMAGE_PLATFORM Build platform; must match the cluster node platform
DOCKER_BIN Path to the docker binary
SITES_NAMESPACE Namespace (default sites-dev)
SITES_PRIVATE_REGISTRY, SITES_PRIVATE_REGISTRY_PROJECT Registry prefix used in rendered site image references (defaults eccr.ecmwf.int, sites); loading stays local
SITES_PRIVATE_REGISTRY_IMAGE_PULL_SECRET Optional pull-secret name published into local config (also set on the service account)
SITES_TLS_HOST, SITES_TLS_SECRET_NAME, SITES_TLS_DAYS Self-signed certificate host (default: routing.host in utilities/values-local-dev.yaml), secret name (default: routing.tls.secretName), validity (default 365 days)
SITES_TLS_HUB_HOST, SITES_TLS_HUB_SECRET_NAME Hub certificate host (default: routing.hubHost) and secret name (default: routing.tls.hubSecretName; when empty, the main Secret covers both hosts)

Local values overlay

utilities/values-local-dev.yaml is the local overlay. Beyond the defaults it documents itself, it:

  • sets dev: true and debug: true (see below);
  • routes through ingress mode on host sites-dev.ecmwf.int (the TLS defaults follow this host) with TLS from Secret sites-dev-tls;
  • provisions a local 10 GiB ReadWriteOnce persistent volume claim (PVC) on the cluster default StorageClass instead of shared storage, and disables website NetworkPolicies;
  • creates placeholder local OpenID Connect (OIDC) and application Secrets through the chart, so the deployment runs without the shared Vault publishing;
  • points OIDC at the accounts-test realm, and selects the prometheus metrics provider and email notifications with the other integrations off.

Local branding assets

Place logo.svg and/or favicon.svg in utilities/branding/ to brand the local platform:

  • The script validates each existing file with the platform startup SVG rules (parse_branding_svg from sites-core, the same checks the Hub and operator apply; see 16). The check runs through uv against this workspace.
  • A valid file is passed to Helm with --set-file sitesConfig.branding.<asset>. An invalid file stops the deployment with the platform error message, instead of breaking pods later.
  • A missing file falls back to the packaged Sites asset.

Local TLS and verification

The script reads routing.host and routing.tls.secretName from the local values file (SITES_TLS_HOST and SITES_TLS_SECRET_NAME override). When routing.hubHost is set, it also makes the Hub certificate: it reads routing.hubHost and routing.tls.hubSecretName (SITES_TLS_HUB_HOST and SITES_TLS_HUB_SECRET_NAME override) and creates a second Secret. When routing.tls.hubSecretName is empty, the chart uses routing.tls.secretName for both hosts, so the script puts both host names in that one certificate. It keeps an existing TLS Secret; delete the Secret to force a new certificate. Otherwise it generates a self-signed certificate with openssl: RSA-2048, CN and a DNS Subject Alternative Name for the host, valid for SITES_TLS_DAYS days, written under a temporary directory with a 077-mask and published with kubectl create secret tls.

The generated certificate is self-signed. The local values set sitesConfig.dev: true, which (in the configuration schema) disables TLS certificate verification for Site API, Splunk, and OpenSearch connections, and enables unredacted logs. That is acceptable only on an isolated development cluster: an active network attacker could read request credentials. Never carry dev: true into a shared environment (CFG).

  • utilities/tests/deploy-sites-branding.test.sh — branding validation and usage text (needs uv, kubectl, helm; skips otherwise). It accepts a plain logo and an internal fragment reference, and rejects a script element, a DOCTYPE, an external image reference, an event attribute, broken XML, non-UTF-8 bytes, and an oversized file. It also checks that the usage text documents utilities/branding/.
  • utilities/tests/local-dev-post-renderer.test.sh — the Helm post-renderer (needs helm plus kustomize or kubectl; skips otherwise). It renders the real chart and asserts five root securityContext patches (Hub plus the four CronJobs), that a representative chart resource survives the render, and that the renderer never injects SITE_VERIFY_TLS. The patches replace the pod security context of the chart to run as root. The container security contexts stay: a read-only root filesystem and no Linux capabilities.

Run them with bash utilities/tests/<name>.test.sh; neither runs in CI, so run them after touching deploy_sites.sh or the post-renderer.

Behaviour requirements

ID Requirement Implemented in Verified by
DEV-01 The script builds all 11 images and loads them locally without pushing. utilities/deploy_sites.sh (build_images, load_images) —
DEV-02 Loaded images are verified before Helm runs; the platform pods are restarted after the upgrade. deploy_sites.sh (verify_loaded_images, deploy_sites) —
DEV-03 A self-signed TLS Secret is created only when absent. deploy_sites.sh (ensure_tls_secret) —
DEV-04 A local branding SVG is validated with the platform rules before install; invalid files stop the deployment; missing files use the packaged asset. deploy_sites.sh (validate_branding_svg) utilities/tests/deploy-sites-branding.test.sh
DEV-05 The usage text documents the utilities/branding/ directory the script reads. deploy_sites.sh (usage) utilities/tests/deploy-sites-branding.test.sh
DEV-06 The local overlay sets dev: true; the script itself never sets TLS verification off. utilities/values-local-dev.yaml utilities/tests/local-dev-post-renderer.test.sh (no SITE_VERIFY_TLS injection)
DEV-07 Runtime auto-detection follows the context-name and server-version order and stops with guidance when it cannot detect a runtime; k3s import support is probed in a fixed command order. deploy_sites.sh (detect_cluster_runtime, configure_k3s_import) —
DEV-08 Deployment stops on a mixed-platform or unsupported cluster, on a platform override that does not match the nodes, on a built image with the wrong platform, and on a multi-node k3s cluster. deploy_sites.sh (select_image_platform, build_images) —
DEV-09 The post-renderer runs the Hub Deployment and every CronJob as root through kustomize; Helm 3 uses the script directly, Helm 4 uses the re-installed sites-local-dev plugin. utilities/local-dev/post-renderer/ (render.sh, kustomization.yaml, plugin.yaml), deploy_sites.sh (local_dev_post_renderer) utilities/tests/local-dev-post-renderer.test.sh
DEV-10 The Helm upgrade pins the release to the locally loaded tag and IfNotPresent pull policies, for platform images and for rendered Site pods. deploy_sites.sh (deploy_sites) —