/Terraform Cloud, Enterprise & CI/CD
Concept
Hard

Terraform Cloud, Enterprise & CI/CD

12 min read·terraform-cloudterraform-enterpriseremote-executionvcs-integrationsentinelopaprivate-registryvariable-setscicdgithub-actionsterraform-associate

Terraform Cloud (TFC) is HashiCorp's managed platform that adds remote execution, secure state storage, VCS integration, policy-as-code, and a private module registry on top of open-source Terraform. Terraform Enterprise (TFE) is the self-hosted equivalent. The exam tests TFC workspace features, the cloud block, run lifecycle, Sentinel enforcement levels, the private registry, and CI/CD pipeline patterns including OIDC auth and the plan-then-apply workflow.


1. OSS vs Terraform Cloud vs Terraform Enterprise

Rendering diagram…
FeatureOSSTerraform Cloud (Free)Terraform Cloud (Plus)TFE
Remote stateManual (S3/GCS)IncludedIncludedIncluded
Remote executionNoIncludedIncludedIncluded
State lockingManual (DynamoDB)AutomaticAutomaticAutomatic
VCS integrationNoYesYesYes
Sentinel policiesNoNoYesYes
OPA policiesNoNoYesYes
Private registryNoYesYesYes
Audit loggingNoNoYesYes
SSO/SAMLNoNoYesYes
Self-hostedN/ANoNoYes

2. Connecting to Terraform Cloud — the cloud Block

Replace the backend block with a cloud block to connect a config to Terraform Cloud:

hcl
1terraform {
2  required_version = ">= 1.1.0"
3
4  cloud {
5    organization = "my-company"       # TFC organization name
6
7    workspaces {
8      name = "production-app"         # single named workspace
9    }
10  }
11
12  required_providers {
13    aws = {
14      source  = "hashicorp/aws"
15      version = "~> 5.0"
16    }
17  }
18}
hcl
1# OR — connect to all workspaces with a specific tag
2terraform {
3  cloud {
4    organization = "my-company"
5    workspaces {
6      tags = ["app", "production"]   # all workspaces tagged app + production
7    }
8  }
9}
bash
1# Authenticate CLI to Terraform Cloud
2terraform login   # opens browser → generate API token → stored in ~/.terraform.d/credentials.tfrc.json
3
4# Then initialize
5terraform init    # downloads providers; state is now managed by TFC

3. TFC Workspaces vs CLI Workspaces

This distinction is a critical exam topic:

AspectCLI WorkspacesTerraform Cloud Workspaces
What they areMultiple state files for one configFull isolated execution environments
Config sharingAll workspaces share same .tf filesEach workspace can have its own VCS repo/branch
VariablesUse terraform.workspace in codePer-workspace variable sets
StateSeparate state file per workspaceSeparate, encrypted, versioned state
ExecutionLocal machineRemote TFC runners
Access controlFile system permissionsPer-workspace team assignments
Run historyNot trackedFull audit trail with who approved
Recommended forSimple state isolationAll production workflows

4. TFC Workspace Features

Each TFC workspace is a complete deployment environment with:

Rendering diagram…

Workspace variable types:

hcl
1# In TFC UI or via API — set per workspace or via variable sets:
2
3# Terraform variables (equivalent to tfvars)
4environment = "production"
5instance_count = 3
6
7# Environment variables (available to Terraform process)
8AWS_DEFAULT_REGION = "us-east-1"
9TF_LOG = "INFO"
10
11# Sensitive variables (write-only in UI — value never shown after setting)
12# TF_VAR_db_password = "..." (marked sensitive)
13# AWS_SECRET_ACCESS_KEY = "..." (marked sensitive)

5. The TFC Run Lifecycle

Rendering diagram…

Run modes:

  • VCS-driven: Plan triggered on every commit to the connected branch; apply requires manual confirmation (or auto-apply if configured)
  • CLI-driven: terraform plan / terraform apply from local terminal; execution happens remotely
  • API-driven: Triggered by external systems (CI/CD, scripts); full programmatic control

6. VCS Integration Workflow

Rendering diagram…

Speculative plans run on PRs — they show what would change but do NOT modify state or create resources. Safe to run on every PR.


