We close the testing chapter with a more advanced idea: contract testing between modules. When you build infrastructure by composing modules that connect to each other (remember Chapter 18), you need to ensure that those connections still fit even as the modules evolve. Contract testing protects those "connection points." It's a subtler concept, but understanding it will help you design better modules.

Reminder: modules connect through their interfaces

In Chapter 18 we saw that modules have a contract: some inputs (variables) and some outputs. And we saw that they are composed by connecting the output of one module to the input of another (subchapter 18.2):

   "network" module                    "servers" module
   output: id_subnet  ──────────►  input: id_subnet

That connection is a contract between the two modules: the servers module trusts that the network module will provide it with a valid id_subnet. As long as that contract is respected, everything fits.

The problem: contracts can be broken

Here's the risk. Imagine someone modifies the network module and, without realizing the impact, changes the name or format of its output id_subnet (for example, renames it to subnet_id, or changes what it returns). The network module, by itself, still "works"... but the servers module that depended on that output breaks, because it no longer finds what it expected.

Before:  network module  →  output "id_subnet"  →  servers module ✓ fits
Change:  network module  →  output "subnet_id"  →  servers module ✗ broken!
        (the contract changed without notice)

This type of failure is treacherous: the modified module seems correct in isolation, but it has broken other modules that depended on it. And in a large organization, there may be many modules depending on just one (remember the shared modules from subchapter 18.4).

What contract testing is

Contract testing verifies that the interface between two modules—the "contract" of inputs and outputs—remains stable and compatible. Instead of testing the entire infrastructure, it focuses on the connection points: it checks that a module still offers the outputs others expect, with the correct name and format.

Contract test: "Does the 'network' module still offer an output 'id_subnet'
                with the format the 'servers' module expects?"
   → YES → the contract is respected ✓
   → NO → the contract is broken, must warn before merging ✗

Analogy: a contract between modules is like a plug and socket. The socket (the output of a module) has a standard shape, and the plug (the input of another) fits into it. Contract testing checks that no one has changed the shape of the socket: if someone modifies it, the devices that depended on it would no longer fit. You verify the "socket," not the whole device.

How it's done in practice

Contract testing between modules is not a single tool with a magic button; it's more of an approach that combines several practices you already know:

  1. Interface-focused tests

You write tests (for example, with Terratest from subchapter 21.3) that specifically verify that the outputs of a module exist and have the expected format. You don't test the whole infrastructure, just the "contract."

  1. Strict module versioning

This is where the versioning from subchapter 18.4 shines. If you change a module's interface (its inputs or outputs) in an incompatible way, that's a major change (increase the MAJOR version, e.g., from v1.x to v2.0). That way, modules that depended on it don't update automatically and continue using the compatible version until they consciously adapt.

Compatible change (add a new output)  → MINOR version (v1.2 → v1.3)
Incompatible change (remove/rename output) → MAJOR version (v1.x → v2.0)

  1. Integration tests between modules in CI

In CI (subchapter 21.1), when someone changes a shared module, you can run tests that combine that module with those that use it, to confirm they still fit before merging the change.

Why it matters, especially at scale

In a small project with few modules, contracts rarely break and it's easy to detect. But in a large organization (which we'll see in Part VII, with internal platforms and modules shared by many teams), a single base module can be used by dozens of projects. Breaking its contract without warning would cause cascading failures throughout the company.

Real-world example: the platform team maintains a corporate-network module used by 30 teams. A developer wants to improve it and, in the process, rename an output to make it "clearer." Without contract testing, that change would break all 30 projects. With contract testing and strict versioning, CI detects that the interface is being changed incompatibly and forces her to publish it as a major version (v2.0). The 30 teams stay on v1.x safely, and migrate to v2.0 when they can, adapting their code. The change improves the module without breaking anyone.

The underlying idea: take care of interfaces

The key message of this subchapter, beyond the tools, is a mindset: when you design reusable modules, their interfaces (inputs and outputs) are a sacred contract. Others rely on them. Changing them lightly breaks those who depend on you. Contract testing and versioning are the tools that protect those contracts, allowing modules to evolve without causing chaos.

What you should remember

  • Modules connect through their interfaces (outputs of one → inputs of another); that connection is a contract that some modules trust.
  • If someone changes a module's interface (renames or alters an output), they can break other modules that depended on it, even if the modified module "works" in isolation. It's a treacherous failure, especially at scale.
  • Contract testing verifies that the interface between modules remains stable and compatible, focusing on the connection points (like checking that "the plug hasn't changed shape").
  • It's achieved by combining: interface-focused tests (Terratest), strict versioning (an incompatible change is a MAJOR version, subchap. 18.4), and integration tests between modules in CI.
  • It matters especially at scale, where a base module is used by many teams. The key mindset: your modules' interfaces are a contract you must take care of.

You've finished Chapter 21! You now know how to ensure the quality and security of your infrastructure at all levels. In Chapter 22 we'll bring all this together in a complete automated flow: Terraform in CI/CD, from linting to automated deployment.

Cloud, AWS & Terraform — From Zero to Expert

Chapter 1 · What is cloud computing

Chapter 2 · The cloud market and major providers

Chapter 3 · Regions, availability zones and edge

Chapter 4 · Compute: EC2

Chapter 5 · Storage: S3

Chapter 6 · Networking: VPC

Chapter 7 · Identity and access: IAM

Chapter 8 · Managed databases

Chapter 9 · Why Infrastructure as Code

Chapter 10 · HCL: the Terraform language

Chapter 11 · Providers and state

Chapter 12 · Your first real infrastructure in Terraform

Chapter 13 · Load balancing and auto scaling

Chapter 14 · Serverless with Lambda

Chapter 15 · Messaging and events

Chapter 16 · Content delivery and DNS

Chapter 17 · Containers on AWS

Chapter 18 · Modules: reuse and composition

Chapter 19 · Workspaces and environment management

Chapter 20 · Remote backends and locking

Chapter 21 · Infrastructure testing

Chapter 22 · Terraform in CI/CD

Chapter 23 · Defense in depth

Chapter 24 · Observability: logs, metrics and traces

Chapter 25 · Cost optimization

Chapter 26 · High availability and disaster recovery

Chapter 27 · AWS Well-Architected Framework

Chapter 28 · Serverless architectures at scale

Chapter 29 · Data platforms on AWS

Chapter 30 · Multi-account and landing zones

Chapter 31 · Platform Engineering and Internal Developer Platform

Chapter 32 · Relevant AWS certifications

Chapter 33 · Projects to consolidate what you've learned

Chapter 34 · Resources and community

© Copyright 2024. All rights reserved