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
Context
Section titled “Context”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).
Options
Section titled “Options”- 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.
- An
externalentry type (chosen). The service’s package is written by hand; the generator only wires it into the environments and pins its image. - Leave such services outside services.yaml. That is today’s state, with manual pins.
Decision
Section titled “Decision”A services.yaml entry may replace the generator keys with external (spec 069):
external: {kustomize: <path>}: a kustomize package underapplications/<app>/({env}allowed in the path), so it is still owned by the gitops-app and reviewed there. The environment’s overlay includes it, setsnamespaceif given, and pins the image inimages.external: {helm: {repoURL, path, revision, imageTag}}withvaluesandenvValues: the overlay holds an Argo CD Application for the chart (server-side apply, for CRDs), with the pinned tag written atimageTag. The Helm release is named after the service (releaseNameoverrides 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.
Consequences
Section titled “Consequences”- 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(defaultmain) 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.