Back to Blog
•
ByteGurus

Infrastructure as Code with Terraform: A Guide for Modern DevOps

Learn how to manage cloud infrastructure as code using Terraform. Discover best practices, real-world examples, and why IaC is essential for modern development.

Terraform IaC DevOps Cloud

Managing infrastructure by hand has become not just inefficient but dangerous. Infrastructure as Code (IaC) turns infrastructure into reviewed, repeatable code. This guide shows how to start with Terraform and keep it healthy as a team.

A licensing note: in 2023 HashiCorp moved Terraform from the open-source MPL to the BSL, and the Linux Foundation’s OpenTofu fork followed. Both use the same language and providers; the examples below also run with tofu.

What is Infrastructure as Code?

Infrastructure as Code is the practice of managing and provisioning computing infrastructure through machine-readable definition files, rather than physical hardware configuration or interactive configuration tools. Instead of clicking through cloud consoles, you write code that describes your infrastructure needs.

Think of it this way: your infrastructure becomes versioned, reproducible, and auditable—just like your application code. This shift fundamentally changes how teams approach cloud architecture and DevOps practices.

Why Terraform?

While several IaC tools exist, Terraform has gained significant traction for compelling reasons:

Multi-Cloud Support: Terraform works across AWS, Azure, Google Cloud, and many other providers. This flexibility prevents vendor lock-in and enables multi-cloud strategies.

Declarative Syntax: With Terraform’s HashiCorp Configuration Language (HCL), you declare what you want, not how to build it. The tool figures out the execution steps.

State Management: Terraform maintains a state file tracking your infrastructure’s current state, enabling intelligent diffing and updates.

Community and Ecosystem: The extensive module registry and active community mean you rarely start from scratch.

Getting Started with Terraform

Installation and Setup

Installing Terraform is straightforward. On macOS:

brew install hashicorp/tap/terraform   # or: brew install opentofu

On Linux or Windows, visit terraform.io for platform-specific instructions. Verify installation:

terraform version

Your First Configuration

Let’s create a simple AWS infrastructure example. Start with a main.tf file:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  tags = {
    Name = "ByteGurus-WebServer"
  }
}

This simple configuration:

  • Specifies the AWS provider
  • Defines an EC2 instance resource
  • Tags it for easy identification

Deploy it with:

terraform init
terraform plan
terraform apply

Real-World Use Case: Complete Web Application Stack

Let’s build something more realistic: the infrastructure for a small fintech application.

# variables.tf
variable "environment" {
  default = "production"
}

variable "instance_count" {
  default = 3
}

variable "db_password" {
  sensitive = true
}

# vpc.tf
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "bytegurus-vpc-${var.environment}"
  }
}

resource "aws_subnet" "private" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
}

# rds.tf
resource "aws_db_instance" "postgres" {
  identifier     = "bytegurus-db"
  engine         = "postgres"
  engine_version = "16"
  instance_class = "db.t3.micro"
  allocated_storage = 20
  
  db_name  = "bytegurusdb"
  username = "admin"
  manage_master_user_password = true   # generated in AWS Secrets Manager, never in state

  storage_encrypted   = true
  deletion_protection = var.environment == "production"
  skip_final_snapshot = var.environment != "production"
  
  tags = {
    Environment = var.environment
  }
}

# alb.tf
resource "aws_lb" "main" {
  name            = "bytegurus-alb"
  internal        = false
  load_balancer_type = "application"
  security_groups = [aws_security_group.alb.id]
  subnets         = aws_subnet.public[*].id
}

resource "aws_lb_target_group" "app" {
  name        = "bytegurus-tg"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
}

Managing State Effectively

One critical aspect of Terraform is state management. For teams, storing state locally is problematic. Use remote state instead:

terraform {
  backend "s3" {
    bucket         = "bytegurus-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    use_lockfile   = true   # native S3 locking (Terraform 1.10+), no DynamoDB table
  }
}

This configuration:

  • Stores state in S3 (durable and shareable)
  • Enables encryption for security
  • Locks state in S3 itself, preventing concurrent modifications
  • State can hold secrets: restrict who can read the bucket

Terraform Modules: Reusability and Scale

As your infrastructure grows, modules become essential. A module is a container for multiple resources used together. Here’s a typical structure:

modules/
├── networking/
│   ├── main.tf
│   ├── variables.tf
│   └── outputs.tf
├── database/
│   ├── main.tf
│   ├── variables.tf
│   └── outputs.tf
└── compute/
    ├── main.tf
    ├── variables.tf
    └── outputs.tf

Using modules:

module "networking" {
  source = "./modules/networking"
  
  vpc_cidr = "10.0.0.0/16"
  environment = var.environment
}

module "database" {
  source = "./modules/database"
  
  db_password = var.db_password
  subnet_ids  = module.networking.subnet_ids
  environment = var.environment
}

Best Practices for Terraform Success

Version Control: Commit all .tf files to Git, but never commit terraform.tfstate or .tfvars files containing secrets.

Use Variables and Outputs: Avoid hardcoding values. Use variables.tf for inputs and outputs.tf for sharing data between modules.

Separate Environments: Keep staging and production in separate directories with separate state files and separate access. Workspaces share one backend and one set of credentials, so a mistake in staging can reach production state.

Code Review: Treat infrastructure changes like application code. Review all Terraform changes before applying them.

Automated Testing: Tools like Terratest enable you to test infrastructure configurations programmatically.

Documentation: Document why infrastructure decisions were made, not just what was built.

Common Pitfalls to Avoid

Forgetting the Plan Step: Always run terraform plan before apply to preview changes.

Mismanaging Secrets: Never hardcode passwords or API keys. Use AWS Secrets Manager, HashiCorp Vault, or environment variables.

Ignoring State: Don’t manually modify infrastructure outside Terraform. Keep your infrastructure defined in code.

Resource Naming: Use consistent, descriptive naming conventions that include environment and purpose.

Conclusion

Infrastructure as Code with Terraform isn’t just a technical practice—it’s a fundamental shift in how organizations manage cloud resources. By codifying your infrastructure, you gain reproducibility, auditability, and the ability to scale confidently.

Terraform turns infrastructure changes into reviewed, repeatable code. Whether you’re managing a startup’s cloud infrastructure or enterprise-grade systems, Terraform provides the flexibility and power needed for modern cloud-native development.

Start small, build incrementally, and gradually transition your infrastructure to code. Your future self—and your team—will thank you.

B

ByteGurus

Author

Back to all articles
Ready to Start?

Ready to start your project?

Let's discuss how we can help bring your ideas to life.