Back to Blog
TerraformInfrastructure as CodeDevOpsCloud

Infrastructure as Code with Terraform: A Practical Guide

Manage cloud infrastructure with code. From basic resources to modules to state management and best practices.

B
Bootspring Team
Engineering
October 5, 2024
5 min read

Infrastructure as Code (IaC) treats infrastructure like software: versioned, tested, and repeatable. Terraform is the leading tool for declarative infrastructure management across cloud providers.

Core Concepts#

Declarative vs Imperative#

1# Declarative: Describe desired state 2# Terraform figures out how to get there 3 4resource "aws_instance" "web" { 5 ami = "ami-0c55b159cbfafe1f0" 6 instance_type = "t3.micro" 7 8 tags = { 9 Name = "web-server" 10 } 11} 12 13# Terraform will: 14# - Create if doesn't exist 15# - Update if configuration changed 16# - Do nothing if already matches

Providers#

1# Configure providers 2terraform { 3 required_providers { 4 aws = { 5 source = "hashicorp/aws" 6 version = "~> 5.0" 7 } 8 cloudflare = { 9 source = "cloudflare/cloudflare" 10 version = "~> 4.0" 11 } 12 } 13} 14 15provider "aws" { 16 region = "us-east-1" 17} 18 19provider "cloudflare" { 20 api_token = var.cloudflare_api_token 21}

Resources and Data Sources#

1# Resource: Managed by Terraform 2resource "aws_s3_bucket" "data" { 3 bucket = "my-data-bucket" 4} 5 6# Data source: Read-only, external data 7data "aws_ami" "ubuntu" { 8 most_recent = true 9 owners = ["099720109477"] 10 11 filter { 12 name = "name" 13 values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"] 14 } 15} 16 17resource "aws_instance" "web" { 18 ami = data.aws_ami.ubuntu.id 19 instance_type = "t3.micro" 20}

Variables and Outputs#

Input Variables#

1# variables.tf 2variable "environment" { 3 description = "Deployment environment" 4 type = string 5 default = "development" 6 7 validation { 8 condition = contains(["development", "staging", "production"], var.environment) 9 error_message = "Environment must be development, staging, or production." 10 } 11} 12 13variable "instance_count" { 14 description = "Number of instances" 15 type = number 16 default = 1 17} 18 19variable "allowed_ips" { 20 description = "List of allowed IP addresses" 21 type = list(string) 22 default = [] 23} 24 25variable "tags" { 26 description = "Resource tags" 27 type = map(string) 28 default = {} 29} 30 31# Usage 32resource "aws_instance" "web" { 33 count = var.instance_count 34 instance_type = var.environment == "production" ? "t3.large" : "t3.micro" 35 36 tags = merge(var.tags, { 37 Environment = var.environment 38 }) 39}

Outputs#

1# outputs.tf 2output "instance_ips" { 3 description = "Public IPs of web instances" 4 value = aws_instance.web[*].public_ip 5} 6 7output "load_balancer_dns" { 8 description = "Load balancer DNS name" 9 value = aws_lb.main.dns_name 10} 11 12output "database_endpoint" { 13 description = "Database connection endpoint" 14 value = aws_db_instance.main.endpoint 15 sensitive = true 16}

State Management#

Remote State#

1# backend.tf 2terraform { 3 backend "s3" { 4 bucket = "my-terraform-state" 5 key = "prod/terraform.tfstate" 6 region = "us-east-1" 7 encrypt = true 8 dynamodb_table = "terraform-locks" 9 } 10} 11 12# Create state infrastructure first 13resource "aws_s3_bucket" "terraform_state" { 14 bucket = "my-terraform-state" 15 16 lifecycle { 17 prevent_destroy = true 18 } 19} 20 21resource "aws_s3_bucket_versioning" "terraform_state" { 22 bucket = aws_s3_bucket.terraform_state.id 23 versioning_configuration { 24 status = "Enabled" 25 } 26} 27 28resource "aws_dynamodb_table" "terraform_locks" { 29 name = "terraform-locks" 30 billing_mode = "PAY_PER_REQUEST" 31 hash_key = "LockID" 32 33 attribute { 34 name = "LockID" 35 type = "S" 36 } 37}

State Commands#

1# View current state 2terraform state list 3terraform state show aws_instance.web 4 5# Move resources 6terraform state mv aws_instance.web aws_instance.web_server 7 8# Import existing resources 9terraform import aws_instance.web i-1234567890abcdef0 10 11# Remove from state (without destroying) 12terraform state rm aws_instance.web

Modules#

Creating a Module#