7. Sentinel — Policy-as-Code Deep Dive

Sentinel policies run as a gate between plan output and apply. They inspect the planned changes and can block applies that violate organizational policy:

python
1# sentinel/required-tags.sentinel
2import "tfplan/v2" as tfplan
3
4required_tags = ["Environment", "Owner", "CostCenter"]
5
6# Get all managed resource changes
7all_resources = filter tfplan.resource_changes as _, rc {
8  rc.mode is "managed" and rc.change.actions contains "create"
9}
10
11# Check that every new resource has all required tags
12tags_enforced = rule {
13  all all_resources as _, resource {
14    all required_tags as tag {
15      resource.change.after.tags contains tag
16    }
17  }
18}
19
20main = rule { tags_enforced }
python
1# sentinel/restrict-regions.sentinel
2import "tfplan/v2" as tfplan
3import "tfconfig/v2" as tfconfig
4
5allowed_regions = ["us-east-1", "us-west-2", "eu-west-1"]
6
7all_providers = tfconfig.providers
8
9region_valid = rule {
10  all all_providers as _, provider {
11    provider.config.region.constant_value in allowed_regions
12  }
13}
14
15main = rule { region_valid }
python
1# sentinel.hcl — policy set configuration
2policy "required-tags" {
3  source            = "./required-tags.sentinel"
4  enforcement_level = "hard-mandatory"   # advisory | soft-mandatory | hard-mandatory
5}
6
7policy "restrict-regions" {
8  source            = "./restrict-regions.sentinel"
9  enforcement_level = "soft-mandatory"
10}
11
12policy "cost-limit" {
13  source            = "./cost-limit.sentinel"
14  enforcement_level = "advisory"
15}

Enforcement levels:

LevelOn FailOverrideUse For
advisoryWarning in run output; apply proceedsN/ANon-blocking reminders, informational
soft-mandatoryApply blockedWorkspace admin can overridePolicy with legitimate exceptions
hard-mandatoryApply blockedNo override possibleAbsolute compliance requirements

8. OPA (Open Policy Agent) Policies

OPA is an alternative policy engine to Sentinel, using Rego policy language:

rego
1# policies/deny-public-s3.rego
2package terraform.deny_public_s3
3
4import future.keywords.in
5
6deny[msg] {
7  resource := input.planned_values.root_module.resources[_]
8  resource.type == "aws_s3_bucket_acl"
9  resource.values.acl in ["public-read", "public-read-write", "authenticated-read"]
10  msg := sprintf("S3 bucket %v has public ACL %v — public buckets not allowed",
11    [resource.name, resource.values.acl])
12}

9. Private Module Registry

TFC/TFE hosts a private module registry for sharing internal modules across teams:

Rendering diagram…
hcl
1# Consuming a module from TFC private registry
2# Source format: <TFC_HOSTNAME>/<ORGANIZATION>/<MODULE_NAME>/<PROVIDER>
3terraform {
4  cloud {
5    organization = "my-company"
6    workspaces { name = "production" }
7  }
8}
9
10module "vpc" {
11  source  = "app.terraform.io/my-company/vpc/aws"
12  version = "~> 2.0"
13
14  cidr_block  = "10.0.0.0/16"
15  environment = var.environment
16}

Publishing a module to TFC private registry:

  1. Connect a VCS repo with the naming convention terraform-<PROVIDER>-<NAME> (e.g., terraform-aws-vpc)
  2. Tag a release (e.g., v1.0.0) — TFC auto-detects semantic version tags
  3. Module appears in registry with auto-generated documentation from README and variable descriptions

10. Variable Sets

Variable sets allow sharing variables across multiple workspaces — eliminating repetition:

TFC Variable Set: "AWS Production Credentials" AWS_ACCESS_KEY_ID = *** (sensitive env var) AWS_SECRET_ACCESS_KEY = *** (sensitive env var) AWS_DEFAULT_REGION = us-east-1 Applied to workspaces: production-app production-network production-database → All three workspaces automatically have AWS credentials TFC Variable Set: "Common Tags" TF_VAR_owner = platform-team TF_VAR_cost_center = engineering TF_VAR_managed_by = terraform Applied globally to all workspaces in organization

