In the previous lesson we dismantled the ClusterIP fiction: an address that exists on no interface and lives only as iptables rules programmed by kube-proxy. But that discovery leaves untouched the problem we have been dragging along since the end of module 3: a ClusterIP is only reachable from inside the cluster. A customer who wants to buy a ticket on www.rutasnorte.example has no way of reaching web-store. This lesson walks through the four Service types —ClusterIP, NodePort, LoadBalancer and ExternalName— plus two variants that are not types but behave as if they were: headless services and selectorless services. By the end you will know exactly which one to use in each situation, why LoadBalancer is not the answer for publishing the platform, and how to give the external payment gateway a name inside the cluster.
Contents
- Overview: six ways to declare a Service
- How each type builds on the previous one
NodePortin detailLoadBalancerand its costexternalTrafficPolicy:ClusterversusLocalExternalName: the payment gateway- Selectorless services: endpoints by hand
- Headless services
sessionAffinity, named ports and multiple ports- What Rutas Norte uses and why
- Overview: six ways to declare a Service
Before going into detail, here is the table worth keeping in front of you throughout the lesson.
| Form | Key spec |
Reach | Who implements it | When to use it |
|---|---|---|---|---|
| ClusterIP | type: ClusterIP (default) |
Inside the cluster only | kube-proxy | Communication between components. 90% of cases |
| NodePort | type: NodePort |
Any node's IP, port 30000-32767 | kube-proxy | Testing, lab, or as the foundation of an external balancer |
| LoadBalancer | type: LoadBalancer |
Public/external IP | A cloud provider controller | Exposing a TCP/UDP service to the outside in the cloud |
| ExternalName | type: ExternalName + externalName |
DNS name redirection | CoreDNS (CNAME record) | Giving an internal name to an outside service |
| Headless | clusterIP: None |
DNS only, no virtual IP | CoreDNS (one A per pod) | StatefulSets, clients that balance on their own |
| Selectorless | No selector, with a manual EndpointSlice |
Same as its type | You, by hand | External service with fixed IPs, migrations |
The last two rows are not values of type: they are modifications that combine with it. A Service can be ClusterIP and headless, or ClusterIP and selectorless.
- How each type builds on the previous one
The first three types are not alternatives: they are cumulative layers. A NodePort still has its ClusterIP and still works from inside. A LoadBalancer still has its NodePort.
flowchart TB
subgraph LB["type: LoadBalancer"]
subgraph NP["type: NodePort"]
subgraph CIP["type: ClusterIP"]
A["internal virtual IP<br/>10.96.x.x:port<br/>kube-proxy rules"]
end
B["Plus: port 30000-32767<br/>open on ALL nodes"]
end
C["Plus: external IP provisioned<br/>by the provider, pointing<br/>at the nodes NodePorts"]
end
Check it against the output of kubectl:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
bookings-api ClusterIP 10.96.201.14 <none> 3000/TCP 9d
web-store NodePort 10.96.88.7 <none> 80:31080/TCP 4m
demo-lb LoadBalancer 10.96.44.190 10.0.0.51 80:32410/TCP 2mLook at the PORT(S) column: 80:31080/TCP means "port 80 of the Service, also published on port 31080 of every node". And all three have a CLUSTER-IP, including the LoadBalancer. That accumulation is why diagnosis always goes from the inside out: if the ClusterIP does not work, the NodePort will not work either.
NodePort in detail
NodePort in detailA NodePort opens the same port on every node in the cluster, regardless of where the pods are. If you call that port on a node that hosts no replica, kube-proxy forwards the traffic to the node that does have one.
# k8s/environments/dev/web-store-service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: web-store
namespace: rutas-norte-dev
labels:
app: web-store
app.kubernetes.io/part-of: rutas-norte
environment: dev
spec:
type: NodePort
selector: # remember: only app and environment in selectors
app: web-store
environment: dev
ports:
- name: http
port: 80 # ClusterIP port
targetPort: 8080 # nginx container port
nodePort: 31080 # OPTIONAL: if omitted, the cluster assigns itPoints to be clear about:
- The default range is 30000-32767 (about 2,700 ports). It can be changed with
--service-node-port-rangeon the apiserver, but that is a cluster decision, not an application team one. - If you omit
nodePort, Kubernetes picks a free one. That is the recommended approach: pinning it introduces a potential clash between teams and anInvalid value: provided port is already allocatedwhen somebody gets there first. - The port is opened on every node, even if only one has pods. With
externalTrafficPolicy: Localthis changes (section 5). - Try it in minikube:
Why it is not used in production
| Problem | Consequence |
|---|---|
| Ugly, arbitrary ports | Nobody is going to type http://rutasnorte.example:31080 into a browser |
| No domain name and no TLS | HTTPS would have to be solved outside |
| No high availability | You point at one node's IP; if that node goes down, the service stops answering even though the pod is alive |
| Requires opening the firewall | 2,700 potentially reachable ports on every node |
| Does not group services | One port per Service, with no host-based or path-based routing |
The legitimate use of NodePort is as a foundation: cloud balancers and many Ingress controllers rest on exactly that. For development and quick demos, it is perfect.
LoadBalancer and its cost
LoadBalancer and its costtype: LoadBalancer tells the cluster: "ask the infrastructure provider for an external balancer pointing at this service". Kubernetes does not materialise it: that is done by a controller that is part of the cloud's cloud-controller-manager, or by an add-on such as MetalLB in on-premises installations.
apiVersion: v1
kind: Service
metadata:
name: web-store
namespace: rutas-norte-pro
spec:
type: LoadBalancer
selector:
app: web-store
environment: pro
ports:
- name: http
port: 80
targetPort: 8080In a cluster with no cloud provider, the result is this:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-store LoadBalancer 10.96.44.190 <pending> 80:32410/TCP 5mThat eternal <pending> is one of the most repeated questions from newcomers. It is not a fault: it is that nobody has picked up the request. The object is created correctly, but there is no controller capable of provisioning an external IP. In minikube it is solved with:
# In another terminal; it asks for an administrator password and must be left open
minikube -p rutas-norte tunnelNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
web-store LoadBalancer 10.96.44.190 10.96.44.190 80:32410/TCPminikube tunnel creates routes on your machine so that the assigned IPs become reachable. It is a local emulation, not a real balancer.
The economic argument that leads to Ingress
Every Service of type LoadBalancer triggers the provisioning of a separate balancer in the cloud, with its own public IP and its own invoice. For Rutas Norte:
| Service to publish | Its own balancer? | Approximate monthly cost |
|---|---|---|
web-store |
Yes | 1 balancer |
bookings-api |
Yes | 1 balancer |
| Internal admin dashboard | Yes | 1 balancer |
| Grafana metrics (07-04) | Yes | 1 balancer |
Four balancers, four public IPs, four TLS certificates to manage separately and no way to route by path or by header. Multiplied by three environments, twelve. The alternative is a single LoadBalancer in front of an Ingress controller, which distributes internally by domain and by path. That is where 04-04 comes from.
On top of that, LoadBalancer operates at L4: it does not understand HTTP, it cannot route by Host, it cannot rewrite paths. For non-HTTP services (an MQTT broker, a postgres exposed to another network) it is still the right answer.
externalTrafficPolicy: Cluster versus Local
externalTrafficPolicy: Cluster versus LocalIt applies to NodePort and LoadBalancer, and it decides what a node does when external traffic for a Service arrives.
flowchart TB
subgraph CL["externalTrafficPolicy: Cluster (default)"]
direction LR
C1["Client 203.0.113.9"] --> N1["Node A<br/>(no pods)"]
N1 -->|"SNAT: source becomes node A IP"| N2["Node B<br/>web-store pod"]
N2 --> R1["The pod sees source = 192.168.49.2"]
end
subgraph LO["externalTrafficPolicy: Local"]
direction LR
C2["Client 203.0.113.9"] --> M1["Node A<br/>(no pods)"]
M1 -->|"DROPS"| X1["No answer"]
C2 --> M2["Node B<br/>web-store pod"]
M2 --> R2["The pod sees source = 203.0.113.9"]
end
Cluster (default) |
Local |
|
|---|---|---|
| Extra hop between nodes | Yes, if the node has no pods | No, never |
| Client source IP | Lost (SNAT to the node) | Preserved |
| Load distribution | Uniform across all pods | Proportional to the pods on that node |
| Balancer health check | Every node answers | Only those with pods (via healthCheckNodePort) |
| If a node has no pods | It forwards | It drops the packet |
The imbalance of Local deserves an example. If web-store has 3 replicas and node A hosts 2 while node B hosts 1, a balancer splitting 50/50 between nodes will give each pod on node A 25% of the traffic and the one on node B 50%. It is corrected with pod anti-affinity (06-05).
For Rutas Norte the source IP matters: the access logs, the request rate limiting during bank-holiday peaks and the blocking of abusive addresses all depend on it. With Cluster, every request would appear to come from the nodes.
When traffic arrives through an HTTP Ingress there is an alternative: the controller adds the X-Forwarded-For header with the real IP. But that only works for HTTP and only if the application reads it. There is also internalTrafficPolicy, with the same idea applied to internal cluster traffic: Local forces a pod to connect only to replicas on its own node.
ExternalName: the payment gateway
ExternalName: the payment gatewaybookings-api has to charge for tickets against pagos.proveedorexterno.example, which lives outside the cluster. You could put that domain in a ConfigMap and be done with it, but ExternalName gives a very useful layer of indirection.
# k8s/environments/pro/payment-gateway-externalname.yaml
apiVersion: v1
kind: Service
metadata:
name: payment-gateway
namespace: rutas-norte-pro
labels:
app.kubernetes.io/part-of: rutas-norte
environment: pro
spec:
type: ExternalName
externalName: pagos.proveedorexterno.exampleWhat happens underneath: CoreDNS creates a CNAME record. When bookings-api resolves payment-gateway.rutas-norte-pro.svc.cluster.local, the answer is an alias to pagos.proveedorexterno.example, which is resolved by external DNS (04-03).
kubectl run t --rm -it --restart=Never -n rutas-norte-pro \
--image=nicolaka/netshoot -- nslookup payment-gatewayServer: 10.96.0.10
Address: 10.96.0.10#53
payment-gateway.rutas-norte-pro.svc.cluster.local
canonical name = pagos.proveedorexterno.example.
Name: pagos.proveedorexterno.example
Address: 198.51.100.44Advantages for our case:
- The application always points at
payment-gateway, in all three environments. What changes is theexternalNamein each namespace: inrutas-norte-devit would point atpagos-sandbox.proveedorexterno.example, inproat the production one. Switching provider does not touch a single line of code. - There is no proxy: traffic goes straight from the pod to the external destination, without passing through kube-proxy.
And its three limitations, which cause surprises:
- It does not translate ports. The
portsof anExternalNameis decorative. If the provider listens on 8443, your client must ask for 8443. - It does not do TLS. The provider's certificate will be the one for
pagos.proveedorexterno.example, not forpayment-gateway, so SNI/hostname validation will use the real name. With HTTP and a rewrittenHost, the check can fail at the other end. - It depends on external DNS. If CoreDNS cannot forward to the node's resolver, this does not work.
An important note for 04-06: an ExternalName does not count as an exception in a network policy. To allow egress towards the provider you will have to authorise its IP range with ipBlock.
- Selectorless services: endpoints by hand
A normal Service finds its destinations through the selector. If you omit the selector, the endpoints controller creates nothing and you can write the EndpointSlice yourself. The result is a normal ClusterIP, with its DNS name and its balancing, pointing at addresses that are not pods.
A realistic Rutas Norte case: during the migration, the billing database is still on a virtual machine in the office, at 10.20.30.40:5432.
# k8s/environments/pro/external-billing.yaml
apiVersion: v1
kind: Service
metadata:
name: legacy-billing
namespace: rutas-norte-pro
spec:
ports:
- name: postgres
port: 5432
targetPort: 5432
protocol: TCP
# NO selector: nobody fills in the endpoints automatically
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: legacy-billing-manual
namespace: rutas-norte-pro
labels:
# MANDATORY: links the slice to the Service
kubernetes.io/service-name: legacy-billing
addressType: IPv4
ports:
- name: postgres # must match the Service port name
port: 5432
protocol: TCP
endpoints:
- addresses:
- "10.20.30.40"
conditions:
ready: trueCritical details:
- The
kubernetes.io/service-namelabel is what joins the slice to the Service. Without it, the Service is left with no endpoints. - The
ports[].namefield of the EndpointSlice must match theports[].nameof the Service. - Nobody health-checks that address. If the machine goes down, the Service keeps sending traffic. Keeping the list up to date is your responsibility.
- The addresses cannot be Service IPs (
10.96.x.x) or loopback addresses.
Main advantage: the application connects to legacy-billing:5432 from day one. The day that database is migrated to a pod in the cluster, you add the selector, delete the manual slice and the application never notices. It is the indirection pattern that makes migrations bearable.
- Headless services
A Service with clusterIP: None is headless: it has no virtual IP, creates no kube-proxy rules and balances nothing. All it does is publish in DNS one A record per pod matching the selector.
apiVersion: v1
kind: Service
metadata:
name: bookings-postgres-headless
namespace: rutas-norte-pro
spec:
clusterIP: None # this is what makes it headless
selector:
app: bookings-postgres
environment: pro
ports:
- name: postgres
port: 5432| Normal Service | Headless Service | |
|---|---|---|
CLUSTER-IP |
10.96.x.x |
None |
| kube-proxy rules | Yes | No |
| DNS answer | One A: the virtual IP | One A per ready pod |
| Balancing | Done by the kernel | Done by the client |
| Typical use | Everything else | Clustered databases, StatefulSets |
$ nslookup bookings-postgres-headless.rutas-norte-pro.svc.cluster.local
Name: bookings-postgres-headless... Address: 10.244.0.14
Name: bookings-postgres-headless... Address: 10.244.0.18
Name: bookings-postgres-headless... Address: 10.244.0.23What is this for? For the cases where the client needs to tell the pods apart:
- A PostgreSQL replica set with one primary and two secondaries: the client writes to the primary and reads from the secondaries. A ClusterIP distributing at random would break it.
- Distributed databases (Cassandra, Elasticsearch, Kafka) where each node must know the others by name.
- Clients with their own balancing (gRPC), which want the list of destinations so they can distribute per request rather than per connection.
The full detail of the DNS records it generates is in 04-03, and its real use —with stable identities such as bookings-postgres-0.bookings-postgres-headless— arrives with the StatefulSets of 06-01, where bookings-postgres will finally stop being a Deployment.
One nuance: a Service that is headless and selectorless generates no A records; instead CoreDNS returns the names from externalName if there is one, or the manual endpoints you define.
sessionAffinity, named ports and multiple ports
sessionAffinity, named ports and multiple portssessionAffinity: ClientIP
By default kube-proxy distributes connections at random. With affinity, all connections from the same source IP go to the same pod for a while.
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3 hours; the default is also 10800It is a blunt hammer and this has to be said plainly:
- It works by IP, not by user. A hundred clients behind a company's NAT are a single IP and all land on the same pod.
- It breaks load distribution and complicates progressive rollouts.
- With
externalTrafficPolicy: Cluster, the source IP already arrives masqueraded, so affinity is applied to the node IP: useless.
For Rutas Norte we do not use it: bookings-api is stateless, and the buyer's session lives in a JWT and in redis-cache. That is the correct solution; session affinity is a patch for applications that keep state in memory.
Named ports and multiple ports
When a Service exposes more than one port, name is mandatory on each of them.
apiVersion: v1
kind: Service
metadata:
name: bookings-api
namespace: rutas-norte-pro
spec:
selector:
app: bookings-api
environment: pro
ports:
- name: http # mandatory when there is more than one
port: 3000
targetPort: api # points at a NAMED container port
protocol: TCP
- name: metrics # Prometheus will consume it in 07-03
port: 9090
targetPort: metrics
protocol: TCPWith the container declaring:
Advantages of using a named targetPort: if tomorrow the application changes port, you only touch the Deployment. And port names are what enable the DNS SRV records (04-03). Constraint: the name must be 15 characters or fewer and consist of lowercase letters, digits and hyphens.
- What Rutas Norte uses and why
| Component | Type chosen | Reason |
|---|---|---|
web-store |
ClusterIP |
It will be published through Ingress (04-04), not directly |
bookings-api |
Multi-port ClusterIP (http + metrics) |
Same; metrics are never exposed outside |
bookings-postgres |
ClusterIP (and headless in 06-01) |
It must never leave the cluster |
redis-cache |
ClusterIP |
Internal use only |
notifications-worker |
No Service | It receives no inbound connections; it only consumes from the queue |
payment-gateway |
ExternalName |
Indirection towards the external provider |
legacy-billing |
Selectorless + manual EndpointSlice | Database still outside the cluster |
| Ingress controller | LoadBalancer (just one) |
One balancer for the whole platform |
That last row is the practical conclusion of the lesson: the entire platform ends up with a single paid entry point, and everything else lives on ClusterIP.
One detail about notifications-worker worth internalising: not every Deployment needs a Service. A Service exists to receive connections. A process that only makes outbound connections needs none. (It will need one if it later exposes metrics for Prometheus.)
Common Mistakes and Tips
- Setting
type: LoadBalanceron a local cluster and expecting an IP. The<pending>is what you should expect with no provider. In minikube,minikube tunnel; on your own server, MetalLB. - Pinning
nodePortby hand "so it is easy to remember". It creates clashes between teams and deployments that fail withprovided port is already allocated. Let the cluster assign it. - Switching from
ClusterIPtoNodePortand believing you lose the ClusterIP. You do not: the types are cumulative. Internal traffic carries on unchanged. - Using
ExternalNamewith different ports. It does not translate ports. If the provider listens on 8443, your client must ask for 8443 explicitly. - A selectorless Service with no EndpointSlice. It is created without a single error and never answers. Always check with
kubectl get endpointslices -l kubernetes.io/service-name=<svc>. - Forgetting
nameon a multi-port Service. The error is clear (must specify name for multiple ports), but it arrives late: get into the habit of always naming ports, even with a single one. - Using
sessionAffinityto fix an application with state in memory. You are papering over the problem. Move the state out toredis-cache. - Expecting a headless Service to balance. It does not. The client receives all the IPs and decides for itself. Many HTTP libraries just take the first one.
- Tip:
kubectl get svc -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,TYPE:.spec.typeis an audit at a glance. Any unexpectedLoadBalanceris money walking out of the door. - Tip: when debugging, always work from the inside out. ClusterIP → NodePort → LoadBalancer. A failure in the inner layer shows up in all the outer ones.
Exercises
Exercise 1: Publish web-store with NodePort and check the layers
In rutas-norte-dev, change the web-store Service to NodePort without pinning the port. Check that it still works from inside through its ClusterIP and from outside through the node's IP. Explain which column of kubectl get svc tells you both ports.
Exercise 2: The payment gateway with ExternalName
Create the payment-gateway Service in rutas-norte-dev pointing at a sandbox (pagos-sandbox.proveedorexterno.example) and in rutas-norte-pro at the real domain. Verify from an ephemeral pod that resolution returns a different CNAME in each namespace. Explain why this lets the bookings-api code be identical in both environments.
Exercise 3: Billing database outside the cluster
Create legacy-billing as a selectorless Service with a manual EndpointSlice pointing at 10.20.30.40:5432. Check that the Service has endpoints and that the name resolves. Then trigger the classic failure: remove the kubernetes.io/service-name label from the slice and observe what happens.
Solutions
Exercise 1
kubectl patch svc web-store -n rutas-norte-dev -p '{"spec":{"type":"NodePort"}}'
kubectl get svc web-store -n rutas-norte-dev# From inside: the ClusterIP is still alive
kubectl run t --rm -it --restart=Never -n rutas-norte-dev \
--image=nicolaka/netshoot -- curl -s -o /dev/null -w '%{http_code}\n' http://web-store
# From outside
curl -s -o /dev/null -w '%{http_code}\n' http://$(minikube -p rutas-norte ip):30417The PORT(S) column, formatted as 80:30417/TCP, gives you both: on the left the ClusterIP port, on the right the NodePort. It proves that NodePort adds a layer on top of ClusterIP.
Exercise 2
for NS in dev pro; do
DEST=$( [ $NS = pro ] && echo pagos.proveedorexterno.example \
|| echo pagos-sandbox.proveedorexterno.example )
kubectl create service externalname payment-gateway \
--external-name=$DEST -n rutas-norte-$NS
done
kubectl run t --rm -it --restart=Never -n rutas-norte-dev \
--image=nicolaka/netshoot -- nslookup payment-gatewaypayment-gateway.rutas-norte-dev.svc.cluster.local
canonical name = pagos-sandbox.proveedorexterno.example.The bookings-api code always uses http://payment-gateway/charges. The namespace determines which real provider it translates to, with no different environment variables and no branches in the code. Switching provider means editing one manifest.
Exercise 3
kubectl apply -f k8s/environments/pro/external-billing.yaml
kubectl get endpointslices -n rutas-norte-pro \
-l kubernetes.io/service-name=legacy-billing# Trigger the failure: remove the label joining slice and Service
kubectl label endpointslice legacy-billing-manual \
-n rutas-norte-pro kubernetes.io/service-name-
kubectl get endpointslices -n rutas-norte-pro \
-l kubernetes.io/service-name=legacy-billing
# No resources foundThe Service still exists, its ClusterIP is still assigned, kubectl get svc shows nothing odd, and every connection hangs until it times out. It is the same clinical picture as a selector that does not match, seen in 02-05: the Service looks healthy and has no destinations. That is why the first check when "the Service does not answer" is always to look at its endpoints.
Conclusion
You now know the full catalogue. ClusterIP is the default type and the one used by nearly the whole platform: reachable from inside only. NodePort adds on top of it a port in the 30000-32767 range open on every node, useful for testing and as the foundation of other mechanisms, but unacceptable in production because of its arbitrary ports, its lack of TLS and its dependence on one specific node's IP. LoadBalancer adds on top of that an external IP provisioned by the provider —hence the eternal <pending> when there is none, and minikube tunnel to emulate one— and its cost of one balancer per service is exactly the argument that justifies Ingress. ExternalName routes no traffic: it makes CoreDNS return a CNAME, and with it we have given the payment gateway a stable internal name, different per environment, without touching the code.
You know that the first three types are cumulative layers, which forces you to always diagnose from the inside out. You understand externalTrafficPolicy: Cluster forwards between nodes but destroys the client's source IP, whereas Local preserves it at the cost of uneven load distribution. And you have a grip on the two variants that are not types: selectorless services, where you write the EndpointSlice yourself to point at an external database and where the kubernetes.io/service-name label is the link everybody forgets; and headless services, which give up the virtual IP in order to publish one A record per pod, indispensable when the client needs to tell replicas apart.
One thread has run through the whole lesson without being developed: DNS. ExternalName works because CoreDNS creates a CNAME. Headless works because CoreDNS creates one A per pod. bookings-api finds bookings-postgres because there is a name that resolves. The next lesson, 04-03, opens that box: what CoreDNS is, which short name forms work and when, why ndots: 5 can multiply queries to the payment gateway by five, and how to diagnose the three DNS failures you will see again and again throughout your career.
Kubernetes Course
Module 1: Introduction to Kubernetes
- What Is Kubernetes?
- Kubernetes Architecture
- Key Concepts and Terminology
- Setting Up a Kubernetes Cluster
- The Kubernetes CLI: kubectl
- Objects, YAML Manifests and the Declarative Model
- The Course Project: the Rutas Norte Platform
Module 2: Core Kubernetes Components
- Pods
- ReplicaSets
- Deployments
- Updates, Rollbacks and Deployment Strategies
- Services
- Namespaces
- Labels, Selectors and Annotations
Module 3: Configuration and Secret Management
- ConfigMaps
- Secrets
- Environment Variables
- Resource Quotas and Limits
- LimitRanges and Quality of Service (QoS) Classes
- ServiceAccounts and API Access from Pods
Module 4: Networking in Kubernetes
- Cluster Networking
- Service Types
- Internal DNS and Service Discovery
- Ingress Controllers
- TLS and Certificate Management with cert-manager
- Network Policies
Module 5: Storage in Kubernetes
- Volumes
- Persistent Volumes
- Persistent Volume Claims
- Storage Classes
- Dynamic Provisioning, Expansion and Snapshots
- Backup and Restore of Persistent Data
Module 6: Advanced Kubernetes Concepts
- StatefulSets
- DaemonSets
- Jobs and CronJobs
- Init Containers, Sidecars and Multi-Container Patterns
- Scheduling: Affinity, Taints and Tolerations
- Custom Resource Definitions (CRDs)
- Operators and the Controller Pattern
Module 7: Monitoring and Logging
- Health Checks and Probes
- Metrics Server and kubectl top
- Monitoring with Prometheus
- Visualization and Alerting with Grafana and Alertmanager
- Centralized Logging with Elasticsearch, Fluentd and Kibana (EFK)
- Application Debugging and Cluster Events
Module 8: Kubernetes Security
- Role-Based Access Control (RBAC)
- Security Contexts and Container Hardening
- Pod Security Policies and Pod Security Standards
- Network Security
- Image Security
- Auditing, Scanning and Vulnerability Management
Module 9: Scaling and Performance
- Horizontal Pod Autoscaling
- Vertical Pod Autoscaling
- Cluster Autoscaling
- Event-Driven and Custom-Metric Scaling with KEDA
- High Availability: PodDisruptionBudgets and Topology
- Performance Tuning
Module 10: Kubernetes Ecosystem and Tooling
- Minikube and Local Environments with kind
- Kubeadm
- Helm
- Kustomize
- GitOps with Argo CD and Flux
- Managed Kubernetes: EKS, AKS and GKE
Module 11: Case Studies and Real-World Applications
- Deploying a Web Application
- Running Stateful Applications
- CI/CD with Kubernetes
- Deployment Strategies: Blue-Green and Canary
- Multi-Cluster Management
- Production Operations: Incidents, Runbooks and Costs
