From 6fa81e6bc8d6e141289be7442bb6a09b7f0fd98c Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Thu, 9 Jul 2026 09:30:41 -0700 Subject: [PATCH 1/7] fix(eks): set resource requests/limits for Calico/Tigera components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tigera-operator chart ships resources: {} and the operator's built-in component defaults set no memory bounds, so the Calico pods had no guaranteed reservations and no ceilings — a source of resource contention on busy nodes. Pin predictable reservations on the operator pod, calico-node, calico-typha, calico-kube-controllers, and calico-apiserver, following a memory-bounded / CPU-unbounded policy: memory request == limit (non-compressible; guaranteed floor + bounded ceiling), CPU request only with no CPU limit (avoids CFS-throttling the dataplane). EKS only; AKS uses the managed Calico add-on. --- lib/steps/eks_aws_test.go | 45 +++++++++++++++++++++++++++++ lib/steps/eks_helpers.go | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/lib/steps/eks_aws_test.go b/lib/steps/eks_aws_test.go index 612101b1..59114410 100644 --- a/lib/steps/eks_aws_test.go +++ b/lib/steps/eks_aws_test.go @@ -364,6 +364,51 @@ func TestAWSEKSDeployTigeraAliasChain(t *testing.T) { "urn:pulumi:"+stack+"::"+proj+"::ptd:AWSWorkloadEKS$ptd:TigeraOperator$kubernetes:core/v1:Namespace::wl01-staging-20250101-tigera-ns") } +// TestAWSEKSDeployTigeraResourceRequests asserts the tigera-operator helm +// release carries the Calico resource requests/limits following PTD's +// memory-bounded, CPU-unbounded policy: memory request == limit and no CPU limit. +func TestAWSEKSDeployTigeraResourceRequests(t *testing.T) { + mocks := &eksStepMocks{} + err := pulumi.RunErr(func(ctx *pulumi.Context) error { + return awsEKSDeploy(ctx, mockAWSWorkloadTarget("wl01-staging"), newTestEKSParams()) + }, pulumi.WithMocks("ptd-aws-workload-eks", "wl01-staging", mocks)) + require.NoError(t, err) + + rel := mocks.findResource("wl01-staging-20250101-tigera-operator") + require.NotNil(t, rel, "tigera-operator helm release not found") + values := rel.Inputs["values"].ObjectValue() + + // containerResources digs out the resources block for the single container + // override under a component-deployment map (calicoNodeDaemonSet etc.). + containerResources := func(component resource.PropertyMap, key string) resource.PropertyMap { + containers := component[resource.PropertyKey(key)].ObjectValue()["spec"].ObjectValue()["template"]. + ObjectValue()["spec"].ObjectValue()["containers"].ArrayValue() + require.Len(t, containers, 1) + return containers[0].ObjectValue()["resources"].ObjectValue() + } + // assertMemoryBounded checks CPU request set with no CPU limit, and memory + // request == limit at the expected value. + assertMemoryBounded := func(res resource.PropertyMap, wantCPU, wantMem string) { + requests := res["requests"].ObjectValue() + limits := res["limits"].ObjectValue() + assert.Equal(t, wantCPU, requests["cpu"].StringValue()) + assert.Equal(t, wantMem, requests["memory"].StringValue()) + assert.Equal(t, wantMem, limits["memory"].StringValue()) + assert.NotContains(t, limits, resource.PropertyKey("cpu"), "CPU must be unbounded (no limit)") + } + + installation := values["installation"].ObjectValue() + assertMemoryBounded(containerResources(installation, "calicoNodeDaemonSet"), "250m", "512Mi") + assertMemoryBounded(containerResources(installation, "typhaDeployment"), "100m", "256Mi") + assertMemoryBounded(containerResources(installation, "calicoKubeControllersDeployment"), "50m", "128Mi") + + apiServer := values["apiServer"].ObjectValue() + assertMemoryBounded(containerResources(apiServer, "apiServerDeployment"), "100m", "256Mi") + + // Operator pod itself. + assertMemoryBounded(values["resources"].ObjectValue(), "100m", "256Mi") +} + func TestAWSEKSDeployEfsEnabled(t *testing.T) { params := newTestEKSParams() params.clusters["20250101"] = types.AWSWorkloadClusterConfig{ diff --git a/lib/steps/eks_helpers.go b/lib/steps/eks_helpers.go index 46507811..823c5b44 100644 --- a/lib/steps/eks_helpers.go +++ b/lib/steps/eks_helpers.go @@ -15,6 +15,51 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) +// calicoResources builds a Kubernetes resources block following PTD's +// "memory-bounded, CPU-unbounded" policy for Calico components: +// - Memory: request == limit. Memory is non-compressible, so equal request +// and limit gives the pod a guaranteed, bounded allocation and prevents a +// runaway component from exhausting node memory (which would crash the node). +// - CPU: request only, no limit. Omitting the CPU limit avoids Linux CFS +// throttling of the dataplane (a throttled calico-node degrades networking +// cluster-wide). +// +// cpuRequest and memory are Kubernetes quantity strings (e.g. "250m", "512Mi"). +func calicoResources(cpuRequest, memory string) pulumi.Map { + return pulumi.Map{ + "requests": pulumi.Map{ + "cpu": pulumi.String(cpuRequest), + "memory": pulumi.String(memory), + }, + "limits": pulumi.Map{ + "memory": pulumi.String(memory), + }, + } +} + +// calicoComponentOverride builds the operator component-deployment override that +// patches resources onto a single named container. The operator strategically +// merges this by container name into the rendered DaemonSet/Deployment, so only +// the name and resources need to be specified. Used under installation's +// calicoNodeDaemonSet / typhaDeployment / calicoKubeControllersDeployment and +// apiServer's apiServerDeployment. +func calicoComponentOverride(containerName, cpuRequest, memory string) pulumi.Map { + return pulumi.Map{ + "spec": pulumi.Map{ + "template": pulumi.Map{ + "spec": pulumi.Map{ + "containers": pulumi.Array{ + pulumi.Map{ + "name": pulumi.String(containerName), + "resources": calicoResources(cpuRequest, memory), + }, + }, + }, + }, + }, + } +} + // deployTigeraOperator ports python-pulumi/src/ptd/pulumi_resources/tigera_operator.py // (the ptd:TigeraOperator nested ComponentResource created by // aws_workload_eks.py:_define_tigera_operator). It installs the Calico/Tigera CNI: @@ -112,6 +157,22 @@ func deployTigeraOperator( "ipam": pulumi.Map{"type": pulumi.String("Calico")}, "type": pulumi.String("Calico"), }, + // Resource overrides for the operator-managed dataplane + // components (memory-bounded, CPU-unbounded — see calicoResources). + // calico-node runs Felix, whose memory scales with the number of + // endpoints and policies, so it gets the largest memory bound and + // keeps the operator's built-in 250m CPU request floor. + "calicoNodeDaemonSet": calicoComponentOverride("calico-node", "250m", "512Mi"), + "typhaDeployment": calicoComponentOverride("calico-typha", "100m", "256Mi"), + "calicoKubeControllersDeployment": calicoComponentOverride("calico-kube-controllers", "50m", "128Mi"), + }, + // Resources for the tigera/operator pod itself. Same + // memory-bounded/CPU-unbounded policy as the dataplane components. + "resources": calicoResources("100m", "256Mi"), + // apiServer.enabled defaults to true in the chart; we merge in a + // resource override for the calico-apiserver container. + "apiServer": pulumi.Map{ + "apiServerDeployment": calicoComponentOverride("calico-apiserver", "100m", "256Mi"), }, "goldmane": pulumi.Map{"enabled": pulumi.Bool(false)}, "whisker": pulumi.Map{"enabled": pulumi.Bool(false)}, From a9cd2d0c391f02f276bbe7ef683283c401bf2b72 Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Fri, 17 Jul 2026 11:46:25 -0700 Subject: [PATCH 2/7] docs(eks): clarify Calico resource-override comments Address review feedback: note that calicoComponentOverride patches only the main container (not ephemeral initContainers like install-cni), and explain why calico-apiserver's resources live under apiServer rather than installation (they map to the separate APIServer CR). --- lib/steps/eks_helpers.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/steps/eks_helpers.go b/lib/steps/eks_helpers.go index 823c5b44..2624841a 100644 --- a/lib/steps/eks_helpers.go +++ b/lib/steps/eks_helpers.go @@ -43,6 +43,10 @@ func calicoResources(cpuRequest, memory string) pulumi.Map { // the name and resources need to be specified. Used under installation's // calicoNodeDaemonSet / typhaDeployment / calicoKubeControllersDeployment and // apiServer's apiServerDeployment. +// +// Only the main container is patched, not initContainers (e.g. calico-node's +// install-cni). Init containers are ephemeral — they exit after CNI setup and +// don't contribute to steady-state resource pressure — so they're left alone. func calicoComponentOverride(containerName, cpuRequest, memory string) pulumi.Map { return pulumi.Map{ "spec": pulumi.Map{ @@ -169,8 +173,11 @@ func deployTigeraOperator( // Resources for the tigera/operator pod itself. Same // memory-bounded/CPU-unbounded policy as the dataplane components. "resources": calicoResources("100m", "256Mi"), - // apiServer.enabled defaults to true in the chart; we merge in a - // resource override for the calico-apiserver container. + // calico-apiserver lives outside "installation": its resources map to + // the APIServer CR (apiServer.apiServerDeployment), a separate CR from + // the Installation, so the chart exposes it under a top-level apiServer + // key rather than under installation. apiServer.enabled defaults to true + // in the chart and PTD does not disable it, so this override is live. "apiServer": pulumi.Map{ "apiServerDeployment": calicoComponentOverride("calico-apiserver", "100m", "256Mi"), }, From 1dba1676277f8afd77ab693c156121ce779e5407 Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Fri, 7 Aug 2026 09:31:41 -0700 Subject: [PATCH 3/7] fix(eks): bound csi-node-driver and correct the calico-node CPU rationale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tigera operator ships no resource defaults of its own — an Installation without the deprecated ComponentResources renders empty resources — so the previous comment claiming calico-node "keeps the operator's built-in 250m CPU request floor" was wrong. 250m is the request upstream sets on calico-node in the self-managed manifests/calico.yaml; under the operator it is a new per-node reservation. Comment corrected to say so. Also bounds csi-node-driver, which was still running unbounded on every node: the operator enables the Calico CSI plugin unless kubeletVolumePluginPath is "None", and PTD leaves it at the default. Both of its containers (calico-csi and csi-node-driver-registrar) are small and idle once the driver registers with kubelet, so they take the smallest bounds of any component here. calicoComponentOverride now takes a variadic container list (via a new calicoContainer helper) to express the two-container CSI DaemonSet, and the test looks containers up by name instead of by position. --- lib/steps/eks_aws_test.go | 36 +++++++++++++++++------- lib/steps/eks_helpers.go | 59 ++++++++++++++++++++++++++------------- 2 files changed, 66 insertions(+), 29 deletions(-) diff --git a/lib/steps/eks_aws_test.go b/lib/steps/eks_aws_test.go index 59114410..8fc79e4e 100644 --- a/lib/steps/eks_aws_test.go +++ b/lib/steps/eks_aws_test.go @@ -378,13 +378,24 @@ func TestAWSEKSDeployTigeraResourceRequests(t *testing.T) { require.NotNil(t, rel, "tigera-operator helm release not found") values := rel.Inputs["values"].ObjectValue() - // containerResources digs out the resources block for the single container - // override under a component-deployment map (calicoNodeDaemonSet etc.). - containerResources := func(component resource.PropertyMap, key string) resource.PropertyMap { - containers := component[resource.PropertyKey(key)].ObjectValue()["spec"].ObjectValue()["template"]. + // componentContainers digs out the container override list under a + // component-deployment map (calicoNodeDaemonSet etc.). + componentContainers := func(component resource.PropertyMap, key string) []resource.PropertyValue { + return component[resource.PropertyKey(key)].ObjectValue()["spec"].ObjectValue()["template"]. ObjectValue()["spec"].ObjectValue()["containers"].ArrayValue() - require.Len(t, containers, 1) - return containers[0].ObjectValue()["resources"].ObjectValue() + } + // containerResources digs out the resources block for the named container + // override. Looked up by name rather than position so the assertions don't + // depend on the order containers are declared in. + containerResources := func(component resource.PropertyMap, key, container string) resource.PropertyMap { + for _, c := range componentContainers(component, key) { + obj := c.ObjectValue() + if obj["name"].StringValue() == container { + return obj["resources"].ObjectValue() + } + } + require.Failf(t, "container override not found", "%s: %s", key, container) + return nil } // assertMemoryBounded checks CPU request set with no CPU limit, and memory // request == limit at the expected value. @@ -398,12 +409,17 @@ func TestAWSEKSDeployTigeraResourceRequests(t *testing.T) { } installation := values["installation"].ObjectValue() - assertMemoryBounded(containerResources(installation, "calicoNodeDaemonSet"), "250m", "512Mi") - assertMemoryBounded(containerResources(installation, "typhaDeployment"), "100m", "256Mi") - assertMemoryBounded(containerResources(installation, "calicoKubeControllersDeployment"), "50m", "128Mi") + assertMemoryBounded(containerResources(installation, "calicoNodeDaemonSet", "calico-node"), "250m", "512Mi") + assertMemoryBounded(containerResources(installation, "typhaDeployment", "calico-typha"), "100m", "256Mi") + assertMemoryBounded(containerResources(installation, "calicoKubeControllersDeployment", "calico-kube-controllers"), "50m", "128Mi") + + // csi-node-driver runs on every node and has two containers; both are bounded. + assert.Len(t, componentContainers(installation, "csiNodeDriverDaemonSet"), 2) + assertMemoryBounded(containerResources(installation, "csiNodeDriverDaemonSet", "calico-csi"), "10m", "64Mi") + assertMemoryBounded(containerResources(installation, "csiNodeDriverDaemonSet", "csi-node-driver-registrar"), "10m", "64Mi") apiServer := values["apiServer"].ObjectValue() - assertMemoryBounded(containerResources(apiServer, "apiServerDeployment"), "100m", "256Mi") + assertMemoryBounded(containerResources(apiServer, "apiServerDeployment", "calico-apiserver"), "100m", "256Mi") // Operator pod itself. assertMemoryBounded(values["resources"].ObjectValue(), "100m", "256Mi") diff --git a/lib/steps/eks_helpers.go b/lib/steps/eks_helpers.go index 2624841a..cbf12a0c 100644 --- a/lib/steps/eks_helpers.go +++ b/lib/steps/eks_helpers.go @@ -37,27 +37,33 @@ func calicoResources(cpuRequest, memory string) pulumi.Map { } } +// calicoContainer builds one container entry for a component override: the +// container's name plus its resources. The operator merges overrides by container +// name and copies across only resources (and ports), so nothing else is needed. +func calicoContainer(name, cpuRequest, memory string) pulumi.Map { + return pulumi.Map{ + "name": pulumi.String(name), + "resources": calicoResources(cpuRequest, memory), + } +} + // calicoComponentOverride builds the operator component-deployment override that -// patches resources onto a single named container. The operator strategically -// merges this by container name into the rendered DaemonSet/Deployment, so only -// the name and resources need to be specified. Used under installation's -// calicoNodeDaemonSet / typhaDeployment / calicoKubeControllersDeployment and -// apiServer's apiServerDeployment. +// patches resources onto the named containers. The operator strategically merges +// them by container name into the rendered DaemonSet/Deployment. Used under +// installation's calicoNodeDaemonSet / typhaDeployment / +// calicoKubeControllersDeployment / csiNodeDriverDaemonSet and apiServer's +// apiServerDeployment. Container names are validated by the CRD against a +// per-component enum, so a typo fails the apply rather than silently no-opping. // -// Only the main container is patched, not initContainers (e.g. calico-node's +// Only long-running containers are patched, not initContainers (e.g. calico-node's // install-cni). Init containers are ephemeral — they exit after CNI setup and // don't contribute to steady-state resource pressure — so they're left alone. -func calicoComponentOverride(containerName, cpuRequest, memory string) pulumi.Map { +func calicoComponentOverride(containers ...pulumi.Input) pulumi.Map { return pulumi.Map{ "spec": pulumi.Map{ "template": pulumi.Map{ "spec": pulumi.Map{ - "containers": pulumi.Array{ - pulumi.Map{ - "name": pulumi.String(containerName), - "resources": calicoResources(cpuRequest, memory), - }, - }, + "containers": pulumi.Array(containers), }, }, }, @@ -163,12 +169,27 @@ func deployTigeraOperator( }, // Resource overrides for the operator-managed dataplane // components (memory-bounded, CPU-unbounded — see calicoResources). + // The operator ships no resource defaults of its own (an + // Installation without ComponentResources renders empty resources), + // so every value here is newly introduced rather than inherited. + // // calico-node runs Felix, whose memory scales with the number of - // endpoints and policies, so it gets the largest memory bound and - // keeps the operator's built-in 250m CPU request floor. - "calicoNodeDaemonSet": calicoComponentOverride("calico-node", "250m", "512Mi"), - "typhaDeployment": calicoComponentOverride("calico-typha", "100m", "256Mi"), - "calicoKubeControllersDeployment": calicoComponentOverride("calico-kube-controllers", "50m", "128Mi"), + // endpoints and policies, so it gets the largest memory bound. Its + // 250m CPU request matches the only reservation upstream itself + // commits to — the request set on calico-node in the self-managed + // manifests/calico.yaml — and is a new per-node reservation here. + "calicoNodeDaemonSet": calicoComponentOverride(calicoContainer("calico-node", "250m", "512Mi")), + "typhaDeployment": calicoComponentOverride(calicoContainer("calico-typha", "100m", "256Mi")), + "calicoKubeControllersDeployment": calicoComponentOverride(calicoContainer("calico-kube-controllers", "50m", "128Mi")), + // csi-node-driver runs on every node: the operator enables the Calico + // CSI plugin unless kubeletVolumePluginPath is "None", and PTD leaves + // it at the default. Both containers are small and effectively idle + // once the driver is registered with kubelet, so they get the + // smallest bounds of any component here. + "csiNodeDriverDaemonSet": calicoComponentOverride( + calicoContainer("calico-csi", "10m", "64Mi"), + calicoContainer("csi-node-driver-registrar", "10m", "64Mi"), + ), }, // Resources for the tigera/operator pod itself. Same // memory-bounded/CPU-unbounded policy as the dataplane components. @@ -179,7 +200,7 @@ func deployTigeraOperator( // key rather than under installation. apiServer.enabled defaults to true // in the chart and PTD does not disable it, so this override is live. "apiServer": pulumi.Map{ - "apiServerDeployment": calicoComponentOverride("calico-apiserver", "100m", "256Mi"), + "apiServerDeployment": calicoComponentOverride(calicoContainer("calico-apiserver", "100m", "256Mi")), }, "goldmane": pulumi.Map{"enabled": pulumi.Bool(false)}, "whisker": pulumi.Map{"enabled": pulumi.Bool(false)}, From 33f64e2696c53308a15336d5cec4eaa753a785b2 Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Fri, 7 Aug 2026 11:37:47 -0700 Subject: [PATCH 4/7] fix(eks): size Calico memory bounds from measured usage Measured peak working set across the fleet over 7 days showed two components sitting at ~1.04x their ceiling, close enough to OOMKill on any growth: tigera-operator 245.5 MiB against 256Mi calico-kube-controllers 123.5 MiB against 128Mi Both are raised to ~1.5x observed peak (384Mi and 192Mi). Both are single-replica Deployments, so neither adds anything to the per-node reservation, which stays at 512Mi for calico-node. calico-node keeps 512Mi: at 374.8 MiB observed that is already ~1.37x, and raising it would reserve another 256Mi on every node. typha (1.94x) and apiserver (3.34x) already clear the bar and are unchanged. tigera-operator's CPU request goes 100m -> 250m; it peaks near 350m and is a single pod, so the reservation is negligible. calico-node stays at 250m despite peaking near 750m: with no CPU limit that peak isn't throttled, and raising the request would cost capacity on every node. ~1.5x rather than 2x because request == limit makes headroom reserved capacity rather than a free kill threshold. Also corrects the csi-node-driver comment: it does not run on every node today. The operator enables CSI when kubeletVolumePluginPath is unset (which PTD leaves it as), but existing clusters carry an out-of-band "None" that disables it, so the override is inert until that is resolved. --- lib/steps/eks_aws_test.go | 4 ++-- lib/steps/eks_helpers.go | 36 +++++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/steps/eks_aws_test.go b/lib/steps/eks_aws_test.go index 8fc79e4e..095dd4e1 100644 --- a/lib/steps/eks_aws_test.go +++ b/lib/steps/eks_aws_test.go @@ -411,7 +411,7 @@ func TestAWSEKSDeployTigeraResourceRequests(t *testing.T) { installation := values["installation"].ObjectValue() assertMemoryBounded(containerResources(installation, "calicoNodeDaemonSet", "calico-node"), "250m", "512Mi") assertMemoryBounded(containerResources(installation, "typhaDeployment", "calico-typha"), "100m", "256Mi") - assertMemoryBounded(containerResources(installation, "calicoKubeControllersDeployment", "calico-kube-controllers"), "50m", "128Mi") + assertMemoryBounded(containerResources(installation, "calicoKubeControllersDeployment", "calico-kube-controllers"), "50m", "192Mi") // csi-node-driver runs on every node and has two containers; both are bounded. assert.Len(t, componentContainers(installation, "csiNodeDriverDaemonSet"), 2) @@ -422,7 +422,7 @@ func TestAWSEKSDeployTigeraResourceRequests(t *testing.T) { assertMemoryBounded(containerResources(apiServer, "apiServerDeployment", "calico-apiserver"), "100m", "256Mi") // Operator pod itself. - assertMemoryBounded(values["resources"].ObjectValue(), "100m", "256Mi") + assertMemoryBounded(values["resources"].ObjectValue(), "250m", "384Mi") } func TestAWSEKSDeployEfsEnabled(t *testing.T) { diff --git a/lib/steps/eks_helpers.go b/lib/steps/eks_helpers.go index cbf12a0c..0f2328c9 100644 --- a/lib/steps/eks_helpers.go +++ b/lib/steps/eks_helpers.go @@ -173,27 +173,37 @@ func deployTigeraOperator( // Installation without ComponentResources renders empty resources), // so every value here is newly introduced rather than inherited. // - // calico-node runs Felix, whose memory scales with the number of - // endpoints and policies, so it gets the largest memory bound. Its - // 250m CPU request matches the only reservation upstream itself - // commits to — the request set on calico-node in the self-managed - // manifests/calico.yaml — and is a new per-node reservation here. + // Memory bounds are ~1.5x the highest working set observed across the + // fleet over 7 days, which covers scrape-interval blind spots, Go heap + // transients, and growth, without reserving capacity that never gets + // used. calico-node runs Felix, whose memory scales with the number of + // endpoints and policies, so it holds the largest bound. Its 250m CPU + // request matches the only reservation upstream itself commits to — the + // request set on calico-node in the self-managed manifests/calico.yaml — + // and is a new per-node reservation here. It stays at 250m despite + // peaking near 750m: with no CPU limit that peak isn't throttled, and + // raising the request would take capacity off every node to buy share + // weight during transient Felix recalcs. "calicoNodeDaemonSet": calicoComponentOverride(calicoContainer("calico-node", "250m", "512Mi")), "typhaDeployment": calicoComponentOverride(calicoContainer("calico-typha", "100m", "256Mi")), - "calicoKubeControllersDeployment": calicoComponentOverride(calicoContainer("calico-kube-controllers", "50m", "128Mi")), - // csi-node-driver runs on every node: the operator enables the Calico - // CSI plugin unless kubeletVolumePluginPath is "None", and PTD leaves - // it at the default. Both containers are small and effectively idle - // once the driver is registered with kubelet, so they get the - // smallest bounds of any component here. + "calicoKubeControllersDeployment": calicoComponentOverride(calicoContainer("calico-kube-controllers", "50m", "192Mi")), + // csi-node-driver is bounded for when the Calico CSI plugin is active: + // the operator enables it whenever kubeletVolumePluginPath is unset, + // which is what PTD leaves it as, so a fresh cluster runs this DaemonSet + // on every node. Existing clusters carry an out-of-band + // kubeletVolumePluginPath="None" that disables it, so the override is + // inert there. Both containers are small and effectively idle once the + // driver is registered with kubelet, so they get the smallest bounds. "csiNodeDriverDaemonSet": calicoComponentOverride( calicoContainer("calico-csi", "10m", "64Mi"), calicoContainer("csi-node-driver-registrar", "10m", "64Mi"), ), }, // Resources for the tigera/operator pod itself. Same - // memory-bounded/CPU-unbounded policy as the dataplane components. - "resources": calicoResources("100m", "256Mi"), + // memory-bounded/CPU-unbounded policy as the dataplane components. Sized + // higher than its idle footprint suggests because it peaks during + // reconciles; it is a single replica, so the extra costs nothing per node. + "resources": calicoResources("250m", "384Mi"), // calico-apiserver lives outside "installation": its resources map to // the APIServer CR (apiServer.apiServerDeployment), a separate CR from // the Installation, so the chart exposes it under a top-level apiServer From 5bb08f34ee6918fc7eb4e1843e57f7a29e5ffec7 Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Fri, 7 Aug 2026 11:47:10 -0700 Subject: [PATCH 5/7] docs(eks): state the memory multipliers accurately The comment claimed all bounds were ~1.5x observed peak, but only the two that were raised land there. 1.5x was the threshold for deciding what needed headroom, not a target every value hits: typha and apiserver were already above it and left alone, and calico-node sits at ~1.37x deliberately because it is the only per-node DaemonSet. --- lib/steps/eks_helpers.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/steps/eks_helpers.go b/lib/steps/eks_helpers.go index 0f2328c9..d54fa393 100644 --- a/lib/steps/eks_helpers.go +++ b/lib/steps/eks_helpers.go @@ -173,11 +173,15 @@ func deployTigeraOperator( // Installation without ComponentResources renders empty resources), // so every value here is newly introduced rather than inherited. // - // Memory bounds are ~1.5x the highest working set observed across the - // fleet over 7 days, which covers scrape-interval blind spots, Go heap - // transients, and growth, without reserving capacity that never gets - // used. calico-node runs Felix, whose memory scales with the number of - // endpoints and policies, so it holds the largest bound. Its 250m CPU + // Memory bounds are set from the highest working set observed across the + // fleet over 7 days. Where headroom had to be added it targets ~1.5x, + // enough to cover scrape-interval blind spots, Go heap transients, and + // growth, without reserving capacity that never gets used; components + // already above that were left as they were. calico-node runs Felix, + // whose memory scales with the number of endpoints and policies, so it + // holds the largest bound — though at ~1.37x of its observed peak it is + // also the thinnest margin, accepted because it is the only per-node + // DaemonSet and every increment lands on every node. Its 250m CPU // request matches the only reservation upstream itself commits to — the // request set on calico-node in the self-managed manifests/calico.yaml — // and is a new per-node reservation here. It stays at 250m despite From 1e4a5c96b342fc828c0404d171dfed40593a99d5 Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Fri, 7 Aug 2026 11:50:25 -0700 Subject: [PATCH 6/7] docs(eks): drop invented justification from the sizing comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multipliers aren't a design target and the cost of memory doesn't vary by component — each bound is just a round value above the measured peak. Says that instead. --- lib/steps/eks_helpers.go | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/steps/eks_helpers.go b/lib/steps/eks_helpers.go index d54fa393..f50191b0 100644 --- a/lib/steps/eks_helpers.go +++ b/lib/steps/eks_helpers.go @@ -173,21 +173,18 @@ func deployTigeraOperator( // Installation without ComponentResources renders empty resources), // so every value here is newly introduced rather than inherited. // - // Memory bounds are set from the highest working set observed across the - // fleet over 7 days. Where headroom had to be added it targets ~1.5x, - // enough to cover scrape-interval blind spots, Go heap transients, and - // growth, without reserving capacity that never gets used; components - // already above that were left as they were. calico-node runs Felix, - // whose memory scales with the number of endpoints and policies, so it - // holds the largest bound — though at ~1.37x of its observed peak it is - // also the thinnest margin, accepted because it is the only per-node - // DaemonSet and every increment lands on every node. Its 250m CPU - // request matches the only reservation upstream itself commits to — the - // request set on calico-node in the self-managed manifests/calico.yaml — - // and is a new per-node reservation here. It stays at 250m despite - // peaking near 750m: with no CPU limit that peak isn't throttled, and - // raising the request would take capacity off every node to buy share - // weight during transient Felix recalcs. + // Memory bounds are round values above the highest working set observed + // across the fleet over 7 days. Headroom is kept modest because + // request == limit makes it reserved capacity, not just a kill + // threshold. calico-node runs Felix, whose memory scales with the + // number of endpoints and policies, so it holds the largest bound and + // has the least headroom relative to its peak — worth watching, since + // it is the only per-node DaemonSet and any increase lands on every + // node. Its 250m CPU request matches upstream's, the only resource + // value set in the self-managed manifests/calico.yaml, and stays there + // despite peaking near 750m: with no CPU limit that peak isn't + // throttled, so raising the request would reserve capacity on every + // node without preventing anything. "calicoNodeDaemonSet": calicoComponentOverride(calicoContainer("calico-node", "250m", "512Mi")), "typhaDeployment": calicoComponentOverride(calicoContainer("calico-typha", "100m", "256Mi")), "calicoKubeControllersDeployment": calicoComponentOverride(calicoContainer("calico-kube-controllers", "50m", "192Mi")), From 9be9454b549b2b3e7028f585ce51a8555ba136ae Mon Sep 17 00:00:00 2001 From: Anna Williamson Date: Fri, 7 Aug 2026 11:53:54 -0700 Subject: [PATCH 7/7] test(eks): fix stale csi-node-driver comment --- lib/steps/eks_aws_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/steps/eks_aws_test.go b/lib/steps/eks_aws_test.go index 095dd4e1..a666e88b 100644 --- a/lib/steps/eks_aws_test.go +++ b/lib/steps/eks_aws_test.go @@ -413,7 +413,7 @@ func TestAWSEKSDeployTigeraResourceRequests(t *testing.T) { assertMemoryBounded(containerResources(installation, "typhaDeployment", "calico-typha"), "100m", "256Mi") assertMemoryBounded(containerResources(installation, "calicoKubeControllersDeployment", "calico-kube-controllers"), "50m", "192Mi") - // csi-node-driver runs on every node and has two containers; both are bounded. + // csi-node-driver has two containers; both are bounded when the override renders. assert.Len(t, componentContainers(installation, "csiNodeDriverDaemonSet"), 2) assertMemoryBounded(containerResources(installation, "csiNodeDriverDaemonSet", "calico-csi"), "10m", "64Mi") assertMemoryBounded(containerResources(installation, "csiNodeDriverDaemonSet", "csi-node-driver-registrar"), "10m", "64Mi")