11. CI/CD with GitHub Actions — Full Pattern

PR workflow (plan only, no apply)

yaml
1# .github/workflows/terraform-plan.yml
2name: Terraform Plan
3
4on:
5  pull_request:
6    branches: [main]
7    paths: ['**.tf', '**.tfvars']
8
9jobs:
10  plan:
11    name: Terraform Plan
12    runs-on: ubuntu-latest
13    permissions:
14      id-token: write      # required for OIDC
15      contents: read
16      pull-requests: write # post plan output as PR comment
17
18    steps:
19      - uses: actions/checkout@v4
20
21      - uses: hashicorp/setup-terraform@v3
22        with:
23          terraform_version: 1.7.5
24          cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}  # TFC token
25
26      # OIDC auth to AWS (no stored access keys)
27      - uses: aws-actions/configure-aws-credentials@v4
28        with:
29          role-to-assume: arn:aws:iam::123456789012:role/github-actions-plan
30          aws-region: us-east-1
31
32      - name: Terraform Init
33        run: terraform init
34
35      - name: Terraform Validate
36        run: terraform validate
37
38      - name: Terraform Format Check
39        run: terraform fmt -check -recursive
40
41      - name: Terraform Plan
42        id: plan
43        run: terraform plan -no-color -out=tfplan
44        continue-on-error: true   # post comment even if plan fails
45
46      # Post plan output as PR comment
47      - uses: actions/github-script@v7
48        with:
49          script: |
50            const output = `#### Terraform Plan \`${{ steps.plan.outcome }}\`
51            <details><summary>Show Plan</summary>
52
53            \`\`\`terraform
54            ${{ steps.plan.outputs.stdout }}
55            \`\`\`
56            </details>
57
58            *Pushed by: @${{ github.actor }}, Action: `${{ github.event_name }}`*`;
59
60            github.rest.issues.createComment({
61              issue_number: context.issue.number,
62              owner: context.repo.owner,
63              repo: context.repo.repo,
64              body: output
65            })
66
67      - name: Upload Plan Artifact
68        uses: actions/upload-artifact@v4
69        with:
70          name: tfplan
71          path: tfplan

Main branch workflow (apply on merge)

yaml
1# .github/workflows/terraform-apply.yml
2name: Terraform Apply
3
4on:
5  push:
6    branches: [main]
7    paths: ['**.tf', '**.tfvars']
8
9jobs:
10  apply:
11    name: Terraform Apply
12    runs-on: ubuntu-latest
13    environment: production   # GitHub environment with required reviewers
14    permissions:
15      id-token: write
16      contents: read
17
18    steps:
19      - uses: actions/checkout@v4
20
21      - uses: hashicorp/setup-terraform@v3
22        with:
23          terraform_version: 1.7.5
24
25      - uses: aws-actions/configure-aws-credentials@v4
26        with:
27          role-to-assume: arn:aws:iam::123456789012:role/github-actions-apply
28          aws-region: us-east-1
29
30      - run: terraform init
31      - run: terraform validate
32
33      - name: Terraform Plan (pre-apply)
34        run: terraform plan -out=tfplan
35
36      - name: Terraform Apply
37        run: terraform apply -auto-approve tfplan

12. CI/CD with GitLab CI — Full Pattern

yaml
1# .gitlab-ci.yml
2image: hashicorp/terraform:1.7.5
3
4variables:
5  TF_ROOT: ${CI_PROJECT_DIR}
6  TF_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_SLUG}"
7
8stages:
9  - validate
10  - plan
11  - apply
12
13cache:
14  paths:
15    - ${TF_ROOT}/.terraform
16
17before_script:
18  - terraform --version
19  - cd ${TF_ROOT}
20  - terraform init
21    -backend-config="address=${TF_ADDRESS}"
22    -backend-config="lock_address=${TF_ADDRESS}/lock"
23    -backend-config="unlock_address=${TF_ADDRESS}/lock"
24    -backend-config="username=gitlab-ci-token"
25    -backend-config="password=${CI_JOB_TOKEN}"
26
27validate:
28  stage: validate
29  script:
30    - terraform validate
31    - terraform fmt -check -recursive
32
33plan:
34  stage: plan
35  script:
36    - terraform plan -out=plan.tfplan -no-color
37  artifacts:
38    name: terraform-plan
39    paths:
40      - plan.tfplan
41    reports:
42      terraform: plan.json   # GitLab MR widget integration
43
44apply:
45  stage: apply
46  script:
47    - terraform apply -auto-approve plan.tfplan
48  dependencies:
49    - plan
50  only:
51    - main
52  when: manual   # requires manual trigger in GitLab UI
53  environment:
54    name: production

