We use cookies to enhance your experience on the site
CodeWorlds

Infrastructure as Code with Terraform — Architectural Plans of the Empire

The Roman Empire was famous for its architectural plans — every aqueduct, road, and fortification was first designed on paper before construction began. This allowed them to be reproduced in different provinces with identical results. In DevOps, the same philosophy is realized by Infrastructure as Code (IaC) — defining infrastructure in configuration files instead of manually clicking through admin panels.

What is Infrastructure as Code?

IaC is an approach in which infrastructure (servers, databases, networks) is defined in code — versioned, testable, and repeatable:

1// Manual management vs IaC
2const comparison = {
3  manual: {
4    method: 'Clicking in AWS Console / Heroku Dashboard',
5    problems: [
6      'No repeatability — every environment is different',
7      'No change history — who changed what?',
8      'No code review — nobody verifies changes',
9      'Hard to recover after failure',
10    ],
11  },
12  iac: {
13    method: 'Terraform / Pulumi / CloudFormation',
14    benefits: [
15      'Repeatability — identical environments',
16      'Git history — full change history',
17      'Code review — team verifies infrastructure',
18      'Disaster recovery — restoration in minutes',
19    ],
20  },
21};

Terraform — Basics

Terraform (HashiCorp) is the most popular IaC tool. It uses the declarative HCL (HashiCorp Configuration Language):

1# Install Terraform
2brew install terraform
3
4# Initialize project
5terraform init
6
7# Preview changes
8terraform plan
9
10# Apply changes
11terraform apply
12
13# Destroy infrastructure
14terraform destroy

Terraform Lifecycle

1// Terraform workflow
2const terraformWorkflow = {
3  step1_init: 'terraform init — download providers and modules',
4  step2_plan: 'terraform plan — show what will change (DRY RUN)',
5  step3_apply: 'terraform apply — apply changes to infrastructure',
6  step4_destroy: 'terraform destroy — remove all infrastructure',
7};
8
9// Plan is like a "blueprint" — you see changes BEFORE applying them
10// Apply is like "construction" — Terraform creates resources in the cloud

Provider — Service Provider

A Provider is a plugin connecting Terraform to a cloud provider:

1# main.tf — Provider configuration
2terraform {
3  required_providers {
4    aws = {
5      source  = "hashicorp/aws"
6      version = "~> 5.0"
7    }
8  }
9
10  # Backend — where to store state
11  backend "s3" {
12    bucket = "roman-imperium-terraform-state"
13    key    = "production/terraform.tfstate"
14    region = "eu-central-1"
15  }
16}
17
18provider "aws" {
19  region = var.aws_region
20
21  default_tags {
22    tags = {
23      Project     = "roman-imperium"
24      Environment = var.environment
25      ManagedBy   = "terraform"
26    }
27  }
28}

Variables — Terraform Variables

1# variables.tf — Variable definitions
2variable "aws_region" {
3  description = "AWS Region"
4  type        = string
5  default     = "eu-central-1"
6}
7
8variable "environment" {
9  description = "Environment (dev, staging, production)"
10  type        = string
11}
12
13variable "app_name" {
14  description = "Application name"
15  type        = string
16  default     = "roman-imperium-api"
17}
18
19variable "db_instance_class" {
20  description = "Database instance class"
21  type        = string
22  default     = "db.t3.micro"
23}
24
25variable "app_port" {
26  description = "NestJS application port"
27  type        = number
28  default     = 4000
29}

Resources — Creating Infrastructure

Server for NestJS (EC2)

