Add a service to dracolich-helm
Onboard a service so ArgoCD deploys it and its CI can bump the image tag.
Mirror, don't invent. Everything you write comes from an existing service in the repo. A plausible chart that doesn't match local conventions is worse than none — it looks reviewable while being wrong, and it deploys successfully while routing to the wrong place.
The repo, as it actually is
~/Dev/Dracolich/dracolich-helm (verified 2026-09-09):
argocd/applicationset.yaml ApplicationSet — LIST generator, hardcoded elements
argocd/project.yaml AppProject "dracolich"; destinations limited to dracolich-dev
infrastructure/mongodb.yaml raw manifests, applied by hand (not Helm)
lib/ library chart "dracolich-service" v0.1.0 (type: library)
templates/_helpers.tpl fullname, labels, selectorLabels
templates/_deployment.tpl _service.tpl _ingress.tpl
services/<short-name>/
Chart.yaml type: application, dependency file://../../lib
Chart.lock generated — do not hand-edit
templates/all.yaml three includes, nothing else
values.yaml full config, ingress disabled
values-dev.yaml image.tag pin + ingress override
Platform: Azure AKS · NGINX ingress · cert-manager (Let's Encrypt) · Cloudflare DNS · ArgoCD ·
in-cluster MongoDB. DEPLOYMENT.md is the authoritative runbook — read it before cluster work.
Chart names are short and the mapping is not mechanical. Repo → chart directory:
| Repo | Chart |
|---|---|
dracolich-ai-api |
ai-api |
dracolich-user-api |
user-api |
dracolich-mtg-library-api |
mtg-library-api |
dracolich-mtg-deck-builder-api |
deck-builder-api — note mtg- is dropped too |
The short name is used consistently as: directory, Chart.yaml name, release name, in-cluster
Service DNS, image tag prefix, and secret name prefix. Pick it deliberately and use it everywhere.
Phase 1 — Confirm the fit before mirroring
The library chart is titled "Shared library chart for dracolich Spring Boot reactive services", and it assumes exactly that:
- Two container ports:
http(appPort, 8080) andmanagement(managementPort, 7980). - Liveness and readiness probes that always render,
httpGetagainstport: management, at/actuator/health/livenessand/actuator/health/readiness.
A Spring Boot service fits with no lib changes. A static frontend or any non-actuator workload
does not — it has no second port and no /actuator/health. Adding one requires extending the lib
chart first (make the management port and probes optional, or add a variant template). Treat that as
its own task, raise it, and do not force-fit by pointing a probe at a path that doesn't exist —
that yields a pod that never passes readiness.
Phase 2 — Gather the service's facts from its own repo
Read them; don't ask for what the code states:
- App port and management port —
application.yml,DockerfileEXPOSE. - Base path (
spring.webflux.base-path) — this becomes the ingresspathverbatim. - Every property or env var read with no default: these must all come from
envor a secret, or the pod crash-loops at startup rather than failing a request.grep -rhoE '\$\{[A-Za-z0-9._-]+' --include='*.yml' <repo>plus the code-side reads. - Which of those are secret vs plain config.
- Whether it needs PEM keys mounted.
- Whether it should be publicly reachable, or internal-only like
ai-api.
Spring relaxed binding is what makes this work: DRACOLICH_MTG_LIBRARY_API_BASE_URL maps to
dracolich.mtg-library.api.base-url. Existing values files carry comments saying so — keep that
habit, because the mapping is not obvious to a later reader.
Phase 3 — Create the chart by copying
cd ~/Dev/Dracolich/dracolich-helm
cp -r services/<closest-existing> services/<new-short-name>
Choose the reference by shape: public HTTP API → deck-builder-api or user-api; internal-only →
ai-api; needs mounted keys → user-api or deck-builder-api.
Then edit:
Chart.yaml — name to the short name, description, keep version/appVersion and the
dracolich-service dependency block unchanged.
Chart.lock — do not hand-edit. Regenerate: helm dependency update services/<new>.
templates/all.yaml — usually unchanged; it is three includes.
values.yaml — the full config:
image.repository: laaasilva/dracolich,image.tag: <short-name>-latestservice.appPort/service.managementPort- probes and
resources— every service currently uses requests100m/384Mi, limits500m/768Mi; copy unless you have a reason, and say that you did env:plain map — including in-cluster DNS for upstreams:http://<other-short-name>:8080/<their-base-path>envFrom: [{secretRef: {name: <short-name>-secrets}}]volumes/volumeMountsonly if PEM keys are needed, mounted at/etc/dracolich/keysingress.enabled: falsewithhost: "",annotations: {},tls: []— the base file never enables ingress
values-dev.yaml — the environment overlay, deliberately small:
image.tag: <short-name>-<version>— this exact key is what CI patches- ingress, if public:
ingress: enabled: true className: nginx host: dev.dracolich.app path: <service base path> annotations: cert-manager.io/cluster-issuer: letsencrypt-prod tls: - hosts: [dev.dracolich.app] secretName: dev-dracolich-app-tls # shared across services — do not invent a new one
Then read every difference and confirm each is intended:
diff -ru services/<reference> services/<new>
A leftover from the reference — its port, path, or secret name — is the characteristic failure here.
Phase 4 — Register with ArgoCD (the step that is easy to miss)
argocd/applicationset.yaml uses a list generator with hardcoded elements, not a git
directory generator. A new chart directory is invisible to ArgoCD until it is listed:
generators:
- list:
elements:
- name: mtg-library-api
- name: user-api
- name: ai-api
- name: deck-builder-api
- name: <new-short-name> # ← add this
The template supplies the rest: path: services/{{name}}, valueFiles: [values-dev.yaml],
namespace dracolich-dev, project dracolich, automated sync with prune and selfHeal.
There are no per-service Application manifests — do not create one; it would duplicate what the
ApplicationSet generates. If the namespace is ever not dracolich-dev, check argocd/project.yaml,
whose destinations currently allow only that namespace.
Phase 5 — Verify by rendering
Read-only and safe:
helm dependency update services/<new>
helm lint services/<new>
helm template <short-name> services/<new> -f services/<new>/values-dev.yaml
In the rendered output check: image tag; both container ports; probes hitting port: management;
ingress host, path and TLS secret; and every required env var present. Confirm .image.tag exists at
the path CI writes to.
Never run helm install/upgrade, kubectl apply, or argocd app sync. This skill produces
files; ArgoCD and a human do the deploying.
Phase 6 — Wire the CI bump
In the service's own repo, the helm-bump job checks out dracolich-helm with
secrets.DRACOLICH_HELM_PAT and runs:
yq -i '.image.tag = "<short-name>-${{ steps.version.outputs.version }}"' \
services/<short-name>/values-dev.yaml
The short name must match the chart directory in both the path and the tag string. A mismatch fails
silently — the chart stays pinned to an old tag forever. If the repo has no pipeline yet, use the
add-github-workflows skill.
Phase 7 — Report what only the user can do
Secrets are created out-of-band with kubectl create secret and are never committed — that is
the repo's stated policy. List, by name and key, never by value:
<short-name>-secrets— the env secret referenced byenvFrom<short-name>-jwt-keys— only if PEM files are mounted
Also report: DNS if the host is new; that resources were copied rather than derived; and that rendering proves syntax and values, not that the service starts. Say that plainly.
Calibration
- Absent conventions are conventions. If no existing service sets a field, adding it needs a justification.
- Don't tidy while you're in there. Mirror the odd thing and mention it; refactoring the lib chart is a separate, separately approved task.
- Secrets never enter git — not as examples, not as realistic-looking placeholders.
prune: trueandselfHeal: trueare on. A resource removed from the chart is deleted from the cluster, and hand-edits are reverted. Deletions are not cosmetic here.