Terraform Cloud, Enterprise & CI/CD
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
| Feature | OSS | Terraform Cloud (Free) | Terraform Cloud (Plus) | TFE |
|---|---|---|---|---|
| Remote state | Manual (S3/GCS) | Included | Included | Included |
| Remote execution | No | Included | Included | Included |
| State locking | Manual (DynamoDB) | Automatic | Automatic | Automatic |
| VCS integration | No | Yes | Yes | Yes |
| Sentinel policies | No | No | Yes | Yes |
| OPA policies | No | No | Yes | Yes |
| Private registry | No | Yes | Yes | Yes |
| Audit logging | No | No | Yes | Yes |
| SSO/SAML | No | No | Yes | Yes |
| Self-hosted | N/A | No | No | Yes |
2. Connecting to Terraform Cloud — the cloud Block
Replace the backend block with a cloud block to connect a config to Terraform Cloud:
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}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}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 TFC3. TFC Workspaces vs CLI Workspaces
This distinction is a critical exam topic:
| Aspect | CLI Workspaces | Terraform Cloud Workspaces |
|---|---|---|
| What they are | Multiple state files for one config | Full isolated execution environments |
| Config sharing | All workspaces share same .tf files | Each workspace can have its own VCS repo/branch |
| Variables | Use terraform.workspace in code | Per-workspace variable sets |
| State | Separate state file per workspace | Separate, encrypted, versioned state |
| Execution | Local machine | Remote TFC runners |
| Access control | File system permissions | Per-workspace team assignments |
| Run history | Not tracked | Full audit trail with who approved |
| Recommended for | Simple state isolation | All production workflows |
4. TFC Workspace Features
Each TFC workspace is a complete deployment environment with:
Workspace variable types:
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
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 applyfrom local terminal; execution happens remotely - API-driven: Triggered by external systems (CI/CD, scripts); full programmatic control
6. VCS Integration Workflow
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:
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 }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 }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:
| Level | On Fail | Override | Use For |
|---|---|---|---|
advisory | Warning in run output; apply proceeds | N/A | Non-blocking reminders, informational |
soft-mandatory | Apply blocked | Workspace admin can override | Policy with legitimate exceptions |
hard-mandatory | Apply blocked | No override possible | Absolute compliance requirements |
8. OPA (Open Policy Agent) Policies
OPA is an alternative policy engine to Sentinel, using Rego policy language:
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:
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:
- Connect a VCS repo with the naming convention
terraform-<PROVIDER>-<NAME>(e.g.,terraform-aws-vpc) - Tag a release (e.g.,
v1.0.0) — TFC auto-detects semantic version tags - 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)
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: tfplanMain branch workflow (apply on merge)
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 tfplan12. CI/CD with GitLab CI — Full Pattern
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: production13. Terraform Enterprise — Additional Features
TFE adds everything in TFC Plus, plus:
| Feature | Detail |
|---|---|
| Self-hosted | Deploy in your own data center or private cloud |
| Air-gapped | No internet access required — all downloads cached internally |
| SAML/SSO | Integrate with Okta, Azure AD, Active Directory |
| Audit logging | Detailed audit trail exported to SIEM systems |
| Clustering | Active/active HA deployment |
| Custom resource limits | Configure concurrent runs, workspace limits |
| Archivist | Customizable state and log storage backends |
14. CI/CD Best Practices Summary
| Practice | Why |
|---|---|
| Plan on PR, apply on merge | Humans review what will change before it changes |
Save plan with -out; apply the file | Guarantees apply matches the reviewed plan exactly |
| OIDC not static keys | No long-lived credentials to rotate or leak |
| Pin Terraform version in CI | Prevents unexpected behavior from version drift |
Commit .terraform.lock.hcl | Reproducible provider versions across all runners |
Never -auto-approve on prod without gates | Human or policy approval required |
| Separate plan and apply IAM roles | Plan role is read-only; apply role is write — principle of least privilege |
| Run security scan before plan | Catch misconfigurations before they reach the API |
| Post plan as PR comment | Reviewers see infrastructure diff alongside code diff |
| Use GitHub Environments | Required reviewers + deployment protection rules for prod |
15. Quick Reference
| Concept | Key Fact |
|---|---|
cloud block | Replaces backend block for TFC/TFE connection |
| TFC workspace | Full execution environment — not just state isolation |
| CLI workspace vs TFC workspace | CLI = state isolation only; TFC = full environment |
| Speculative plan | Plan on PR — shows diff, does NOT modify state |
| VCS-driven run | Auto-plan on push/PR; auto or manual apply on merge |
| CLI-driven run | terraform apply from local — execution happens on TFC |
| Sentinel advisory | Warning; apply proceeds |
| Sentinel soft-mandatory | Blocked; authorized override available |
| Sentinel hard-mandatory | Blocked; no override |
| Private registry source | app.terraform.io/ORG/MODULE/PROVIDER |
| Module naming convention | terraform-<PROVIDER>-<NAME> repo name for auto-publishing |
| Variable sets | Share vars across multiple workspaces without repetition |
| TFE vs TFC | TFE is self-hosted; adds SSO, audit logging, air-gap support |
| OIDC in CI | No stored credentials; short-lived tokens per job |
| Saved plan pattern | -out=plan.tffile then apply plan.tffile — apply matches reviewed plan |
Practice Questions7
Q1. What is the main advantage of using Terraform Cloud's remote execution over running Terraform locally?
Select one answer before revealing.
Q2. What is a Sentinel policy in Terraform Cloud/Enterprise?
Select one answer before revealing.
Q3. Which approach correctly uses GitHub Actions to run `terraform plan` on pull requests with AWS authentication?
Select one answer before revealing.
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.
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.
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.
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.