Skip to content

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_type is a closed-set enum (6 values: azure_subscription, azure_management_group, azure_resource_group, aws_account, gcp_project, kubernetes_namespace)
  • object_store.immutability_locked = true requires immutability_enabled = true

F-Module Rules

  1. No new F-module before F0 is built (F0 sequencing constraint).
  2. Every F-module that creates a network/identity/observability/cluster/registry/kv/object_store resource must emit the corresponding F0 contract.
  3. F-modules never call B-modules (D20). B-modules compose F-modules.
  4. 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).