feat(deploy): Helm chart for Kubernetes with make apply - #43
Conversation
Adds charts/aeman and make targets (apply/diff/delete/image-push) mirroring the docs-enterprise layout. `NAMESPACE=aenix-aeman make apply` deploys aeman into the namespace: an AEMAN_* env Secret, an RWO PVC for the MCP client registry, a single-replica Deployment (distroless, non-root, read-only rootfs), a ClusterIP Service, and a tenant-root Ingress with a cert-manager cluster-issuer. No credentials are committed: the GitHub OAuth secret and the ghcr pull secret are supplied out of band (values-secret.yaml is gitignored; see values-secret.example.yaml). Assisted-By: Claude Claude-Session: https://claude.ai/code/session_011v4Qn75TKERtJvkV5V87wt Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a Helm chart for the aeman application, along with updates to the .gitignore and Makefile to support local development, image building, and deployment. The review feedback highlights several critical improvements: resolving a potential malformed image tag issue in the Makefile when yq fails, adhering to Helm best practices by dynamically resolving the namespace via .Release.Namespace (and cleaning up static namespace overrides), ensuring compatibility with a read-only root filesystem when persistence is disabled by using an emptyDir volume, and making the Ingress TLS configuration conditional to prevent validation errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| @TAG=$(IMAGE_TAG)@$$(yq e '."containerimage.digest"' .build-metadata.json -o json -r 2>/dev/null || echo $(IMAGE_TAG)) \ | ||
| yq -i '.image.tag = strenv(TAG)' $(CHART)/values.yaml |
There was a problem hiding this comment.
If yq fails or .build-metadata.json does not contain a valid digest, the fallback || echo $(IMAGE_TAG) results in a malformed tag like latest@latest (or latest@ if yq returns an empty string or null). This will cause Kubernetes to fail to pull the image.
We should safely check if a valid digest is returned before appending it with @.
@DIGEST=$$(yq e '."containerimage.digest"' .build-metadata.json 2>/dev/null); \
if [ -n "$$DIGEST" ] && [ "$$DIGEST" != "null" ]; then \
TAG="$(IMAGE_TAG)@$$DIGEST"; \
else \
TAG="$(IMAGE_TAG)"; \
fi; \
TAG=$$TAG yq -i '.image.tag = strenv(TAG)' $(CHART)/values.yaml
| {{/* Target namespace. */}} | ||
| {{- define "aeman.namespace" -}} | ||
| {{- default "aenix-aeman" .Values.namespace }} | ||
| {{- end }} |
There was a problem hiding this comment.
Hardcoding a default namespace like aenix-aeman and using a custom .Values.namespace is a Helm anti-pattern. It breaks standard Helm behavior where the target namespace is determined by the release context (.Release.Namespace), such as when running helm install -n <namespace>.
We should define aeman.namespace to return .Release.Namespace directly. This allows standard Helm and GitOps tools (like ArgoCD or Flux) to manage the namespace natively without requiring explicit values overrides.
{{/* Target namespace. */}}
{{- define "aeman.namespace" -}}
{{- .Release.Namespace -}}
{{- end }}
| {{- if .Values.persistence.enabled }} | ||
| volumeMounts: | ||
| - name: data | ||
| mountPath: {{ .Values.persistence.mountPath }} | ||
| {{- end }} |
There was a problem hiding this comment.
When readOnlyRootFilesystem is set to true (line 68), any write operations to /data will fail if persistence is disabled, because /data will be part of the read-only root filesystem.
To support running with a read-only root filesystem even when persistence is disabled, we should always mount the data volume, and configure it as an emptyDir when persistence.enabled is false.
volumeMounts:
- name: data
mountPath: {{ .Values.persistence.mountPath }}| {{- if .Values.persistence.enabled }} | ||
| volumes: | ||
| - name: data | ||
| persistentVolumeClaim: | ||
| claimName: {{ include "aeman.fullname" . }}-data | ||
| {{- end }} |
There was a problem hiding this comment.
To support running with a read-only root filesystem even when persistence is disabled, we should always mount the data volume, and configure it as an emptyDir when persistence.enabled is false.
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "aeman.fullname" . }}-data
{{- else }}
emptyDir: {}
{{- end }}| # Target namespace, overridden at apply time (NAMESPACE=... make apply). | ||
| namespace: aenix-aeman |
| apply: | ||
| helm upgrade -i $(RELEASE) $(CHART) -n $(NAMESPACE) --create-namespace \ | ||
| --set namespace=$(NAMESPACE) $(HELM_SECRET_ARG) |
| diff: | ||
| helm diff upgrade $(RELEASE) $(CHART) -n $(NAMESPACE) \ | ||
| --set namespace=$(NAMESPACE) $(HELM_SECRET_ARG) |
| tls: | ||
| - hosts: | ||
| - {{ .Values.ingress.host | quote }} | ||
| secretName: {{ .Values.ingress.tlsSecretName }} |
There was a problem hiding this comment.
The tls block is currently hardcoded. If a user wants to disable TLS or doesn't specify a tlsSecretName, this will render an empty secretName which can cause validation errors in some ingress controllers.
We should make the tls block conditional on ingress.tlsSecretName being set.
{{- if .Values.ingress.tlsSecretName }}
tls:
- hosts:
- {{ .Values.ingress.host | quote }}
secretName: {{ .Values.ingress.tlsSecretName }}
{{- end }}The repository moved to the public aenix-io organization; update the default image repository, the make registry, and the appVersion. Assisted-By: Claude Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
Adds a Helm chart and
maketargets so aeman can be deployed to a Kubernetes cluster, mirroring the docs-enterprise layout.NAMESPACE=aenix-aeman make applydeploys aeman into the namespace:AEMAN_*env Secret (config from values),AEMAN_SESSION_FILE),seccompProfile: RuntimeDefault(the namespace enforces PodSecurityrestricted),tenant-rootIngress with a cert-manager cluster-issuer and long WebSocket timeouts for the/api/v1/watchstream.No credentials are committed. The GitHub OAuth secret and the ghcr pull secret are supplied out of band —
values-secret.yamlis gitignored; seevalues-secret.example.yaml. The chart renders its ownNamespaceonly whencreateNamespace=true(off by default, sincemake applyalready bootstraps it viahelm --create-namespace); the flag is there for GitOps flows that apply the chart directly.maketargets:apply/diff/delete/image-push(build+push the amd64 image and stamp its digest into the chart).Draft — not for merge yet; the cluster cutover is deferred while the existing deployment is stable.
https://claude.ai/code/session_011v4Qn75TKERtJvkV5V87wt