Homework: LoadBalancer Deep Dive
Design multi-cloud LoadBalancer Services with provider annotations, traffic policies, and cost-aware Helm templates
Task 1 — Multi-Cloud LoadBalancer Template
Create a Helm chart called api-gateway with a Service template that provisions the correct LoadBalancer type depending on which cloud provider the chart is deployed to. The provider is selected via a single cloud.provider value.
Requirements
- Support three providers:
aws,gcp, andonprem - When
cloud.providerisaws:- Default to NLB by setting
service.beta.kubernetes.io/aws-load-balancer-type: "nlb" - Add
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing" - Add
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
- Default to NLB by setting
- When
cloud.providerisgcp:- Add
networking.gke.io/load-balancer-type: "External" - Add
cloud.google.com/neg: '{"ingress": true}'
- Add
- When
cloud.providerisonprem:- Add
metallb.universe.tf/address-pool: "production" - Add
metallb.universe.tf/allow-shared-ip: "api-gateway"
- Add
- All providers share the annotation set from
service.annotations(user-defined), merged with the provider-specific annotations - The Service listens on port 443 (HTTPS) and forwards to target port 8443
Hint
Build a dict of provider annotations using conditionals, then merge with user annotations:
{{- $cloud := dict }}
{{- if eq .Values.cloud.provider "aws" }}
{{- $_ := set $cloud "service.beta.kubernetes.io/aws-load-balancer-type" "nlb" }}
{{- /* ... more annotations */ -}}
{{- end }}
{{- $merged := merge $cloud (.Values.service.annotations | default dict) }}
Deliverables
templates/service.yamlwith the conditional annotation logicvalues.yamlwithcloud.provider: awsas the default- Rendered output from
helm templatefor each of the three providers (use--set cloud.provider=<value>)
Self-Check
| Criterion | Pass |
|---|---|
AWS render includes all three service.beta.kubernetes.io/* annotations | |
GCP render includes networking.gke.io/load-balancer-type and cloud.google.com/neg | |
On-prem render includes both metallb.universe.tf/* annotations | |
User-defined annotations from service.annotations appear in all three renders | |
| No provider-specific annotations leak into other providers' output | |
| Service port is 443 with targetPort 8443 in all renders | |
helm template succeeds without errors for all three providers |
Task 2 — Traffic Policy and Health Check Configuration
Extend your api-gateway chart to support externalTrafficPolicy, session affinity, and cloud-native health checks -- all driven by values.
Requirements
- Add
service.externalTrafficPolicyto values (default:Cluster). The template should setspec.externalTrafficPolicyon the Service. - Add
service.sessionAffinitysection with:enabled(bool, default false)timeoutSeconds(int, default 10800)
- When session affinity is enabled, set
spec.sessionAffinity: ClientIPandspec.sessionAffinityConfig.clientIP.timeoutSeconds - Add
service.healthChecksection with:path(string, default/healthz)intervalSeconds(int, default 15)thresholdCount(int, default 3)
- When
cloud.providerisaws, map health check values to AWS annotations:service.beta.kubernetes.io/aws-load-balancer-healthcheck-pathservice.beta.kubernetes.io/aws-load-balancer-healthcheck-intervalservice.beta.kubernetes.io/aws-load-balancer-healthcheck-healthy-threshold
Deliverables
- Updated
templates/service.yamlandvalues.yaml - Rendered output with
externalTrafficPolicy: Localand session affinity enabled - Rendered output on AWS showing health check annotations
Self-Check
| Criterion | Pass |
|---|---|
externalTrafficPolicy: Local appears in rendered Service spec | |
sessionAffinity: ClientIP and timeoutSeconds appear when enabled | |
Session affinity fields are absent when sessionAffinity.enabled: false | |
| AWS render includes all three health check annotations with correct values | |
| GCP/on-prem renders do not include AWS health check annotations | |
| Default values render cleanly (Cluster policy, no session affinity) |
Task 3 — Internal vs External LB with Cost Controls
Create environment-specific values files that demonstrate internal vs external LoadBalancer configuration and apply cost-conscious patterns for non-production environments.
Requirements
- Add
service.internal(bool, default false) to values. When true, the template should add the appropriate internal LB annotation per provider:- AWS:
service.beta.kubernetes.io/aws-load-balancer-scheme: "internal"(overridinginternet-facing) - GCP:
networking.gke.io/load-balancer-type: "Internal" - On-prem:
metallb.universe.tf/address-pool: "internal"(overridingproduction)
- AWS:
- Create three values files:
- values-dev.yaml: Service type overridden to
ClusterIP(no LB provisioned -- zero cost),externalTrafficPolicyomitted - values-staging.yaml: Internal LB (
service.internal: true),externalTrafficPolicy: Cluster, no session affinity - values-prod.yaml: External LB,
externalTrafficPolicy: Local, session affinity enabled with 1-hour timeout, AWS health checks configured
- values-dev.yaml: Service type overridden to
Deliverables
- Updated template that respects
service.internaland allowsservice.typeoverride - All three values files
- Rendered output for each environment, confirming:
- Dev:
type: ClusterIP, no LB annotations - Staging:
type: LoadBalancerwith internal annotations - Prod:
type: LoadBalancerwith external annotations, Local traffic policy, session affinity
- Dev:
Self-Check
| Criterion | Pass |
|---|---|
Dev render shows type: ClusterIP with no cloud annotations | |
| Staging render shows internal LB annotation for the configured provider | |
Prod render shows external LB with externalTrafficPolicy: Local | |
Prod render includes sessionAffinity: ClientIP with 3600s timeout | |
| Prod render includes AWS health check annotations | |
No externalTrafficPolicy field appears on the ClusterIP dev Service | |
| All three environments render without Helm errors |
Grading Criteria
| Area | Points |
|---|---|
| Task 1 -- Multi-cloud annotations render correctly per provider | 35 |
| Task 2 -- Traffic policy, session affinity, and health checks are value-driven | 35 |
| Task 3 -- Environment files demonstrate internal/external and cost patterns | 30 |
| Total | 100 |
These tasks build on each other. Start with Task 1 to establish the base template with provider-specific annotations, then layer in traffic policy and health checks in Task 2. Task 3 ties everything together with environment-specific overrides. If you get stuck on annotation merging, review the Sprig merge and set functions and remember that provider annotations should take precedence over user-defined ones when keys collide.