import-unit-granularity
A decision helper for one question: how many Units, and what goes in each? Produces a concrete split — Unit slugs + the --where-resource predicate (or equivalent) for each — and hands off to the import skill that will execute it.
When to use
- User has a YAML bundle, a rendered chart, a namespace full of running resources, or a pile of overlays, and is asking "should this be one Unit or several?"
- User has already picked an import path (
import-from-helm / -kustomize / -argocd / -flux / -cluster) but is unsure how many Units it should produce.
- User is mid-import and wants to validate the split before running the command.
Do not load for
- Authoring new YAML from scratch — use
config-as-data.
- Running the actual import — hand off to the matching
import-from-* skill after the decision is made here.
- Decisions about where the Units go (which Space) — that's
space-topology.
- Cases where the tool already decided for the user:
cub helm install always produces <release> + <release>-crds; cub gitops import always produces dry + wet + crds with the right link predicates. Don't re-litigate their defaults.
Rules, in priority order
1. CRDs always in their own Unit
Independent of everything else, CustomResourceDefinitions go in a Unit separate from anything that uses them. This is not negotiable:
- CRDs must be applied and established before the custom resources that reference them (
kubectl wait --for=condition=established). Separate Units let you sequence apply cleanly without a shell script around one big Unit.
- CRDs have different lifecycle and blast radius than workloads: deleting a CRD cascades to every CR; deleting a Deployment doesn't.
cub helm install and cub gitops import already split CRDs (<release>-crds / -crds wet Unit linked with where-resource "kind = 'CustomResourceDefinition'"). Hand-rolled imports should match.
Slug convention: <app>-crds.
2. Rendered from a generator → keep as one bundle (minus CRDs)
If the source is Helm, Kustomize, Argo, or Flux, the generator already reasoned about what belongs together. Don't re-split unless there's a concrete reason. One workload-Unit + one CRDs-Unit is the default.
Reasons that would justify further splitting from a generator:
- The chart ships with its own cluster-scoped resources (ClusterRole, StorageClass, PriorityClass) mixed with namespaced workloads and you need to grant different permissions — split cluster-scoped into its own Unit.
- The chart bundles an operator and instances of the operator's CRs — split the CRs out so they have a different change cadence.
- The user needs ApplyGates or approval policies that differ across parts of the chart.
Otherwise, one Unit. Run cub helm install or cub gitops import and stop.
3. Hand-rolled or import-from-cluster → split by Kubernetes best practices
When there's no generator to defer to, split by how the resources actually want to be operated. Axes, in order:
- Ownership. Who edits this? Platform team vs. app team. Different owners ⇒ different Units.
- References. Does resource A not work without resource B? They stay together.
- Lifecycle. Does this change daily vs. monthly vs. yearly? Different cadences ⇒ different Units, so low-churn config doesn't generate revision noise.
- Blast radius. If I break this, what else breaks? Contain the blast in a single Unit.
Recommended groupings (for hand-rolled / cluster imports)
| Unit slug |
Contents |
Rationale |
<ns>-namespace |
The Namespace resource itself |
Cluster-scoped; platform-team owned; changes almost never; different lifecycle from anything inside the namespace. |
<ns>-policy |
NetworkPolicy, ServiceAccount, Role, RoleBinding, per-namespace ResourceQuota, LimitRange |
Namespace-scoped policy; usually platform- or platform-plus-app co-owned; changes with policy updates, not app releases. |
<app> |
Deployment (or StatefulSet / DaemonSet), Service, ConfigMap, HorizontalPodAutoscaler, PodDisruptionBudget, ServiceMonitor |
App-team owned; day-to-day change cadence; tightly cross-referenced. Blast radius is the workload. |
<app>-crds |
Any CRDs the app ships |
Lifecycle + apply-order distinct from the workload. |
<infra>-cluster |
ClusterRole, ClusterRoleBinding, StorageClass, PriorityClass, cluster-scoped CRDs |
Cluster-wide blast radius; typically platform-team owned. |
<operator>-crs |
Custom resources (Certificate, HelmRelease, etc.) |
Referenced resources; change independently of the operator itself. |
Multi-workload apps: one <app> Unit per workload (<app>-api, <app>-worker), not one mega-Unit with every workload — they usually have different replica counts, different canary policies, and different incident ownership.
Anti-patterns
- One Unit per resource by default. Only split that far when a specific resource truly has its own lifecycle (e.g., a ConfigMap rotated by a separate process). Otherwise it's noise: twice the revisions, twice the ApplyGates, cross-referenced resources that must be applied in order but live in different Units.
- Everything in one Unit (including CRDs). Apply-order breaks (
CRD not established before CR), rollback of a workload accidentally takes down its CRDs.
- Cluster-scoped mixed with namespaced. Different permissions to apply; different ownership; different blast radius. Splits cleanly along the line cub already draws for
import.include_cluster.
- Splitting purely by
kind. Doesn't map to ownership or lifecycle; produces "all ConfigMaps" or "all Services" Units that cut across unrelated apps.
The splitting trick (for cub unit import and hand rolls)
To execute a split with cub unit import: pre-create each Unit, bind it to the same cluster Target, then call cub unit import per Unit with a scoped --where-resource. This is exactly what cub gitops import does internally when it splits CRDs off the wet Unit.
Example for splitting a namespace's state into three Units:
SPACE=<app>-<env>
TARGET=<workers-space>/<cluster-target>
NS=<namespace>
# Shell
for u in <ns>-namespace <ns>-policy <app> <app>-crds; do
cub unit create --space "$SPACE" "$u"
cub unit set-target --space "$SPACE" "$u" "$TARGET"
done
# CRDs (cluster-scoped; needs include_cluster)
cub unit import --space "$SPACE" <app>-crds \
--where-resource "kind = 'CustomResourceDefinition' AND import.include_cluster = true AND metadata.labels.app = '<app>'" \
--dry-run
# Namespace (cluster-scoped; platform-team owned).
cub unit import --space "$SPACE" <ns>-namespace \
--where-resource "kind = 'Namespace' AND metadata.name = '$NS' AND import.include_cluster = true" \
--dry-run
# Namespace-scoped policy (NetworkPolicy, RBAC, ResourceQuota, LimitRange).
# `--where-resource` supports AND only — if you'd want Namespace + policy in a
# single Unit, do two imports into separate Units rather than trying to OR.
cub unit import --space "$SPACE" <ns>-policy \
--where-resource "metadata.namespace = '$NS' AND kind IN ('NetworkPolicy','ServiceAccount','Role','RoleBinding','ResourceQuota','LimitRange')" \
--dry-run
# Workload
cub unit import --space "$SPACE" <app> \
--where-resource "metadata.namespace = '$NS' AND kind IN ('Deployment','StatefulSet','DaemonSet','Service','ConfigMap','HorizontalPodAutoscaler','PodDisruptionBudget','ServiceMonitor') AND metadata.labels.app = '<app>'" \
--dry-run
Dry-run each, confirm the resource set, then re-run without --dry-run. Same pattern for Helm-rendered bundles split by hand: store the output of helm template in files, then cub unit create each from its scoped file. (Though unless you have a specific reason, cub helm install already does CRD splitting correctly — prefer it.)
The decision flow
- What's the source? → Helm / Kustomize / ArgoCD / Flux / live cluster / hand-rolled YAML.
- If a generator, default to
<release> + <release>-crds (Helm) or dry/wet/crds (Argo/Flux) per the matching import skill. Only split further if ownership / lifecycle / blast radius diverge within the render.
- If hand-rolled or cluster-imported: walk the four axes (ownership, references, lifecycle, blast radius). Map to the recommended groupings above. Produce slugs and
--where-resource predicates.
- Hand off to the right
import-from-* skill for execution.
Preflight (for making a recommendation)
- User has told you the source (Helm / Kustomize / ArgoCD / Flux / cluster / hand-rolled). If not, ask once.
- For cluster / hand-rolled cases:
kubectl get -n <ns> -o yaml > /tmp/inventory.yaml or a similar inventory is available for reference — you need to see what's actually there before prescribing a split.
- User has picked a Space layout per
space-topology (or will as part of this). Units don't exist in a vacuum.
Tool boundary
Read-only and decision-making only. kubectl get, cub ... list/get to inspect inventory. No cub unit create / import / update — hand that off to the specialized import skill after the decision is made.
Stop conditions
- User hasn't disclosed the source. Ask; don't guess.
- User is importing generator-rendered output and wants a per-resource split without a stated reason. Push back: match the generator's bundle + CRD split.
- User is hand-rolling and insists on one mega-Unit including CRDs. Push back: CRD separation is non-negotiable for apply-order correctness.
Verify chain
There's nothing to apply here; the skill's output is a split proposal. The user (or the import skill called next) verifies by running --dry-run and reviewing the per-Unit resource set before importing.
Evidence
kubectl get -n <ns> --show-labels — source inventory the recommendation was based on.
- The matching import skill's Evidence section once execution starts.
References
references/cub-cli.md — --where-resource / --where-data scoping mechanics (including ConfigHub.ResourceType, ConfigHub.ResourceName, import.include_system, import.include_cluster, import.include_custom).
https://docs.confighub.com/markdown/guide/rendered-manifests.md — the cub gitops import splitting flow this skill mirrors.
https://docs.confighub.com/markdown/guide/helm-charts.md — cub helm install release + crds default.
- Companion skills:
space-topology (where the Units go), import-from-helm, import-from-kustomize, import-from-argocd, import-from-flux, import-from-cluster, config-as-data (post-import doctrine).
Source: confighub/confighub-skills — distributed by TomeVault.
1---2name: import-unit-granularity3description: Use when the user is deciding how to slice Kubernetes YAML into ConfigHub Units — phrases like "one Unit or many?", "how should I split these resources?", "should Deployment and Service be one Unit?", "where do CRDs go?", "Unit per resource or per bundle?", "should my namespace and its RBAC be together?", "per-app or per-namespace?". Applies a short set of rules (CRDs separate; rendered-from-generator stays bundled; otherwise split by ownership / references / lifecycle / blast radius) and routes the user to the right import skill with a concrete Unit-slug plan. Do not load for authoring new YAML (use `config-as-data`), for executing an import the user has already scoped (use the matching `import-from-*` skill), or when the split is already forced by the tool in use (`cub helm install` and `cub gitops import` split CRDs automatically — just run them).4---56# import-unit-granularity78A decision helper for one question: **how many Units, and what goes in each?** Produces a concrete split — Unit slugs + the `--where-resource` predicate (or equivalent) for each — and hands off to the import skill that will execute it.910## When to use1112- User has a YAML bundle, a rendered chart, a namespace full of running resources, or a pile of overlays, and is asking "should this be one Unit or several?"13- User has already picked an import path (`import-from-helm` / `-kustomize` / `-argocd` / `-flux` / `-cluster`) but is unsure how many Units it should produce.14- User is mid-import and wants to validate the split before running the command.1516## Do not load for1718- Authoring new YAML from scratch — use `config-as-data`.19- Running the actual import — hand off to the matching `import-from-*` skill after the decision is made here.20- Decisions about *where* the Units go (which Space) — that's `space-topology`.21- Cases where the tool already decided for the user: `cub helm install` always produces `<release>` + `<release>-crds`; `cub gitops import` always produces dry + wet + crds with the right link predicates. Don't re-litigate their defaults.2223## Rules, in priority order2425### 1. CRDs always in their own Unit2627Independent of everything else, CustomResourceDefinitions go in a Unit separate from anything that uses them. This is not negotiable:2829- CRDs must be applied and established before the custom resources that reference them (`kubectl wait --for=condition=established`). Separate Units let you sequence apply cleanly without a shell script around one big Unit.30- CRDs have different lifecycle and blast radius than workloads: deleting a CRD cascades to every CR; deleting a Deployment doesn't.31- `cub helm install` and `cub gitops import` already split CRDs (`<release>-crds` / `-crds` wet Unit linked with `where-resource "kind = 'CustomResourceDefinition'"`). Hand-rolled imports should match.3233Slug convention: `<app>-crds`.3435### 2. Rendered from a generator → keep as one bundle (minus CRDs)3637If the source is Helm, Kustomize, Argo, or Flux, the generator already reasoned about what belongs together. Don't re-split unless there's a concrete reason. One workload-Unit + one CRDs-Unit is the default.3839Reasons that *would* justify further splitting from a generator:4041- The chart ships with its own cluster-scoped resources (ClusterRole, StorageClass, PriorityClass) mixed with namespaced workloads and you need to grant different permissions — split cluster-scoped into its own Unit.42- The chart bundles an operator *and* instances of the operator's CRs — split the CRs out so they have a different change cadence.43- The user needs ApplyGates or approval policies that differ across parts of the chart.4445Otherwise, one Unit. Run `cub helm install` or `cub gitops import` and stop.4647### 3. Hand-rolled or `import-from-cluster` → split by Kubernetes best practices4849When there's no generator to defer to, split by how the resources actually want to be operated. Axes, in order:5051- **Ownership.** Who edits this? Platform team vs. app team. Different owners ⇒ different Units.52- **References.** Does resource A not work without resource B? They stay together.53- **Lifecycle.** Does this change daily vs. monthly vs. yearly? Different cadences ⇒ different Units, so low-churn config doesn't generate revision noise.54- **Blast radius.** If I break this, what else breaks? Contain the blast in a single Unit.5556## Recommended groupings (for hand-rolled / cluster imports)5758| Unit slug | Contents | Rationale |59|---|---|---|60| `<ns>-namespace` | The `Namespace` resource itself | Cluster-scoped; platform-team owned; changes almost never; different lifecycle from anything inside the namespace. |61| `<ns>-policy` | `NetworkPolicy`, `ServiceAccount`, `Role`, `RoleBinding`, per-namespace `ResourceQuota`, `LimitRange` | Namespace-scoped policy; usually platform- or platform-plus-app co-owned; changes with policy updates, not app releases. |62| `<app>` | `Deployment` (or `StatefulSet` / `DaemonSet`), `Service`, `ConfigMap`, `HorizontalPodAutoscaler`, `PodDisruptionBudget`, `ServiceMonitor` | App-team owned; day-to-day change cadence; tightly cross-referenced. Blast radius is the workload. |63| `<app>-crds` | Any CRDs the app ships | Lifecycle + apply-order distinct from the workload. |64| `<infra>-cluster` | `ClusterRole`, `ClusterRoleBinding`, `StorageClass`, `PriorityClass`, cluster-scoped `CRDs` | Cluster-wide blast radius; typically platform-team owned. |65| `<operator>-crs` | Custom resources (`Certificate`, `HelmRelease`, etc.) | Referenced resources; change independently of the operator itself. |6667Multi-workload apps: one `<app>` Unit per workload (`<app>-api`, `<app>-worker`), not one mega-Unit with every workload — they usually have different replica counts, different canary policies, and different incident ownership.6869## Anti-patterns7071- **One Unit per resource by default.** Only split that far when a specific resource truly has its own lifecycle (e.g., a ConfigMap rotated by a separate process). Otherwise it's noise: twice the revisions, twice the ApplyGates, cross-referenced resources that must be applied in order but live in different Units.72- **Everything in one Unit (including CRDs).** Apply-order breaks (`CRD` not established before `CR`), rollback of a workload accidentally takes down its CRDs.73- **Cluster-scoped mixed with namespaced.** Different permissions to apply; different ownership; different blast radius. Splits cleanly along the line cub already draws for `import.include_cluster`.74- **Splitting purely by `kind`.** Doesn't map to ownership or lifecycle; produces "all ConfigMaps" or "all Services" Units that cut across unrelated apps.7576## The splitting trick (for `cub unit import` and hand rolls)7778To execute a split with `cub unit import`: pre-create each Unit, bind it to the same cluster Target, then call `cub unit import` per Unit with a scoped `--where-resource`. This is exactly what `cub gitops import` does internally when it splits CRDs off the wet Unit.7980Example for splitting a namespace's state into three Units:8182```bash83SPACE=<app>-<env>84TARGET=<workers-space>/<cluster-target>85NS=<namespace>8687# Shell88for u in <ns>-namespace <ns>-policy <app> <app>-crds; do89 cub unit create --space "$SPACE" "$u"90 cub unit set-target --space "$SPACE" "$u" "$TARGET"91done9293# CRDs (cluster-scoped; needs include_cluster)94cub unit import --space "$SPACE" <app>-crds \95 --where-resource "kind = 'CustomResourceDefinition' AND import.include_cluster = true AND metadata.labels.app = '<app>'" \96 --dry-run9798# Namespace (cluster-scoped; platform-team owned).99cub unit import --space "$SPACE" <ns>-namespace \100 --where-resource "kind = 'Namespace' AND metadata.name = '$NS' AND import.include_cluster = true" \101 --dry-run102103# Namespace-scoped policy (NetworkPolicy, RBAC, ResourceQuota, LimitRange).104# `--where-resource` supports AND only — if you'd want Namespace + policy in a105# single Unit, do two imports into separate Units rather than trying to OR.106cub unit import --space "$SPACE" <ns>-policy \107 --where-resource "metadata.namespace = '$NS' AND kind IN ('NetworkPolicy','ServiceAccount','Role','RoleBinding','ResourceQuota','LimitRange')" \108 --dry-run109110# Workload111cub unit import --space "$SPACE" <app> \112 --where-resource "metadata.namespace = '$NS' AND kind IN ('Deployment','StatefulSet','DaemonSet','Service','ConfigMap','HorizontalPodAutoscaler','PodDisruptionBudget','ServiceMonitor') AND metadata.labels.app = '<app>'" \113 --dry-run114```115116Dry-run each, confirm the resource set, then re-run without `--dry-run`. Same pattern for Helm-rendered bundles split by hand: store the output of `helm template` in files, then `cub unit create` each from its scoped file. (Though unless you have a specific reason, `cub helm install` already does CRD splitting correctly — prefer it.)117118## The decision flow1191201. What's the source? → Helm / Kustomize / ArgoCD / Flux / live cluster / hand-rolled YAML.1212. If a generator, default to `<release>` + `<release>-crds` (Helm) or dry/wet/crds (Argo/Flux) per the matching import skill. Only split further if ownership / lifecycle / blast radius diverge within the render.1223. If hand-rolled or cluster-imported: walk the four axes (ownership, references, lifecycle, blast radius). Map to the recommended groupings above. Produce slugs and `--where-resource` predicates.1234. Hand off to the right `import-from-*` skill for execution.124125## Preflight (for making a recommendation)1261271. User has told you the source (Helm / Kustomize / ArgoCD / Flux / cluster / hand-rolled). If not, ask once.1282. For cluster / hand-rolled cases: `kubectl get -n <ns> -o yaml > /tmp/inventory.yaml` or a similar inventory is available for reference — you need to see what's actually there before prescribing a split.1293. User has picked a Space layout per `space-topology` (or will as part of this). Units don't exist in a vacuum.130131## Tool boundary132133Read-only and decision-making only. `kubectl get`, `cub ... list/get` to inspect inventory. No `cub unit create / import / update` — hand that off to the specialized import skill after the decision is made.134135## Stop conditions136137- User hasn't disclosed the source. Ask; don't guess.138- User is importing generator-rendered output and wants a per-resource split without a stated reason. Push back: match the generator's bundle + CRD split.139- User is hand-rolling and insists on one mega-Unit including CRDs. Push back: CRD separation is non-negotiable for apply-order correctness.140141## Verify chain142143There's nothing to apply here; the skill's output is a split proposal. The user (or the import skill called next) verifies by running `--dry-run` and reviewing the per-Unit resource set before importing.144145## Evidence146147- `kubectl get -n <ns> --show-labels` — source inventory the recommendation was based on.148- The matching import skill's Evidence section once execution starts.149150## References151152- `references/cub-cli.md` — `--where-resource` / `--where-data` scoping mechanics (including `ConfigHub.ResourceType`, `ConfigHub.ResourceName`, `import.include_system`, `import.include_cluster`, `import.include_custom`).153- `https://docs.confighub.com/markdown/guide/rendered-manifests.md` — the `cub gitops import` splitting flow this skill mirrors.154- `https://docs.confighub.com/markdown/guide/helm-charts.md` — `cub helm install` release + crds default.155- Companion skills: `space-topology` (where the Units go), `import-from-helm`, `import-from-kustomize`, `import-from-argocd`, `import-from-flux`, `import-from-cluster`, `config-as-data` (post-import doctrine).156157---158> Source: [confighub/confighub-skills](https://github.com/confighub/confighub-skills) — distributed by [TomeVault](https://tomevault.io).159<!-- tomevault:4.0:skill_md:2026-05-22 -->