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

ScenarioTerraform OSSTerraform Cloud
Small team (1-3)YesOptional
Medium team (4-10)DifficultRecommended
Enterprise (10+)NoNecessary
Compliance/AuditLimitedYes
Policy as CodeNoYes (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:

  1. Modular structure from the start
  2. Remote state with locking always
  3. CI/CD for every environment
  4. Tests for critical modules
  5. Terraform Cloud for teams > 3 people

Resources