Terraform is an Infrastructure as Code (IaC) tool created by HashiCorp that allows you to define and provision infrastructure using a declarative configuration language. With Terraform, you can manage resources across multiple cloud providers and services through a consistent workflow.
Key Benefits:
- Infrastructure as Code: Version control your infrastructure
- Multi-Cloud Support: Works with AWS, Azure, GCP, and many others
- Declarative: Describe what you want, not how to get there
- State Management: Keeps track of your infrastructure
- Plan & Apply: Preview changes before applying them
Core Concepts
- Providers: Plugins that interact with cloud platforms or services
- Resources: Infrastructure objects managed by Terraform (e.g., VMs, networks)
- Data Sources: Read-only information fetched from providers
- Variables: Parameterize your configurations
- Outputs: Return values from your infrastructure
- Modules: Reusable components
- State: Terraform’s record of managed resources
Installation
Windows
1
2
3
4
5
6
7
8
9
10
| # Using Chocolatey
choco install terraform
# Using Scoop
scoop install terraform
# Manual installation
# 1. Download from https://www.terraform.io/downloads.html
# 2. Extract the zip file
# 3. Add to PATH environment variable
|
Linux
1
2
3
4
5
6
7
8
9
10
11
12
| # Using apt (Ubuntu/Debian)
sudo apt update
sudo apt install -y gnupg software-properties-common
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install terraform
# Using yum (RHEL/CentOS)
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum install terraform
|
macOS
1
2
3
| # Using Homebrew
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
|
Manual Installation:
- Download from terraform.io
- Extract the binary
- Add to your PATH
Verify Installation
TF Basics
File Structure:
1
2
3
4
5
6
7
8
9
10
11
| project/
├── main.tf # Main configuration
├── variables.tf # Variable definitions
├── outputs.tf # Output definitions
├── providers.tf # Provider configurations
├── terraform.tfvars # Variable values
└── modules/ # Custom modules
└── vpc/
├── main.tf
├── variables.tf
└── outputs.tf
|
Basic Workflow
1
2
3
4
5
6
7
8
| terraform init # Initialize working directory
terraform plan # Preview changes
terraform apply # Apply changes
terraform destroy # Destroy infrastructure
terraform validate # Validate configuration
terraform fmt # Format configuration files
terraform show # Show current state
terraform output # Show output values
|
TF Configuration
Create a file named main.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
| # Configure the AWS Provider
provider "aws" {
region = "us-east-1"
}
# Create a VPC
resource "aws_vpc" "example" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "example-vpc"
}
}
|
TF Variables
Variables make your configurations flexible and reusable. They can be defined in multiple ways and support various data types.
Variable Types
Terraform supports several variable types:
- String: Text values
- Number: Numeric values
- Bool: Boolean values (true/false)
- List: Ordered collection of values
- Map: Collection of key-value pairs
- Set: Unordered collection of unique values
- Object: Complex structured data
- Tuple: Fixed-length collection of values with potentially different types
Variable Declaration Syntax:
1
2
3
4
5
6
7
8
9
10
| variable "variable_name" {
description = "Description of the variable"
type = string # string, number, bool, list, map, object, tuple, set
default = "default_value"
sensitive = false # Hide value in logs
validation {
condition = length(var.variable_name) > 4
error_message = "Variable must be more than 4 characters."
}
}
|
Demo 1: Variable with String
Create a file named variables.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| variable "instance_name" {
description = "Name of the EC2 instance"
type = string
default = "my-instance"
validation {
condition = length(var.instance_name) > 0
error_message = "Instance name cannot be empty."
}
}
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "us-west-2"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
|
Use it in main.tf
:
1
2
3
4
5
6
7
8
9
| resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1d0"
instance_type = "t2.micro"
tags = {
Name = var.instance_name
Environment = var.environment
}
}
|
terraform.tfvars
1
2
3
| instance_name = "web-server-01"
aws_region = "us-east-1"
environment = "dev"
|
Demo 2: Variable with List
In variables.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| variable "availability_zones" {
description = "List of availability zones"
type = list(string)
default = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
variable "instance_types" {
description = "List of instance types"
type = list(string)
default = ["t2.micro", "t2.small", "t2.medium"]
}
variable "subnet_cidrs" {
description = "CIDR blocks for subnets"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
|
Use it in main.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
resource "aws_subnet" "public" {
count = length(var.subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
Type = "Public"
}
}
resource "aws_instance" "web" {
count = length(var.instance_types)
ami = "ami-0c55b159cbfafe1d0"
instance_type = var.instance_types[count.index]
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "web-server-${count.index + 1}"
}
}
|
Demo 3: Variable with Map
In variables.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| variable "instance_config" {
description = "Map of instance configurations"
type = map(string)
default = {
ami = "ami-0c55b159cbfafe1d0"
instance_type = "t2.micro"
key_name = "my-key"
}
}
variable "tags" {
description = "Map of tags to apply to resources"
type = map(string)
default = {
Environment = "dev"
Owner = "devops-team"
Project = "web-app"
}
}
variable "port_mappings" {
description = "Map of port configurations"
type = map(number)
default = {
http = 80
https = 443
ssh = 22
}
}
|
Use it in main.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| resource "aws_instance" "web" {
ami = var.instance_config["ami"]
instance_type = var.instance_config["instance_type"]
key_name = var.instance_config["key_name"]
tags = var.tags
}
resource "aws_security_group" "web" {
name_prefix = "web-sg"
dynamic "ingress" {
for_each = var.port_mappings
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
tags = var.tags
}
|
Demo 4: Variable with Object
In variables.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| variable "database_config" {
description = "Database configuration object"
type = object({
engine = string
engine_version = string
instance_class = string
allocated_storage = number
db_name = string
username = string
backup_retention_period = number
multi_az = bool
storage_encrypted = bool
})
default = {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
db_name = "myapp"
username = "admin"
backup_retention_period = 7
multi_az = false
storage_encrypted = true
}
}
variable "vpc_config" {
description = "VPC configuration object"
type = object({
cidr_block = string
enable_dns_hostnames = bool
enable_dns_support = bool
tags = map(string)
})
default = {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "main-vpc"
Environment = "dev"
}
}
}
variable "application_config" {
description = "Complete application configuration"
type = object({
name = string
instances = object({
count = number
type = string
ami = string
})
database = object({
engine = string
size = string
})
networking = object({
vpc_cidr = string
subnets = list(string)
})
})
validation {
condition = var.application_config.instances.count > 0
error_message = "Instance count must be greater than 0."
}
}
|
Use it in main.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| resource "aws_db_instance" "main" {
identifier = "${var.application_config.name}-db"
engine = var.database_config.engine
engine_version = var.database_config.engine_version
instance_class = var.database_config.instance_class
allocated_storage = var.database_config.allocated_storage
db_name = var.database_config.db_name
username = var.database_config.username
password = "changeme123!" # In production, use AWS Secrets Manager
backup_retention_period = var.database_config.backup_retention_period
multi_az = var.database_config.multi_az
storage_encrypted = var.database_config.storage_encrypted
skip_final_snapshot = true
tags = {
Name = "${var.application_config.name}-database"
}
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_config.cidr_block
enable_dns_hostnames = var.vpc_config.enable_dns_hostnames
enable_dns_support = var.vpc_config.enable_dns_support
tags = var.vpc_config.tags
}
|
in terraform.tfvars
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| application_config = {
name = "webapp"
instances = {
count = 3
type = "t2.small"
ami = "ami-0c55b159cbfafe1d0"
}
database = {
engine = "postgres"
size = "db.t3.small"
}
networking = {
vpc_cidr = "10.0.0.0/16"
subnets = ["10.0.1.0/24", "10.0.2.0/24"]
}
}
### Variable Validation
You can add validation rules to variables:
```hcl
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
validation {
condition = contains(["t2.micro", "t2.small", "t3.micro"], var.instance_type)
error_message = "Instance type must be t2.micro, t2.small, or t3.micro."
}
}
|
- Default values in variable declarations
- Command line with
-var
flag: terraform apply -var="region=us-west-2"
- Variable files (.tfvars):
terraform apply -var-file="prod.tfvars"
- Environment variables:
TF_VAR_region=us-west-2 terraform apply
- Interactive input when running Terraform commands
TF Dynamic Blocks
Dynamic blocks allow you to dynamically create nested blocks within resource configurations based on collections like lists or maps.
Basic Syntax
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| resource "aws_security_group" "example" {
name = "example"
description = "Example security group"
dynamic "ingress" {
for_each = var.ingress_rules
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
|
Dynamic Block Example
In variables.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| variable "ingress_rules" {
description = "Ingress rules for security group"
type = list(object({
description = string
port = number
}))
default = [
{
description = "HTTP"
port = 80
},
{
description = "HTTPS"
port = 443
},
{
description = "SSH"
port = 22
}
]
}
|
In main.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| resource "aws_security_group" "example" {
name = "example"
description = "Example security group"
vpc_id = aws_vpc.example.id
dynamic "ingress" {
for_each = var.ingress_rules
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
|
TF Resources
Resources are the most important element in Terraform. They represent infrastructure objects like virtual machines, networks, or DNS records.
Resource Syntax:
1
2
3
4
| resource "resource_type" "resource_name" {
argument = "value"
# ...
}
|
AWS Resource Examples:
VPC and Networking:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "main-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "public-rt"
}
}
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
}
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
|
EC2 Instance with Security Group:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| resource "aws_security_group" "web" {
name = "web-sg"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-security-group"
}
}
resource "aws_key_pair" "main" {
key_name = "main-key"
public_key = file("~/.ssh/id_rsa.pub")
}
resource "aws_instance" "web" {
count = 2
ami = data.aws_ami.amazon_linux.id
instance_type = "t2.micro"
key_name = aws_key_pair.main.key_name
vpc_security_group_ids = [aws_security_group.web.id]
subnet_id = aws_subnet.public[count.index].id
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "<h1>Web Server ${count.index + 1}</h1>" > /var/www/html/index.html
EOF
tags = {
Name = "web-server-${count.index + 1}"
}
}
|
Load Balancer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| resource "aws_lb" "main" {
name = "main-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.web.id]
subnets = aws_subnet.public[*].id
enable_deletion_protection = false
tags = {
Name = "main-load-balancer"
}
}
resource "aws_lb_target_group" "web" {
name = "web-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}
}
resource "aws_lb_target_group_attachment" "web" {
count = length(aws_instance.web)
target_group_arn = aws_lb_target_group.web.arn
target_id = aws_instance.web[count.index].id
port = 80
}
resource "aws_lb_listener" "web" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
}
|
TF Outputs
Outputs allow you to extract and display information about your infrastructure.
outputs.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "vpc_cidr_block" {
description = "CIDR block of the VPC"
value = aws_vpc.main.cidr_block
}
output "public_subnet_ids" {
description = "IDs of public subnets"
value = aws_subnet.public[*].id
}
output "web_instance_ips" {
description = "Public IP addresses of web instances"
value = aws_instance.web[*].public_ip
}
output "load_balancer_dns" {
description = "DNS name of the load balancer"
value = aws_lb.main.dns_name
}
output "database_endpoint" {
description = "Database endpoint"
value = aws_db_instance.main.endpoint
sensitive = true # Hide sensitive information
}
# Complex output with maps
output "instance_details" {
description = "Detailed information about instances"
value = {
for i, instance in aws_instance.web :
"instance-${i}" => {
id = instance.id
public_ip = instance.public_ip
az = instance.availability_zone
}
}
}
# Conditional output
output "environment_specific_info" {
description = "Environment specific information"
value = var.environment == "prod" ? {
backup_enabled = true
monitoring = "enhanced"
} : {
backup_enabled = false
monitoring = "basic"
}
}
|
Accessing Outputs:
1
2
3
4
5
6
7
8
9
10
11
| # View all outputs
terraform output
# View specific output
terraform output vpc_id
# Output in JSON format
terraform output -json
# Use output in other configurations
terraform output -raw load_balancer_dns
|
TF Providers
AWS Provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
access_key = var.aws_access_key
secret_key = var.aws_secret_key
# Alternative: Use AWS CLI profiles
# profile = "default"
default_tags {
tags = {
Terraform = "true"
Environment = var.environment
}
}
}
# Multiple provider configurations
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
provider "aws" {
alias = "us_west_2"
region = "us-west-2"
}
# Use specific provider
resource "aws_instance" "east" {
provider = aws.us_east_1
ami = "ami-0c55b159cbfafe1d0"
instance_type = "t2.micro"
}
|
GCP Provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
}
}
provider "google" {
credentials = file("path/to/service-account-key.json")
project = var.gcp_project_id
region = var.gcp_region
zone = var.gcp_zone
}
resource "google_compute_instance" "vm" {
name = "terraform-instance"
machine_type = "e2-micro"
zone = var.gcp_zone
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
}
}
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
}
|
Proxmox Provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| terraform {
required_providers {
proxmox = {
source = "telmate/proxmox"
version = "2.9.14"
}
}
}
provider "proxmox" {
pm_api_url = "https://proxmox.example.com:8006/api2/json"
pm_user = "terraform@pve"
pm_password = var.proxmox_password
pm_tls_insecure = true
}
resource "proxmox_vm_qemu" "vm" {
name = "terraform-vm"
target_node = "proxmox-node"
clone = "ubuntu-20.04-template"
cores = 2
sockets = 1
memory = 2048
disk {
size = "20G"
type = "scsi"
storage = "local-lvm"
}
network {
model = "virtio"
bridge = "vmbr0"
}
}
|
TF Multiple Providers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| provider "aws" {
region = "us-east-1"
alias = "east"
}
provider "aws" {
region = "us-west-2"
alias = "west"
}
resource "aws_instance" "east_instance" {
provider = aws.east
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
resource "aws_instance" "west_instance" {
provider = aws.west
ami = "ami-0892d3c7ee96c0bf7"
instance_type = "t2.micro"
}
|
TF Vault Integration
HashiCorp Vault is a tool for securely storing and accessing secrets. Terraform can integrate with Vault to fetch credentials (like database passwords, API keys, SSH keys) dynamically at runtime. This prevents hardcoding secrets inside Terraform configuration.
Installing Vault (Ubuntu)
SSH into the EC2 instance and install Vault:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # Update system
sudo apt update && sudo apt install -y gpg
# Add HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg \
| sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
# Verify the key
gpg --no-default-keyring \
--keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
# Add HashiCorp repo
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list
# Install Vault
sudo apt update && sudo apt install -y vault
|
Starting Vault
Run Vault in dev mode for testing (not recommended for production):
1
| vault server -dev -dev-listen-address="0.0.0.0:8200"
|
Export the Vault address and root token:
1
2
| export VAULT_ADDR='http://<EC2_PUBLIC_IP>:8200'
export VAULT_TOKEN='root'
|
Enabling AppRole Authentication
Vault supports multiple authentication methods. Terraform commonly uses AppRole.
Enable AppRole auth method:
1
| vault auth enable approle
|
Creating Vault Policies
Policies define what secrets Terraform can read/write. Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| vault policy write terraform - <<EOF
path "*" {
capabilities = ["list", "read"]
}
path "secret/data/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "kv/db_cred/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "auth/token/create" {
capabilities = ["create", "read", "update", "list"]
}
EOF
|
Creating an AppRole
1
2
3
4
5
6
7
| vault write auth/approle/role/terraform \
secret_id_ttl=10m \
token_num_uses=10 \
token_ttl=20m \
token_max_ttl=30m \
secret_id_num_uses=40 \
token_policies=terraform
|
Gen Role & Secret ID
Adding Secrets in Vault
Enable KV secrets engine:
1
| vault secrets enable -path=kv kv-v2
|
Add a secret:
1
| vault kv put kv/db_cred username="dbadmin" password="dbpassword123"
|
Example 1: Fetch Secret for EC2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| provider "vault" {
address = "http://<VAULT_SERVER_IP>:8200"
skip_child_token = true
auth_login {
path = "auth/approle/login"
parameters = {
role_id = "<ROLE_ID>"
secret_id = "<SECRET_ID>"
}
}
}
data "vault_kv_secret_v2" "example" {
mount = "kv"
name = "db_cred"
}
resource "aws_instance" "my_instance" {
ami = "ami-053b0d53c279acc90"
instance_type = "t2.micro"
tags = {
Name = "vault-ec2"
Secret = data.vault_kv_secret_v2.example.data["username"]
}
}
|
Example 2: Fetch Secret for RDS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| data "vault_kv_secret_v2" "db_creds" {
mount = "kv"
name = "db_cred"
}
resource "aws_db_instance" "default" {
db_name = "mydb"
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
username = data.vault_kv_secret_v2.db_creds.data["username"]
password = data.vault_kv_secret_v2.db_creds.data["password"]
parameter_group_name = "default.mysql5.7"
skip_final_snapshot = true
}
|
TF Modules
Modules are reusable Terraform configurations that help organize and standardize infrastructure code.
Creating a Module
Create a directory structure:
1
2
3
4
5
6
| modules/
vpc/
main.tf
variables.tf
outputs.tf
main.tf
|
In modules/vpc/variables.tf
:
1
2
3
4
5
6
7
8
9
| variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
}
variable "subnet_cidrs" {
description = "CIDR blocks for subnets"
type = list(string)
}
|
In modules/vpc/main.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
| resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
tags = {
Name = "module-vpc"
}
}
resource "aws_subnet" "this" {
count = length(var.subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.subnet_cidrs[count.index]
}
|
In modules/vpc/outputs.tf
:
1
2
3
4
5
6
7
8
9
| output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.this.id
}
output "subnet_ids" {
description = "IDs of the subnets"
value = aws_subnet.this[*].id
}
|
Using a Module
In your root main.tf
:
1
2
3
4
5
6
7
8
9
10
11
12
| module "vpc" {
source = "./modules/vpc"
vpc_cidr = "10.0.0.0/16"
subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
subnet_id = module.vpc.subnet_ids[0]
}
|
Using Public Modules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
}
|
TF State Management
Terraform state is a critical component that maps real-world resources to your configuration.
Local State
By default, Terraform stores state locally in a file named terraform.tfstate
.
Remote State
For team environments, it’s better to use remote state storage:
1
2
3
4
5
6
7
8
9
| terraform {
backend "s3" {
bucket = "terraform-state-bucket"
key = "terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
|
State Commands
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # List resources in state
terraform state list
# Show details of a resource
terraform state show aws_instance.example
# Move a resource within state
terraform state mv aws_instance.old aws_instance.new
# Remove a resource from state
terraform state rm aws_instance.example
# Import existing infrastructure into state
terraform import aws_instance.example i-1234567890abcdef0
|
TF Workspaces
Workspaces allow you to manage multiple states with the same configuration.
1
2
3
4
5
6
7
8
9
10
11
| # List workspaces
terraform workspace list
# Create a new workspace
terraform workspace new dev
# Switch workspace
terraform workspace select prod
# Delete workspace
terraform workspace delete dev
|
Use workspace in configuration:
1
2
3
4
5
6
7
| resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = terraform.workspace == "prod" ? "t2.medium" : "t2.micro"
tags = {
Environment = terraform.workspace
}
}
|
TF Functions
Terraform provides various built-in functions for string manipulation, numeric operations, collections, and more.
The format
and formatlist
functions are useful for string formatting.
1
2
3
4
5
6
7
8
9
10
11
12
13
| locals {
app = "nginx"
env = "dev"
port = 8080
message1 = format("Deploying %s on %s environment", local.app, local.env)
message2 = format("%s:%d", local.app, local.port)
message3 = formatlist("service available: %s", ["mysql", "redis", "mongodb"])
}
output "format_out1" { value = local.message1 }
output "format_out2" { value = local.message2 }
output "format_out3" { value = local.message3 }
|
Output:
1
2
3
4
5
6
7
| format_out1 = "Deploying nginx on dev environment"
format_out2 = "nginx:8080"
format_out3 = [
"service available: mysql",
"service available: redis",
"service available: mongodb",
]
|
Length Function
The length
function is often used for counting resources.
1
2
3
4
5
6
7
8
9
10
11
12
| locals {
az_list = ["us-east-1a", "us-east-1b", "us-east-1c"]
domain = "terraformcloud"
}
output "az_count" {
value = format("Number of AZs: %d", length(local.az_list))
}
output "domain_len" {
value = format("Domain name has %d characters", length(local.domain))
}
|
Output:
1
2
| az_count = "Number of AZs: 3"
domain_len = "Domain name has 14 characters"
|
Join Function
The join
function helps create strings, such as CIDR notations or DNS names.
1
2
3
4
5
6
7
8
9
10
| locals {
dns_parts = ["app", "example", "com"]
cidr_parts = ["192.168.0.0", "24"]
dns_name = join(".", local.dns_parts)
cidr = join("/", local.cidr_parts)
}
output "dns_output" { value = local.dns_name }
output "cidr_output" { value = local.cidr }
|
Output:
1
2
| dns_output = "app.example.com"
cidr_output = "192.168.0.0/24"
|
Flatten Function
The flatten
function is useful when dealing with security group rules or nested lists.
1
2
3
4
5
6
7
8
9
10
11
12
13
| locals {
sg_rules = [
["22", "80"],
["443"],
["3306", "5432"]
]
all_ports = flatten(local.sg_rules)
}
output "ports" {
value = local.all_ports
}
|
Output:
1
2
3
4
5
6
7
| ports = [
"22",
"80",
"443",
"3306",
"5432",
]
|
Lookup Function
The lookup
function is commonly used for environment-based configurations.
1
2
3
4
5
6
7
8
9
10
11
12
| variable "instance_type" {
default = {
dev = "t3.micro"
test = "t3.small"
prod = "t3.large"
}
}
resource "aws_instance" "app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = lookup(var.instance_type, terraform.workspace, "t3.medium")
}
|
- If the workspace is
prod
, it picks t3.large
.
- If it’s
qa
(not defined), it defaults to t3.medium
.
File Function
The file
function is perfect for reading SSH keys, configs, or templates.
1
2
3
4
5
6
7
8
| locals {
ssh_key = file("~/.ssh/id_rsa.pub")
}
resource "aws_key_pair" "deployer" {
key_name = "deployer-key"
public_key = local.ssh_key
}
|
String Functions
1
2
3
4
5
6
7
| locals {
upper_name = upper("example") # "EXAMPLE"
lower_name = lower("EXAMPLE") # "example"
title_name = title("hello world") # "Hello World"
joined = join("-", ["a", "b", "c"]) # "a-b-c"
substr_ell = substr("hello", 1, 3) # "ell"
}
|
Numeric Functions
1
2
3
4
5
6
| locals {
max_value = max(5, 12, 9) # 12
min_value = min(5, 12, 9) # 5
ceil_value = ceil(10.1) # 11
floor_value = floor(10.9) # 10
}
|
Collection Functions
1
2
3
4
5
6
7
8
9
| locals {
list_length = length(["a", "b", "c"]) # 3
merged_map = merge({ a = "1" }, { b = "2" }) # { a = "1", b = "2" }
map_keys = keys({ a = "1", b = "2" }) # ["a", "b"]
map_values = values({ a = "1", b = "2" }) # ["1", "2"]
contains_b = contains(["a", "b", "c"], "b") # true
second_item = element(["a", "b", "c"], 1) # "b"
looked_up = lookup({ a = "1", b = "2" }, "c", "default") # "default"
}
|
Conditional Expressions
1
2
3
4
5
6
7
8
| variable "environment" {
type = string
default = "dev"
}
locals {
instance_type = var.environment == "prod" ? "t2.medium" : "t2.micro"
}
|
1
2
3
4
5
6
7
8
9
10
| variable "enable_ec2" {
type = bool
default = true
}
resource "aws_instance" "example" {
count = var.enable_ec2 ? 1 : 0
ami = "ami-0123456789abcdef0"
instance_type = local.instance_type
}
|
TF Provisioners
Provisioners let you execute actions on local or remote machines as part of resource creation or destruction.
Local Exec Provisioner
1
2
3
4
5
6
7
8
| resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
provisioner "local-exec" {
command = "echo 'Instance ${self.id} created with IP ${self.private_ip}' >> instance_info.txt"
}
}
|
Remote Exec Provisioner
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
key_name = "example-key"
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/example-key.pem")
host = self.public_ip
}
provisioner "remote-exec" {
inline = [
"sudo yum update -y",
"sudo yum install -y httpd",
"sudo systemctl start httpd",
"sudo systemctl enable httpd"
]
}
}
|
TF Data Sources
Data sources allow you to fetch information about existing infrastructure that wasn’t created by your current Terraform configuration.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| # Get latest Amazon Linux AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Get default VPC
data "aws_vpc" "default" {
default = true
}
# Get availability zones
data "aws_availability_zones" "available" {
state = "available"
}
# Get existing security group
data "aws_security_group" "existing" {
filter {
name = "group-name"
values = ["existing-sg"]
}
}
# Use data sources in resources
resource "aws_instance" "example" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t2.micro"
availability_zone = data.aws_availability_zones.available.names[0]
vpc_security_group_ids = [data.aws_security_group.existing.id]
}
|
Version Constraints
1
2
3
4
5
6
7
8
9
10
| terraform {
required_version = ">= 1.0.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
|
Sensitive Data Handling
1
2
3
4
5
| variable "db_password" {
description = "Database password"
type = string
sensitive = true
}
|
Tagging Strategy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| locals {
common_tags = {
Environment = var.environment
Project = var.project_name
Owner = "DevOps Team"
ManagedBy = "Terraform"
}
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = merge(local.common_tags, { Name = "example-instance" })
}
|
Advanced TF Features
Count and For Each
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # Using count
resource "aws_instance" "server" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "server-${count.index}"
}
}
# Using for_each with a map
resource "aws_instance" "server" {
for_each = {
web = "t2.micro"
app = "t2.small"
db = "t2.medium"
}
ami = "ami-0c55b159cbfafe1f0"
instance_type = each.value
tags = {
Name = "${each.key}-server"
}
}
# Using for_each with a set
resource "aws_instance" "server" {
for_each = toset(["web", "app", "db"])
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "${each.key}-server"
}
}
|
TF Expressions
1
2
3
4
5
6
7
8
9
10
11
12
13
| locals {
# Conditional expression
instance_type = var.environment == "prod" ? "t2.medium" : "t2.micro"
# For expression
upper_names = [for name in var.names : upper(name)]
# For expression with map
name_map = {for idx, name in var.names : idx => upper(name)}
# For expression with filtering
long_names = [for name in var.names : name if length(name) > 5]
}
|
TF Lifecycle
1
2
3
4
5
6
7
8
9
10
| resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
lifecycle {
create_before_destroy = true
prevent_destroy = false
ignore_changes = [tags]
}
}
|
TF Import
Import existing infrastructure into Terraform:
1
| terraform import aws_instance.example i-1234567890abcdef0
|
TF Modules with Versioning
1
2
3
4
5
6
| module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
# Module parameters
}
|
TF Cloud and Enterprise Features
Remote Backends
1
2
3
4
5
6
7
8
9
| terraform {
backend "remote" {
organization = "example-org"
workspaces {
name = "example-workspace"
}
}
}
|
Sentinel Policies
Sentinel is a policy as code framework integrated with Terraform Cloud/Enterprise:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # Example Sentinel policy
import "tfplan"
# Require specific instance types
allowed_types = ["t2.micro", "t2.small", "t3.micro", "t3.small"]
ec2_instances = filter tfplan.resource_changes as _, rc {
rc.type is "aws_instance" and
(rc.change.actions contains "create" or rc.change.actions contains "update")
}
violations = filter ec2_instances as _, instance {
not (instance.change.after.instance_type in allowed_types)
}
main = rule {
length(violations) is 0
}
|
TF with CI/CD
GitHub Actions Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| name: Terraform
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.0.0
- name: Terraform Init
run: terraform init
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan
if: github.event_name == 'pull_request'
- name: Terraform Apply
run: terraform apply -auto-approve
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
Troubleshooting TF
Common Issues and Solutions
- State Lock Issues
1
2
| # Force unlock state
terraform force-unlock LOCK_ID
|
- Debugging
1
2
3
| # Enable detailed logs
export TF_LOG=DEBUG
export TF_LOG_PATH=terraform.log
|
- Plan File
1
2
3
4
5
| # Save plan to file
terraform plan -out=tfplan
# Apply plan file
terraform apply tfplan
|
- Refresh State
1
2
| # Refresh state without making changes
terraform refresh
|
Conclusion
Terraform is a powerful tool for managing infrastructure as code across multiple providers. This guide covered the basics to advanced concepts, but there’s always more to learn. As you become more comfortable with Terraform, explore the official documentation and community resources to deepen your knowledge.
Remember these key principles:
- Infrastructure as Code: Treat your infrastructure like software
- Declarative Approach: Define what you want, not how to get there
- State Management: Understand and properly manage Terraform state
- Modularity: Create reusable components
- Version Control: Keep your Terraform code in version control
Happy Terraforming!