41 pages Β· 8 sections
Ctrl K
GitHub Portfolio

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

CapabilityOpen SourceTerraform Cloud (Free/Team)Terraform Enterprise
Local CLI executionYesYesYes
Remote state storageS3/GCS/Azure (self-managed)Managed, encrypted at restManaged, encrypted at rest
State lockingDynamoDB (self-managed)AutomaticAutomatic
Remote execution (runs)NoYesYes
VCS-driven workflowsNoYesYes
Cost estimationNoYesYes
Module registryNoPrivate module registryPrivate module registry
Sentinel policy as codeNoBusiness tier onlyYes
Run tasks (custom hooks)NoYesYes
SSO / SAML 2.0NoBusiness tierYes
Audit loggingNoYesYes
Self-hosted deploymentN/ANo (SaaS)Yes (air-gapped supported)
Agent-based execution (private infra)N/AAvailableYes
Drift detectionNoContinuous validationContinuous 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:

  1. Developer commits changes to Terraform configuration in Git
  2. TFE detects the change via webhook and queues a plan
  3. Plan phase executes in isolated runner; results posted to PR
  4. Policy checks run (Sentinel policies validate the plan)
  5. Human approval required for applies to production workspaces
  6. Apply executes the planned changes automatically or on approval
  7. 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 LevelManage WorkspacesExecute RunsView StateManage Teams
Organization OwnerYesYesYesYes
Organization ManagerYesYesYesNo
Project AdminWithin projectYesYesNo
Maintainer (Workspace)Specific workspaceYesYesNo
Write (Workspace)NoPlan + apply own runsYesNo
Plan (Workspace)NoPlan onlyYesNo
Read (Workspace)NoNoYesNo

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

PracticeDescriptionImplementation
Never edit state manuallyState files should only be modified by TerraformUse terraform import, state mv, state rm
State encryptionEncrypt state at restTFE encrypts by default; for S3 use SSE-KMS
State lockingPrevent concurrent modificationsDynamoDB for S3; automatic for TFE
State versioningEnable versioning for recoveryS3 versioning enabled on state bucket
Least privilege accessLimit who can read/write stateTFE RBAC; IAM policies for S3
State separationSeparate state per componentDifferent state keys or workspaces
Remote state data sourcesRead outputs from other statesdata.terraform_remote_state
State backupRegular state backupsS3 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
}
Warning: Remote state data sources create workspace dependencies. If the upstream workspace's outputs change, downstream workspaces must be planned and applied to pick up the new values. Use run triggers to automate this cascade.

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
}