13. Terraform Enterprise — Additional Features

TFE adds everything in TFC Plus, plus:

FeatureDetail
Self-hostedDeploy in your own data center or private cloud
Air-gappedNo internet access required — all downloads cached internally
SAML/SSOIntegrate with Okta, Azure AD, Active Directory
Audit loggingDetailed audit trail exported to SIEM systems
ClusteringActive/active HA deployment
Custom resource limitsConfigure concurrent runs, workspace limits
ArchivistCustomizable state and log storage backends

14. CI/CD Best Practices Summary

Rendering diagram…
PracticeWhy
Plan on PR, apply on mergeHumans review what will change before it changes
Save plan with -out; apply the fileGuarantees apply matches the reviewed plan exactly
OIDC not static keysNo long-lived credentials to rotate or leak
Pin Terraform version in CIPrevents unexpected behavior from version drift
Commit .terraform.lock.hclReproducible provider versions across all runners
Never -auto-approve on prod without gatesHuman or policy approval required
Separate plan and apply IAM rolesPlan role is read-only; apply role is write — principle of least privilege
Run security scan before planCatch misconfigurations before they reach the API
Post plan as PR commentReviewers see infrastructure diff alongside code diff
Use GitHub EnvironmentsRequired reviewers + deployment protection rules for prod

15. Quick Reference

ConceptKey Fact
cloud blockReplaces backend block for TFC/TFE connection
TFC workspaceFull execution environment — not just state isolation
CLI workspace vs TFC workspaceCLI = state isolation only; TFC = full environment
Speculative planPlan on PR — shows diff, does NOT modify state
VCS-driven runAuto-plan on push/PR; auto or manual apply on merge
CLI-driven runterraform apply from local — execution happens on TFC
Sentinel advisoryWarning; apply proceeds
Sentinel soft-mandatoryBlocked; authorized override available
Sentinel hard-mandatoryBlocked; no override
Private registry sourceapp.terraform.io/ORG/MODULE/PROVIDER
Module naming conventionterraform-<PROVIDER>-<NAME> repo name for auto-publishing
Variable setsShare vars across multiple workspaces without repetition
TFE vs TFCTFE is self-hosted; adds SSO, audit logging, air-gap support
OIDC in CINo stored credentials; short-lived tokens per job
Saved plan pattern-out=plan.tffile then apply plan.tffile — apply matches reviewed plan

Practice Questions7

hard

Q1. What is the main advantage of using Terraform Cloud's remote execution over running Terraform locally?


Select one answer before revealing.

hard

Q2. What is a Sentinel policy in Terraform Cloud/Enterprise?


Select one answer before revealing.

hard

Q3. Which approach correctly uses GitHub Actions to run `terraform plan` on pull requests with AWS authentication?


Select one answer before revealing.

hard

Q4. Which Terraform feature allows you to write a policy that enforces "all S3 buckets must have versioning enabled" before changes are applied in Terraform Enterprise?


Select one answer before revealing.

hard

Q5. Which of the following are features of Terraform Cloud that are NOT available in the open-source Terraform CLI? (Select all that apply — more than one answer may be correct.)


Select one answer before revealing.

hard

Q6. Scenario: You are setting up a CI/CD pipeline where `terraform plan` runs on every pull request and `terraform apply` runs after merge to `main`. A plan output from PR #42 shows 3 changes. PR #43 merges first, modifying the same infrastructure. When PR #42 merges and applies, what is the risk?


Select one answer before revealing.

hard

Q7. Scenario: Your GitHub Actions workflow runs `terraform apply -auto-approve` directly without a prior `terraform plan` review. A security engineer flags this as a risk. What specific dangers does this introduce?


Select one answer before revealing.