Stage 4 · Provision
Modules & Composition
Module Interface Design
Input types, validation blocks, optional attributes, outputs, and compatibility contracts — designing modules that scale.
Interface Principles
A module interface is its contract with consumers. A well-designed interface is minimal, stable, and self-documenting. Poor interfaces break consumers on every update and create confusion about what values to pass.
- Minimal inputs — only expose what consumers need to customize.
- Sensible defaults — most inputs should have safe default values.
- Clear types — use specific types, not string for everything.
- Stable outputs — once an output is published, changing it breaks consumers.
- Descriptive names — variable names should explain their purpose without comments.
Input Design
variable "name" {
description = "Name prefix for all resources"
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]{1,61}[a-z0-9]$", var.name))
error_message = "Name must be lowercase alphanumeric, 3-63 characters."
}
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "Must be a valid CIDR block."
}
}
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod."
}
}Each variable has a description, specific type, and validation. The validation blocks reject invalid inputs at plan time. Sensible defaults make most inputs optional.
Validation Rules
variable "instance_count" {
type = number
validation {
condition = var.instance_count >= 1 && var.instance_count <= 10
error_message = "Must be between 1 and 10 instances."
}
}
variable "allowed_cidrs" {
type = list(string)
validation {
condition = alltrue([for cidr in var.allowed_cidrs : can(cidrhost(cidr, 0))])
error_message = "All values must be valid CIDR blocks."
}
}
variable "tags" {
type = map(string)
default = {}
validation {
condition = contains(keys(var.tags), "Environment")
error_message = "Environment tag is required."
}
}Validation rules use can() for safe error handling. alltrue() checks all items in a list. contains() checks for required keys. These patterns catch misconfigurations before API calls.
Optional Attributes
variable "cluster_config" {
description = "EKS cluster configuration"
type = object({
name = string
version = string
enabled = optional(bool, true)
log_types = optional(list(string), ["api", "audit", "authenticator"])
vpc_config = optional(object({
subnet_ids = list(string)
security_group_ids = list(string)
}), null)
})
}The optional() function provides default values for object attributes. Consumers can omit optional attributes. This keeps the interface flexible while maintaining type safety.
Output Design
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "IDs of private subnets"
value = aws_subnet.private[*].id
}
output "nat_gateway_ip" {
description = "Public IP of the NAT gateway"
value = aws_eip.nat.public_ip
}
output "connection_info" {
description = "Connection information for the cluster"
value = {
cluster_name = aws_eks_cluster.main.name
cluster_endpoint = aws_eks_cluster.main.endpoint
cluster_ca = aws_eks_cluster.main.certificate_authority[0].data
}
sensitive = true
}Each output has a description and returns a specific type. Sensitive outputs are marked. Output names should be clear and follow a consistent naming convention.
Compatibility Contracts
- Never remove a published output — deprecate it first.
- Never change an output type — add a new output instead.
- New optional variables should have defaults — do not break existing callers.
- Changing variable types requires a major version bump.
- Document all changes in a CHANGELOG.
When adding a new optional attribute to an object variable, use optional() with a default value. This allows existing callers to continue working without changes.
Removing an output breaks every module that references it. Deprecate outputs by adding a comment and removing them in the next major version. Never remove outputs in a patch or minor release.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.