This page is generated from skills/eks-ingress-migration/references/alb-migration.md. Edit the source, not this page.
ALB Controller Migration Path
Rating model: Express every finding as Impact 1–5 using the Impact Indicator rubric (security/reputation · business/revenue · nature & effort to remediate). Band mapping is a starting point — GREEN→🟡 1–2, AMBER→🟠 3–4, RED→🔴 5 — but the Impact Indicator criteria set the final score (e.g. an easy-to-deploy prerequisite stays 🟡 low even if it blocks a path). All checks are read-only (
kubectl get/describe,aws … describe/list).
Purpose
Guide migration from NGINX Ingress Controller to AWS Load Balancer Controller (ALB Ingress), converting all NGINX-specific annotations to their ALB equivalents.
When to Recommend This Path
- Customer wants to stay on Ingress API (not ready for Gateway API)
- Customer needs ALB features (WAF, Cognito/OIDC, Shield)
- Customer has AWS Transform (ATX) access → fully automated migration
- Customer is on Classic Load Balancer via NGINX and wants ALB
Annotation Mapping: NGINX → ALB
Core Changes (Every Ingress)
| Step | Before (NGINX) | After (ALB) |
|---|---|---|
| IngressClass | ingressClassName: nginx | ingressClassName: alb |
| Deprecated class | kubernetes.io/ingress.class: "nginx" annotation | Remove annotation, add spec.ingressClassName: alb |
| Scheme | (implicit: CLB) | alb.ingress.kubernetes.io/scheme: internet-facing or internal |
| Target type | (implicit: NodePort) | alb.ingress.kubernetes.io/target-type: ip |
URI Rewrite
| Before (NGINX) | After (ALB) |
|---|---|
nginx.ingress.kubernetes.io/use-regex: "true" | Remove |
nginx.ingress.kubernetes.io/rewrite-target: /$2 | alb.ingress.kubernetes.io/transforms.<svc> with url-rewrite JSON |
path: /something(/|$)(.*) + pathType: ImplementationSpecific | path: /something + pathType: Prefix |
Transforms JSON format:
alb.ingress.kubernetes.io/transforms.<service-name>: |
[
{
"type": "url-rewrite",
"urlRewriteConfig": {
"rewrites": [
{
"regex": "^\\/something\\/(.*)$",
"replace": "/$1"
}
]
}
}
]
Rules:
<service-name>must match the backend service name inspec.rules- Forward slashes in regex must be escaped as
\\/in JSON - NGINX
$2often becomes ALB$1(ALB doesn't need the separator capture group) - Multi-path Ingress needs separate
transforms.<svc>per backend service
TLS / Certificates
| Before (NGINX) | After (ALB) |
|---|---|
spec.tls[].secretName: my-secret | Remove spec.tls section entirely |
| K8s Secret with cert/key | alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:... |
| Multiple TLS secrets | Comma-separated ARNs, or alb.ingress.kubernetes.io/certificate-discovery: "true" |
nginx...ssl-redirect: "true" | alb.ingress.kubernetes.io/ssl-redirect: "443" |
| (none) | alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]' |
| (none) | alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06 |
Post-migration: Remove orphaned K8s TLS Secrets that are no longer referenced.
Edge termination ≠ done — address the ALB→pod hop (Zero-Trust / compliance). Moving TLS to ACM edge termination on the ALB and then sending plaintext HTTP to pods may violate organizational/Zero-Trust requirements, especially on EKS Auto Mode where in-cluster traffic is expected to be encrypted. When recommending edge termination, you MUST state how the ALB→backend hop is protected: e.g.
alb.ingress.kubernetes.io/backend-protocol: HTTPSto pods, aTargetGroupBindingwith HTTPS health/traffic, or a service mesh (mTLS). Never present "terminate at ALB, plaintext to pods" as the finished state without calling out the in-cluster encryption decision.
Proxy Timeouts
| Before (NGINX) | After (ALB) |
|---|---|
nginx...proxy-read-timeout: "120" | alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=120 |
nginx...proxy-send-timeout: "120" | (same — ALB uses single idle timeout) |
CORS
| Before (NGINX) | After (ALB) |
|---|---|
nginx...enable-cors: "true" | Remove — handle via AWS WAF or application-level |
nginx...cors-allow-origin | Remove — handle via AWS WAF or application-level |
nginx...cors-allow-methods | Remove — handle via AWS WAF or application-level |
nginx...cors-allow-headers | Remove — handle via AWS WAF or application-level |
Note: Add comment in manifest: # REMOVED: CORS — configure via AWS WAF rules or application middleware
Effort reality (High, not Medium): CORS has no faithful ALB/WAF equivalent — AWS WAF cannot inject CORS response headers; in practice CORS must move to the application backend (code change). NGINX
rate-limit/limit-rpsis per-second, per-path/per-client; AWS WAF rate-based rules are coarser (per-IP over a multi-minute window), cost extra, and use a completely different config model. Treat CORS / rate-limit / IP-allowlist conversions as 🔴 High impact/effort, not a simple Medium swap.
Authentication
| Before (NGINX) | After (ALB) |
|---|---|
nginx...auth-url + nginx...auth-signin | alb.ingress.kubernetes.io/auth-type: oidc |
| (external auth service) | alb.ingress.kubernetes.io/auth-idp-oidc: '{"issuer":"...","authorizationEndpoint":"...","tokenEndpoint":"...","userInfoEndpoint":"...","secretName":"..."}' |
🔴 Behavior change — NOT a like-for-like conversion (High impact). Basic Auth (
auth-type: basic, anAuthorization: Basicheader — commonly used by scripts, cron jobs and machine/API clients) → ALB OIDC/Cognito replaces header auth with an interactive browser login redirect to an Identity Provider. Every non-interactive caller (automation, CI, partner APIs) breaks immediately. This requires client re-architecture (e.g. OIDC client-credentials flow, app-level token auth, or mTLS) and stakeholder coordination — never presentauth-*→ OIDC as a simple annotation swap. Score it High and call out the affected client types.
Body Size
| Before (NGINX) | After (ALB) |
|---|---|
nginx...proxy-body-size: "50m" | Remove — no ALB annotation equivalent |
Note: Add comment: # REMOVED: proxy-body-size — configure at application level
Access Control / Internal
| Before (NGINX) | After (ALB) |
|---|---|
nginx...whitelist-source-range: "10.0.0.0/8" | alb.ingress.kubernetes.io/scheme: internal |
| (IP restriction) | Use ALB security groups for IP-based access control |
Verify before switching internet-facing → internal (High-risk if skipped):
- Subnet readiness (read-only): an internal ALB needs private subnets tagged
kubernetes.io/role/internal-elbin ≥2 AZs. Checkaws ec2 describe-subnetsfor the cluster VPC; if absent, the internal ALB will fail to provision. Internet-facing ALBs likewise needkubernetes.io/role/elbtags.- External-consumer impact:
whitelist-source-rangeon an internet-facing endpoint still served external callers (partners, devices, off-cluster clients). Switching toscheme: internalmakes it reachable only from inside the VPC — every external consumer is cut off. Confirm no off-VPC client depends on it, or keep it internet-facing and restrict viaalb.ingress.kubernetes.io/inbound-cidrs/ security groups instead. Do not silently convert a public allowlisted endpoint to internal.
ALB Grouping (Cost Optimization)
To share a single ALB across multiple Ingress resources:
alb.ingress.kubernetes.io/group.name: shared-alb
alb.ingress.kubernetes.io/group.order: "10"
Blast radius — do NOT default to one shared ALB/Gateway across all teams. Consolidating
team-api,team-payments,team-webonto a single shared ALB (or a single Gateway) maximizes blast radius: one team's broken route, bad annotation, or traffic overload degrades everyone. Split by security boundary, e.g. apublicgroup/Gateway for general web and a separateprivate/payments group/Gateway for sensitive systems — accept the extra LB cost for isolation. Recommend grouping by trust/security boundary, not "one group to save money." This applies equally to Gateway API: prefer per-boundaryGateways over a single shared listener.
Migration Phases (ALB Path)
Phase 1: Prerequisites
- Install AWS Load Balancer Controller (v2.7.2+ for the ALB Ingress path)
- Provision ACM certificates for all TLS hosts
- Ensure IAM roles/policies for LB Controller
Phase 2: Convert Manifests
- Apply annotation mapping (above) to each Ingress
- Use ATX for automated conversion (if available) — see
references/atx-guide.md - Validate with
kubectl apply --dry-run=client -f <file>
Phase 3: Deploy & Shift Traffic
- Deploy migrated Ingress (creates new ALB)
- Use DNS weighted routing to shift traffic CLB→ALB
- Monitor error rates, latency
Phase 4: Cleanup
- Delete old NGINX Ingress resources
- Remove NGINX Ingress Controller deployment
- Remove orphaned TLS Secrets
- Update IaC/GitOps references
Checks to Execute
ALB.1 — Annotation Conversion Completeness
What to check:
- All
nginx.ingress.kubernetes.io/*annotations identified - Each has a mapped ALB equivalent or documented removal reason
Impact (per Impact Indicator):
- 🟡 1–2 (Low): Only mechanical changes (class swap on the same controller, scheme/target-type, simple path) — but see the cutover note below.
- 🟠 3–4 (Medium): URI rewrites →
transforms, TLS → ACM, timeouts; plus the new-ALB + DNS cutover every class switch incurs. - 🔴 5 (High):
configuration-snippet/server-snippet, CORS, rate-limit, IP-allowlist, external auth — no faithful ALB equivalent; needs WAF (extra cost, coarser) or application/code changes. - ⬜ Unknown: Cannot parse annotations
Cutover note (applies to EVERY nginx→alb conversion): switching
ingressClassName: nginx → albis a cross-controller change — the AWS LB Controller provisions a new ALB, and traffic does not move until DNS is cut over to it. This is a parallel-run + DNS cutover, never a zero-impact file edit. Therefore a bare class switch is at least Medium, even when the YAML diff is one line. (Changing the deprecatedkubernetes.io/ingress.classannotation tospec.ingressClassNameon the same class is the only genuinely Low case.)
ALB.2 — ACM Certificate Readiness
What to check:
- All TLS hosts have matching ACM certificates (or can use certificate-discovery)
- Certificates are in ISSUED state in the correct region
Impact (per Impact Indicator):
- 🟡 1–2 (Low): All certs available in ACM
- 🟠 3–4 (Medium): Some certs need provisioning
- 🔴 5 (High): Certs use private CA or non-standard issuance
- ⬜ Unknown: Cannot check ACM
ALB.3 — AWS LB Controller Readiness
What to check:
- AWS LB Controller installed and version ≥ 2.7
- IAM role with correct policy attached
- IngressClass
albexists
Impact (per Impact Indicator):
- 🟡 1–2 (Low): Controller installed, correct version, IAM ready
- 🟠 3–4 (Medium): Controller present but needs upgrade
- 🔴 5 (High): Controller not installed
- ⬜ Unknown: Cannot determine