The production server was set up a year ago by clicking around the provider's console. It works. But nobody remembers exactly which machine size was chosen, which ports were opened, or whether the disk is 50 or 100 gigabytes. Standing up an identical test environment is now a few hours of guesswork - and it will still come out different.
The Romans did not build aqueducts from memory. Each began with an architectural plan: a drawing from which any crew could reproduce the same structure in another province. Infrastructure as Code is the same idea: infrastructure defined in code, not by clicking.
Do not confuse it with tools that sound similar but do something else: it is not a framework for testing infrastructure, not a server monitoring system, and not a tool for compressing Docker images. It is a way of describing what should exist.
Terraform reads files with the
.tf extension. A description is made of a few kinds of block:1provider "aws" {
2 region = "eu-central-1"
3}
4
5variable "app_name" {
6 type = string
7 default = "legion-api"
8}
9
10variable "app_port" {
11 type = number
12 default = 3000
13}
14
15resource "aws_instance" "legion_server" {
16 ami = "ami-0abcdef1234567890"
17 instance_type = "t3.micro"
18
19 tags = {
20 Name = var.app_name
21 }
22}
23
24output "server_ip" {
25 value = aws_instance.legion_server.public_ip
26}
says where we are building - here AWS in the provider
eu-central-1 region. variable is a parameter of the plan; thanks to it the same description can stand up a test and a production environment differing only in values. resource describes a thing that should exist - a machine, a network, a database. output names what Terraform should show at the end; an IP address is usually known only after the server is created.Note how references work:
var.app_name reaches for a variable, and aws_instance.legion_server.public_ip for a field of a created resource. From these references Terraform derives the order of creation itself: since the output needs the machine's address, the machine must come first.Working with Terraform is always the same sequence:
terraform init - initialise the project; it downloads the providers named in the files.terraform plan - a preview of the changes without applying them.terraform apply - apply the changes.terraform output - check the results.The second step is the heart of the tool, so let's name it outright:
is a DRY RUN. It compares your description with the actual state and lists what it intends to create, change and destroy - touching nothing. It does not create resources, does not destroy infrastructure and does not download providers; downloading is what terraform plan
init is for.That preview is the only moment you see the tool's intent before it becomes fact. Read it, @name - especially the lines beginning with a minus, because Terraform may decide a change cannot be made in place and destroy a resource in order to recreate it. On a test machine that is an inconvenience; on a database with data it is a disaster.
In a team and in CI a two-stage variant is used, so you can be sure you deploy exactly what you reviewed:
1terraform init
2terraform plan -out=tfplan
3terraform apply tfplan
4terraform output-out=tfplan writes the computed plan to a file. apply tfplan applies that exact plan instead of computing a new one.The difference matters when minutes pass between review and approval and somebody changes the infrastructure meanwhile. A plain
apply would recompute the plan and do something other than what you read on screen.Terraform has to remember what it already created - otherwise every run would build everything from scratch. It records that in a file named
.terraform.tfstate
That file does not belong in a Git repository, for two reasons at once. First, it contains sensitive data - database passwords, keys, addresses - written in plain text, because Terraform must remember them between runs. Second, Git provides no locking for teamwork: when two people run
apply at the same time, two versions of the state appear and a conflict arises that no merge can settle, because this is not a file meant for humans to read.The reasons sometimes offered - that the file is too big for Git, or that Terraform deletes it after
apply - are simply untrue.The answer is a remote backend: a shared place that holds the state and locks it for the duration of an operation.
1terraform {
2 backend "s3" {
3 bucket = "imperium-terraform-state"
4 key = "legion-api/terraform.tfstate"
5 region = "eu-central-1"
6 dynamodb_table = "terraform-locks"
7 }
8}The table named in
dynamodb_table is what provides the lock - a second person running apply sees a message that the state is busy, instead of breaking shared infrastructure.The architectural plan is ready and the provinces are reproducible:
.tf files hold blocks: provider (where), variable (parameters), resource (what should exist), output (what to show at the end),terraform init → terraform plan → terraform apply → terraform output,terraform plan is a DRY RUN - it previews changes without applying them; it creates nothing, destroys nothing and downloads no providers,plan -out=tfplan, then apply tfplan - you deploy exactly what you reviewed,terraform.tfstate stays out of Git: it contains sensitive data and offers no locking for teamwork,In the next lesson you will gather this whole module into one project - containerisation, pipeline and deployment. For now remember: clicking in a console creates a server nobody can reproduce; a plan in a file creates one anybody can.