Kubernetes Services provide stable network endpoints for ephemeral Pods. This lesson covers how to template every Service type in Helm, wire up DNS-based discovery, and build flexible values.yaml patterns for production charts.
| Type | Accessible From | Use Case |
|---|---|---|
| ClusterIP | Inside the cluster only | Internal microservice communication |
| NodePort | Any node IP + static port (30000–32767) | Dev/testing, on-prem without LB |
| LoadBalancer | External IP via cloud provider | Production internet-facing services |
| ExternalName | DNS CNAME alias | Proxy to external systems (RDS, SaaS APIs) |
# ClusterIP (default)
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
type: ClusterIP
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
protocol: TCP
type, Kubernetes defaults to ClusterIP.spec:
type: NodePort
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
nodePort: 30080
Exposes the service on every node at a static port. Traffic arriving at <NodeIP>:30080 is forwarded to port 8080 on matching Pods.
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 443
targetPort: 8443
protocol: TCP
Cloud provider provisions an external load balancer. The LB forwards traffic to the NodePort range automatically.
Cost alert: Each LoadBalancer Service typically creates a cloud LB resource with its own hourly cost. Use an Ingress controller to consolidate multiple services behind one LB.
Returns individual Pod IPs instead of a single virtual IP. Essential for StatefulSets where each Pod needs a stable DNS name.
apiVersion: v1
kind: Service
metadata:
name: {{ include "mychart.fullname" . }}-headless
spec:
clusterIP: None
selector:
{{- include "mychart.selectorLabels" . | nindent 4 }}
ports:
- name: gossip
port: 7946
targetPort: gossip
- name: client
port: 9042
targetPort: client
DNS result: Each Pod gets pod-0.myapp-headless.namespace.svc.cluster.local. Clients can address individual replicas directly.
name: gossip) in multi-port Services. Unnamed ports cause ambiguity errors.| Value | Behaviour | Trade-off |
|---|---|---|
Cluster (default) | Traffic may hop between nodes; source IP is masked (SNAT) | Even load distribution, but lose client IP |
Local | Traffic stays on the receiving node; source IP is preserved | Preserves client IP, but uneven distribution if Pods are not on every node |
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3 hours
Routes repeat requests from the same client IP to the same Pod. Useful for stateful applications that store session data in memory.
Best practice: Prefer stateless Pods with external session stores (Redis). Use sessionAffinity only as a short-term migration strategy.
Every Service gets a DNS entry automatically:
<service>.<namespace>.svc.cluster.local
# Examples
my-app.default.svc.cluster.local # full FQDN
my-app.default # within the cluster
my-app # within the same namespace
Kubelet injects MY_APP_SERVICE_HOST and MY_APP_SERVICE_PORT into every Pod that starts after the Service is created. DNS is preferred because it handles ordering and updates dynamically.
Endpoints object in Kubernetes 1.21+A flexible values.yaml pattern that covers all Service types:
# values.yaml
service:
type: ClusterIP
port: 80
targetPort: 8080
nodePort: "" # only used when type=NodePort
annotations: {}
# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if eq .Values.service.type "ClusterIP" }}
{{- if .Values.service.clusterIP }}
clusterIP: {{ .Values.service.clusterIP }}
{{- end }}
{{- end }}
selector:
{{- include "mychart.selectorLabels" . | nindent 4 }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
protocol: TCP
name: http
{{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
nodePort field is only valid when type: NodePort. The conditional guard prevents rendering errors for other types.Key takeaway: A well-designed Helm Service template should let operators switch between ClusterIP, NodePort, and LoadBalancer with nothing more than a values.yaml change.