1# compute.tf — Application server
2resource "aws_instance" "api_server" {
3  ami           = "ami-0c55b159cbfafe1f0"  # Amazon Linux 2
4  instance_type = "t3.small"
5
6  vpc_security_group_ids = [aws_security_group.api_sg.id]
7  subnet_id              = aws_subnet.public.id
8
9  user_data = <<-EOF
10    #!/bin/bash
11    yum update -y
12    yum install -y docker
13    systemctl start docker
14    docker pull ${var.app_name}:latest
15    docker run -d -p ${var.app_port}:${var.app_port} ${var.app_name}:latest
16  EOF
17
18  tags = {
19    Name = "${var.app_name}-server"
20  }
21}
22
23resource "aws_security_group" "api_sg" {
24  name        = "${var.app_name}-sg"
25  description = "Security group for NestJS API"
26  vpc_id      = aws_vpc.main.id
27
28  ingress {
29    from_port   = var.app_port
30    to_port     = var.app_port
31    protocol    = "tcp"
32    cidr_blocks = ["0.0.0.0/0"]
33  }
34
35  ingress {
36    from_port   = 443
37    to_port     = 443
38    protocol    = "tcp"
39    cidr_blocks = ["0.0.0.0/0"]
40  }
41
42  egress {
43    from_port   = 0
44    to_port     = 0
45    protocol    = "-1"
46    cidr_blocks = ["0.0.0.0/0"]
47  }
48}

MongoDB Database (DocumentDB)

1# database.tf — Database
2resource "aws_docdb_cluster" "mongodb" {
3  cluster_identifier  = "${var.app_name}-db"
4  engine              = "docdb"
5  master_username     = "imperator"
6  master_password     = var.db_password
7  skip_final_snapshot = var.environment != "production"
8
9  vpc_security_group_ids = [aws_security_group.db_sg.id]
10  db_subnet_group_name   = aws_docdb_subnet_group.db.name
11}
12
13resource "aws_docdb_cluster_instance" "mongodb_instance" {
14  count              = var.environment == "production" ? 2 : 1
15  identifier         = "${var.app_name}-db-${count.index}"
16  cluster_identifier = aws_docdb_cluster.mongodb.id
17  instance_class     = var.db_instance_class
18}

State Management

Terraform stores state — a mapping between configuration and actual infrastructure:

1// Terraform state
2const terraformState = {
3  localState: {
4    file: 'terraform.tfstate',
5    problems: ['Not shared', 'No locking', 'Risk of loss'],
6  },
7  remoteState: {
8    backend: 'S3, Azure Blob, GCS, Terraform Cloud',
9    benefits: ['Shared', 'Locking', 'Versioning'],
10  },
11};
12
13// NEVER commit terraform.tfstate to the repository!
14// It contains sensitive data (passwords, keys)

Modules — Reusable Components

Modules are reusable infrastructure blocks — like prefabricated elements of Roman buildings:

1# modules/nestjs-app/main.tf
2module "nestjs_app" {
3  source = "./modules/nestjs-app"
4
5  app_name        = "roman-imperium-api"
6  environment     = "production"
7  instance_type   = "t3.small"
8  app_port        = 4000
9  db_instance_class = "db.t3.medium"
10}
11
12# The module can be used multiple times
13module "staging_app" {
14  source = "./modules/nestjs-app"
15
16  app_name        = "roman-imperium-api"
17  environment     = "staging"
18  instance_type   = "t3.micro"
19  app_port        = 4000
20  db_instance_class = "db.t3.micro"
21}

Outputs — Results

1# outputs.tf
2output "api_server_ip" {
3  description = "API server IP address"
4  value       = aws_instance.api_server.public_ip
5}
6
7output "database_endpoint" {
8  description = "Database endpoint"
9  value       = aws_docdb_cluster.mongodb.endpoint
10  sensitive   = true
11}
12
13output "app_url" {
14  description = "Application URL"
15  value       = "http://${aws_instance.api_server.public_ip}:${var.app_port}"
16}

Practical Workflow

1# 1. Initialize project
2terraform init
3
4# 2. Check formatting
5terraform fmt
6
7# 3. Validate configuration
8terraform validate
9
10# 4. Preview changes (DRY RUN)
11terraform plan -out=tfplan
12
13# 5. Apply changes
14terraform apply tfplan
15
16# 6. Check results
17terraform output

Terraform is the architectural plans of our Empire — we define infrastructure in code, version it in Git, review it in code reviews, and apply it automatically. Thanks to IaC we can recreate an entire environment in minutes, not days.

Go to CodeWorlds