SnowOps — Cloud-Agnostic Module Pattern
Overview
To enforce D6 (Azure-first, cloud-agnostic by contracts), modules/azure/* consume + emit types from modules/_contracts/ (F0). F0 must be built before any new F-module.
As of v0.17, F0 ships with 7 contracts and both F1 + F6 implement them. See modules/_contracts/README.md.
The 7 Contracts
| Contract | File | Implementers |
|---|---|---|
identity |
modules/_contracts/identity/ |
F1 (emits identity_contract) |
observability |
modules/_contracts/observability/ |
F1 (emits observability_contract), J1 |
network |
modules/_contracts/network/ |
F2 (emits spoke_network_contracts map) |
cluster |
modules/_contracts/cluster/ |
F3 (emits cluster_contract) |
registry |
modules/_contracts/registry/ |
F4 (emits registry_contract) |
kv |
modules/_contracts/kv/ |
F5 (emits kv_contract) |
object_store |
modules/_contracts/object_store/ |
F6 (emits object_store_contract), B4 |
How F0 Works (D19)
Each contract sub-module has:
- variables.tf — a typed variable "candidate" with the expected object shape
- outputs.tf — echoes output "candidate" back out
- versions.tf — Terraform >= 1.6.0, no providers
# modules/_contracts/network/variables.tf
variable "candidate" {
type = object({
id = string
cidr = string
private_subnets = list(string)
public_subnets = list(string)
egress_ips = list(string)
flow_logs_enabled = bool
})
}
# modules/_contracts/network/outputs.tf
output "candidate" {
value = var.candidate
}
Usage pattern — pipe an F-module's output through the contract module to gate shape via terraform validate:
module "spoke_contract_check" {
source = "../../_contracts/network"
candidate = module.network.spoke_network_contracts["apps"]
}
Key: Strict structural check works on LITERAL candidates. When a candidate is wired from un-applied resources (inter-module), Terraform validate is permissive — full apply-time conformance lives in F-module sandbox runbooks. This is documented in modules/_contracts/README.md.
Cross-Field Rules
identity.scope_typeis a closed-set enum (6 values:azure_subscription,azure_management_group,azure_resource_group,aws_account,gcp_project,kubernetes_namespace)object_store.immutability_locked = truerequiresimmutability_enabled = true
F-Module Rules
- No new F-module before F0 is built (F0 sequencing constraint).
- Every F-module that creates a network/identity/observability/cluster/registry/kv/object_store resource must emit the corresponding F0 contract.
- F-modules never call B-modules (D20). B-modules compose F-modules.
- Breaking the future AWS analogue's contract signature requires updating the F0 contract first, then both implementations.
Conformance Tests
The test suite at tests/terratest/modules/azure/contracts_conformance_test.go includes:
- TestF1ContractConformance — identity + observability
- TestF2NetworkContractConformance — spoke_network_contracts map
- TestF3ClusterContractConformance — cluster_contract
- TestF4RegistryContractConformance — registry_contract
- TestF5KVContractConformance — kv_contract
- TestF6ObjectStoreContractConformance — object_store_contract
- TestContractsRejectBadLiterals — 8 subtests proving contracts mechanically reject bad shapes
Map Output Pattern (F2's spoke_network_contracts)
F2 emits a map of network contracts (one per spoke), not a single contract. Consumers pick the spoke they want:
module "aks" {
source = "../aks-secure"
node_subnet_id = module.network.spoke_network_contracts["apps"].private_subnets[0]
network_id = module.network.spoke_network_contracts["apps"].id
}
The adapter in apps/diagram-generator/src/adapt.ts handles map outputs via the same shape-based classification — it expands each map entry and classifies it by the fields its F0 contract guarantees (name-agnostic, shape-based, per D38).