Skip to main content
Source

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 upstream main (post-#129/#153/#154).

Differences from the Claude Code version​

AspectClaude Code (skills/eks-ingress-migration/)DevOps Agent (this skill)
Execution modelInteractive — asks "assess all clusters, or select specific ones?" on ambiguityFully autonomous — Step 0 hard-stop decision table, no interactive prompts
Cluster selectionDiscovers all clusters across regions, assesses many per runDiscovers 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 accessaws CLI, kubectl, EKS MCP serverAWS control-plane APIs (read-only) + Kubernetes API via the Agent Space EKS access entry (read-only). No Bash, no kubectl, no MCP.
Kubernetes readskubectl get/describe, or MCP list_k8s_resourcesExpressed as "Via Kubernetes API" capability descriptions, not kubectl pipelines
Report outputMarkdown file written to ~/ingress_migration/<cluster>/report.mdMarkdown rendered inline in the response; no files written, no output path reported
HTML reporttools/report_to_html.py renders an interactive dashboard with a 3D routing diagram, score gauge, gate badge, cluster dropdown, and per-option download buttonsOmitted — 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 JSONWritten to ~/ingress_migration/<cluster>/topology.json, consumed by the 3D viewEmitted 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 exportYAML 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 enumerationkubectl exec <nginx-pod> -- nginx -T as an optional deep readNot reachable — no pod exec. The snippet blind spot becomes a permanent, mandatory caveat and the routing inventory is marked Unverified (see below).
Directory namingreferences/samples/, references/atx/ nested under references/assets/samples/, assets/atx/ — matches the port convention (references/ stays flat; data files live in assets/)
PrerequisitesAWS credentials, Python 3.10+, EKS MCP server, AWS CLIreferences/iam-policy.json for the Agent Space role + an EKS access entry; no Python, no CLI, no MCP
allowed-tools frontmatterNot present upstream eitherNot 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 tokenPort 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.

  1. Snippet-injected route enumeration. Upstream can exec into the controller pod and read nginx -T. This runtime has no shell, no pod exec, and no in-cluster network path, and pods/exec is 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".
  2. Admission-webhook exposure. ValidatingWebhookConfiguration lives in admissionregistration.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 a 403 lands 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.
  3. Gateway API adoption state. gateway.networking.k8s.io and apiextensions.k8s.io are 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" } — never crdsInstalled: false, and never a recommendation to install CRDs on the strength of a 403.
  4. 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-migrate toolkit 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:

  1. certificate-discovery is 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 omitting certificate-arn with an HTTPS entry in listen-ports. The sample now uses the omit pattern.
  2. 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.)
  3. transforms URI rewrites need a controller floor. The install floor was stated as v2.7.2 while most ALB samples use alb.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.)
  4. EKS Auto Mode does not provide Gateway API. The built-in eks.amazonaws.com controller covers Ingress and Service type: LoadBalancer only (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 contradicted ingress-discovery.md, which had it right.
  5. 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; the lbc-migrate build-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.
  6. 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.)
  7. Cosmetic parity. ingress-resources.md auth-url row now reads "No native equivalent", matching its CORS and affinity siblings, and the upstream SKILL.md navigation-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:

  1. The v2.x production warning persists to v2.17.1, not "through v2.15.x." Checked docs/guide/gateway/gateway.md at 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.
  2. The transforms floor is v2.14.1, not v2.15.0. transforms appears in docs/guide/ingress/annotations.md and pkg/ingress/enhanced_backend_builder.go at 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 API HTTPRoute URLRewrite filter (PR #4418, v2.15.0) — a different feature. Both copies now state v2.14.1 and warn against the conflation.
  3. alb.ingress.kubernetes.io/* annotations do not configure a Gateway. The Phase-2 Gateway example set scheme and certificate-arn as Gateway annotations and TLS via the listener's certificateRefs. 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 the certificateRefs field of a Gateway Listener." As shipped the example yielded an internal ALB with no certificate — scheme defaults to internal. Rewritten around LoadBalancerConfiguration (+ TargetGroupConfiguration / ListenerRuleConfiguration) attached via spec.infrastructure.parametersRef, and the four mapping-table rows were corrected to match.
  4. There is no aws-load-balancer-controller EKS add-on. The Phase-1 upgrade step offered aws eks update-addon --addon-name aws-load-balancer-controller labelled "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 live describe-addon-versions call was not possible — no valid credentials in the authoring environment.)
  5. Cleanup must uninstall the release, not the Deployment. Deleting only the ingress-nginx Deployment leaves the ingress-nginx-admission ValidatingWebhookConfiguration (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.md Phase 4, migration-plan.md Phase 4, the two report-generation.md phase tables, the Option-2 migration-steps table and atx-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 bare kubectl delete deploy / "Remove NGINX Ingress Controller after validation" and were fixed in round 3. All six now say helm uninstall (or delete the admission webhook + Service explicitly), mark it OPERATOR-only, and enumerate the survivors (controller LoadBalancer Service and its CLB/NLB + security groups + DNS, admission VWC and Service, IngressClass, external-dns TXT records).
  6. 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.
  7. 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.
  8. cert-manager Gateway API support is beta and off by default. §4.2 checked only the pre-v1.15 ExperimentalGatewayAPISupport gate, 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.
  9. L4 tcp-services / udp-services flows 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. New ingress-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.
  10. 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-timeout is mapped to deregistration_delay.timeout_seconds (connection draining) instead of the ALB idle timeout. Both are now in the atx-guide.md known-defects block and in the user-facing report template's ATX checklist, which previously carried only the certificate-discovery caveat 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​

  1. 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).
  2. TLSRoute is standard channel, not experimental. It graduated to v1 in Gateway API v1.5.0, so both CRD versions this skill pins carry it. Only TCPRoute/UDPRoute waited for v1.6.0.
  3. 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 set ListenerAttribute at all.
  4. EKS Auto Mode is not annotation-equivalent to a self-managed LBC. auth-type: oidc, group.name, the waf-acl-id/web-acl-id family and dry-run-plan are 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 the transforms floor was dropped.
  5. Cleanup: six surfaces, not four (see item 5 of the Round-2 list, now corrected) — the Option-2 steps table and atx-guide.md still carried a bare kubectl delete deploy / "Remove NGINX Ingress Controller after validation". All six now helm uninstall, are OPERATOR-marked, and enumerate survivors.
  6. The cutover doctrine reached the last two surfaces. The Option-2 steps table and the ATX post-transform steps described an in-place ingressClassName flip 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.
  7. annotations-risk-level / allow-snippet-annotations history corrected. allow-snippet-annotations does exist at v1.9.0 (defaulted true); v1.9.1 is where the default flipped. Only annotations-risk-level is absent at v1.9.0. Its default is High, which admits High-risk annotations.
  8. 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.
  9. migration-risk.md auth mechanism now names ListenerRuleConfiguration + parametersRef instead of a "Gateway annotation".

Port-only in round 3

  • lbc-migrate fence re-cut (F10b). The fence previously called Translate and the --console diff "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. Every lbc-migrate step is now an OWNER / local-machine action, and the reachable in-runtime equivalent is named: a read-only API GET of the Gateway object's gateway.k8s.aws/dry-run-plan annotation instead of the console.
  • iam-policy.json gains cloudwatch: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.