Infrastructure as Code

What is Infrastructure as Code?

This post summarizes Infrastructure as Code (2nd edition) by Kief Morris, focusing on practical patterns and principles for managing infrastructure.

Infrastructure as Code (IaC) treats infrastructure configuration as software—using version-controlled definition files instead of manual configuration. Teams apply software development practices like peer review, automated testing, and continuous integration to infrastructure management. Cloud providers’ APIs make this possible by exposing programmatic interfaces for resource provisioning.

The Speed vs Quality Fallacy

Organizations often fall into two failure modes when managing infrastructure. Some implement heavy change control processes that accumulate over time, creating overhead so significant that teams avoid changes. Technical debt builds as systems stagnate. Others optimize for speed without systematic approaches, creating infrastructure that functions but requires tribal knowledge to modify safely.

High-performing organizations achieve both speed and quality through automation. The DORA metrics validate this: delivery lead time, deployment frequency, change fail percentage, and mean time to restore. Teams excelling in these metrics share common IaC practices.

graph TD
    subgraph "Traditional Approaches"
        A[Manual Infrastructure] --> B{Choose Priority}
        B -->|Speed| C[Move Fast<br/>Break Things]
        B -->|Quality| D[Heavy Process<br/>Slow Changes]
        C --> E[Technical Debt<br/>Tribal Knowledge]
        D --> F[Stagnation<br/>Process Overhead]
    end

    subgraph "Infrastructure as Code"
        G[IaC Practices] --> H[Automation]
        H --> I[Speed + Quality]
        I --> J[High Performance<br/>DORA Metrics]
    end

Core Practices

Define everything as code transforms infrastructure into reviewable, testable artifacts. This extends beyond servers to networks, security policies, and monitoring rules. Teams gain version control history, peer review capabilities, and self-documenting infrastructure.

Continuously test and deliver prevents untested modifications from accumulating. Small, frequent changes are easier to understand and debug than large batches. Automated testing catches errors early, and failures remain limited in scope.

Build small, independent pieces simplifies development and operations. Monolithic definitions become unwieldy as they grow. Small components with clear interfaces evolve independently and enable reuse across projects.

Design Principles

Assume systems are unreliable. At scale, failures are statistical certainties. Design for resilience through retry logic, circuit breakers, and graceful degradation rather than attempting to prevent all failures.

Make everything reproducible. When any component can be recreated from its definition, experimentation becomes safe. Teams test in isolated environments and roll back by redeploying previous configurations.

Create disposable resources. Treat infrastructure as cattle, not pets—identical and replaceable. Apply patches by replacing instances rather than modifying running systems.

Minimize variation. Supporting multiple configurations multiplies operational complexity. Standardize where possible, even if not optimal for every use case.

Infrastructure Stacks

A stack is a collection of resources managed as a unit—like load balancers, servers, and databases for a web application. Tools like Terraform, CloudFormation, and Pulumi handle dependency resolution and lifecycle management.

graph TB
    subgraph "Monolithic Stack (Antipattern)"
        M[Single Stack Instance]
        M --> MA[App A Resources]
        M --> MB[App B Resources]
        M --> MC[App C Resources]
        M --> MD[Shared Infrastructure]
    end

    subgraph "Service Stack Pattern"
        SA[App A Stack]
        SB[App B Stack]
        SC[App C Stack]
        SD[Shared Stack]
        SA --> SD
        SB --> SD
        SC --> SD
    end

Stack Antipatterns

Monolithic stack places too many resources together. Changes become risky, testing time-consuming, and teams hesitant to modify anything. The blast radius encompasses everything.

Multi-environment stack manages all environments in one definition. Development changes can break production, and testing in isolation becomes impossible.

Copy-paste environments duplicate definitions for each environment. This leads to drift as updates aren’t propagated consistently.

graph LR
    subgraph "Multi-Environment Stack (Antipattern)"
        Stack[Single Stack Definition]
        Stack --> Dev[Dev Resources]
        Stack --> Test[Test Resources]
        Stack --> Prod[Prod Resources]
        Change[Change to Dev] -.->|Risk| Prod
    end

    subgraph "Reusable Stack Pattern"
        Template[Stack Template]
        Template -->|params: dev.tfvars| DevStack[Dev Stack Instance]
        Template -->|params: test.tfvars| TestStack[Test Stack Instance]
        Template -->|params: prod.tfvars| ProdStack[Prod Stack Instance]
    end

Stack Patterns

Application group stack collects related services owned by one team, aligning stack and team boundaries.

Service stack gives each service its own infrastructure definition, matching microservice architectures.

Micro stack decomposes further based on change frequency or operational characteristics.

Reusable stack uses a single parameterized definition for all environments. Production might specify larger instances while development uses smaller ones.

Configuration Patterns

flowchart TD
    subgraph "Configuration Flow"
        Code[Infrastructure Code]

        Vars[Environment Variables]
        Files[Config Files]
        Pipeline[CI/CD Pipeline]
        Registry[Parameter Registry]

        Vars --> Code
        Files --> Code
        Pipeline --> Code
        Registry --> Code

        Code --> Stack[Stack Instance]
    end

Manual parameters invite human error and block automation. Avoid typing values on command lines.

Stack environment variables provide programmatic configuration that integrates with CI/CD systems.

Stack configuration files externalize parameters into version-controlled artifacts, enabling review and history tracking.

Wrapper stacks treat infrastructure modules as libraries, with each environment importing and configuring shared components.

Pipeline parameters leverage CI/CD systems to manage configuration, though this creates pipeline dependencies.

Parameter registry centralizes configuration in systems like Consul or databases, adding flexibility but operational complexity.

Testing Infrastructure

Declarative definitions limit unit testing value—verifying that a template declares 2 CPUs merely restates the declaration. Integration testing provides real validation by provisioning resources and verifying behavior.

Focus integration tests on high-risk areas: security boundaries, data persistence, and external integrations. Balance thorough testing with feedback speed by carefully selecting what to test.

Operational Changes

Infrastructure as Code shifts operations from manual troubleshooting to software practices. Version control identifies recent changes, rollbacks restore known configurations, and test environments enable safe reproduction of issues.

The infrastructure code becomes living documentation. Traditional runbooks give way to automated procedures, with comments and commit messages explaining architectural decisions.

Summary

Infrastructure as Code enables both speed and quality through systematic automation. Small, versioned, testable components provide manageable infrastructure. Choose patterns that separate concerns while avoiding unnecessary coupling or duplication. The practice transforms infrastructure management from manual processes to predictable, repeatable operations.