Terraform Enterprise / Cloud
Terraform Enterprise provides collaboration, governance, and self-service workflows for Infrastructure-as-Code at scale. This guide covers workspace architecture, module registry, and RBAC based on production deployments managing hundreds of workspaces across multiple business units.
TFE vs. Terraform Cloud vs. Open Source
| Capability | Open Source | Terraform Cloud (Free/Team) | Terraform Enterprise |
|---|---|---|---|
| Local CLI execution | Yes | Yes | Yes |
| Remote state storage | S3/GCS/Azure (self-managed) | Managed, encrypted at rest | Managed, encrypted at rest |
| State locking | DynamoDB (self-managed) | Automatic | Automatic |
| Remote execution (runs) | No | Yes | Yes |
| VCS-driven workflows | No | Yes | Yes |
| Cost estimation | No | Yes | Yes |
| Module registry | No | Private module registry | Private module registry |
| Sentinel policy as code | No | Business tier only | Yes |
| Run tasks (custom hooks) | No | Yes | Yes |
| SSO / SAML 2.0 | No | Business tier | Yes |
| Audit logging | No | Yes | Yes |
| Self-hosted deployment | N/A | No (SaaS) | Yes (air-gapped supported) |
| Agent-based execution (private infra) | N/A | Available | Yes |
| Drift detection | No | Continuous validation | Continuous validation |
Workspace-per-Environment Architecture
The workspace-per-environment model is the recommended pattern for Terraform at scale. Each logical component of your infrastructure gets a workspace for each environment:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Terraform Enterprise Organization β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Project: Networking β β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββ β β
β β β vpc-dev β β vpc-staging β β vpc-prodβ β β
β β β (workspace) β β (workspace) β β(workspace)β β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Project: Compute β β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββ β β
β β β eks-dev β β eks-staging β β eks-prodβ β β
β β β (workspace) β β (workspace) β β(workspace)β β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Project: Databases β β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββ β β
β β β rds-dev β β rds-staging β β rds-prodβ β β
β β β (workspace) β β (workspace) β β(workspace)β β
β β ββββββββββββββββ ββββββββββββββββ βββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Workspace Naming Convention
# Naming pattern: {component}-{environment}
# Examples:
vpc-dev
vpc-staging
vpc-prod
eks-dev
eks-staging
eks-prod
# For multi-region deployments:
vpc-prod-us-east-1
vpc-prod-us-west-2
VCS-Driven Workflows (GitOps for Terraform)
VCS-driven workflows are the cornerstone of Terraform Enterprise. Every Terraform run is triggered by changes in version control:
- Developer commits changes to Terraform configuration in Git
- TFE detects the change via webhook and queues a plan
- Plan phase executes in isolated runner; results posted to PR
- Policy checks run (Sentinel policies validate the plan)
- Human approval required for applies to production workspaces
- Apply executes the planned changes automatically or on approval
- State is updated and stored securely in Terraform Enterprise
Connecting a GitHub Repository to TFE
# Step 1: In TFE, go to Organization Settings > VCS Providers > Add VCS Provider
# Select GitHub (OAuth) or GitHub Enterprise
# Step 2: Create a new workspace
# Settings > Workspaces > New Workspace
# Choose "Version control workflow"
# Select the VCS provider
# Choose the repository (e.g., my-org/terraform-aws-vpc)
# Set workspace name: vpc-prod
# Step 3: Configure workspace settings
# Terraform Working Directory: environments/prod (if applicable)
# Terraform Version: 1.7.0 (pin for consistency)
# Auto Apply: false for production (manual approval required)
# Terraform Variables: Set via UI or *.auto.tfvars files
# Step 4: Configure Run Triggers (optional)
# Set upstream workspaces that trigger this workspace
# e.g., vpc-prod triggers eks-prod on successful apply
Module Registry
The Terraform Enterprise module registry provides a central catalog of approved, versioned modules. It enforces standards and accelerates infrastructure delivery.
Publishing Modules
# Module repository structure
terraform-aws-vpc/
βββ main.tf
βββ variables.tf
βββ outputs.tf
βββ README.md # Required: usage documentation
βββ versions.tf
βββ examples/
βββ complete/
β βββ main.tf # Reference example
β βββ variables.tf
β βββ outputs.tf
βββ minimal/
βββ main.tf
# Publishing process:
# 1. Push module code to Git repository
# 2. In TFE, go to Registry > Publish > Module
# 3. Select the VCS provider and repository
# 4. TFE automatically detects semantic version tags
# 5. Publish v1.0.0, v1.1.0, etc.
# Required: Repository must have SemVer tags (v1.0.0)
git tag -a v1.0.0 -m "Initial release"
git push origin v1.0.0
Consuming Registry Modules
# Reference a private registry module
module "vpc" {
source = "app.terraform.io/my-org/vpc/aws"
version = "~> 1.2.0"
name = "production-vpc"
cidr = "10.0.0.0/16"
availability_zones = ["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"]
tags = {
Environment = "production"
Team = "platform"
}
}
RBAC: Teams and Permissions
Terraform Enterprise uses a hierarchical permission model: Organization β Project β Workspace.
| Permission Level | Manage Workspaces | Execute Runs | View State | Manage Teams |
|---|---|---|---|---|
| Organization Owner | Yes | Yes | Yes | Yes |
| Organization Manager | Yes | Yes | Yes | No |
| Project Admin | Within project | Yes | Yes | No |
| Maintainer (Workspace) | Specific workspace | Yes | Yes | No |
| Write (Workspace) | No | Plan + apply own runs | Yes | No |
| Plan (Workspace) | No | Plan only | Yes | No |
| Read (Workspace) | No | No | Yes | No |
Recommended Team Structure
# Organization: my-org
#
# Teams:
# βββ platform-admins (Organization Owner)
# βββ platform-engineers (Project Admin on all projects)
# βββ sre-team (Maintainer on compute, networking)
# βββ database-team (Maintainer on database workspaces)
# βββ security-team (Read on all + Write on security)
# βββ developers (Plan on dev workspaces only)
# βββ auditors (Read on all workspaces)
Sentinel Policy-as-Code
Sentinel is HashiCorp's policy-as-code framework for Terraform Enterprise. It enforces governance rules before apply:
# sentinel/restrict-instance-types.sentinel
# Policy: Only allow approved instance types
import "tfplan"
# Approved instance types by environment
approved_types = {
"dev": ["t3.micro", "t3.small", "t3.medium"],
"staging": ["t3.medium", "t3.large", "t3.xlarge"],
"prod": ["t3.large", "t3.xlarge", "t3.2xlarge", "m6i.large"],
}
# Get all AWS instances from the plan
aws_instances = filter tfplan.resource_changes as _, rc {
rc.type is "aws_instance" and
(rc.change.actions contains "create" or rc.change.actions contains "update")
}
# Validate each instance
violations = 0
for aws_instances as _, instance {
instance_type = instance.change.after.instance_type
workspace_env = strings.split(tfplan.workspace_name, "-")[-1]
allowed = approved_types[workspace_env] else approved_types["dev"]
if instance_type not in allowed {
violations += 1
print("Violation:", instance.address,
"uses instance type", instance_type,
"which is not approved for", workspace_env,
"environment. Allowed:", allowed)
}
}
main = rule {
violations is 0
}
# sentinel/enforce-tags.sentinel
# Policy: All resources must have required tags
import "tfplan"
import "strings"
required_tags = ["Environment", "Project", "Owner", "CostCenter", "ManagedBy"]
# Get all resources being created
all_resources = filter tfplan.resource_changes as _, rc {
rc.change.actions contains "create"
}
violations = 0
for all_resources as _, resource {
tags = resource.change.after.tags else {}
for required_tags as tag {
if tags[tag] is empty {
violations += 1
print("Violation:", resource.address,
"is missing required tag:", tag)
}
}
}
main = rule {
violations is 0
}
Policy Set Configuration
# In TFE UI:
# 1. Go to Organization Settings > Policy Sets
# 2. Create new policy set "production-governance"
# 3. Add policies: restrict-instance-types, enforce-tags
# 4. Scope to workspaces with glob pattern: "*-prod"
# 5. Set enforcement level: hard-mandatory (fails run) or soft-mandatory (advisory)
Run Triggers and Workspace Dependencies
Run triggers allow one workspace to automatically trigger runs in downstream workspaces:
# Workspace dependency chain:
# vpc-prod (applied) β eks-prod (auto-triggered) β app-prod (auto-triggered)
#
# Configuration in TFE:
# 1. Go to eks-prod workspace > Settings > Run Triggers
# 2. Add source workspace: vpc-prod
# 3. Now when vpc-prod apply succeeds, eks-prod automatically plans
# For complex dependencies, use tfe_run_trigger resource:
resource "tfe_run_trigger" "vpc_to_eks" {
workspace_id = tfe_workspace.eks_prod.id
sourceable_id = tfe_workspace.vpc_prod.id
}
resource "tfe_run_trigger" "eks_to_app" {
workspace_id = tfe_workspace.app_prod.id
sourceable_id = tfe_workspace.eks_prod.id
}
Complete Example: Setting Up a Workspace with GitHub Integration
# main.tf β Terraform configuration managed by TFE
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
tfe = {
source = "hashicorp/tfe"
version = "~> 0.51"
}
}
# TFE manages its own state β no backend block needed
}
provider "tfe" {
hostname = "app.terraform.io"
# Token set via TFE_TOKEN environment variable
}
provider "aws" {
region = var.aws_region
}
# ββ Variables ββββββββββββββββββββββββββββββββββββββββββββ
variable "organization" {
type = string
default = "my-org"
}
variable "aws_region" {
type = string
default = "us-east-1"
}
variable "oauth_token_id" {
type = string
description = "OAuth token ID for GitHub VCS integration"
}
# ββ Project ββββββββββββββββββββββββββββββββββββββββββββββ
resource "tfe_project" "compute" {
organization = var.organization
name = "Compute"
}
# ββ Workspace ββββββββββββββββββββββββββββββββββββββββββββ
resource "tfe_workspace" "eks_prod" {
organization = var.organization
name = "eks-prod"
project_id = tfe_project.compute.id
description = "Production EKS cluster"
# VCS integration
vcs_repo {
identifier = "my-org/terraform-aws-eks"
oauth_token_id = var.oauth_token_id
branch = "main"
ingress_submodules = false
}
# Working directory within the repo
working_directory = "environments/prod"
# Terraform version
terraform_version = "1.7.0"
# Execution mode
execution_mode = "remote" # Use TFE runners
# Auto-apply settings
auto_apply = false # Require manual approval
auto_apply_run_trigger = true # Auto-apply on run trigger
# Run triggers
trigger_patterns = ["environments/prod/**"]
# Global remote state sharing
global_remote_state = false
tags = ["production", "eks", "compute"]
}
# ββ Workspace Variables ββββββββββββββββββββββββββββββββββ
resource "tfe_variable" "eks_prod_region" {
key = "aws_region"
value = "us-east-1"
category = "terraform"
workspace_id = tfe_workspace.eks_prod.id
description = "AWS region for EKS cluster"
}
resource "tfe_variable" "eks_prod_environment" {
key = "environment"
value = "prod"
category = "terraform"
workspace_id = tfe_workspace.eks_prod.id
description = "Environment name"
}
# Sensitive variables (marked as sensitive in TFE)
resource "tfe_variable" "eks_prod_db_password" {
key = "db_password"
value = "PLACEHOLDER" # Set manually in UI or via data source
category = "terraform"
workspace_id = tfe_workspace.eks_prod.id
sensitive = true
description = "Database master password"
}
# Environment variables for AWS authentication
resource "tfe_variable" "eks_prod_aws_key" {
key = "AWS_ACCESS_KEY_ID"
value = var.aws_access_key_id
category = "env"
workspace_id = tfe_workspace.eks_prod.id
sensitive = true
}
resource "tfe_variable" "eks_prod_aws_secret" {
key = "AWS_SECRET_ACCESS_KEY"
value = var.aws_secret_access_key
category = "env"
workspace_id = tfe_workspace.eks_prod.id
sensitive = true
}
# ββ Team Access ββββββββββββββββββββββββββββββββββββββββββ
resource "tfe_team_access" "sre_eks" {
access = "maintain"
team_id = tfe_team.sre.id
workspace_id = tfe_workspace.eks_prod.id
}
resource "tfe_team_access" "devs_eks" {
access = "read"
team_id = tfe_team.developers.id
workspace_id = tfe_workspace.eks_prod.id
}
# ββ Outputs ββββββββββββββββββββββββββββββββββββββββββββββ
output "workspace_id" {
value = tfe_workspace.eks_prod.id
}
output "workspace_name" {
value = tfe_workspace.eks_prod.name
}
State Locking and Remote State Best Practices
| Practice | Description | Implementation |
|---|---|---|
| Never edit state manually | State files should only be modified by Terraform | Use terraform import, state mv, state rm |
| State encryption | Encrypt state at rest | TFE encrypts by default; for S3 use SSE-KMS |
| State locking | Prevent concurrent modifications | DynamoDB for S3; automatic for TFE |
| State versioning | Enable versioning for recovery | S3 versioning enabled on state bucket |
| Least privilege access | Limit who can read/write state | TFE RBAC; IAM policies for S3 |
| State separation | Separate state per component | Different state keys or workspaces |
| Remote state data sources | Read outputs from other states | data.terraform_remote_state |
| State backup | Regular state backups | S3 versioning + periodic snapshots |
# Reading remote state from another workspace
data "terraform_remote_state" "vpc" {
backend = "remote"
config = {
organization = "my-org"
workspaces = {
name = "vpc-prod"
}
}
}
# Use outputs from the VPC workspace
module "eks" {
source = "app.terraform.io/my-org/eks/aws"
cluster_name = "prod-cluster"
cluster_version = "1.29"
vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id
subnet_ids = data.terraform_remote_state.vpc.outputs.private_subnet_ids
}
Cost Estimation Integration
Terraform Enterprise integrates with Infracost to provide cost estimates before apply:
# Enable in TFE: Organization Settings > Cost Estimation
# The cost estimate appears in the run UI and can be used in Sentinel policies:
# sentinel/enforce-budget.sentinel
import "tfrun"
import "decimal"
max_monthly_cost = decimal.new("5000")
main = rule {
decimal.new(tfrun.cost_estimate.proposed_monthly_cost) <= max_monthly_cost
}