Skip to content

feat(deploy): Helm chart for Kubernetes with make apply - #43

Draft
kvaps wants to merge 3 commits into
mainfrom
feat/helm-chart
Draft

feat(deploy): Helm chart for Kubernetes with make apply#43
kvaps wants to merge 3 commits into
mainfrom
feat/helm-chart

Conversation

@kvaps

@kvaps kvaps commented Jul 7, 2026

Copy link
Copy Markdown
Member

Adds a Helm chart and make targets so aeman can be deployed to a Kubernetes cluster, mirroring the docs-enterprise layout.

NAMESPACE=aenix-aeman make apply deploys aeman into the namespace:

  • an AEMAN_* env Secret (config from values),
  • an RWO PVC for the MCP client registry (AEMAN_SESSION_FILE),
  • a single-replica Deployment — distroless, non-root, read-only rootfs, seccompProfile: RuntimeDefault (the namespace enforces PodSecurity restricted),
  • a ClusterIP Service,
  • a tenant-root Ingress with a cert-manager cluster-issuer and long WebSocket timeouts for the /api/v1/watch stream.

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. The chart renders its own Namespace only when createNamespace=true (off by default, since make apply already bootstraps it via helm --create-namespace); the flag is there for GitOps flows that apply the chart directly.

make targets: 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

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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Makefile
Comment on lines +62 to +63
@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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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

Comment on lines +41 to +44
{{/* Target namespace. */}}
{{- define "aeman.namespace" -}}
{{- default "aenix-aeman" .Values.namespace }}
{{- end }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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 }}

Comment on lines +45 to +49
{{- if .Values.persistence.enabled }}
volumeMounts:
- name: data
mountPath: {{ .Values.persistence.mountPath }}
{{- end }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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 }}

Comment on lines +71 to +76
{{- if .Values.persistence.enabled }}
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ include "aeman.fullname" . }}-data
{{- end }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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 }}

Comment thread charts/aeman/values.yaml
Comment on lines +1 to +2
# Target namespace, overridden at apply time (NAMESPACE=... make apply).
namespace: aenix-aeman

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the namespace should be managed dynamically via .Release.Namespace, we can remove the static namespace field from values.yaml.

# Target namespace is dynamically determined by the release namespace (.Release.Namespace).

Comment thread Makefile
Comment on lines +67 to +69
apply:
helm upgrade -i $(RELEASE) $(CHART) -n $(NAMESPACE) --create-namespace \
--set namespace=$(NAMESPACE) $(HELM_SECRET_ARG)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the namespace helper updated to use .Release.Namespace, we no longer need to explicitly pass --set namespace=$(NAMESPACE) to the Helm command.

apply:
	helm upgrade -i $(RELEASE) $(CHART) -n $(NAMESPACE) --create-namespace \
		$(HELM_SECRET_ARG)

Comment thread Makefile
Comment on lines +72 to +74
diff:
helm diff upgrade $(RELEASE) $(CHART) -n $(NAMESPACE) \
--set namespace=$(NAMESPACE) $(HELM_SECRET_ARG)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the namespace helper updated to use .Release.Namespace, we no longer need to explicitly pass --set namespace=$(NAMESPACE) to the Helm command.

diff:
	helm diff upgrade $(RELEASE) $(CHART) -n $(NAMESPACE) \
		$(HELM_SECRET_ARG)

Comment on lines +18 to +21
tls:
- hosts:
- {{ .Values.ingress.host | quote }}
secretName: {{ .Values.ingress.tlsSecretName }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 }}

kvaps added 2 commits July 16, 2026 20:20
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant