Terraform in 2026: New Features and Best Practices
HashiCorp's Terraform remains the most widely used Infrastructure as Code (IaC) tool in the world. With over 70% market share in the sector, it has become the de facto standard for managing cloud infrastructure. In 2026, Terraform has reached remarkable maturity with new features that simplify the management of complex infrastructures.
Terraform 1.7/1.8 New Features
Removed Block (1.7)
Finally, a clean way to remove resources from state without destroying them:
# Removes the resource from state without deleting it in the cloud
removed {
from = aws_instance.legacy_server
lifecycle {
destroy = false
}
}
Before, you had to use terraform state rm, now it's declarative and versionable.
Improved Import Block (1.7+)
Import existing resources directly in code:
import {
to = aws_instance.web_server
id = "i-0123456789abcdef0"
}
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "WebServer"
}
}
# Generate automatic configuration
terraform plan -generate-config-out=generated.tf
Native Test Framework (1.6+)
Testing integrated into the language:
# tests/vpc.tftest.hcl
run "create_vpc" {
command = apply
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR block is incorrect"
}
}
run "check_subnets" {
command = plan
assert {
condition = length(aws_subnet.private) == 3
error_message = "Expected 3 private subnets"
}
}
terraform test
Provider-Defined Functions (1.8)
Providers can now define custom functions:
# Function defined by AWS provider
locals {
arn_parts = provider::aws::arn_parse(aws_s3_bucket.main.arn)
account_id = local.arn_parts.account
}
Enterprise Project Structure
Recommended Layout
terraform/
├── modules/ # Reusable modules
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── compute/
│ ├── database/
│ └── monitoring/
├── environments/ # Environment configurations
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ └── production/
├── tests/ # Terraform tests
│ ├── networking.tftest.hcl
│ └── compute.tftest.hcl
└── .github/
└── workflows/
└── terraform.yml
Reusable Modules
modules/networking/main.tf:
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(var.tags, {
Name = "${var.environment}-vpc"
})
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = merge(var.tags, {
Name = "${var.environment}-private-${count.index + 1}"
Type = "private"
})
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.environment}-public-${count.index + 1}"
Type = "public"
})
}
modules/networking/variables.tf:
variable "environment" {
description = "Environment name"
type = string
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "private_subnet_cidrs" {
description = "CIDR blocks for private subnets"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
variable "public_subnet_cidrs" {
description = "CIDR blocks for public subnets"
type = list(string)
default = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
}
variable "availability_zones" {
description = "Availability zones"
type = list(string)
}
variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {}
}
Using Modules
environments/production/main.tf:
module "networking" {
source = "../../modules/networking"
environment = "production"
vpc_cidr = "10.0.0.0/16"
availability_zones = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
private_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnet_cidrs = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
tags = {
Environment = "production"
ManagedBy = "terraform"
Project = "my-project"
}
}
module "compute" {
source = "../../modules/compute"
environment = "production"
vpc_id = module.networking.vpc_id
private_subnet_ids = module.networking.private_subnet_ids
instance_type = "t3.large"
min_size = 3
max_size = 10
}
State Management Best Practices
Remote State with Locking
backend.tf:
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "production/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
DynamoDB setup for locking:
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = {
Name = "Terraform Lock Table"
}
}
State Isolation
Separate state by environment and component:
s3://terraform-state/
├── networking/
│ ├── dev/terraform.tfstate
│ ├── staging/terraform.tfstate
│ └── production/terraform.tfstate
├── compute/
│ ├── dev/terraform.tfstate
│ └── production/terraform.tfstate
└── database/
└── production/terraform.tfstate
Data Sources for Cross-State
# Read output from another state
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-company-terraform-state"
key = "networking/production/terraform.tfstate"
region = "eu-west-1"
}
}
# Use the values
resource "aws_instance" "web" {
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
}
Terraform Cloud/Enterprise
When to Use It
| Scenario | Terraform OSS | Terraform Cloud |
|---|---|---|
| Small team (1-3) | Yes | Optional |
| Medium team (4-10) | Difficult | Recommended |
| Enterprise (10+) | No | Necessary |
| Compliance/Audit | Limited | Yes |
| Policy as Code | No | Yes (Sentinel) |
Terraform Cloud Setup
terraform {
cloud {
organization = "my-organization"
workspaces {
name = "my-app-production"
}
}
}
Sentinel Policies
Policy as Code for governance:
# policies/enforce-tags.sentinel
import "tfplan/v2" as tfplan
mandatory_tags = ["Environment", "Project", "Owner"]
aws_resources = filter tfplan.resource_changes as _, rc {
rc.provider_name matches "(.*)aws$" and
rc.mode is "managed" and
(rc.change.actions contains "create" or rc.change.actions contains "update")
}
tags_contain_mandatory = rule {
all aws_resources as _, resource {
all mandatory_tags as tag {
resource.change.after.tags contains tag
}
}
}
main = rule {
tags_contain_mandatory
}
CI/CD with Terraform
GitHub Actions
.github/workflows/terraform.yml:
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TF_VERSION: 1.8.0
AWS_REGION: eu-west-1
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: environments/production
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: $
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: $
aws-secret-access-key: $
aws-region: $
- name: Terraform Format Check
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
continue-on-error: true
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
Security Best Practices
Secrets Management
NEVER commit secrets. Use:
# AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/database/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
# HashiCorp Vault
data "vault_generic_secret" "db" {
path = "secret/database"
}
resource "aws_db_instance" "main" {
password = data.vault_generic_secret.db.data["password"]
}
Least Privilege IAM
# Minimal policy for Terraform
data "aws_iam_policy_document" "terraform" {
statement {
effect = "Allow"
actions = [
"ec2:*",
"rds:*",
"s3:*"
]
resources = ["*"]
condition {
test = "StringEquals"
variable = "aws:RequestedRegion"
values = ["eu-west-1"]
}
}
}
State Encryption
# S3 backend with encryption
terraform {
backend "s3" {
bucket = "terraform-state"
key = "production/terraform.tfstate"
region = "eu-west-1"
encrypt = true
kms_key_id = "alias/terraform-state-key"
dynamodb_table = "terraform-locks"
}
}
Performance and Optimization
Parallelism
# Increase parallelism (default: 10)
terraform apply -parallelism=20
# Reduce for rate limiting
terraform apply -parallelism=5
Specific Targets
# Apply only specific resources
terraform apply -target=module.networking
terraform apply -target=aws_instance.web[0]
Selective Refresh
# Skip refresh (faster, less safe)
terraform plan -refresh=false
# Refresh only specific resources
terraform apply -refresh-only -target=aws_instance.web
Best Practices Checklist
Code
- Use modules for reusable code
- Variables with types and validation
- Documented outputs
- README for each module
- Consistent formatting (
terraform fmt)
State
- Remote state with locking
- State isolation per environment
- Encryption at rest
- Automatic backups
CI/CD
- Plan on every PR
- Apply only from main
- Mandatory review
- Automated tests
Security
- No secrets in code
- Least privilege IAM
- State encryption
- Audit logging
Conclusions
Terraform in 2026 is more mature and powerful than ever. New features like the native test framework, declarative import, and provider-defined functions make IaC development more productive.
Key recommendations:
- Modular structure from the start
- Remote state with locking always
- CI/CD for every environment
- Tests for critical modules
- Terraform Cloud for teams > 3 people