Skip to content

Reconciling a tenant kubernetes app triggers MetalLB L2 VIP re-announcement and brief edge outage #2754

Closed as not planned

Description

@mattia-eleuteri

Summary

Any Flux reconcile / Helm upgrade of a tenant kubernetes application (group apps.cozystack.io, kind Kubernetes) causes MetalLB to re-reconcile and re-elect the L2 owner of that tenant's LoadBalancer VIPs. The resulting ARP churn drops existing TCP connections, producing a brief edge outage (~minutes) for services exposed via those VIPs — even though no node, pod, or control plane is restarted and the rendered manifest barely changes.

Reproduction (observed on a production cluster)

A trivial change to a Kubernetes CR (bumping one node group's maxReplicas) was applied. The Helm manifest diff between the two release revisions was 2 lines (a MachineDeployment's maxSurge and the cluster-autoscaler max-size annotation) — no rollout occurred (machines/control-plane untouched).

Timeline (UTC):

T+0      Kubernetes CR patched (maxReplicas 3 -> 6)
10:04:40 Flux: HelmRelease UpgradeSucceeded
10:04:41 auth.<tenant> and vpn.<tenant> drop (connection refused)
10:04:47 MetalLB controller: burst of ServiceReconciler reconciles on the
         tenant's *-ingress-* LoadBalancer services
10:05:15 speaker: serviceWithdrawn (reason: serviceDeleted)
10:07:15 speaker: serviceWithdrawn 10.x.x.90  (reason: notOwner) -> re-announce
10:07:29 speaker: serviceWithdrawn 10.x.x.100 (reason: notOwner) -> re-announce
...      ~40 announce/withdraw events over ~5 min (baseline: ~0)
10:09:38 services recover

MetalLB speaker logs (L2) during the window show repeated withdrawing service announcement with reason: notOwner immediately followed by service has IP, announcing for the tenant ingress VIPs — i.e. the L2 owner keeps flapping between speaker nodes.

Reverting the change (another 2-line diff) reproduced the same churn, confirming it is the reconcile/apply itself, not the changed value.

Impact

  • Brief but real outage of every service fronted by the affected L2 VIPs on each reconcile.
  • Makes routine operations (scaling a node group, editing any tenant kubernetes app, and migrations that patch these CRs) unsafe outside a maintenance window.

Hypothesis

Flux re-applies the full release via 3-way merge, bumping resourceVersion/managedFields on ~all release objects → a wave of watch events → MetalLB re-runs L2 leader election for the tenant's VIPs. To confirm: identify why MetalLB re-reconciles VIPs that belong to other releases, and whether speaker memberlist / node events are involved.

Environment

  • Cozystack tenant Kubernetes app (Kamaji + CAPI + KubeVirt workers)
  • MetalLB in L2 (layer2) mode, edge ingress exposed via LoadBalancer

Happy to provide more speaker/controller logs.

Activity

  1. dosubot commented on May 27, 2026

    @dosubot

    This is a well-documented issue. Based on the codebase, I can confirm the likely root cause and point to a few contributing factors:

    Root Cause: upgrade.force: true on tenant addon HelmReleases

    All tenant Kubernetes addon HelmReleases (including ingress-nginx) are configured with upgrade.force: true [1] and a 5-minute reconciliation interval [2]. Helm's --force flag can cause Service objects to be replaced (delete + recreate) rather than patched in-place during upgrades. When a parent kubernetes CR changes and Flux propagates that into sub-HelmReleases, the LoadBalancer services created by the KubeVirt cloud provider in the infrastructure cluster get re-applied, triggering MetalLB to treat them as new services and re-run L2 leader election.

    Contributing factors:

    1. No MetalLB-protective annotations — the ingress-nginx Service templates have no annotations section at all [3], so there's nothing telling MetalLB to preserve IP assignment or skip reconciliation.
    2. No helm.sh/resource-policy on Services — unlike CRDs and critical ConfigMaps which use helm.sh/resource-policy: keep extensively across the platform [4], LoadBalancer services in tenant charts have no such protection.
    3. Aggressive reconciliation — every 5 minutes with infinite retries and force upgrades means even a no-op diff can produce a full replace cycle [5].

    Potential mitigations (in rough order of impact):

    • Remove upgrade.force: true from the ingress-nginx (and possibly other) addon HelmReleases — this is the most direct fix and would cause Helm to use a three-way strategic merge patch instead of replacement.
    • Add metallb.universe.tf/loadBalancerIPs annotation to pin the VIP, reducing the chance MetalLB treats the re-applied Service as needing a fresh election.
    • Longer term: the team is already tracking migration from MetalLB to Cilium-native L2 announcement [6], which uses lease-based leader election and may be less sensitive to Service object churn.

    Related known MetalLB bug: Cozystack is on MetalLB v0.15.2 which also has a ServiceL2Status immutability bug [7] — if your speaker nodes have experienced any reboot or failover recently, this could compound the flapping you're seeing, since the speaker can't properly update L2 status objects on a new leader node.

    Would you like me to look deeper into whether removing upgrade.force: true from the ingress-nginx HelmRelease would be safe, or into the specific Helm template path that renders the tenant's LoadBalancer services?

    To reply, just mention @dosu.


    Docs are dead. Just use Dosu.

    Leave Feedback Ask Dosu about cozystack Share Dosu with your team

  2. added
    kind/bugCategorizes issue or PR as related to a bug
    area/networkingIssues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)
    on Jun 4, 2026
  3. lexfrei commented on Jul 1, 2026

    @lexfrei
    Contributor

    The root cause cited earlier in the thread (upgrade.force: true) is no longer present in packages/apps/kubernetes/templates/helmreleases/ingress-nginx.yaml on main — only ouroboros.yaml still uses force. The separate Flux 3-way-merge / managedFields hypothesis cannot be confirmed from static source. Does this still reproduce on current main? Marking needs-information pending a runtime repro.

  4. github-actions commented on Aug 31, 2026

    @github-actions

    This issue has had no activity for 60 days and was marked lifecycle/stale.
    It will be closed in 14 days unless commented or labelled lifecycle/frozen.

  5. added
    lifecycle/staleDenotes an issue or PR has remained open with no activity and has become stale
    on Aug 31, 2026
  6. github-actions commented on Sep 14, 2026

    @github-actions

    Closed because no activity followed the lifecycle/stale warning.
    Reopen if the issue is still relevant.

  7. added
    lifecycle/rottenDenotes an issue or PR that has aged beyond stale and will be auto-closed
    on Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area/networkingIssues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn)kind/bugCategorizes issue or PR as related to a buglifecycle/rottenDenotes an issue or PR that has aged beyond stale and will be auto-closedlifecycle/staleDenotes an issue or PR has remained open with no activity and has become staletriage/needs-informationIndicates an issue needs more information in order to work on it

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions