Imagine you've set up a server in the cloud: you clicked the right checkboxes in the web console, created a database, opened a couple of ports. Everything works. A month later you need to spin up an exact copy for testing — and you no longer remember which checkboxes you clicked. Sound familiar? This is exactly the pain that Infrastructure as Code (IaC for short) solves.

The idea is simple: instead of configuring the cloud by hand, you describe the desired infrastructure in text files — and a special program creates everything from that description. You can put these files in git, share them with a colleague for review, roll them back to a previous version. No more "what did I click" — there's code, and code is always right.

What Infrastructure as Code is

Infrastructure is everything your application runs on: virtual machines, databases, networks, storage, access rules. In the past all of this was created manually through a control panel or a series of commands. The IaC approach moves it into files that are read by both a human and a machine.

An analogy: a recipe for a dish. You can cook "by eye" and get a slightly different result every time, or you can write down an exact recipe — grams, minutes, temperature. With a recipe, both another person and you a year later can reproduce the dish. An infrastructure file is exactly such a recipe for the cloud.

What it gives you in practice:

  • Reproducibility. With the same file you deploy identical environments for development, testing and production.
  • Change history in git. You can see who added a database, when and why — plain git log.
  • Review. An infrastructure change goes through a pull request, like any code.
  • Rollback. Something broke — you go back to the last working version of the file.
  • No more "forgot what I clicked": there's a single source of truth — the file.

Background on the cloud itself is in the article on AWS fundamentals.

Declarative vs imperative

This is the main fork in the IaC world, and it's important to understand it from the very start. There are two ways to describe what you want.

Imperative — you describe HOW, step by step: "create a server, then attach a disk to it, then open port 80." It's a sequence of commands. It's like a navigator dictating every turn: "in 200 meters, turn right."

Declarative — you describe WHAT you want as the end result: "there must be a server with a disk and open port 80." The tool itself then decides which steps are needed to bring reality to that state. It's like telling a driver the address — and they figure out the route themselves.

Most popular IaC tools are declarative. Terraform and CloudFormation work exactly this way. For example, in Terraform using the HCL language, you describe the final state — a storage bucket in the cloud:

resource "aws_s3_bucket" "example" {
  bucket = "my-unique-bucket-name"

  tags = {
    Name        = "My bucket"
    Environment = "Dev"
  }
}

The same thing in CloudFormation looks like a YAML template:

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  ExampleBucket:
    Type: AWS::S3::Bucket
    Properties:
      Tags:
        - Key: Name
          Value: My bucket

Notice: neither one has the words "create", "then", "if it already exists — skip it." You just described that the bucket must exist with these tags. Whether to create it or modify an existing one is decided by the tool itself.

CDK (Cloud Development Kit) stands apart. You write it like a regular program — in TypeScript, Python, Java or other languages. But that doesn't make it imperative: CDK code doesn't create resources directly, it synthesizes a declarative CloudFormation template, which is then applied. In other words, CDK is a convenient wrapper that gives you a familiar programming language on top of a declarative engine:

import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export class MyStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    new s3.Bucket(this, 'MyFirstBucket');
  }
}

The cdk synth command turns this code into a CloudFormation template (which you can view in a human-readable form as YAML), and cdk deploy applies it. A detailed breakdown of each tool is in the articles on Terraform, CloudFormation and CDK.

What state is

For a declarative tool to understand what needs to change, it needs a memory. This memory is called state — it's a "snapshot" of what the tool knows about the real infrastructure: which resources it created, with which settings and identifiers.

An analogy: a shopping list with checkmarks. You came to the store with a list (that's your code — what you want) and you mark off what's already in your cart (that's the state — what already exists). By comparing one with the other, you know exactly what else to buy.

When you run terraform plan, Terraform does exactly this: it compares your code (the desired state) with the state (what, according to its records, has already been created) and shows the difference — what it will add, what it will change, what it will delete. Only after terraform apply are the changes actually applied.

terraform plan
terraform apply

In CloudFormation, the role of state is played by the stack itself on the AWS side — the cloud stores which resources belong to this template. In Terraform, the state is a separate file (terraform.tfstate), which in team work is kept not on a laptop but in a shared place, for example in object storage, so that everyone has one version of the truth.

Idempotency in plain words

The scary word idempotency actually means a very clear thing: running it again with no changes to the code breaks nothing and duplicates nothing.

An analogy: a light switch labeled "off" instead of "toggle." No matter how many times you press "off" — the light stays off. But if the button were "toggle," each press would change the state, and the result would depend on the number of presses.

That's exactly how declarative tools behave. You described "there must be one storage bucket." You ran it — the bucket was created. You ran the same code again without changing it — the tool compares the code with the state, sees that everything is already in place, and does nothing. A second bucket won't appear, nothing will break.

terraform apply
terraform apply

The second apply on unchanged code will finish with a message like "0 added, 0 changed, 0 destroyed." That's idempotency in action — and it's precisely why IaC is safe to run even every day: it won't do anything extra.

What drift is

Now imagine: one of your colleagues went into the web console and changed a setting by hand — for example, switched the server type. The code still says one thing, but in reality it's already something else. This discrepancy is called drift: reality has diverged from the code.

Drift is dangerous because it's invisible. The files look the same as before, but the cloud works differently. And on the next apply, the tool may "roll back" the manual change to what's written in the code — and break what the colleague fixed by hand.

That's why tools have a way to detect drift. In Terraform it's a run that only reconciles reality with the state, changing nothing in the infrastructure:

terraform apply -refresh-only

Terraform will go to the cloud, look at the actual state of the resources, and report: "objects have changed outside of Terraform." In CloudFormation there's a separate command for the same purpose to detect drift:

aws cloudformation detect-stack-drift --stack-name my-stack

Fixing drift is simple in principle: either bring reality back to the code (re-apply the template), or make the manual change in the code so it reflects the new desired state. The main rule that saves your nerves: change infrastructure through code, not by hand in the console — then drift simply has nowhere to come from.

Where this is used

Infrastructure as Code is the foundation on which almost all modern cloud operations stand. A few typical places where you'll run into it:

  • Automated deployment. IaC is a natural part of pipelines: infrastructure code is applied just as automatically as application code. See pipeline principles and release strategies.
  • Identical environments. Development, testing and production are spun up from the same files — they differ only in parameters.
  • Kubernetes. Kubernetes manifests are also a declarative description of the desired state, the same "describe WHAT, not HOW" philosophy. It's worth starting with Kubernetes fundamentals and deployment and configuration.

Typical beginner mistakes that are easy to avoid if you remember the material above:

  • Editing infrastructure by hand "real quick" — and getting drift, which later surfaces at the worst possible moment. Any change goes through code.
  • Losing or not sharing the state. In a team the state must live in a shared place, otherwise two developers will accidentally overwrite each other's changes.
  • Being afraid to run apply again. Thanks to idempotency, re-running on unchanged code is safe — there's nothing to fear.
  • Confusing imperative and declarative and trying to write "step by step" where the tool expects a description of the final state.

What to learn next: take one tool and go through it end to end — for example, Terraform as the most universal, CloudFormation as native to AWS, or CDK if a regular programming language is closer to you. Then look at how state and delivery work in team settings in the article on state and delivery, and how it all ties together with automated releases in the section on branching and releases.