# Helm Add Service

> Add a service or app to the dracolich-helm GitOps repo — chart directory, values files, ArgoCD registration, and the CI image-tag bump — by mirroring an existing service. Use when onboarding a new service to the cluster, asked to "add X to the helm chart" or "create the argo files for X", or when a deployed service needs new config, secrets, or ingress.

- Skill: `laasilva/helm-add-service` (Agent Skill)
- Install (CLI): `npx skillmds@latest add laasilva/helm-add-service`
- Raw SKILL.md: https://api.skillmd.com/api/skills/laasilva/helm-add-service/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: laasilva (https://skillmd.com/u/laasilva)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/laasilva/helm-add-service

---


# 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) and `management` (managementPort, 7980).
- Liveness and readiness probes that **always render**, `httpGet` against `port: management`, at
  `/actuator/health/liveness` and `/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`, `Dockerfile` `EXPOSE`.
- Base path (`spring.webflux.base-path`) — this becomes the ingress `path` **verbatim**.
- Every property or env var read **with no default**: these must all come from `env` or 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

```bash
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>-latest`
- `service.appPort` / `service.managementPort`
- probes and `resources` — every service currently uses requests `100m`/`384Mi`, limits
  `500m`/`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`/`volumeMounts` only if PEM keys are needed, mounted at `/etc/dracolich/keys`
- `ingress.enabled: false` with `host: ""`, `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:
  ```yaml
  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:

```bash
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:

```yaml
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:

```bash
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:

```bash
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 by `envFrom`
- `<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: true` and `selfHeal: true` are on.** A resource removed from the chart is deleted from
  the cluster, and hand-edits are reverted. Deletions are not cosmetic here.

