This page is generated from devops-agent/eks-ingress-migration/references/porting-notes.md. Edit the source, not this page.
Porting Notes — eks-ingress-migration
This file documents the differences between the Claude Code version (skills/eks-ingress-migration/) and this DevOps Agent port. It is for maintainers, not for the agent to read during execution. Exclude it from the uploaded skill zip (zip -r ../eks-ingress-migration-skill.zip . -x './references/porting-notes.md') — it ships in the repository for maintainers only.
Staleness check: the tables below describe the upstream skill at a point in time and can drift as
skills/eks-ingress-migration/evolves. Re-verify each row against upstream when materially changing either copy, and update the date here. Last verified: 2026-08-30, against upstreammain(post-#129/#153/#154).
Differences from the Claude Code version
| Aspect | Claude Code (skills/eks-ingress-migration/) | DevOps Agent (this skill) |
|---|---|---|
| Execution model | Interactive — asks "assess all clusters, or select specific ones?" on ambiguity | Fully autonomous — Step 0 hard-stop decision table, no interactive prompts |
| Cluster selection | Discovers all clusters across regions, assesses many per run | Discovers across regions, assesses one cluster per run; HARD STOP when more than one is found and none was named (never auto-selects, never assesses all) |
| Tool access | aws CLI, kubectl, EKS MCP server | AWS control-plane APIs (read-only) + Kubernetes API via the Agent Space EKS access entry (read-only). No Bash, no kubectl, no MCP. |
| Kubernetes reads | kubectl get/describe, or MCP list_k8s_resources | Expressed as "Via Kubernetes API" capability descriptions, not kubectl pipelines |
| Report output | Markdown file written to ~/ingress_migration/<cluster>/report.md | Markdown rendered inline in the response; no files written, no output path reported |
| HTML report | tools/report_to_html.py renders an interactive dashboard with a 3D routing diagram, score gauge, gate badge, cluster dropdown, and per-option download buttons | Omitted — no script execution. Report generation is expressed as instructions + the markdown template; the tool's deterministic renders became literal text (see "Renderer tokens" below). |
| Topology JSON | Written to ~/ingress_migration/<cluster>/topology.json, consumed by the 3D view | Emitted as one fenced json block in the report's References section; same schema, same controller-naming contract, now joined against the Routing Topology table instead of a 3D renderer |
| Manifest export | YAML files written under <cluster>-manifests/current/ and target/ | Same content rendered inline as labelled fenced yaml blocks, with a volume guard (all routes at ≤ 10; above that, shared target resources + the 10 highest-Impact routes + a table of the deferred remainder, counts stated) |
| Snippet route enumeration | kubectl exec <nginx-pod> -- nginx -T as an optional deep read | Not reachable — no pod exec. The snippet blind spot becomes a permanent, mandatory caveat and the routing inventory is marked Unverified (see below). |
| Directory naming | references/samples/, references/atx/ nested under references/ | assets/samples/, assets/atx/ — matches the port convention (references/ stays flat; data files live in assets/) |
| Prerequisites | AWS credentials, Python 3.10+, EKS MCP server, AWS CLI | references/iam-policy.json for the Agent Space role + an EKS access entry; no Python, no CLI, no MCP |
allowed-tools frontmatter | Not present upstream either | Not present (prohibited in this runtime) |
Renderer tokens → literal text
report_to_html.py substituted four token families that plain markdown would show literally. Each became text carrying the same information:
| Upstream token | Port replacement |
|---|---|
[[SCORE:nn:LABEL]] (colored gauge) | **Migration Difficulty Score: <nn> / 100 — <LABEL>** |
[[GATE:n]] (badge) | The tool's own strings, verbatim: ✓ No re-architecture blockers (n = 0) / ⛔ <n> blocker(s) need(s) redesign / approval (n > 0) |
[[DL:current]] / [[DL:gateway-api]] / [[DL:alb]] / [[DL:atx]] (download buttons) | Cross-references to the Export Materials section, where the YAML is rendered inline |
!!red highlight!! | **bold** |
The !!…!! conversion is not cosmetic: report_to_html.py's inline() implements **bold**, backticks, links and !!hot!!, so an unrendered !!…!! in a markdown-only runtime would reach the reader as literal exclamation marks. The same applies to the [[…]] tokens.
Capability losses (and how each fails closed)
Every loss below is a runtime reachability gap, not a content cut. In each case the port fails closed — it reports reduced confidence rather than a clean negative.
- Snippet-injected route enumeration. Upstream can
execinto the controller pod and readnginx -T. This runtime has no shell, no pod exec, and no in-cluster network path, andpods/execis not granted by the access entry.traffic-routing.md§5.5 therefore states that the snippet under-count is permanent and unmeasured, requires the report to say the true route count is ≥ the counted one, and hands both deep reads to the cluster owner as verification steps with the inventory marked Unverified. It never infers "no hidden routes". - Admission-webhook exposure.
ValidatingWebhookConfigurationlives inadmissionregistration.k8s.io, outside the built-in groups an access entry is likely to grant (see the caveat below — the managed policy's rules are unpublished). The #153 tri-state already fails closed, so a403lands in Unverified → treated as exposed and keeps the 🔴 5 CVE-2025-1974 band. The controller Deployment args (apps, which is covered) are the primary signal; the VWC read is corroboration. - Gateway API adoption state.
gateway.networking.k8s.ioandapiextensions.k8s.ioare CRD groups, which an access-entry policy scoped to built-in groups would not reach. A denied read reports Gateway API as unconfirmed and sets"gatewayApi": { "readStatus": "unconfirmed" }— nevercrdsInstalled: false, and never a recommendation to install CRDs on the strength of a403. - Interactive multi-cluster selection. Replaced by a hard stop, so the port never silently assesses the wrong cluster.
Supplementary ClusterRole (optional — closes losses 2 and 3)
The manifest and its rationale live in references/supplementary-rbac.md, which — unlike this file — is included in the shipped skill zip (setup.sh excludes only porting-notes.md). It is kept there, not here, so the operator-facing instruction is reachable from the packaged skill; do not duplicate the YAML back into this file.
Summary: binding it grants exactly the reads this skill needs, so the webhook tri-state and the Gateway API adoption check resolve definitively instead of degrading. Without it the assessment still completes and simply reports Unverified/unconfirmed. The AmazonAIOpsAssistantPolicy sourcing caveat (its rules are unpublished, so coverage is unconfirmable and every read fails closed) is stated there in full.
Scoring model — carried over unchanged
The Migration Difficulty Score is the part most at risk from a port, so it was copied without modification and re-derived by hand after porting:
- The deduction model (Impact 5→10, 4→6, 3→4, 2→2, 1→1, non-event 0), the per-category caps,
score = max(0, 100 − Σ), and the bands (90–100 TRIVIAL · 80–89 EASY · 70–79 MODERATE · 60–69 HARD · 0–59 VERY HARD) are byte-identical to upstream. - The #129 learning is present: an empty/absent estate is not rated — no controller + no IngressClass + no Ingress → 100 / TRIVIAL with a "nothing to migrate" note (
report-generation.md§1.0), including the §1.0-A carve-out that a reachable CVE/EOL controller is still a security finding at zero routes, and the orphaned-Ingress "Migration Crew Alert" path. - The #153 auth-tier resolution (Basic Auth → OIDC is Tier-B, escalating to Tier-A only with both a closed/unmodifiable backend and non-interactive callers) and the #154
lbc-migratetoolkit path with its assessor/operator fence are carried over intact. - The §1.7 worked example is byte-identical between the two copies. As of round 2 it totals 10+6+6+4+2+1+4+4+0 = 37 → 63 HARD, gate 1 (round 1 shipped 34 → 66; the two cutover categories the example had been omitting are now priced — see round 2 item 6 below).
Mutating commands are documentation, not actions
The skill is assessment-only upstream; here it is also incapable of mutation. Operator steps (Gateway API CRD install, LBC feature-gate enable, lbc-migrate applies, DNS cutover, kubectl delete ingress cleanup) remain in the references as instructions for the cluster owner to run under their change-management process. The #154 assessor/operator fence in lbc-migrate-toolkit.md is unchanged, and SKILL.md Tool Usage Rule 17 states the agent never executes them. Fenced command blocks use plain fences rather than ```bash, matching the other ports, so nothing implies a shell.
Faithful port vs. faithful defect — and what was corrected
The port is deliberately minimal-delta, but "faithful" has two very different meanings and they must not be conflated:
- Design artifacts (the scoring model, tier rubric, gate pseudocode, bands, report shape, worked example) — here identity is correctness. These were copied unchanged and re-derived by hand to prove it; a divergence would be a defect.
- Factual content (version floors, annotation names, service capabilities) — here identity is not correctness. Copying a wrong version or a non-existent annotation byte-for-byte reproduces the error and ships it to a second audience. Provenance ("it was already like that upstream") is an explanation, never a mitigation.
Review round 1 on #204 found several inherited factual defects. Because the port and the published skill would otherwise disagree — or agree on something untrue — they are fixed in both copies in this PR, parity-preserving, applying the same rule already used for the navigation-count: diverge from upstream where the value is plainly wrong, stay identical everywhere else. Corrected in both skills/eks-ingress-migration/ and here:
certificate-discoveryis not a real annotation.alb.ingress.kubernetes.io/certificate-discovery: "true"was shipped as a functional annotation (a sample manifest plus four doc sites). It does not exist in the AWS Load Balancer Controller — absent from the Ingress annotation reference and from the controller's annotation constants (checked v2.7.2 → v3.5.0). Unknown annotations are silently ignored, so an operator would believe TLS discovery was active while the listener had no certificate. Certificate discovery is triggered by omittingcertificate-arnwith an HTTPS entry inlisten-ports. The sample now uses the omit pattern.- Gateway API production floor is v3.0.0, not v2.14. v2.13.3 / v2.14.0 are where L4 / L7 reconciliation began; the upstream guide carried the warning "Using the LBC and Gateway API together is not suggested for production workloads (yet!)" across the whole v2.x line — still present at v2.17.1, the last v2.x release — and it is absent from v3.0.0 (2026-01-23) onward. The old text let the Option-1 prerequisite gate pass a pre-production controller as production-ready. (Round 1 said "through v2.15.x"; corrected in round 2 — see item 1 there.)
transformsURI rewrites need a controller floor. The install floor was stated as v2.7.2 while most ALB samples usealb.ingress.kubernetes.io/transforms.<svc>. Below the floor that annotation is silently ignored and no rewrite happens. (Round 1 set the floor at v2.15.0. That was wrong — it is v2.14.1; corrected in round 2, see item 2 there.)- EKS Auto Mode does not provide Gateway API. The built-in
eks.amazonaws.comcontroller covers Ingress and Servicetype: LoadBalanceronly (Auto Mode load-balancing docs, checked 2026-09-01). A Gateway API target still needs a self-managed LBC at ≥ v3.0.0. The old wording also contradictedingress-discovery.md, which had it right. - Version currency. Dropped "current" from the v1.5.0 CRD statements (Gateway API's current release is v1.6.2, 2026-09-03 — round 2 said v1.6.1, which had already been superseded; currency is dated in
gateway-api.md) and retargeted the runtime-upgrade recommendation to the current LBC v3.5.0 line. The CRD pairing is now stated per controller line — v1.5.0 for v3.4.0, v1.6.0 for v3.5.0 — because v3.5.0 is built for Gateway API v1.6.0; thelbc-migratebuild-tag pin stays at v3.4.0. From Gateway API v1.6.0 TCPRoute/UDPRoute are in the standard channel, so no experimental install is needed for L4. - Tier-A phrasing applied to Tier-B features. The "Execution risk counts" blockquote called CORS / rate-limit / external auth "no faithful equivalent" — the literal Tier-A phrase — while §1.3 classifies them Tier-B because a workaround exists. Reworded to "no native ALB annotation" with an explicit pointer that Tier-A requires §1.3's escalation conditions. (Pre-existing, predates #129, disclosed on #153; closed rather than carried again.)
- Cosmetic parity.
ingress-resources.mdauth-urlrow now reads "No native equivalent", matching its CORS and affinity siblings, and the upstreamSKILL.mdnavigation-count heading now says 6 to match its own 6-row table.
The two certificate-discovery occurrences that remain are inside assets/atx/ — the vendored AWS Transform definition and the AWS blog reproduced beside it. Those are kept byte-faithful to the artifact ATX actually ships rather than silently forked, so atx-guide.md now carries an explicit caveat that the TD instructs a non-existent annotation and that ATX output must be reviewed for it.
Round 2 — further corrections in both copies
Review round 2 ran a fresh whole-branch pass and surfaced a second batch, most of it predating this PR (traceable to the original skill-add #92) but in scope under the same fix-both-copies rule. Independently verified against primary sources before landing:
- The v2.x production warning persists to v2.17.1, not "through v2.15.x." Checked
docs/guide/gateway/gateway.mdat every v2.1x tag: the warning is present at v2.15.0, v2.16.0, v2.17.0 and v2.17.1, and absent at v3.0.0. Every v2.x line is pre-production, not just ≤ v2.15. - The
transformsfloor is v2.14.1, not v2.15.0.transformsappears indocs/guide/ingress/annotations.mdandpkg/ingress/enhanced_backend_builder.goat v2.14.1 (2025-10-17) and is absent at v2.14.0 and all v2.13.x. The v2.15.0 value conflated the Ingress annotation (LBC PR #4396) with the Gateway APIHTTPRouteURLRewritefilter (PR #4418, v2.15.0) — a different feature. Both copies now state v2.14.1 and warn against the conflation. alb.ingress.kubernetes.io/*annotations do not configure a Gateway. The Phase-2 Gateway example setschemeandcertificate-arnas Gateway annotations and TLS via the listener'scertificateRefs. LBC ignores those annotations on a Gateway (their documented location is Ingress/Service), and upstream states outright that "configuration of TLS certificates cannot be done via thecertificateRefsfield of a Gateway Listener." As shipped the example yielded an internal ALB with no certificate —schemedefaults tointernal. Rewritten aroundLoadBalancerConfiguration(+TargetGroupConfiguration/ListenerRuleConfiguration) attached viaspec.infrastructure.parametersRef, and the four mapping-table rows were corrected to match.- There is no
aws-load-balancer-controllerEKS add-on. The Phase-1 upgrade step offeredaws eks update-addon --addon-name aws-load-balancer-controllerlabelled "EKS managed add-on". It is in neither the AWS add-on catalog nor the community add-on list, so the branch was removed and Helm is the only documented path. (Verified from the two doc enumerations; a livedescribe-addon-versionscall was not possible — no valid credentials in the authoring environment.) - Cleanup must uninstall the release, not the Deployment. Deleting only the ingress-nginx Deployment leaves the
ingress-nginx-admissionValidatingWebhookConfiguration (failurePolicy: Fail, no selectors) with no endpoints, so the API server rejects every Ingress create/update cluster-wide — including re-applying the migrated ALB Ingresses. There are six cleanup surfaces, not four:alb-migration.mdPhase 4,migration-plan.mdPhase 4, the tworeport-generation.mdphase tables, the Option-2 migration-steps table andatx-guide.md's post-transform steps. Round 2 corrected only the first four and the round-2 note wrongly claimed "all four"; the last two still carried a barekubectl delete deploy/ "Remove NGINX Ingress Controller after validation" and were fixed in round 3. All six now sayhelm uninstall(or delete the admission webhook + Service explicitly), mark it OPERATOR-only, and enumerate the survivors (controllerLoadBalancerService and its CLB/NLB + security groups + DNS, admission VWC and Service, IngressClass, external-dns TXT records). - The §1.7 worked example was skipping two of its own categories. §1.6 requires every category to be listed, yet the example omitted DNS Cutover & Blast Radius and Downtime / Rollback Readiness — both plainly applicable to an estate doing a new-ALB DNS cutover. They are now priced (Impact 2 and 1, with the stipulations stated) along with Backend Compatibility at 0, which moves the example from 66 to 63 / HARD; the band and the gate are unchanged. The headline token and every quoting site were swept.
- The example's Controller score now carries an explicit webhook stipulation. It scored an EOL nginx 1.9.x controller at Impact 3, which is only legal if the admission webhook is confirmed not-exposed — otherwise §1.4's fail-closed rule makes it Impact 5 plus a gate condition. The stipulation and its verification row are now stated, together with the counterfactual (57 / VERY HARD, gate 2 — a whole band lower), so the example can no longer be read as licence to under-score a 9.8 RCE.
- cert-manager Gateway API support is beta and off by default. §4.2 checked only the pre-v1.15
ExperimentalGatewayAPISupportgate, so a v1.15+ install with the feature unenabled false-passed as 🟡 Low and certificate issuance would fail after cutover. It is now 🟠 Medium when unenabled, and the check names the right place to look: on v1.15+ enablement is file-based Helm config (config.gatewayAPI.enabled: true), so scanning container args alone gives a false negative. - L4
tcp-services/udp-servicesflows were invisible. They have no Ingress object, so nothing in the inventory could see them and the fail-closed rule never fired — nothing was read, so nothing was denied. Newingress-discovery.md§1.3-A reads the controller args, the referenced ConfigMaps and the non-80/443 controller Service ports, and the routing blind-spot note now covers L4. Migration target is TCPRoute/UDPRoute on a separate NLB Gateway, since LBC does not allow mixing protocol layers on one Gateway. Unlike the snippet blind spot, this one is closable in this runtime — these are ordinary API reads. - Two further vendored-TD defects are now disclosed (the TD itself stays byte-faithful): its rewrite regex is over-escaped so it matches nothing and the rewrite silently never fires — verified by decoding the block scalar, and it contradicts the AWS blog shipped in the same bundle — and
proxy-read/send-timeoutis mapped toderegistration_delay.timeout_seconds(connection draining) instead of the ALB idle timeout. Both are now in theatx-guide.mdknown-defects block and in the user-facing report template's ATX checklist, which previously carried only thecertificate-discoverycaveat in the operator workflow.
Also corrected in both copies from the round-2 should-fix list: the ALB parallel-run sequence (an in-place ingressClassName flip cannot be weighted — it is all-or-nothing, so the plan now creates a renamed parallel Ingress); cutover step order (verify Accepted/Programmed before touching DNS) plus an explicit rollback column; the ingress-nginx snippet-hardening floor (allow-snippet-annotations: false and annotations-risk-level land in v1.9.1, not v1.9.0 — and not v1.9.5/v1.10.0 as round 2 proposed); the unsourceable "v2.7.2+" floor, replaced with a supported-release statement plus the feature-specific floors that are actually documented; a deterministic count→Impact mapping for Scale/Volume; the six rewrite samples, whose regex now mirrors NGINX's own (/|$) separator group so the bare prefix rewrites identically instead of passing through un-rewritten; the OIDC sample's missing Secret and HTTPS-listener caveat; the samples' TLS policy, aligned to the TLS13-1-2-2021-06 the docs recommend; a single dated version-currency anchor in gateway-api.md that the other files defer to; ingress2gateway noted as a complementary NGINX-hop option (it has an ingress-nginx provider but no AWS emitter, so it cannot produce the LBC CRDs); and the SKILL.md reference-table wording that read as "never load alb-migration.md" while export rule 7 depends on it.
Round 3 — corrections in both copies
- ingress-nginx is retired; the skill now says so. The project was archived 2026-03-24 and receives no further security patches.
ingress-discovery.md§1.4 states this as a dated fact, the supported-versions table is described as a frozen artifact rather than a live matrix, and a new floor makes a running ingress-nginx controller score minimum 🟠 3 on Controller Currency — a healthy, fully-patched-as-possible v1.15.1 estate can no longer read as "clean", which had let the report frame migration as optional. The floor only raises bands; 🟢 0 still applies to an absent/fully-down controller, and the published §1.7 worked example is unaffected (its controller was already Impact 4). - TLSRoute is standard channel, not experimental. It graduated to
v1in Gateway API v1.5.0, so both CRD versions this skill pins carry it. Only TCPRoute/UDPRoute waited for v1.6.0. - ALB static CORS is a listener attribute, not a per-rule action (
routing.http.response.*, applied to all responses on the listener) — so it cannot vary per host/path on a shared ALB. The absolute "WAF cannot inject CORS headers at all" was softened to the accurate form, and the workaround is now marked unavailable on EKS Auto Mode, which cannot setListenerAttributeat all. - EKS Auto Mode is not annotation-equivalent to a self-managed LBC.
auth-type: oidc,group.name, thewaf-acl-id/web-acl-idfamily anddry-run-planare documented "Not supported". Caveats added at the auth mapping, the ALB-grouping snippet, the ATX prerequisite and the SKILL.md Auto Mode step; the unsourced "or use Auto Mode" equivalence for thetransformsfloor was dropped. - Cleanup: six surfaces, not four (see item 5 of the Round-2 list, now corrected) — the Option-2 steps table and
atx-guide.mdstill carried a barekubectl delete deploy/ "Remove NGINX Ingress Controller after validation". All six nowhelm uninstall, are OPERATOR-marked, and enumerate survivors. - The cutover doctrine reached the last two surfaces. The Option-2 steps table and the ATX post-transform steps described an in-place
ingressClassNameflip followed by "DNS weighted routing" — nothing to weight. Both now create a renamed parallel object, verify the ALB before touching DNS, and name the all-or-nothing fallback. annotations-risk-level/allow-snippet-annotationshistory corrected.allow-snippet-annotationsdoes exist at v1.9.0 (defaultedtrue); v1.9.1 is where the default flipped. Onlyannotations-risk-levelis absent at v1.9.0. Its default isHigh, which admits High-risk annotations.- Version currency re-dated to Gateway API v1.6.2 (2026-09-03); the round-2 note's v1.6.1 had already been superseded.
migration-risk.mdauth mechanism now namesListenerRuleConfiguration+parametersRefinstead of a "Gateway annotation".
Port-only in round 3
lbc-migratefence re-cut (F10b). The fence previously called Translate and the--consolediff "assessor-safe". They are non-mutating, but they need a shell, a local filesystem and a local web UI — none of which exist in this runtime. Everylbc-migratestep is now an OWNER / local-machine action, and the reachable in-runtime equivalent is named: a read-only API GET of theGatewayobject'sgateway.k8s.aws/dry-run-planannotation instead of the console.iam-policy.jsongainscloudwatch:GetMetricData+GetMetricStatistics, with the SKILL.md IAM table row explaining that without them the dead-vs-live orphan check degrades and the estate is scored fail-closed as live.- Export path removed from the Option-2 template (
<cluster>-manifests/target/alb/) — this runtime writes no files; it now defers to Export Materials like its siblings. - "so they render red" dropped from the Executive-Summary rule: this port emits
**bold**, and colourisation belongs to the upstream renderer.
Still open, deliberately not in this PR
- Report-template duplication (a duplicate
## Current Configuration, doubled Phase 3/4 sub-sections) is carried over from upstream. It reshapes the report template rather than correcting a fact, so it carries different regression risk and belongs in its own change.