Direct answer: Design Terraform modules for Kubernetes so root modules show platform composition, target selection, module versions, and high-level dependency flow—while child modules encode stable platform decisions behind narrow, honest interfaces.
The hardest module decision in a Kubernetes platform is rarely “how do we remove repetition?” It’s how do we prevent reusable Terraform abstractions from hiding the operational behavior people need during risky changes—upgrades, rollbacks, incidents, and security reviews.
This guide is for platform engineers and DevOps engineers who build shared Kubernetes infrastructure in Terraform: composing root modules, authoring reusable child modules, and defining platform conventions that multiple teams rely on. The focus is on module boundaries that support reviewability (what can be understood from a Terraform plan) and operability (what can be reasoned about when things go wrong).
Practical note: This is a practitioner-oriented decision framework. It’s not a measured study of incidents, drift, costs, or review latency.
Direct answer: A Terraform module boundary is an intent boundary. It should separate what’s essential to review from what can be safely encapsulated, while keeping dependency direction and operational ownership understandable.
In Kubernetes platform engineering, module boundaries matter because:
To make this concrete, remember the two common abstractions people mix up:
They are related, but not the same thing. A child module boundary is about design clarity and interface honesty; a state boundary is about operational control. Treat both as intentional, separate decisions.
Direct answer: Codebases drift because reuse pressures teams to extract “shared patterns,” but Kubernetes platform layers often have different lifecycles, owners, blast radii, and dependency directions.
Most Kubernetes Terraform platforms start with a root module that’s readable:
Then reality arrives:
That extraction can help—but it can also erase review intent. If a reviewer has to open multiple nested modules to understand what is changing, why it’s risky, and who owns the outcome, the abstraction is likely too opaque for platform work.
The goal is not “keep everything explicit forever.” The goal is to design Terraform modules for Kubernetes so reuse doesn’t erase operational meaning.
Direct answer: Two common boundary failure modes are (1) root modules that become a thin wrapper around a monolithic child module, and (2) root modules that duplicate the same patterns everywhere and become hard to maintain safely.
Kubernetes platform infrastructure usually includes layers with different lifecycles. Here’s a practical taxonomy:
| Area | Common examples | Why module boundaries get difficult |
|---|---|---|
| Foundation | Network prerequisites, identity prerequisites, DNS/certificate prerequisites (where applicable) | Often needed before cluster creation; typically reviewed by different owners |
| Cluster | Managed cluster/control plane configuration | Lifecycle may involve upgrade/replacement constraints and provider initialization timing |
| Capacity | Node pools, node groups, compute pools | Scaling/upgrades often differ from cluster lifecycle |
| Access | IAM/RBAC bindings, service accounts, identity mapping | Security-sensitive; often subject to stricter review |
| Add-ons | Ingress, storage integration, observability, policy, autoscaling | In-cluster dependencies may differ from foundational infrastructure dependencies |
| Workload-specific platform components | Team add-ons, environment-specific controllers | Variation may be high and ownership may be distributed |
Direct answer: If the root module becomes “just one call,” reviewers can’t tell from the call site which layers and risks are involved.
Example anti-pattern (illustrative):
module "platform_cluster" {
source = "git::ssh://example.invalid/platform-cluster.git?ref=v1.2.3"
environment = "prod"
region = "example-region"
size = "large"
}
Even if the child module is well-written, the review surface shrinks: reviewers may need to inspect child modules (and nested modules) to understand whether a change touches networking, identity, cluster settings, node pools, add-ons, access rules, or provider wiring.
Direct answer: If environments duplicate logic in root modules, you risk maintenance divergence and inconsistencies—not because reuse is bad, but because boundaries and conventions aren’t enforced.
Example pattern duplication (illustrative):
# Pseudocode: illustrative only
resource "example_network_attachment" "cluster" {
name = "${var.environment}-cluster"
environment = var.environment
}
resource "example_cluster" "this" {
name = "${var.environment}-cluster"
network_id = example_network_attachment.cluster.id
}
resource "example_node_pool" "default" {
cluster_id = example_cluster.this.id
profile = var.default_capacity_profile
}
resource "example_addon" "observability" {
cluster_id = example_cluster.this.id
enabled = true
}
The useful middle ground is not “abstract more.” It’s:
Abstract only where the boundary preserves reviewability, dependency clarity, and operational ownership.
Direct answer: Before you extract or standardize a boundary, run the LOBD test: Lifecycle, Ownership, Blast radius, and Dependency direction. If those differ, the boundary likely hides important operational behavior.
This heuristic doesn’t prove a design is better in all cases. It gives reviewers vocabulary for deciding whether reuse is hiding coupling.
| Dimension | Review question | If the answer is “no” or “unclear” |
|---|---|---|
| Lifecycle | Do these resources change, upgrade, replace, and roll back together? | Keep separate modules or compose explicitly in the root. |
| Ownership | Does one team own support, versioning, docs, migration, and incident response? | Don’t publish as a shared platform interface until ownership is clear. |
| Blast radius | Can a reviewer identify what could break from the root module and plan? | Move intent, dependency flow, or risky configuration closer to the root. |
| Dependency direction | Are upstream and downstream contracts visible through inputs and outputs? | Prefer explicit output/input contracts over hidden nested dependencies. |
| Interface honesty | Do inputs represent platform choices, not just renamed provider fields? | Use provider resources directly, define profiles, or delay abstraction. |
| Output contract | Are outputs stable downstream contracts, or leaked internals? | Replace broad outputs like everything with contract-shaped outputs. |
| Security review | Are IAM/RBAC, credentials, access paths, and network reachability reviewed? | Treat access examples and real configuration as security-sensitive before reuse. |
| Exceptions | Are overrides rare and documented? | If exceptions are the main path, the abstraction is likely premature. |
Direct answer: If upgrades, replacements, or rollback procedures differ, group them separately or expose them via root composition.
Example insight: if node pool scaling follows a different process than an ingress/controller upgrade, hiding both behind one abstraction can make plans harder to reason about.
Direct answer: If approval and operations responsibilities differ, boundaries should reflect that. A shared module without an owner becomes shared risk.
For Kubernetes infrastructure, ownership often crosses technical layers: identity/network may require security review; add-ons might be owned by platform, observability, security, or workload teams. When in doubt, treat real access patterns and permissions as security-sensitive.
Direct answer: If failure modes differ, they shouldn’t be coupled merely because the code resembles each other.
Direct answer: Prefer boundaries that keep dependency flow visible. Data-flow contracts (inputs/outputs) are usually more reviewable than hidden ordering inside nested modules.
Direct answer: Name the alternatives before selecting a pattern. Each layout optimizes for something—and fails when Kubernetes layer lifecycles don’t match.
Below are four frequently seen Terraform modules for Kubernetes layouts. Use them as mental models, not prescriptions.
Direct answer: Root modules declare most resources directly, maximizing plan-time visibility.
Good fit when:
Breaks when:
Summary: Optimizes for visibility. It does not optimize for convention enforcement.
Direct answer: The root module calls one child module that creates most layers, optimizing for a simple call site.
module "platform_cluster" {
source = "../modules/platform-cluster"
environment = var.environment
region = var.region
profile = var.profile
}
Good fit when:
Breaks when:
Summary: The call site looks clean; lifecycle coupling can be invisible until reviewers dig in.
Direct answer: Split by technical categories (network, IAM, cluster, node pools, Helm add-ons, RBAC) rather than operational lifecycle.
modules/
network/
iam/
cluster/
node-pool/
helm-addons/
kubernetes-rbac/
Good fit when:
Breaks when:
Summary: Better than a monolith, but may not align with operability.
Direct answer: Root modules compose layers that map to lifecycle, ownership, blast radius, and dependency direction.
modules/
foundation-network/
identity-prereqs/
cluster/
capacity/
access/
core-addons/
workload-addons/
Good fit when:
Breaks when:
Recommendation: This framework favors starting from lifecycle and ownership boundaries with explicit root composition—unless the platform is small enough that repetition is cheaper than abstraction.
Direct answer: A good module design helps a reviewer answer operational questions without spelunking through every nested child module.
Design each Terraform modules for Kubernetes boundary around what reviewers need to know:
This is why root modules and child modules have different responsibilities.
| Concern | Prefer root module visibility when… | Prefer child module encapsulation when… |
|---|---|---|
| Platform composition | Reviewers need to see which layers are included. | The layer’s internal implementation is stable and owned. |
| Module versions | Shared modules are upgraded independently. | Internal resources remain hidden behind a versioned contract. |
| Provider target selection | A module can affect different clusters/accounts/environments. | Provider wiring is fixed and not operationally meaningful to callers. |
| Dependency flow | Ordering affects safe review or apply behavior. | Dependencies are internal and do not affect caller reasoning. |
| Security-sensitive access | IAM/RBAC, credentials, cluster access, network reachability are involved. | The module exposes a narrow, reviewed access contract. |
| Naming, labels, defaults | Exceptions need visibility. | Defaults encode a documented platform convention. |
| Add-ons | Add-on lifecycle differs from cluster/foundation lifecycle. | Add-on installation pattern is stable and owned. |
| Capacity | Scaling/replacement needs separate review from cluster creation. | Capacity profiles are standardized and explicit. |
Tip: For readers who like adjacent review-first frameworks, see the internal post Boundaries are your friends. It complements this boundary-and-review model.
Direct answer: A Kubernetes Terraform root module should answer: what is being changed, which layers are included, which versions are used, and how dependencies flow.
Root modules can be concise without becoming empty. The goal is readable composition, not “no code.”
A provider-neutral root module might look like this (illustrative):
# Pseudocode: provider-neutral illustration only.
# Module source/version pinning style depends on your registry/VCS convention.
module "foundation" {
source = "../modules/foundation"
environment = var.environment
region = var.region
}
module "identity_prereqs" {
source = "../modules/identity-prereqs"
environment = var.environment
}
module "cluster" {
source = "../modules/cluster"
environment = var.environment
region = var.region
network_contract = module.foundation.network_contract
identity_contract = module.identity_prereqs.cluster_identity_contract
}
module "capacity" {
source = "../modules/capacity"
cluster_contract = module.cluster.cluster_contract
capacity_profile = var.capacity_profile
}
module "access" {
source = "../modules/access"
cluster_contract = module.cluster.cluster_contract
identity_contract = module.identity_prereqs.access_identity_contract
}
module "core_addons" {
source = "../modules/core-addons"
cluster_contract = module.cluster.cluster_contract
access_contract = module.access.addon_access_contract
addon_profile = var.core_addon_profile
}
What matters is that a reviewer can see the story:
| Concern | Why keep it visible? |
|---|---|
| Environment or cluster identity | Reviewers need to know what target is changing. |
| Major platform layers | Shows what’s included in this root module. |
| Module versions or pins | Makes shared module upgrades explicit. |
| Provider aliases or target selection | Helps reviewers understand where resources apply. |
| High-level dependency flow | Prevents hidden ordering assumptions. |
| Intentional exceptions | Makes deviations reviewable instead of buried in maps. |
Design reminder: Avoid burying dependency sequencing in deeply nested child modules when ordering is essential to safe review. (This is a design recommendation, not a universal rule.)
Direct answer: Use child modules to package stable decisions. Avoid child modules that simply rename provider arguments or expose dozens of unrelated knobs.
A child module is valuable when it captures repeated, stable platform choices.
Examples of reasonable responsibilities:
A child module is less useful when it simply renames provider fields and turns every operational decision into a variable for callers to handle.
Direct answer: If every provider argument becomes a variable, you may be preserving complexity rather than reducing it.
# Pseudocode: illustrative anti-pattern
module "node_pool" {
source = "../modules/node-pool"
name = var.name
cluster_id = var.cluster_id
min_size = var.min_size
max_size = var.max_size
instance_type = var.instance_type
disk_size = var.disk_size
labels = var.labels
taints = var.taints
update_strategy = var.update_strategy
replacement_policy = var.replacement_policy
image_type = var.image_type
runtime_config = var.runtime_config
provider_specific_flags = var.provider_specific_flags
}
This might still be useful as a compatibility wrapper, but it doesn’t encode much platform intent. If the module interface is not meaningfully smaller than the implementation, the abstraction may not be pulling its weight.
Direct answer: Make callers choose from profiles and contracts, not raw provider fields.
# Pseudocode: provider-neutral illustration only
module "capacity" {
source = "../modules/capacity"
cluster_contract = module.cluster.cluster_contract
pools = {
default = {
profile = "general"
size = "medium"
}
system = {
profile = "system"
size = "small"
}
}
allowed_exception_ids = var.capacity_exception_ids
}
Here, the interface makes a platform decision visible: callers choose from approved profiles. Exceptions are still possible, but they are named and reviewable.
This only works if profiles remain stable. If every consumer needs a unique override pattern, the abstraction is probably premature.
| Copy this | Don’t copy this |
|---|---|
Inputs that describe platform choices: addon_profile = "baseline" |
Inputs that mirror every provider field without adding intent. |
Typed contract objects: cluster_contract = object({ name = string, endpoint_ref = string, auth_context_ref = string }) |
Unstructured bags: settings = any. |
Explicit root composition showing foundation → cluster → capacity/access/add-ons |
A root module that hides all layers behind a single large module call. |
Contract-shaped outputs like addon_access_contract |
Broad outputs like everything = resource.this. |
| Documented escape hatches with named exception IDs | Generic override maps that become the primary usage path. |
depends_on with a comment explaining the real ordering constraint |
depends_on everywhere to force imperative ordering. |
Direct answer: When the module owns a decision, encode it in a typed interface with validation and clear error messages.
variable "addon_profile" {
description = "Named add-on profile approved for this platform root."
type = string
validation {
condition = contains(["baseline", "restricted", "observability-only"], var.addon_profile)
error_message = "addon_profile must be one of: baseline, restricted, observability-only."
}
}
Use objects for cohesive concepts:
variable "cluster_contract" {
description = "Outputs from the cluster layer required by downstream modules."
type = object({
name = string
endpoint_ref = string
auth_context_ref = string
})
}
Avoid one giant any input:
# Pseudocode: avoid this shape unless there is a strong reason
variable "settings" {
type = any
}
The problem isn’t objects vs primitives. The problem is hiding unrelated changes behind an unstructured interface.
Direct answer: Outputs should say what downstream modules need—not leak the entire internal resource graph.
output "cluster_contract" {
description = "Minimal cluster information required by capacity, access, and add-on layers."
value = {
name = local.cluster_name
endpoint_ref = local.endpoint_ref
auth_context_ref = local.auth_context_ref
}
}
A useful output contract answers:
Review signal: If operators routinely need to open the child module to understand a plan, your abstraction may be too opaque. Treat that as a design feedback signal, not an automatic failure.
Direct answer: Split Terraform modules for Kubernetes into layers that align with how changes roll out, who owns the change, and how dependencies flow.
| Layer | Typical contents | Likely lifecycle | Boundary guidance |
|---|---|---|---|
| Foundation | Network prerequisites, identity prerequisites, DNS/cert prerequisites where applicable | Changes less frequently; may require broader review | Keep separate from in-cluster add-ons when ownership/credentials differ |
| Cluster | Control plane or managed cluster resource, cluster contract outputs | Upgrades/replacements need careful review | Avoid mixing add-on lifecycle into cluster layer |
| Capacity | Node pools, node groups, compute pools | Scaling/replacement may be more frequent | Keep capacity changes reviewable without changing foundation |
| Access | IAM/RBAC, service accounts, identity mapping | Security-sensitive; may have separate approval | Document ownership and review expectations |
| Core add-ons | Ingress, storage integration, observability, policy, autoscaling | Often upgraded independently | Depend on cluster/access contracts; avoid owning foundational resources |
| Workload add-ons | Team/environment-specific controllers | Highly variable | Keep explicit until patterns stabilize |
Important: This table is a design taxonomy for boundaries, not provider-specific implementation instructions. Cloud-provider specifics require separate sourcing and review.
Direct answer: If a dependency can be expressed via outputs/inputs, do that. Reviewers can understand it from the module boundaries.
module "cluster" {
source = "../modules/cluster"
network_contract = module.foundation.network_contract
identity_contract = module.identity_prereqs.cluster_identity_contract
}
module "core_addons" {
source = "../modules/core-addons"
cluster_contract = module.cluster.cluster_contract
access_contract = module.access.addon_access_contract
}
This doesn’t mean every dependency must be exposed as a giant object. It means important dependency direction should be visible at the composition layer.
depends_on only to document real ordering constraintsDirect answer: Use depends_on intentionally when Terraform can’t infer the ordering from references, and explain the reason.
module "core_addons" {
source = "../modules/core-addons"
cluster_contract = module.cluster.cluster_contract
access_contract = module.access.addon_access_contract
# Use only when the dependency is real and not inferable from inputs.
# Explain why this ordering matters.
depends_on = [
module.capacity
]
}
Do not use depends_on as a general scripting mechanism. When provider-specific behavior matters, verify behavior against official documentation or a Terraform SME before publishing.
Direct answer: Define a “dependency contract” for each module layer so reviewers know what the module requires, creates, exports, and must not manage.
A child module dependency contract should clearly state:
You can make this readable in the module’s README:
## Dependency contract
### Requires
- `network_contract` from the foundation layer
- `cluster_contract` from the cluster layer
- Provider alias targeting the intended cluster/environment
### Creates
- Baseline add-on resources for the selected `addon_profile`
### Exports
- `addon_status_contract`
- `addon_identity_refs`
### Must not manage
- Cluster creation
- Node pools
- Foundational network resources
- Broad access bindings outside the add-on scope
Direct answer: If a module can target different clusters or accounts, make the targeting explicit at the root call site.
Illustrative (provider-neutral):
# Pseudocode: provider-neutral illustration only
provider "example_kubernetes" {
alias = "target"
config_ref = module.cluster.cluster_contract.auth_context_ref
}
module "core_addons" {
source = "../modules/core-addons"
providers = {
example_kubernetes = example_kubernetes.target
}
cluster_contract = module.cluster.cluster_contract
addon_profile = var.core_addon_profile
}
Credential patterns and real provider configuration details are sensitive. Don’t publish real endpoints, accounts, or access flows without review.
Direct answer: If a data source hides when values exist, it can obscure dependency direction. Prefer outputs + inputs when possible.
Review question:
Would a reviewer understand when this data is available and which resource owns it?
If the answer is “no,” prefer an explicit output from the producing module and an input to the consuming module.
Direct answer: Don’t extract child modules just because code looks repetitive. Extract when the abstraction reduces review ambiguity and encodes stable decisions with a smaller interface.
Use this decision tree before standardizing an abstraction:
Start: two or more Terraform resources look repetitive.
1. Is the platform decision stable?
- No → Keep explicit for now.
- Yes → Continue.
2. Do the resources share lifecycle, ownership, blast radius, and dependency direction?
- No → Split by boundaries or keep root composition explicit.
- Yes → Continue.
3. Will the module interface be meaningfully smaller than the implementation?
- No → It may just rename provider arguments. Use resources directly or rethink the boundary.
- Yes → Continue.
4. Can downstream consumers depend on a documented output contract instead of internals?
- No → Define the contract before publishing.
- Yes → Continue.
5. Is there an owner for versioning, docs, migration, and support?
- No → Do not standardize yet.
- Yes → A child module is probably reasonable.
Key point: This is a heuristic. It doesn’t guarantee improvements like fewer incidents, less drift, or shorter review time. It helps you build boundaries that are more likely to remain reviewable as systems grow.
Tradeoff mindset: Accept some repetition when the alternative is an interface no one can reason about.
Direct answer: Boundary smells usually show up as review ambiguity: reviewers can’t identify risk, ownership, or dependency direction without opening internals.
| Smell | Why it matters | Better option |
|---|---|---|
Root module is one large platform_cluster call |
Reviewers cannot see lifecycle, ownership, or dependency boundaries at the call site. | Compose foundation, cluster, capacity, access, and add-ons explicitly in the root. |
| Child module exposes every provider argument | The module may not encode a platform decision. | Use provider resources directly or define named profiles and contracts. |
Inputs are mostly any, map(any), or one large settings object |
Unrelated changes become hard to review. | Use typed variables and cohesive objects. |
| Outputs expose whole resources | Downstream modules start depending on internals. | Export minimal, documented contracts. |
| Add-ons manage foundational infrastructure | Different lifecycles and credentials may be coupled. | Keep add-ons dependent on foundation and cluster contracts. |
depends_on is used as general sequencing |
Terraform code starts behaving like a script. | Use data-flow references first; explain depends_on only when needed. |
| Operators inspect internals for routine plan review | The abstraction hides operational intent. | Move important intent to the root or narrow the child module. |
| Exceptions are common | The platform decision isn’t stable. | Delay abstraction, split the module, or create explicit variants. |
Direct answer: A map(any) input can exist, but it should be an escape hatch—never the primary interface.
variable "extra_settings" {
type = map(any)
description = "Additional settings."
}
If overrides become the normal usage path, your module isn’t encoding platform decisions—it’s delegating complexity to every consumer.
Direct answer: Defaults are useful when they encode documented conventions. They are risky when they create broad permissions, network reachability, or production topology effects without explicit caller intent.
Direct answer: Avoid outputs like “everything,” because they make downstream modules depend on implementation details.
# Pseudocode: avoid exposing internals by default
output "everything" {
value = example_cluster.this
}
Prefer contract-shaped outputs:
output "addon_access_contract" {
description = "Identity references approved for add-on installation."
value = {
service_account_ref = local.service_account_ref
role_ref = local.role_ref
}
}
Mark security-sensitive outputs as sensitive where appropriate, and review real examples before standardizing.
Direct answer: Before publishing shared Terraform modules for Kubernetes, run a light design review focused on boundaries, not aesthetics.
Exercise for reviewers:
From the root module alone, identify the likely blast radius of this change.
If reviewers can’t identify the risk without opening internals, the composition may be too hidden.
Also run representative plans for more than one environment or cluster variant:
The goal is to see whether the module handles variation without hiding important changes. This is recommended practice, not an evidence-backed performance claim.
If you want a broader operating-model perspective on how teams structure review and responsibility, you may also like the internal post Human Review in AI Workflows: When to Decide, Escalate, or Stop. While it’s about AI workflows, it reinforces the same review-safety mindset: make the “stop and escalate” points explicit.
Direct answer: If you want to validate whether new Terraform module boundaries improve operability, define measurements before and after the refactor—don’t guess after the fact.
Here are examples of validation questions you can turn into measurements (choose what fits your org):
| Question | Possible measurement idea |
|---|---|
| Are modules actually reused? | Consumer count by module, environment, or team. |
| Are interfaces becoming too broad? | Input count, output count, and number of any-typed variables. |
| Are plans easier to review? | PR review latency or qualitative “unclear blast radius” feedback themes. |
| Are applies failing less often? | Failed apply count by root module or stack. |
| Is drift changing? | Drift findings by module or environment. |
| Are upgrades manageable? | Module version lag across consumers. |
| Is onboarding affected? | Time for a new platform engineer to complete defined Terraform tasks. |
| Are dependency issues visible? | Events linked to dependency ordering confusion. |
Reminder: Define terms before collecting data. “Reuse,” “failed apply,” “drift,” and “onboarding complete” can mean different things across teams.
Direct answer: This framework is designed to help you reason about module boundaries for reviewability. It does not promise measurable outcomes like fewer incidents or lower cloud spend.
What it is meant to do:
Intended outcomes (not guaranteed):
Direct answer: The recommended boundary approach costs more upfront design. It can be unnecessary for very small platforms, but it becomes more valuable as multiple teams, environments, and risk domains grow.
A root module that calls foundation, cluster, capacity, access, and add-ons is longer than a single platform_cluster call.
That verbosity may be worthwhile when reviewability matters. It may be unnecessary for a small platform with one owner and few variants.
Narrow modules often require:
If no one owns those responsibilities, fewer modules may be better.
Separating foundation, cluster, capacity, access, and add-ons can raise state and apply-order questions. Some teams keep layers in one root module and one state; others split state boundaries to match ownership, locking, recovery, permissions, or blast radius.
This guide doesn’t prescribe a universal state layout. Treat state boundaries as operational decisions.
If a platform decision isn’t stable, a narrow module may not fit all consumers. You may need to keep some configuration explicit until profiles and contracts settle.
That’s not failure. It’s often better than forcing variation through generic maps and any-typed variables.
Provider aliases, target selection, and dependency contracts can make root modules look less “abstract.” But they make review targets more obvious—the central tradeoff is polish at the call site versus operational visibility at review time.
Direct answer: The right boundary for Terraform modules for Kubernetes is rarely the one that removes the most lines of code. It’s the one that makes lifecycle, ownership, blast radius, and dependency direction easier to reason about at plan time.
A reusable module is valuable when it encodes a stable platform decision behind a clear, narrow interface. It becomes harmful when it hides behavior operators need during changes or incidents.
Practical next step: pick one existing Kubernetes platform module and apply the boundary LOBD test. Then ask:
If answers are unclear, don’t start by adding another layer of abstraction. Start by making the boundary honest.
Reuse is a tool. Reviewability is the constraint.
A module boundary is a code design boundary (inputs/outputs and encapsulation). A state boundary changes operational behavior such as locking, permissions, state recovery, and apply blast radius. They are related but not the same decision.
Don’t extract just because two files look similar. Use the LOBD test (Lifecycle, Ownership, Blast radius, Dependency direction) and check whether the module interface is meaningfully smaller than the implementation, with contract-shaped inputs/outputs.
Usually no for platform-critical Kubernetes infrastructure. A thin wrapper can hide which layers and risks are changing. Root modules should show platform composition and high-level dependency flow so reviewers can understand blast radius from the plan.
An honest interface encodes stable platform choices (often via profiles and typed variables), supports validation where the module owns decisions, and exports narrow downstream contracts instead of leaking internals.
depends_on in Terraform modules for Kubernetes?Use depends_on only when Terraform can’t infer ordering from references/inputs/outputs, and document the real ordering constraint. Avoid using it broadly to turn Terraform into an imperative script.
A contract-shaped output is what downstream modules need, expressed as stable values and references (often structured objects). It’s narrower than “expose everything” and aims to prevent downstream coupling to the child module’s internal resource graph.
Use boundaries that keep access and security-sensitive contracts reviewable: keep key decisions and dependency flow visible in the root module, design narrow access contracts for child modules, and treat IAM/RBAC and network reachability configuration as security-sensitive during review.
Founder & CEO
A practical primer on Kubernetes as a desired-state control system: what pods, deployments, services, ingress, config, secrets, autoscaling, namespaces, and cluster operations actually do, and what they do not do.
A practical, workflow-first guide to Docker and Kubernetes that explains images, registries, runtimes, deployment automation, and the boundaries that keep container systems understandable and secure.
A cloud-native development operating model turns delivery into a paved road for the common case—automated, observable, and governed—while keeping exceptions explicit and reviewable.
Platform engineering vs DevOps isn’t either/or. Use the right operating model for the bottleneck you actually have—ownership and feedback loops for DevOps, and self-service to reduce delivery toil for platform engineering.