Migration Tool (lbc-migrate)¶
lbc-migrate is a CLI tool that translates AWS Load Balancer Controller (LBC) Ingress resources to Gateway API equivalents. It reads Ingress, Service, IngressClass, and IngressClassParams resources from YAML/JSON files or a live Kubernetes cluster, translates annotations to Gateway API CRD fields, and writes the output manifests.
For the end-to-end migration workflow, see Migrate from Ingress to Gateway API.
Installation¶
Build from source (requires Go):
# From the root of the aws-load-balancer-controller repo
make lbc-migrate
The binary will be at bin/lbc-migrate. To install it on your PATH (creates a symlink, so future make lbc-migrate rebuilds are picked up automatically):
make install-lbc-migrate
Alternatively, use go run directly without building:
go run ./cmd/lbc-migrate/ [flags]
Usage¶
lbc-migrate [flags]
Input Modes¶
The tool supports three input modes. You must provide at least one.
Option 1: Individual files¶
lbc-migrate -f ingress1.yaml,ingress2.yaml
Option 2: Directory of files¶
lbc-migrate --input-dir ./my-manifests/
Option 3: Read from a live cluster¶
Read-only access
The --from-cluster mode only performs read operations (list/get). It never creates, updates, or deletes any cluster resources. We recommend using a user or service account with read-only RBAC permissions (e.g. a ClusterRole with only get and list verbs on Ingress, Service, IngressClass, and IngressClassParams resources).
lbc-migrate --from-cluster --all-namespaces
lbc-migrate --from-cluster --namespaces production
lbc-migrate --from-cluster --namespaces production --ingress-name my-ingress
You can combine -f and --input-dir, but --from-cluster cannot be used with file-based input.
Flags¶
| Flag | Required | Description | Default |
|---|---|---|---|
-f, --file |
One of -f, --input-dir, or --from-cluster is required |
Comma-separated input YAML/JSON file paths (e.g. -f file1.yaml,file2.yaml) |
|
--input-dir |
One of -f, --input-dir, or --from-cluster is required |
Directory containing YAML/JSON files | |
--from-cluster |
One of -f, --input-dir, or --from-cluster is required |
Read resources from a live Kubernetes cluster | |
--namespaces |
Optional | Comma-separated namespaces to read from (e.g. --namespaces ns-a,ns-b). Only valid with --from-cluster. Mutually exclusive with --all-namespaces |
|
--all-namespaces |
Optional | Read from all namespaces. Only valid with --from-cluster. Mutually exclusive with --namespaces |
|
--ingress-name |
Optional | Name of a specific Ingress to migrate. Requires exactly one --namespaces value. Only valid with --from-cluster. Mutually exclusive with --all-namespaces |
|
--kubeconfig |
Optional | Path to kubeconfig file. Only valid with --from-cluster |
$KUBECONFIG or ~/.kube/config |
--output-dir |
Optional | Directory to write output manifests | ./gateway-output |
--output-format |
Optional | Output format: yaml or json |
yaml |
--split |
Optional | Split output layout. Empty (default) writes one combined file; namespace writes one file per namespace plus a gatewayclass file for cluster-scoped resources |
"" |
--dry-run |
Optional | Add gateway.k8s.aws/dry-run annotation to generated Gateway manifests so LBC previews the plan without creating AWS resources. Pass --dry-run=false to generate live Gateway manifests. |
true |
--console |
Optional | Launch the migration console web UI to compare ingress and gateway dry-run models | false |
--port |
Optional | Local port for the console web server (only with --console) |
8080 |
Output Resources¶
By default the tool generates a single manifest file (gateway-resources.yaml or .json) containing all translated Gateway API resources separated by ---. To split the output into one file per namespace, see Split output by namespace below.
| Output Kind | API Group | Description |
|---|---|---|
GatewayClass |
gateway.networking.k8s.io |
Static, always controllerName: gateway.k8s.aws/alb. One per run. |
Gateway |
gateway.networking.k8s.io |
One per Ingress (or per group.name group, when supported). Listeners from listen-ports. |
HTTPRoute |
gateway.networking.k8s.io |
One or more per Ingress. Routes from spec.rules. When the Ingress has a defaultBackend and host-based rules, a separate catch-all HTTPRoute is generated (see below). |
LoadBalancerConfiguration |
gateway.k8s.aws |
LB-level settings. Only generated when LB-level annotations are present. |
TargetGroupConfiguration |
gateway.k8s.aws |
Per-service TG settings. Only generated when TG-level annotations are present. |
ListenerRuleConfiguration |
gateway.k8s.aws |
Auth, fixed-response, source-ip conditions. |
Existing Deployment and Service resources are reused as-is and are not generated by the tool. Gateway API HTTPRoute backendRefs point directly to your existing Services by name — no changes to your application workload are needed. You keep your existing Deployment and Service manifests and replace only the Ingress manifest with the generated Gateway API resources.
Split output by namespace¶
By default the tool writes a single gateway-resources.<ext> file containing every generated resource. Pass --split=namespace to produce one file per namespace plus a file for cluster-scoped resources:
lbc-migrate --from-cluster --all-namespaces --output-dir ./gw/ --split=namespace
Resulting layout:
gw/
├── gatewayclass.yaml # GatewayClass (cluster-scoped)
├── team-a/gateway-resources.yaml # Gateway resources scoped to team-a
└── team-b/gateway-resources.yaml # Gateway resources scoped to team-b
Apply everything recursively:
kubectl apply -R -f ./gw/
For cross-namespace IngressGroups (Ingresses in different namespaces sharing a group.name), the generated resources naturally split across namespace directories: the shared Gateway and LoadBalancerConfiguration land in the primary member's namespace (determined by group.order, then lexical namespace/name), while each member's HTTPRoute lands in its own namespace. No ReferenceGrant is required because HTTPRoutes only reference backends in their own namespace and the generated Gateway sets allowedRoutes.namespaces.from: All.
Default Backend Handling¶
Gateway API has no defaultBackend equivalent (upstream docs). When an Ingress has both a defaultBackend and host-based rules, the tool generates a separate catch-all HTTPRoute with no hostnames or match conditions. This results in one additional ALB listener rule compared to the original Ingress, which is the expected Gateway API behavior. Additionally, the gateway controller scopes target groups per route, so the separate default-backend HTTPRoute creates its own target group even if it points to the same service as other rules. This means the migrated setup may have one extra target group compared to the Ingress setup, where a single target group is shared across all rules for the same service.
IngressGroup Support¶
The migration tool detects alb.ingress.kubernetes.io/group.name and produces one shared Gateway + LoadBalancerConfiguration per group, with separate HTTPRoutes per member Ingress (preserving team ownership).
LB-level annotations (scheme, subnets, security groups, tags, etc.) must be consistent across all Ingresses in a group — the tool errors if conflicting values are detected. TLS certificates (certificate-arn) are unioned across members. Tags and load-balancer-attributes are unioned with per-key conflict detection.
Each member's listen-ports are unioned to build the shared Gateway's listeners. Each member's HTTPRoutes are scoped to only the listeners that member declared via sectionName in parentRefs. When ssl-redirect is set on any member, it applies group-wide: all members' routes attach to the HTTPS listener only, and a single redirect HTTPRoute is generated for HTTP listeners.
Cross-namespace groups (Ingresses in different namespaces with the same group.name) are supported. When detected, the migration tool generates the Gateway in the primary member's namespace (determined by group.order annotation, then lexical namespace/name — matching LBC's runtime behavior) and sets allowedRoutes.namespaces.from: All on each listener, which permits HTTPRoutes from any namespace to attach. You can move the Gateway to a different namespace after generation if needed.
For cross-namespace groups, use --namespaces ns-a,ns-b (listing all namespaces the group spans) or --all-namespaces to ensure all members are included. Using a single --namespaces that omits some members will produce incomplete output without warning.
Security consideration
From: All allows HTTPRoutes from any namespace to attach to the Gateway. If you need tighter scoping, you can manually change From: All to From: Selector with a label selector after generation.
Known Differences from Ingress¶
Rule Priority and group.order¶
The alb.ingress.kubernetes.io/group.order annotation has no equivalent in Gateway API. In the Ingress model, group.order gives explicit control over ALB listener rule priority — rules from a lower-order Ingress always get lower priority numbers (higher precedence). In Gateway API, rule precedence is determined by the Gateway API specification: hostname specificity, path match type, path length, header/query param count, route creation timestamp, and route name (alphabetical) etc. There is no annotation or field to override this ordering.
For most configurations — where grouped Ingresses have non-overlapping hosts or paths — this produces equivalent behavior. If your Ingresses rely on group.order to resolve overlapping rules (same host + same path across different Ingresses), the migrated Gateway API rules may be evaluated in a different order. Review the generated HTTPRoutes and verify with the dry-run feature before switching traffic.
The migration tool emits a warning when group.order is detected.
ALB Listener Rule Count and Priority Order¶
The migrated Gateway API manifests may produce more ALB listener rules than the original Ingress. This happens because ALB supports OR within a single condition (e.g., one http-request-method condition with values [GET, HEAD]), while Gateway API represents OR as separate HTTPRouteMatch entries — each becoming its own ALB rule. The routing behavior is functionally equivalent, but the rule count and priority numbers may differ. This affects conditions with multiple values for path-pattern, http-request-method, and query-string.
Additionally, ALB listener rule priority order may differ between Ingress and Gateway. The Ingress controller assigns priorities based on Ingress spec rule ordering, while the gateway controller follows the Gateway API precedence specification (path type, path length, creation timestamp etc). For most configurations this produces equivalent behavior, but users with overlapping rules that depend on specific priority ordering should verify after migration.
TargetGroupBinding¶
AWS Load Balancer Controller uses TargetGroupBinding (TGB) internally to register pod targets for Ingress, Service, and Gateway resources. These controller-managed TGBs are created and deleted automatically — when you delete an Ingress, its TGBs are cleaned up, and the Gateway controller creates new ones when it reconciles. You do not need to migrate these.
If you have user-created TGBs that register pods into externally-managed AWS Target Groups (see TargetGroupBinding documentation), these are independent of Ingress and Gateway resources. They are reconciled by the TargetGroupBinding controller regardless of how traffic is routed. No changes needed during migration.
External Target Group References in Actions¶
If your Ingress uses actions.* annotations that reference external target groups (via targetGroupARN or targetGroupName), the migration tool translates these to Gateway API backendRefs with kind: TargetGroupName. When the original annotation uses an ARN, the tool extracts the target group name from it. See Specify out-of-band Target Groups for how the gateway controller handles these references.
Note: external target groups can only be associated with one ALB at a time. During side-by-side migration (Step 3-5), the Gateway ALB cannot attach the same external TG that the Ingress ALB is using. You must either delete the Ingress rules referencing the external TG before applying Gateway manifests (cutover), or create a duplicate TG and update the generated manifest to use the new name.
Limited Support for Certain Ingress Annotations¶
The following Ingress annotation types have limited support in the migration tool and Gateway API:
host-headerconditions — values are passed through to Gateway API hostnames. Since hostnames are route-level in Gateway API (not per-rule), rules with host-header conditions are split into separate HTTPRoutes with their own hostnames. Complex wildcards (e.g.,www.*.example.com) or regex values that don't conform to Gateway API hostname format will be rejected by the K8s API server when the manifest is applied.url-rewriteandhost-header-rewritetransforms with regex capture group references (e.g.,replace: "/$1"orreplace: "$1.example.org") — Gateway API'sURLRewritefilter only supports static replacements (ReplaceFullPathfor paths,PreciseHostnamefor hostnames). Transforms with static replacements (no$Nreferences) are translated; transforms with capture group references are skipped. Additionally, the original Ingress transform regex is discarded during migration — the gateway controller generates its own fixed regex from the Gateway API filter type (^([^?]*)for path rewrites,.*for hostname rewrites). This means the ALB transform will always match all paths or all hostnames for the rule, regardless of what the original Ingress regex was. This is functionally equivalent because the ALB rule's conditions (from the HTTPRoute match) already narrow the traffic before the transform is applied. Verify the generated output if your Ingress uses transforms.
Annotation Priority Chain¶
LBC resolves annotations with a priority chain (highest wins):
- IngressClassParams (cluster-level policy)
- Service annotations (per-backend override)
- Ingress annotations (per-Ingress config)
- LBC controller defaults
The migration tool applies this same priority chain when translating. For the most accurate results, use --from-cluster which automatically fetches all referenced resources (Services, IngressClasses, IngressClassParams).
Examples¶
Basic: single Ingress file¶
lbc-migrate -f ingress.yaml --output-dir ./gateway-output/
Directory of manifests¶
lbc-migrate --input-dir ./k8s-manifests/ --output-dir ./gateway-output/
From a live cluster (all namespaces)¶
lbc-migrate --from-cluster --all-namespaces --output-dir ./gateway-output/
From a live cluster (specific namespace)¶
lbc-migrate --from-cluster --namespaces production --output-dir ./gateway-output/
From a live cluster (multiple namespaces)¶
lbc-migrate --from-cluster --namespaces team-a,team-b --output-dir ./gateway-output/
From a live cluster (single Ingress)¶
lbc-migrate --from-cluster --namespaces production --ingress-name my-api --output-dir ./gateway-output/
Missing Resource Warnings¶
When using file-based input (-f or --input-dir), the tool checks for missing referenced resources and emits warnings to stderr. For example, if an Ingress references a Service that was not provided in the input files:
WARNING: Ingress "default/my-ingress" references Service "api-service"
but it was not provided. Service-level annotation overrides may be missing.
WARNING: Ingress "default/my-ingress" uses IngressClass "alb"
but it was not provided. IngressClassParams overrides may be missing.
Tip: Use --from-cluster to automatically read all referenced resources,
or include Service/IngressClass/IngressClassParams files in your input.
These warnings are informational — the tool still generates output. For the most accurate results, use --from-cluster which automatically fetches all referenced resources.
Dry-Run Mode¶
How it works¶
When a Gateway is annotated with gateway.k8s.aws/dry-run: "true", LBC builds its built model stack and writes the serialized plan back to the Gateway as an annotation — without creating any AWS resources.
lbc-migrate adds this annotation by default, so the first apply always previews the plan:
lbc-migrate -f ingress.yaml --output-dir ./gw/
To generate manifests for live deployment:
lbc-migrate -f ingress.yaml --output-dir ./gw/ --dry-run=false
Manual dry-run annotation¶
You can add the annotation manually to any Gateway manifest:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: default
annotations:
gateway.k8s.aws/dry-run: "true"
spec:
gatewayClassName: aws-alb
listeners:
- name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- name: tls-secret
Viewing the plan¶
kubectl get gateway my-gateway \
-o jsonpath='{.metadata.annotations.gateway\.k8s\.aws/dry-run-plan}' | jq .
Deploying after review¶
Remove the dry-run annotation to let LBC create the actual AWS resources:
kubectl annotate -n <NAMESPACE> gateway <GATEWAY_NAME> gateway.k8s.aws/dry-run-
This triggers normal reconciliation: LBC creates the ALB, listeners, target groups, and attaches routes.
What dry-run does NOT do¶
Dry-run is only effective on Gateways that have not yet been deployed. If the Gateway already has AWS resources (indicated by the presence of a controller finalizer), the gateway.k8s.aws/dry-run annotation is ignored and normal reconciliation proceeds.
Dry-run intentionally skips every action that would touch AWS or cluster state beyond the Gateway annotation/status it owns:
- No AWS resources are created, updated, or deleted.
- No finalizer is added to the Gateway.
- No backend security group is allocated or released.
- No Kubernetes Secrets are monitored for certificate rotation.
- No add-on state (WAF, Shield) is persisted.
- No
serviceReferenceCounterrelations are updated.
Annotation reference¶
| Annotation | Set by | Description |
|---|---|---|
gateway.k8s.aws/dry-run |
User | Set to "true" to enable dry-run mode on a Gateway. |
gateway.k8s.aws/dry-run-plan |
Controller | Serialized stack JSON written by LBC when dry-run is enabled. Do not edit. |
Annotation Support¶
The tool translates the following Ingress annotations to Gateway API equivalents. Annotations not listed here are not yet supported.
| Ingress Annotation | Gateway API Equivalent | Status |
|---|---|---|
alb.ingress.kubernetes.io/scheme |
LoadBalancerConfiguration.spec.scheme |
Supported |
alb.ingress.kubernetes.io/ip-address-type |
LoadBalancerConfiguration.spec.ipAddressType |
Supported |
alb.ingress.kubernetes.io/subnets |
LoadBalancerConfiguration.spec.subnetMappings |
Supported |
alb.ingress.kubernetes.io/security-groups |
LoadBalancerConfiguration.spec.securityGroups |
Supported |
alb.ingress.kubernetes.io/certificate-arn |
Gateway.spec.listeners[].tls.certificateRefs |
Supported |
alb.ingress.kubernetes.io/listen-ports |
Gateway.spec.listeners[] |
Supported |
alb.ingress.kubernetes.io/ssl-policy |
LoadBalancerConfiguration.spec.listenerConfigs[].sslPolicy |
Supported |
alb.ingress.kubernetes.io/ssl-redirect |
Redirect HTTPRoute | Supported |
alb.ingress.kubernetes.io/group.name |
Shared Gateway per group | Supported |
alb.ingress.kubernetes.io/group.order |
Used to determine primary group member (Gateway placement) and sort order. No Gateway API equivalent for ALB rule priority — a warning is emitted. See Rule Priority and group.order. | No equivalent (priority) |
alb.ingress.kubernetes.io/target-type |
TargetGroupConfiguration.spec.targetType |
Supported |
alb.ingress.kubernetes.io/backend-protocol |
TargetGroupConfiguration.spec.protocolVersion |
Supported |
alb.ingress.kubernetes.io/healthcheck-* |
TargetGroupConfiguration.spec.healthCheck.* |
Supported |
alb.ingress.kubernetes.io/tags |
LoadBalancerConfiguration.spec.tags |
Supported |
alb.ingress.kubernetes.io/load-balancer-attributes |
LoadBalancerConfiguration.spec.loadBalancerAttributes |
Supported |
alb.ingress.kubernetes.io/target-group-attributes |
TargetGroupConfiguration.spec.targetGroupAttributes |
Supported |
alb.ingress.kubernetes.io/actions.* |
HTTPRoute filters / ListenerRuleConfiguration |
Supported |
alb.ingress.kubernetes.io/conditions.* |
HTTPRoute matches / ListenerRuleConfiguration |
Supported |
alb.ingress.kubernetes.io/auth-type |
ListenerRuleConfiguration.spec.authConfig |
Supported |
alb.ingress.kubernetes.io/auth-idp-* |
ListenerRuleConfiguration.spec.authConfig |
Supported |
alb.ingress.kubernetes.io/wafv2-acl-arn |
LoadBalancerConfiguration.spec.wafv2ACLArn |
Supported |
alb.ingress.kubernetes.io/wafv2-acl-name |
LoadBalancerConfiguration.spec.wafv2ACLName |
Supported |
alb.ingress.kubernetes.io/shield-advanced-protection |
LoadBalancerConfiguration.spec.shieldAdvancedProtection |
Supported |
alb.ingress.kubernetes.io/mutual-authentication |
LoadBalancerConfiguration.spec.mutualAuthentication |
Supported |
alb.ingress.kubernetes.io/load-balancer-name |
LoadBalancerConfiguration.spec.name |
Supported |
alb.ingress.kubernetes.io/customer-owned-ipv4-pool |
LoadBalancerConfiguration.spec.customerOwnedIPv4Pool |
Supported |
alb.ingress.kubernetes.io/ipam-ipv4-pool-id |
LoadBalancerConfiguration.spec.ipamIPv4PoolId |
Supported |
alb.ingress.kubernetes.io/manage-security-group-rules |
LoadBalancerConfiguration.spec.manageSecurityGroupRules |
Supported |
alb.ingress.kubernetes.io/inbound-cidrs |
LoadBalancerConfiguration.spec.inboundCIDRs |
Supported |
alb.ingress.kubernetes.io/security-group-prefix-lists |
LoadBalancerConfiguration.spec.securityGroupPrefixLists |
Supported |
alb.ingress.kubernetes.io/load-balancer-capacity-reservation |
LoadBalancerConfiguration.spec.capacityReservation |
Supported |
alb.ingress.kubernetes.io/backend-protocol-version |
TargetGroupConfiguration.spec.protocolVersion |
Supported |
alb.ingress.kubernetes.io/target-node-labels |
TargetGroupConfiguration.spec.targetNodeLabels |
Supported |
alb.ingress.kubernetes.io/target-control-port |
TargetGroupConfiguration.spec.targetControlPort |
Supported |
alb.ingress.kubernetes.io/use-regex-path-match |
HTTPRoute path match type (RegularExpression) |
Supported |
alb.ingress.kubernetes.io/auth-scope |
ListenerRuleConfiguration.spec.authConfig.scope |
Supported |
alb.ingress.kubernetes.io/auth-session-cookie |
ListenerRuleConfiguration.spec.authConfig.sessionCookieName |
Supported |
alb.ingress.kubernetes.io/auth-session-timeout |
ListenerRuleConfiguration.spec.authConfig.sessionTimeout |
Supported |
alb.ingress.kubernetes.io/auth-on-unauthenticated-request |
ListenerRuleConfiguration.spec.authConfig.onUnauthenticatedRequest |
Supported |
alb.ingress.kubernetes.io/jwt-validation |
ListenerRuleConfiguration.spec.authConfig.jwtValidation |
Supported |
alb.ingress.kubernetes.io/waf-acl-id |
WAF Classic is not supported by the migration tool. Migrate to WAFv2 (alb.ingress.kubernetes.io/wafv2-acl-arn) on the source Ingress before running lbc-migrate — Ingresses still on WAF Classic when the tool runs will produce Gateway manifests with no WAF protection. |
Not supported |
alb.ingress.kubernetes.io/web-acl-id |
Deprecated alias of waf-acl-id. switch to WAFv2 first before running lbc-migrate. |
Not supported |
alb.ingress.kubernetes.io/frontend-nlb-* |
All Frontend NLB annotations (enable-frontend-nlb, frontend-nlb-scheme, frontend-nlb-subnets, etc.) are not yet supported in the migration tool. |
Not supported |