NestJS course Β· Module 12: Containers and CI/CD
Infrastructure as Code with Terraform
In this lesson5
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.
The plan in a file
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}provider says where we are building - here AWS in the 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.
Four commands
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: terraform plan 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 init is for.
That preview is the only moment you see the tool's intent before it becomes fact. Read it - 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.
A plan written to a file
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.
The state you must not commit
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.
Summary
The architectural plan is ready and the provinces are reproducible:
- Infrastructure as Code is the approach in which infrastructure is defined in code - not a testing framework, not monitoring, not image compression,
.tffiles hold blocks:provider(where),variable(parameters),resource(what should exist),output(what to show at the end),- Terraform derives the order of creation itself, from references between resources,
- four steps:
terraform initβterraform planβterraform applyβterraform output, terraform planis a DRY RUN - it previews changes without applying them; it creates nothing, destroys nothing and downloads no providers,- read the plan, especially the destroys - Terraform may recreate a resource rather than change it in place,
- in a team:
plan -out=tfplan, thenapply tfplan- you deploy exactly what you reviewed, terraform.tfstatestays out of Git: it contains sensitive data and offers no locking for teamwork,- the answer is a remote backend that locks the state for the duration of an operation.
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.
Code for this lesson: src/terraform-iac.ts
1// Infrastructure as Code - Imperium Architectural Plans
2console.log("=== TERRAFORM - INFRASTRUCTURE AS CODE ===\n");
3
4interface TerraformCommand {
5 command: string;
6 description: string;
7 analogy: string;
8}
9
10const commands: TerraformCommand[] = [
11 {
12 command: 'terraform init',
13 description: 'Initialization - fetch providers and modules',
14 analogy: 'Gathering building materials',
15 },
16 {
17 command: 'terraform plan',
18 description: 'Preview of changes (DRY RUN)',
19 analogy: 'Reviewing the architectural plans',
20 },
21 {
22 command: 'terraform apply',
23 description: 'Applying changes to the infrastructure',
24 analogy: 'Starting construction',
25 },
26 {
27 command: 'terraform destroy',
28 description: 'Deletion of the entire infrastructure',
29 analogy: 'Demolishing the building',
30 },
31];
32
33console.log("Terraform commands:\n");
34commands.forEach((c, i) => {
35 console.log(`${i + 1}. ${c.command}`);
36 console.log(` Description: ${c.description}`);
37 console.log(` Analogy: ${c.analogy}\n`);
38});
39
40// Manual vs IaC comparison
41console.log("=== MANUAL vs IaC ===\n");
42const comparison = {
43 'Repeatability': ['Each environment different', 'Identical environments'],
44 'Change history': ['None', 'Full history in Git'],
45 'Code review': ['None', 'The team reviews the infrastructure'],
46 'Disaster recovery': ['Days', 'Minutes'],
47};
48
49console.log("Feature | Manual | IaC (Terraform)");
50console.log("-".repeat(65));
51Object.entries(comparison).forEach(([key, [manual, iac]]) => {
52 console.log(`${key.padEnd(17)}| ${manual.padEnd(20)}| ${iac}`);
53});
54
55// Key resources
56console.log("\n=== TERRAFORM RESOURCES FOR NestJS ===\n");
57const resources = [
58 'aws_instance - EC2 server for API',
59 'aws_docdb_cluster - MongoDB (DocumentDB)',
60 'aws_security_group - firewall',
61 'aws_s3_bucket - state backend',
62];
63resources.forEach(r => console.log(` - ${r}`));
64Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is Infrastructure as Code (IaC)?
2. What does the 'terraform plan' command do?
These are 2 of 8 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Define the AWS provider (region eu-central-1), variables (environment, app_name, app_port), resource aws_instance
- Vertical ordering
Arrange the Terraform workflow steps from first to last:
- Click in order
Arrange the elements of the terraform plan + apply workflow in the correct order:
- Code editor
Mark done=true for all required checklist elements: Docker, CI/CD, Kubernetes, Observability
- Vertical ordering
Arrange the elements of an EC2 resource definition in Terraform (from outer to inner):
- Vertical ordering
Arrange the application deployment steps from start to finish:
- Click in order
Arrange the healthcheck configuration elements in Docker:
- Horizontal ordering
Arrange the elements of the copy instruction from the builder stage:
- Vertical ordering
Arrange secrets providers by priority (from highest):
- Click in order
Arrange the elements of the Kubernetes resource deployment command:
- Horizontal ordering
Arrange the elements of the command to scale a Deployment to 5 replicas: