March 30, 2026·9 min·By

Terraform for Beginners: Infrastructure as Code That Actually Makes Sense

Terraforminfrastructure as codeAWSDevOpsIaC

Clicking through cloud consoles creates infrastructure you can't reproduce, track, or version control. Terraform fixes this. Here's how to start.

Core Concepts

# main.tf

# Provider: tells Terraform where to deploy
provider "aws" {
    region = "ca-central-1"
}

# Resource: a piece of infrastructure
resource "aws_instance" "web" {
    ami           = "ami-0c9bef50e5e424e5d"
    instance_type = "t3.micro"

    tags = {
        Name = "web-server"
        Environment = "production"
    }
}

# Data source: read existing infrastructure
data "aws_vpc" "default" {
    default = true
}

Variables and Outputs

# variables.tf
variable "environment" {
    type        = string
    description = "Deployment environment"
    default     = "production"
}

variable "instance_type" {
    type    = string
    default = "t3.micro"
}

# outputs.tf
output "instance_ip" {
    value       = aws_instance.web.public_ip
    description = "Public IP of the web server"
}

output "instance_id" {
    value = aws_instance.web.id
}
# Pass variables at apply time
terraform apply -var="environment=staging" -var="instance_type=t3.small"

# Or use .tfvars files
terraform apply -var-file="staging.tfvars"

A Real-World Example: VPC + EC2 + RDS

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

resource "aws_subnet" "public" {
    count             = 2
    vpc_id            = aws_vpc.main.id
    cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
    availability_zone = data.aws_availability_zones.available.names[count.index]
    map_public_ip_on_launch = true
}

resource "aws_subnet" "private" {
    count             = 2
    vpc_id            = aws_vpc.main.id
    cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 10)
    availability_zone = data.aws_availability_zones.available.names[count.index]
}

# RDS PostgreSQL
resource "aws_db_instance" "postgres" {
    identifier        = "${var.environment}-postgres"
    engine            = "postgres"
    engine_version    = "16"
    instance_class    = "db.t3.micro"
    allocated_storage = 20
    storage_type      = "gp3"

    db_name  = "myapp"
    username = "myapp"
    password = var.db_password  # Store in SSM, not in code

    db_subnet_group_name   = aws_db_subnet_group.main.name
    vpc_security_group_ids = [aws_security_group.rds.id]

    backup_retention_period = 7
    skip_final_snapshot     = false
    final_snapshot_identifier = "${var.environment}-final-snapshot"

    tags = { Environment = var.environment }
}

State Management

Terraform stores state in terraform.tfstate. Never commit this to git -- it contains secrets.

Use remote state with locking:

# backend.tf
terraform {
    backend "s3" {
        bucket         = "mycompany-terraform-state"
        key            = "production/terraform.tfstate"
        region         = "ca-central-1"
        encrypt        = true
        dynamodb_table = "terraform-state-lock"  # Prevents concurrent applies
    }
}
# Create the S3 bucket and DynamoDB table first (chicken-and-egg):
aws s3 mb s3://mycompany-terraform-state --region ca-central-1
aws dynamodb create-table \
    --table-name terraform-state-lock \
    --attribute-definitions AttributeName=LockID,AttributeType=S \
    --key-schema AttributeName=LockID,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST

Modules

Modules are reusable units of Terraform configuration:

# modules/rds/main.tf
variable "environment" {}
variable "instance_class" { default = "db.t3.micro" }

resource "aws_db_instance" "this" {
    identifier     = "${var.environment}-postgres"
    instance_class = var.instance_class
    # ...
}

output "endpoint" {
    value = aws_db_instance.this.endpoint
}

# In your main configuration:
module "database" {
    source = "./modules/rds"

    environment    = "production"
    instance_class = "db.t3.medium"
}

# Access module outputs
resource "aws_ssm_parameter" "db_url" {
    name  = "/production/db_url"
    value = "postgresql://${module.database.endpoint}/myapp"
}

Workflow

# Initialize: download providers and modules
terraform init

# See what will change
terraform plan

# Apply changes
terraform apply

# Destroy everything (careful!)
terraform destroy

# Format code
terraform fmt

# Validate syntax
terraform validate

# Import existing resources into state
terraform import aws_instance.web i-1234567890abcdef0

Secrets Management

Never put secrets in Terraform files. Reference them from AWS SSM or Secrets Manager:

data "aws_ssm_parameter" "db_password" {
    name = "/production/db_password"
    with_decryption = true
}

resource "aws_db_instance" "postgres" {
    password = data.aws_ssm_parameter.db_password.value
    # ...
}

Terraform solves infrastructure drift (when someone manually changes something in the console and no one knows). After adopting it, your infrastructure becomes reproducible, reviewable, and auditable.

K
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

Does Innovibe build this kind of thing for clients?+

Yes — this is exactly what we do day-to-day for clients across BC and Canada. If you'd rather have us build and maintain it than implement it yourself, reach out.

How do I decide whether to build this in-house or hire an agency?+

Build in-house if your team has the skills and bandwidth and this is core to your product. Hire out if it's infrastructure, if speed matters, or if the expertise gap would take months to close. We're biased, obviously — but we'll tell you honestly when in-house makes more sense.

What tech stack does Innovibe use for projects like this?+

Next.js + TypeScript on the frontend, Node.js or Go on the backend, Postgres for the primary data store, and GCP (Cloud Run, BigQuery, Pub/Sub) for infrastructure. We pick tools that are boring in the best way — proven, well-documented, and easy to hire for.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation