feat: add label-based sharding for horizontal scaling#270
Open
aashishtomar wants to merge 1 commit into
Open
Conversation
Currently the controller only supports active/passive HA via leader election — a single replica reconciles all Workspaces regardless of replica count. For deployments with hundreds of Workspaces, this becomes a throughput bottleneck since each reconciliation runs terraform init/plan/apply. This change introduces a --shard-name flag (also configurable via SHARD_NAME env var) that partitions Workspaces across controller instances using the label terraform.crossplane.io/shard=<name>. Changes: cmd/provider/main.go: - Add --shard-name flag with SHARD_NAME env var - Move scheme registration before manager creation so cache.ByObject can resolve types at startup - Configure cache.ByObject with a label selector on both cluster-scoped and namespaced Workspace types when shard name is set - Append shard name to leader election lease ID for per-shard leaders internal/controller/gc/gc.go: - Accept shardName parameter and pass it to the GarbageCollector for logging context internal/workdir/workdir.go: - Add ShardLabel constant for terraform.crossplane.io/shard - Add shardName field and WithShardName option to GarbageCollector - GC intentionally lists ALL workspaces without shard filtering to avoid cross-shard directory deletion internal/workdir/workdir_test.go: - Add TestCollectWithShardName with two table-driven cases: ShardedGCListsAllWorkspaces verifies the GC deletes only directories for workspaces that no longer exist in any shard. ShardedGCPreservesOtherShardDirs verifies the GC does not delete directories belonging to other shards' workspaces. - Add TestWithShardName to verify the option function - Add TestShardLabel to verify the constant value When --shard-name is empty (default), behavior is identical to the existing single-controller mode for full backward compatibility. Fixes crossplane-contrib#269 Signed-off-by: AshTom <aashish.tomar@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What problem does this solve?
Fixes #269 | Related: #212, crossplane/crossplane#2411
Currently, deploying multiple controller replicas with
--leader-electiononly provides active/passive HA — all replicas compete for a single leader election lease, so only one replica reconciles Workspaces at any time. The remaining replicas sit idle.For deployments managing hundreds of Workspaces, this becomes a throughput bottleneck. Each Workspace reconciliation involves running
terraform init,plan, andapply, which are CPU-intensive operations. A single controller can only process a limited number of Workspaces per reconciliation cycle, leading to increased drift detection latency and slower convergence.What changed?
This PR introduces a
--shard-nameflag (also configurable viaSHARD_NAMEenv var) that enables label-based sharding. Users assign Workspaces to shards by labeling them withterraform.crossplane.io/shard=<name>, then deploy one controller instance per shard.cmd/provider/main.go--shard-nameflag withSHARD_NAMEenv var supportcache.ByObjectneeds the Workspace types registered in the scheme at startup to determine whether they are cluster-scoped or namespaced. Previously, schemes were registered afterctrl.NewManager(), which worked because the manager auto-created a default scheme — but withByObjectreferencing specific types, early registration is necessary.--shard-nameis set, configurescache.ByObjectwith a label selector on bothclusterv1beta1.Workspaceandnamespacedv1beta1.Workspace. This filters at the informer/watch level — the API server only sends events for matching Workspaces, which is more efficient than filtering in the reconciler.crossplane-leader-election-provider-terraform-shard-0), so each shard gets its own lease and multiple shards can be active simultaneously.internal/controller/gc/gc.goSetup()signature to acceptshardName stringand pass it to the GarbageCollector viaWithShardName().internal/workdir/workdir.goShardLabelconstant (terraform.crossplane.io/shard) as a single source of truth for the label key.shardNamefield andWithShardName()option toGarbageCollector.internal/workdir/workdir_test.goTestCollectWithShardNamewith two table-driven test cases following existing test patterns:ShardedGCListsAllWorkspaces: Sets up a sharded GC (shard-0) with directories from shard-0, shard-1, and a deleted workspace. Verifies that only the deleted workspace's directory is removed — both shard-0 and shard-1 directories are preserved.ShardedGCPreservesOtherShardDirs: Sets up a sharded GC (shard-1) with directories from both shard-0 and shard-1. Verifies that shard-1's GC does not delete shard-0's directories.TestWithShardName: Verifies the option function correctly sets theshardNamefield.TestShardLabel: Verifies the constant value matches the expected label key.All new tests use table-driven patterns with
test.MockClientconsistent with the existing test suite. No third-party test frameworks are introduced.Why this approach?
I evaluated three alternatives:
Label-based sharding was chosen because:
cache.ByObjectfor informer-level filtering, which is the most efficient approach (events are filtered at the API server watch, not in the reconciler)Does this change user-facing behavior?
No, when
--shard-nameis not set (default). The controller behaves identically to today — it reconciles all Workspaces and uses the existing leader election lease. This is a purely additive, opt-in feature.When
--shard-nameis set:terraform.crossplane.io/shardlabelExample deployment
# Assign workspaces to shards kubectl label workspace my-vpc terraform.crossplane.io/shard=shard-0 kubectl label workspace my-rds terraform.crossplane.io/shard=shard-1Backward compatibility
--shard-name) is identical to current behaviorTests added
TestCollectWithShardName(2 sub-tests): Validates GC correctness in sharded modeTestWithShardName: Validates option functionTestShardLabel: Validates label constantAll existing tests continue to pass. Ran
make reviewablelocally — clean.Manual testing performed
Tested on a local Kind cluster (3 nodes) with Crossplane 2.2.0 and provider-terraform v1.1.1:
kubectl get leases)Docs impact
This PR adds inline code comments explaining the sharding behavior. User-facing documentation (e.g., a guide on setting up horizontal scaling) could be added in a follow-up once the approach is reviewed and approved.
Anything reviewers should pay special attention to?
ctrl.NewManager()to supportcache.ByObject. This is a structural change to the startup sequence, though the end result is functionally equivalent.