1# modules/vpc/main.tf 2variable "name" { 3 type = string 4} 5 6variable "cidr_block" { 7 type = string 8 default = "10.0.0.0/16" 9} 10 11variable "availability_zones" { 12 type = list(string) 13} 14 15resource "aws_vpc" "main" { 16 cidr_block = var.cidr_block 17 enable_dns_hostnames = true 18 19 tags = { 20 Name = var.name 21 } 22} 23 24resource "aws_subnet" "public" { 25 count = length(var.availability_zones) 26 vpc_id = aws_vpc.main.id 27 cidr_block = cidrsubnet(var.cidr_block, 8, count.index) 28 availability_zone = var.availability_zones[count.index] 29 30 tags = { 31 Name = "${var.name}-public-${count.index + 1}" 32 } 33} 34 35output "vpc_id" { 36 value = aws_vpc.main.id 37} 38 39output "public_subnet_ids" { 40 value = aws_subnet.public[*].id 41}

Using Modules#

1# main.tf 2module "vpc" { 3 source = "./modules/vpc" 4 5 name = "production" 6 cidr_block = "10.0.0.0/16" 7 availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"] 8} 9 10module "web_cluster" { 11 source = "./modules/ecs-cluster" 12 13 name = "web" 14 vpc_id = module.vpc.vpc_id 15 subnet_ids = module.vpc.public_subnet_ids 16} 17 18# Registry modules 19module "vpc" { 20 source = "terraform-aws-modules/vpc/aws" 21 version = "5.0.0" 22 23 name = "production" 24 cidr = "10.0.0.0/16" 25}

Workspaces#

1# Create environments 2terraform workspace new staging 3terraform workspace new production 4 5# Switch workspace 6terraform workspace select production 7 8# List workspaces 9terraform workspace list
1# Use workspace in configuration 2locals { 3 environment = terraform.workspace 4 5 instance_type = { 6 development = "t3.micro" 7 staging = "t3.small" 8 production = "t3.large" 9 } 10} 11 12resource "aws_instance" "web" { 13 instance_type = local.instance_type[local.environment] 14}

Best Practices#

Project Structure#

infrastructure/ ├── environments/ │ ├── development/ │ │ ├── main.tf │ │ ├── variables.tf │ │ └── terraform.tfvars │ ├── staging/ │ └── production/ ├── modules/ │ ├── vpc/ │ ├── ecs-cluster/ │ └── rds/ └── global/ ├── iam/ └── dns/

Naming Conventions#

1# Use consistent naming 2resource "aws_security_group" "web_server" { # snake_case 3 name = "web-server-sg" # kebab-case for AWS names 4 5 tags = { 6 Name = "web-server-sg" 7 Environment = var.environment 8 ManagedBy = "terraform" 9 Project = var.project 10 } 11}

Prevent Accidental Destruction#

1resource "aws_db_instance" "main" { 2 # ... 3 4 lifecycle { 5 prevent_destroy = true 6 } 7} 8 9resource "aws_instance" "web" { 10 # ... 11 12 lifecycle { 13 create_before_destroy = true 14 } 15}

CI/CD Integration#

1# GitHub Actions 2name: Terraform 3 4on: 5 pull_request: 6 paths: 7 - 'infrastructure/**' 8 push: 9 branches: 10 - main 11 paths: 12 - 'infrastructure/**' 13 14jobs: 15 plan: 16 runs-on: ubuntu-latest 17 steps: 18 - uses: actions/checkout@v3 19 20 - uses: hashicorp/setup-terraform@v2 21 with: 22 terraform_version: 1.6.0 23 24 - name: Terraform Init 25 run: terraform init 26 working-directory: infrastructure/environments/production 27 28 - name: Terraform Plan 29 run: terraform plan -out=tfplan 30 working-directory: infrastructure/environments/production 31 32 - name: Upload Plan 33 uses: actions/upload-artifact@v3 34 with: 35 name: tfplan 36 path: infrastructure/environments/production/tfplan 37 38 apply: 39 needs: plan 40 runs-on: ubuntu-latest 41 if: github.ref == 'refs/heads/main' 42 environment: production 43 steps: 44 - uses: actions/checkout@v3 45 46 - name: Download Plan 47 uses: actions/download-artifact@v3 48 with: 49 name: tfplan 50 path: infrastructure/environments/production 51 52 - name: Terraform Apply 53 run: terraform apply tfplan 54 working-directory: infrastructure/environments/production

Conclusion#

Terraform enables reproducible, version-controlled infrastructure. Start with simple resources, graduate to modules for reusability, and use remote state for team collaboration.

Treat infrastructure code like application code: review changes, test in lower environments, and automate deployments. The investment in IaC pays dividends in reliability and speed.

Share this article

Help spread the word about Bootspring