A reusable Helm chart for deploying single or multiple Kubernetes applications from one release. It supports common application resources such as Deployments, Services, Ingress, ConfigMaps, Secrets, PVCs, StorageClasses, RBAC, HPA, KEDA ScaledObjects, PDBs, NetworkPolicies, Jobs, CronJobs, and custom extra objects.
.
├── Chart.yaml
├── values.yaml
├── values.schema.json
├── templates
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── pvc.yaml
│ ├── storageclass.yaml
│ ├── networkpolicy.yaml
│ ├── rbac.yaml
│ ├── hpa.yaml
│ ├── scaledobject.yaml
│ ├── pdb.yaml
│ ├── jobs.yaml
│ ├── cronjobs.yaml
│ ├── extra-objects.yaml
│ └── tests
└── README.md
Render the manifests locally:
helm template cypik .
Run Helm lint:
helm lint .
If you want to test without creating a custom StorageClass:
helm template cypik . --set storageClass.enabled=false
Install into the current namespace:
helm upgrade --install cypik .
Install into a dedicated namespace:
helm upgrade --install cypik . \
--namespace cypik \
--create-namespace
Use the applications array to deploy multiple applications in the same Helm release.
applications:
- name: frontend
image:
repository: nginx
tag: "1.25"
service:
enabled: true
ports:
- name: http
port: 80
targetPort: 80
ingress:
enabled: true
className: alb
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
hosts:
- host: app.example.com
paths:
- path: /
pathType: Prefix
servicePort: http
- name: api
image:
repository: my-api
tag: "1.0.0"
service:
enabled: true
ports:
- name: http
port: 80
targetPort: 8080
env:
- name: APP_ENV
value: production
Create a dedicated ServiceAccount for an application:
applications:
- name: api
serviceAccount:
create: true
rbac:
create: true
clusterWide: false
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
Use an existing ServiceAccount:
applications:
- name: api
serviceAccount:
create: false
name: default
When enabling RBAC, set serviceAccount.create: true or provide serviceAccount.name explicitly so the RoleBinding or ClusterRoleBinding grants permissions to the same ServiceAccount used by the Pod.
Create a ConfigMap and inject it with envFrom:
applications:
- name: api
config:
enabled: true
data:
APP_NAME: api
LOG_LEVEL: info
Create a Secret and inject it with envFrom:
applications:
- name: api
secret:
enabled: true
stringData:
DATABASE_URL: postgres://user:password@postgres:5432/app
Use direct environment variables:
applications:
- name: api
env:
- name: APP_ENV
value: production
Use Kubernetes field references with envRaw:
applications:
- name: api
envRaw:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
Use an existing ConfigMap or Secret:
applications:
- name: api
envFrom:
- configMapRef:
name: existing-config
- secretRef:
name: existing-secret
Avoid storing production secrets directly in values files. Prefer External Secrets, sealed secrets, or a managed secret store.
Enable health checks:
applications:
- name: api
probes:
readiness:
enabled: true
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
liveness:
enabled: true
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 20
Set CPU and memory requests and limits:
applications:
- name: api
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
Configure lifecycle hooks:
applications:
- name: api
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]
Set Pod and container security contexts:
applications:
- name: api
podSecurityContext:
runAsNonRoot: true
fsGroup: 1000
securityContext:
allowPrivilegeEscalation: false
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
Use stricter settings only after confirming the image supports non-root execution.
Use node selectors:
applications:
- name: api
nodeSelector:
kubernetes.io/os: linux
Use tolerations:
applications:
- name: worker
tolerations:
- key: dedicated
operator: Equal
value: workers
effect: NoSchedule
Use affinity:
applications:
- name: api
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/component: api
topologyKey: kubernetes.io/hostname
Use topology spread constraints:
applications:
- name: api
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/component: api
Enable a PVC for an application:
applications:
- name: frontend
persistence:
enabled: true
size: 1Gi
mountPath: /usr/share/nginx/html
When persistence.storageClass is empty, Kubernetes uses the cluster default StorageClass.
Use a specific existing StorageClass:
applications:
- name: frontend
persistence:
enabled: true
size: 10Gi
storageClass: gp3
mountPath: /data
By default, prefer the cluster default StorageClass and keep custom StorageClass creation disabled:
storageClass:
enabled: false
Create a StorageClass only when required:
storageClass:
enabled: true
name: cypik-gp3
provisioner: ebs.csi.aws.com
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
type: gp3
StorageClass is cluster-scoped. Use unique names and avoid enabling it by default across multiple releases.
Mount an additional existing Secret:
applications:
- name: api
volumes:
- name: app-cert
secret:
secretName: app-cert
volumeMounts:
- name: app-cert
mountPath: /etc/certs
readOnly: true
Run an init container before the main container:
applications:
- name: api
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ["sh", "-c", "sleep 10"]
Add a sidecar:
applications:
- name: api
sidecars:
- name: metrics-sidecar
image: busybox:1.36
command: ["sh", "-c", "while true; do sleep 30; done"]
Deny all ingress traffic to an application:
applications:
- name: frontend
networkPolicy:
enabled: true
policyTypes:
- Ingress
ingress: []
Allow only pods with app=allowed-client to reach port 80:
applications:
- name: frontend
networkPolicy:
enabled: true
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: allowed-client
ports:
- protocol: TCP
port: 80
Test from a blocked client:
kubectl run np-client --image=busybox:1.36 -- sleep 3600
kubectl exec np-client -- wget -qO- --timeout=5 http://<release>-cypik-frontend
Expected result for a denied client is a timeout.
Test from an allowed client:
kubectl run allowed-client --image=busybox:1.36 --labels=app=allowed-client -- sleep 3600
kubectl exec allowed-client -- wget -qO- --timeout=5 http://<release>-cypik-frontend
Expected result is an application response. An HTTP error such as 403 Forbidden still means NetworkPolicy allowed the traffic and the application responded.
On EKS, ensure network policy enforcement is enabled in the CNI or through a compatible policy engine such as Calico.
Use a PDB to keep at least one pod available during voluntary disruptions:
applications:
- name: api
pdb:
enabled: true
minAvailable: 1
Or limit unavailable pods:
applications:
- name: api
pdb:
enabled: true
maxUnavailable: 1
Enable Kubernetes HPA:
applications:
- name: api
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
Enable KEDA:
applications:
- name: worker
keda:
enabled: true
minReplicaCount: 1
maxReplicaCount: 10
triggers:
- type: cpu
metricType: Utilization
metadata:
value: "70"
Do not enable HPA and KEDA for the same application at the same time.
Run a one-off Helm hook Job:
applications:
- name: api
jobs:
- name: db-migrate
enabled: true
command: ["python", "manage.py", "migrate"]
Run a CronJob:
cronjobs:
- name: cleanup
schedule: "0 2 * * *"
image:
repository: busybox
tag: "1.36"
command: ["sh", "-c", "echo cleanup"]
Use extraObjects for Kubernetes resources that are not covered by built-in templates.
extraObjects:
- apiVersion: v1
kind: ConfigMap
metadata:
name: extra-config
data:
key: value
Values in extra objects are rendered with Helm tpl, so chart values can be referenced when needed.
Enable the built-in Helm test pod for single-app mode:
tests:
enabled: true
Run tests:
helm test <release>
Upgrade an existing release:
helm upgrade cypik . -f values.yaml
Uninstall a release:
helm uninstall cypik
PVC deletion depends on the StorageClass reclaim policy and Helm ownership. Confirm data retention behavior before uninstalling production releases.
Check rendered manifests:
helm template cypik . --debug
Check release resources:
kubectl get all
kubectl get ingress
kubectl get pvc
kubectl get networkpolicy
Check rollout status:
kubectl rollout status deployment/<release>-cypik-frontend
Check a pod’s ServiceAccount:
kubectl get pod <pod-name> -o jsonpath='{.spec.serviceAccountName}'
Check PVC binding:
kubectl get pvc
kubectl describe pvc <pvc-name>
Check NetworkPolicy behavior:
kubectl describe networkpolicy <policy-name>
kubectl run np-client --image=busybox:1.36 -- sleep 3600
kubectl exec np-client -- wget -qO- --timeout=5 http://<service-name>
latest.storageClass.enabled=false unless the release should manage a cluster-scoped StorageClass.MIT License. See LICENSE for details.