Skip to content

ADR-029: External services in services.yaml

Status: Accepted Date: 2026-10-22 Amends: ADR-017: The GitOps repo owns every Kubernetes manifest; services ship an image

ADR-017 made the gitops-app the owner of every manifest, generated by render.py from services.yaml. The generator writes a Deployment, a Service, and optional routes and secrets. Real services need more: stock-manager runs three CronJobs, a PVC, a CloudNativePG cluster with its own settings and an oauth2-proxy in front of the web pod; ai-ops is a Helm chart with CRDs. Both are therefore deployed by hand-written Argo CD Applications: stock-manager from its own repo’s deploy/ folder, ai-ops from its chart with a hand-pinned tag. pin-dev, /gitops:promote and /shared:status do not see them, and every deploy is a manual edit (review).

  1. Teach the generator every kind (CronJob, PVC, NetworkPolicy, proxies, charts). This is the cleanest in the end, but it is a long road, and each new need waits for a platform release.
  2. An external entry type (chosen). The service’s package is written by hand; the generator only wires it into the environments and pins its image.
  3. Leave such services outside services.yaml. That is today’s state, with manual pins.

A services.yaml entry may replace the generator keys with external (spec 069):

  • external: {kustomize: <path>}: a kustomize package under applications/<app>/ ({env} allowed in the path), so it is still owned by the gitops-app and reviewed there. The environment’s overlay includes it, sets namespace if given, and pins the image in images.
  • external: {helm: {repoURL, path, revision, imageTag}} with values and envValues: the overlay holds an Argo CD Application for the chart (server-side apply, for CRDs), with the pinned tag written at imageTag. The Helm release is named after the service (releaseName overrides it), not after the Application: charts derive their selector labels from the release name, and a Deployment’s selector cannot change, so a chart that ran before keeps working.

The pin always lives in the overlay’s kustomization.yaml images, so pin.py, /gitops:promote and /shared:status work unchanged. For a chart, pin.py and promote re-render, so the Application gets the tag. External entries may not carry generator keys (port, probes, env, secrets, expose …); render refuses them and names the problem.

  • stock-manager and ai-ops can be pinned by pin-dev and promoted like any service.
  • The hand-written packages are not covered by the generator’s guarantees: the Kyverno guard rails apply only in <app>-<env>, and nothing checks probes or resources. Review them like code.
  • A chart’s revision (default main) moves independently of the image tag. Pin it to a tag where chart and image must match.
  • The ApplicationSet’s app for an external service still creates the <app>-<env> namespace, even when the package deploys elsewhere.