Scenario: Globomantics — Taco Wagon team (AWS infrastructure: VPC, subnets, Auto Scaling Group, ELB)
Table of Contents
- Overview and Objectives
- Module 1 — Automating Terraform
- Module 2 — Building a CI/CD Pipeline
- Module 3 — Multiple Environments
- Module 4 — HCP Terraform for Automation
- Architecture Diagrams
- Reference — Terraform Automation Tools
- Project File Structure
1. Overview and Objectives
This course covers complete automation of the Terraform workflow through GitOps practices: from local commits all the way to production deployment, including CI/CD pipelines and dedicated automation platforms.
Course Objectives
| # | Objective |
|---|---|
| 1 | Create automatic pre-commit hooks on local workstations to validate code |
| 2 | Build a Continuous Integration pipeline that validates code and generates plans |
| 3 | Create a Continuous Delivery pipeline that automates deployment to lower environments |
| 4 | Create a release pipeline that automates deployment to production |
Target Infrastructure (Taco Wagon)
AWS
├── VPC
├── Public Subnets
├── Auto Scaling Group
├── Elastic Load Balancer
└── Security Groups
Environments: dev → staging → prod-east || prod-west
2. Module 1 — Automating Terraform
Automation Planning
The standard Terraform workflow includes three stages that are candidates for automation:
flowchart LR
A[📝 Write code] --> B[✅ Validation\nfmt / validate / lint]
B --> C[📋 Plan\nReview changes]
C --> D{Approval?}
D -- Yes --> E[🚀 Apply]
D -- No --> A
E --> A
| Stage | Recommended Automation | Where to Run |
|---|---|---|
| Code validation | terraform fmt, terraform validate, tflint, terraform-docs | Locally (pre-commit) |
| Plan generation | terraform plan, terraform show, tf-summarize | CI/CD pipeline (remote) |
| Apply | terraform apply | CD pipeline (remote, controlled) |
GitOps Terminology
GitOps = using Git as the single source of truth to trigger infrastructure automation.
| Term | Definition |
|---|---|
| Repository | Code repository hosted on GitHub, GitLab, Bitbucket, CodeCommit… |
| Branch | Development branch isolated from main |
| Pull Request (PR) | Request to merge a branch into main — main CI trigger |
| Commit | Versioned unit of change |
| Merge | Integration of a branch into main — main CD trigger |
| Release Tag | Versioned tag (e.g., v1.0.0) — production pipeline trigger |
Validation and Automation Tools
| Tool | Role | Type |
|---|---|---|
terraform fmt | HCL code formatting | Official HashiCorp |
terraform validate | Syntax validation | Official HashiCorp |
terraform plan | Change plan generation | Official HashiCorp |
terraform show -json | Export plan as JSON | Official HashiCorp |
| TFLint | Advanced linting, best practices, provider-specific rules | Open source |
| terraform-docs | Automatic Markdown documentation generation | Open source |
| Trivy | Security scanning and misconfigurations | Aqua Security |
| Checkov | IaC static security analysis | Bridgecrew/Palo Alto |
| KICS | Multi-IaC security scanning | Checkmarx |
| tf-summarize | Human-readable Terraform plan summary | Open source |
| pre-commit | Git hook framework (Python) | Open source |
Pre-commit Hooks
Pre-commit hooks run automatically before each git commit. They block the commit if a check fails.
How they work:
- Hooks are defined in
.pre-commit-config.yaml - The
pre-committool (Python project) generates Git hooks from this YAML file - Hooks live in
.git/hooks/
.pre-commit-config.yaml
repos:
- repo: local
hooks:
# Format Terraform configuration files
- id: terraform-fmt
name: Terraform Format
entry: terraform fmt -recursive
language: system
files: \.tf$
pass_filenames: false
# Validate Terraform configuration files
- id: terraform-validate
name: Terraform Validate
entry: terraform validate
language: system
files: \.tf$
pass_filenames: false
# Linting with TFLint
- id: tflint
name: TFLint
entry: tflint --config=.tflint.hcl
language: system
files: \.tf$
pass_filenames: false
# Generate documentation from Terraform modules
- id: terraform-docs
name: Terraform Docs
entry: terraform-docs markdown table --output-file TACOWAGON.md --output-mode inject .
language: system
files: \.tf$
pass_filenames: false
.tflint.hcl — TFLint Configuration
plugin "aws" {
enabled = true
version = "0.31.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
Installation and usage commands:
# Install the pre-commit framework (Python required)
pip install pre-commit
# Install hooks in the local Git repo
pre-commit install
# Manually run all hooks
pre-commit run --all-files
# Skip hooks during a commit (avoid!)
git commit --no-verify
3. Module 2 — Building a CI/CD Pipeline
CI/CD Concepts for IaC
flowchart TD
subgraph CI["🔄 Continuous Integration"]
PR[PR created / updated] --> FMT[terraform fmt]
FMT --> VAL[terraform validate]
VAL --> LINT[tflint]
LINT --> SEC[Trivy / Checkov]
SEC --> PLAN[terraform plan]
PLAN --> SUM[tf-summarize → PR Comment]
end
subgraph CD["📦 Continuous Delivery"]
MERGE[PR merged to main] --> DEV[Apply → DEV]
DEV --> STAGING[Apply → STAGING]
end
subgraph PROD["🚀 Production Release"]
TAG[Release Tag v*] --> PE[Apply → PROD-EAST]
TAG --> PW[Apply → PROD-WEST]
end
CI --> CD
CD --> PROD
| Phase | Trigger | Environments | Action |
|---|---|---|---|
| CI | PR creation/update | All (dev, staging, prod-east, prod-west) | Validation + Plan |
| CD | PR merge to main | dev → staging (sequential) | Apply |
| Release | Push of a v* tag | prod-east || prod-west (parallel) | Apply |
Terraform Automation Considerations
Files to manage in .gitignore:
# Terraform initialization directory
.terraform/
# State files (never commit in prod)
*.tfstate
*.tfstate.backup
# Sensitive variables (optional depending on context)
*.tfvars
# Crash file
crash.log
crash.*.log
# Override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json
Important environment variables:
| Variable | Value | Effect |
|---|---|---|
TF_IN_AUTOMATION | true | Reduces interactive messages |
TF_LOG | INFO | Logging level |
TF_INPUT | false | Disables interactive prompts |
TF_PLUGIN_CACHE_DIR | /path/to/cache | Caches provider plugins |
The .terraform.lock.hcl file:
- Records the exact provider versions used during initialization
- Must be committed to source control to ensure consistency between machines and CI systems
- Prevents provider version drift
CI Pipeline — GitHub Actions
Terraform Backend Configuration (S3 backend)
# terraform.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~>6.0"
}
}
backend "s3" {
# Use backend files: backends/*.hcl
# terraform init -backend-config="./backends/dev.hcl"
}
}
# backends/dev.hcl
key = "tw-dev.tfstate"
bucket = "my-terraform-state-bucket"
region = "us-east-1"
Complete CI Workflow (.github/workflows/ci.yml) — Module 2
name: Terraform CI
on:
pull_request:
branches:
- main
env:
TF_LOG: INFO
TF_INPUT: false
TF_VERSION: ${{ vars.TF_VERSION }}
jobs:
# Format check
format:
name: Terraform Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format Check
run: terraform fmt -check -recursive
# Syntax validation
validate:
name: Terraform Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- run: terraform init -backend=false
- run: terraform validate
# Linting
tflint:
name: TFLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: terraform-linters/setup-tflint@v4
- run: tflint --init
- run: tflint --recursive
# Security scan
trivy:
name: Trivy Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@master
with:
scan-type: config
scan-ref: .
exit-code: 1
severity: CRITICAL
# Plan generation
plan:
name: Terraform Plan Dev
runs-on: ubuntu-latest
needs: [format, validate, tflint, trivy]
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- run: terraform init -backend-config="./backends/dev.hcl"
- run: terraform plan -var-file="./environments/dev.tfvars" -out=tf.plan
# Save plan as artifact
- uses: actions/upload-artifact@v4
with:
name: tf-plan-dev-${{ github.run_id }}
path: tf.plan
retention-days: 5
# Plan summary as PR comment
- uses: kishaningithub/setup-tf-summarize@v2
- name: Generate Plan Summary
run: |
terraform show -json tf.plan > plan.json
tf-summarize -md plan.json > summary.md
- uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const summary = fs.readFileSync('summary.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Terraform Plan Summary - dev\n\n${summary}\n\n*Run ID: ${{ github.run_id }}*`
});
CD Pipeline — Continuous Delivery
Deployment Trigger Options
flowchart TD
PR[Pull Request opened] --> CI[CI Pipeline]
CI --> PLAN[Plan generated + artifact saved]
subgraph OPT1["Option 1 — Deploy on Merge"]
PLAN --> REVIEW1[Review plan]
REVIEW1 --> MERGE1[Merge PR → main]
MERGE1 --> APPLY1[terraform apply]
end
subgraph OPT2["Option 2 — Deploy via PR Comment"]
PLAN --> REVIEW2[Review plan]
REVIEW2 --> COMMENT[Comment /deploy in the PR]
COMMENT --> APPLY2[terraform apply BEFORE merge]
APPLY2 --> MERGE2[Merge if successful]
end
Advantage of Option 2: If deployment fails, the buggy code stays in the feature branch (not in
main). The fix is done in the same PR, without pollutingmain.
CD Workflow (.github/workflows/cd.yml)
name: Terraform CD
on:
pull_request:
branches:
- main
types:
- closed
permissions:
contents: read
actions: read
env:
TF_LOG: INFO
TF_INPUT: false
TF_VERSION: ${{ vars.TF_VERSION }}
jobs:
# Get the CI run ID to retrieve the plan artifact
get-run-id:
name: Get CI Run ID
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
outputs:
run_id: ${{ steps.get-run.outputs.run_id }}
steps:
- name: Get CI Workflow Run ID
id: get-run
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RUN_ID=$(gh run list \
--repo ${{ github.repository }} \
--workflow "Terraform CI" \
--branch ${{ github.head_ref }} \
--status success \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId')
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
# Deploy to development
deploy-dev:
name: Deploy dev
needs: [get-run-id]
runs-on: ubuntu-latest
environment: dev
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- run: terraform init -backend-config="./backends/dev.hcl"
- uses: actions/download-artifact@v4
with:
name: tf-plan-dev-${{ needs.get-run-id.outputs.run_id }}
run-id: ${{ needs.get-run-id.outputs.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- run: terraform apply tf.plan
4. Module 3 — Multiple Environments
Multi-environment Strategies
flowchart LR
subgraph APPROACHES["Approaches for multi-environments"]
W[Workspaces\nCommunity Edition]
F[Folders\nSeparate Directories]
B[Branches\nLong-lived Branches]
P[Pipelines\n✅ Recommended]
end
W -->|"❌ No access control\n❌ Shared backend"| FAIL1[Limited]
F -->|"❌ Code duplication\n❌ Difficult to scale"| FAIL2[Limited]
B -->|"⚠️ Complex merges\n⚠️ Long-lived branches"| PARTIAL[Acceptable]
P -->|"✅ Reusable\n✅ Promoted env to env"| OK[Recommended]
| Approach | Advantages | Disadvantages |
|---|---|---|
| Workspaces (Community) | Simple to start | No access control, shared backend |
| Folders | Clear separation | Code duplication, does not scale |
| Branches | Same root module | Complex merges, long-lived branches |
| Pipelines + tfvars | Reusable, scalable, promoted | Initial setup complexity |
Terraform Community Workspaces
# Create a workspace
terraform workspace new dev
# List workspaces
terraform workspace list
# Select a workspace
terraform workspace select staging
# Reference the current workspace in HCL code
# terraform.workspace returns the active workspace name
# Example usage of terraform.workspace in code
resource "aws_vpc" "main" {
cidr_block = var.vpc_address_range
tags = {
Name = "${var.prefix}-${terraform.workspace}-vpc"
Environment = terraform.workspace
}
}
Community Workspaces Limitations:
- All workspaces share the same backend → no access isolation
- No native access control
- Unaware of version control (branch/tag)
- Designed for temporary environments, not long-term use
Pipeline-based Approach
Project Structure
taco-wagon-app/
├── .github/
│ └── workflows/
│ ├── ci.yml # Validation + Plans
│ ├── cd.yml # Deploy dev + staging
│ ├── release.yml # Deploy prod-east + prod-west
│ ├── deploy-environment.yml # Reusable workflow
│ ├── manual_deploy.yml # Manual deploy with approval
│ └── manual_destroy.yml # Destroy an environment
├── backends/
│ ├── dev.hcl
│ ├── staging.hcl
│ ├── prodeast.hcl
│ └── prodwest.hcl
├── environments/
│ ├── dev.tfvars
│ ├── staging.tfvars
│ ├── prodeast.tfvars
│ └── prodwest.tfvars
├── main.tf
├── variables.tf
├── providers.tf
├── outputs.tf
├── terraform.tf
├── .pre-commit-config.yaml
└── .tflint.hcl
CI with matrix for all environments (Module 3)
name: Terraform CI
on:
pull_request:
branches:
- main
env:
TF_LOG: INFO
TF_INPUT: false
TF_VERSION: ${{ vars.TF_VERSION }}
ENVIRONMENTS: '["dev","staging","prodeast","prodwest"]'
jobs:
# Setup job to export the environment list
setup:
name: Setup
runs-on: ubuntu-latest
outputs:
environments: ${{ env.ENVIRONMENTS }}
steps:
- run: echo "Environments - ${{ env.ENVIRONMENTS }}"
# Plans in parallel for all environments via matrix
plan:
name: Terraform Plan (${{ matrix.environment }})
runs-on: ubuntu-latest
needs: [setup, format, validate, tflint, trivy]
strategy:
fail-fast: false
matrix:
environment: ${{ fromJson(needs.setup.outputs.environments) }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
terraform_wrapper: false
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- run: terraform init -backend-config="./backends/${{ matrix.environment }}.hcl"
- run: terraform plan -var-file="./environments/${{ matrix.environment }}.tfvars" -out=tf.plan
- uses: actions/upload-artifact@v4
with:
name: tf-plan-${{ matrix.environment }}-${{ github.run_id }}
path: tf.plan
retention-days: 5
Reusable Workflow — Deploy Environment
name: Deploy Environment
on:
workflow_call:
inputs:
environment:
description: 'Environment to deploy to'
required: true
type: string
terraform_version:
required: true
type: string
ci_run_id:
description: 'CI workflow run ID for artifact retrieval'
required: true
type: string
secrets:
AWS_ACCESS_KEY_ID:
required: true
AWS_SECRET_ACCESS_KEY:
required: true
AWS_REGION:
required: true
jobs:
deploy:
name: Deploy to ${{ inputs.environment }}
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
env:
TF_LOG: INFO
TF_INPUT: false
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ inputs.terraform_version }}
terraform_wrapper: false
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- run: terraform init -backend-config="./backends/${{ inputs.environment }}.hcl"
- uses: actions/download-artifact@v4
with:
name: tf-plan-${{ inputs.environment }}-${{ inputs.ci_run_id }}
run-id: ${{ inputs.ci_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- run: terraform apply tf.plan
Production Release Pipeline
name: Production Release
on:
push:
tags:
- 'v*' # Triggered by a version tag, e.g.: v1.2.0
env:
TF_LOG: INFO
TF_INPUT: false
TF_VERSION: ${{ vars.TF_VERSION }}
ENVIRONMENTS: '["prodeast", "prodwest"]'
jobs:
setup:
name: Setup
runs-on: ubuntu-latest
outputs:
environments: ${{ env.ENVIRONMENTS }}
steps:
- run: echo "Deploying to production - ${{ env.ENVIRONMENTS }}"
get-run-id:
name: Get CI Run ID
runs-on: ubuntu-latest
outputs:
run_id: ${{ steps.get-run.outputs.run_id }}
steps:
- name: Get CI Workflow Run ID
id: get-run
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RUN_ID=$(gh run list \
--repo ${{ github.repository }} \
--workflow "Terraform CI" \
--status success \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId')
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
# Deploy in parallel to prod-east and prod-west
deploy:
name: Deploy ${{ matrix.environment }}
needs: [setup, get-run-id]
strategy:
fail-fast: false
matrix:
environment: ${{ fromJson(needs.setup.outputs.environments) }}
uses: ./.github/workflows/deploy-environment.yml
with:
environment: ${{ matrix.environment }}
terraform_version: ${{ vars.TF_VERSION }}
ci_run_id: ${{ needs.get-run-id.outputs.run_id }}
secrets:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
Complete Promotion Flow
sequenceDiagram
participant DEV as Developer
participant GH as GitHub
participant CI as CI Pipeline
participant CD as CD Pipeline
participant REL as Release Pipeline
DEV->>GH: git push feature-branch
DEV->>GH: Create Pull Request
GH->>CI: Trigger CI workflow
CI->>CI: fmt + validate + tflint + trivy
CI->>CI: terraform plan (dev, staging, prod-east, prod-west)
CI->>GH: Add plan summaries to PR comment
DEV->>GH: Review + Merge PR
GH->>CD: Trigger CD workflow
CD->>CD: terraform apply → dev
CD->>CD: terraform apply → staging
DEV->>GH: git tag v1.2.0 && git push --tags
GH->>REL: Trigger Release workflow
REL-->>REL: terraform apply → prod-east (parallel)
REL-->>REL: terraform apply → prod-west (parallel)
5. Module 4 — HCP Terraform for Automation
Introduction to HCP Terraform
HCP Terraform (HashiCorp Cloud Platform Terraform) is a SaaS platform that resolves the limitations of Terraform Community Edition:
| Community Limitation | HCP Terraform Solution |
|---|---|
| No centralized state management | Managed state backend, encrypted at rest and in transit |
| No access control | Granular RBAC per organization/project/workspace |
| No run history logs | Complete run history |
| Credentials on each workstation/CI | Credentials centralized per workspace or project |
| Repeated setup for each config | Reusable workspaces with config inheritance |
Two editions:
- HCP Terraform — SaaS hosted by HashiCorp (free up to 500 resources)
- Terraform Enterprise — Self-hosted, feature-equivalent
HCP Terraform Workspaces
Organizational Hierarchy
Organization
└── Project (taco-wagon-app)
├── Variable Sets (shared aws-creds)
├── Policies (Sentinel / OPA)
└── Workspaces
├── taco-wagon-app-dev
├── taco-wagon-app-staging
├── taco-wagon-app-prodeast
└── taco-wagon-app-prodwest
Three Workflow Types
| Workflow | Description | Use Case |
|---|---|---|
| CLI Workflow | Uses HCP Terraform as cloud backend, local commands | Progressive migration from Community |
| VCS Workflow | Direct integration with GitHub/GitLab/Bitbucket/Azure Repos | Automated pipeline triggered by PR/tag/commit |
| API-driven Workflow | Triggered by external API | Integration with other automation systems |
Cloud Backend Configuration
# terraform.tf — use HCP Terraform as backend
terraform {
cloud {
organization = "globomantics"
workspaces {
name = "taco-wagon-app-dev"
}
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~>6.0"
}
}
}
Migrating to HCP Terraform
flowchart LR
subgraph BEFORE["Before — GitHub Actions only"]
A1[PR created] --> A2[CI: fmt+validate+lint+scan]
A2 --> A3[CI: terraform plan ×4 envs]
A3 --> A4[Merge PR]
A4 --> A5[CD: apply dev]
A5 --> A6[CD: apply staging]
A6 --> A7[Release tag]
A7 --> A8[Release: apply prod ×2]
end
subgraph AFTER["After — HCP Terraform + GitHub Actions"]
B1[PR created] --> B2[CI: fmt+validate+lint+scan]
B2 --> B3[HCP: Speculative Plans ×4]
B3 --> B4[Merge PR]
B4 --> B5[HCP: apply dev workspace]
B5 --> B6[HCP: apply staging\nvia Run Triggers]
B6 --> B7[Release tag]
B7 --> B8[HCP: apply prod-east\nHCP: apply prod-west\nvia tag trigger]
end
Elements replaced by HCP Terraform:
Plan phase in ci.yml→ Automatic Speculative Plans per workspacecd.yml→ Built-in automation + Run Triggers between workspacesrelease.yml→ VCS tag trigger on prod workspacesmanual_deploy.yml→ Native workspace featuremanual_destroy.yml→ Native workspace feature
What remains in GitHub Actions:
- Code validation:
terraform fmt,terraform validate,tflint,trivy
Implementation and Customization
Workspace Configuration per Environment
| Workspace | VCS Trigger | Run Trigger |
|---|---|---|
taco-wagon-app-dev | Branch-based (main) | — |
taco-wagon-app-staging | Non-existent path (disabled) | Runs after dev |
taco-wagon-app-prodeast | Tag-based (v*) | — |
taco-wagon-app-prodwest | Tag-based (v*) | — |
Advantage of Run Trigger staging → dev: When a run completes successfully on dev, the staging workspace triggers automatically — no YAML workflow to maintain.
Variable Sets at the Project Level
AWS credentials are configured once in a Variable Set at the project level and are inherited by all workspaces:
Project: taco-wagon-app
└── Variable Set: aws-creds (scope: entire project)
├── AWS_ACCESS_KEY_ID (env var, sensitive)
└── AWS_SECRET_ACCESS_KEY (env var, sensitive)
Sentinel Policy (policy-as-code example)
# Example Sentinel policy — require environment tag
import "tfplan/v2" as tfplan
# All AWS resources must have an "environment" tag
main = rule {
all tfplan.resource_changes as _, rc {
rc.type starts_with "aws_" and rc.change.actions contains "create" implies
rc.change.after.tags["environment"] is not null
}
}
Simplified CI with HCP Terraform (Module 4)
name: Terraform CI
on:
pull_request:
branches:
- main
env:
TF_LOG: INFO
TF_INPUT: false
TF_VERSION: ${{ vars.TF_VERSION }}
ENVIRONMENTS: '["dev","staging","prodeast","prodwest"]'
jobs:
setup:
name: Setup
runs-on: ubuntu-latest
outputs:
environments: ${{ env.ENVIRONMENTS }}
steps:
- run: echo "Environments - ${{ env.ENVIRONMENTS }}"
# Only validation remains in GitHub Actions
# Plans are now managed by HCP Terraform (Speculative Plans)
format:
name: Terraform Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- run: terraform fmt -check -recursive
validate:
name: Terraform Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- run: terraform init -backend=false
- run: terraform validate
tflint:
name: TFLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: terraform-linters/setup-tflint@v4
- run: tflint --init
- run: tflint --recursive
trivy:
name: Trivy Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@master
with:
scan-type: config
scan-ref: .
exit-code: 1
severity: CRITICAL
# Note: "plan" job removed — managed by HCP Terraform Speculative Plans
6. Architecture Diagrams
Terraform GitOps Pipeline — Global Overview
flowchart TD
DEV[👨💻 Developer\nworkstation]
subgraph LOCAL["Local"]
HC[pre-commit hooks\nfmt + validate + tflint + docs]
end
subgraph GH["GitHub Repository"]
FEAT[feature branch]
MAIN[main branch]
TAG[Release Tag v*]
end
subgraph CI["CI Pipeline\nGitHub Actions"]
FMT[terraform fmt]
VAL[terraform validate]
LINT[tflint]
SEC[trivy scan]
PLAN[terraform plan ×4 envs]
end
subgraph CD["CD Pipeline\nGitHub Actions / HCP Terraform"]
DEV_ENV[Apply → DEV]
STG_ENV[Apply → STAGING]
end
subgraph PROD_PIPE["Release Pipeline"]
PE[Apply → PROD-EAST]
PW[Apply → PROD-WEST]
end
subgraph STATE["Remote State\nAWS S3"]
S1[tw-dev.tfstate]
S2[tw-staging.tfstate]
S3[tw-prodeast.tfstate]
S4[tw-prodwest.tfstate]
end
DEV --> HC
HC --> FEAT
FEAT -->|PR created| CI
CI --> FMT & VAL & LINT & SEC
FMT & VAL & LINT & SEC --> PLAN
PLAN -->|PR comment| FEAT
FEAT -->|Approved merge| MAIN
MAIN --> CD
CD --> DEV_ENV --> STG_ENV
DEV_ENV --> S1
STG_ENV --> S2
MAIN -->|git tag v*| TAG
TAG --> PROD_PIPE
PE --> S3
PW --> S4
PR → Plan → Apply Flow (Atlantis style)
sequenceDiagram
actor DEV as Developer
participant GIT as GitHub PR
participant BOT as Atlantis / CI Bot
participant TF as Terraform
participant INFRA as AWS Infrastructure
DEV->>GIT: git push + Create PR
GIT->>BOT: Webhook: PR opened
BOT->>TF: terraform init
BOT->>TF: terraform plan
TF-->>BOT: Plan output
BOT->>GIT: Comment plan on the PR
DEV->>GIT: Review plan
DEV->>GIT: atlantis apply (or merge)
GIT->>BOT: Webhook: apply request
BOT->>TF: terraform apply
TF->>INFRA: Provision resources
INFRA-->>TF: Success
TF-->>BOT: Apply output
BOT->>GIT: Comment result
DEV->>GIT: Merge PR
HCP Terraform Architecture with Drift Detection
flowchart LR
subgraph HCP["HCP Terraform"]
subgraph ORG["Organization: globomantics"]
subgraph PROJ["Project: taco-wagon-app"]
VS[Variable Set\naws-creds]
WS_DEV[Workspace: dev\nBranch trigger: main]
WS_STG[Workspace: staging\nRun trigger: ← dev]
WS_PE[Workspace: prod-east\nTag trigger: v*]
WS_PW[Workspace: prod-west\nTag trigger: v*]
end
end
DRIFT[Drift Detection\nPeriodic planning]
SENTINEL[Policy Engine\nSentinel / OPA]
end
GH[GitHub Repository] -->|VCS Integration| WS_DEV
WS_DEV -->|Run Trigger| WS_STG
GH -->|Tag v*| WS_PE & WS_PW
DRIFT -.->|Detects diffs| WS_DEV & WS_STG & WS_PE & WS_PW
SENTINEL -->|Enforce policies| WS_DEV & WS_STG & WS_PE & WS_PW
WS_DEV --> AWS_DEV[(AWS DEV\nState)]
WS_STG --> AWS_STG[(AWS STAGING\nState)]
WS_PE --> AWS_PE[(AWS PROD-EAST\nState)]
WS_PW --> AWS_PW[(AWS PROD-WEST\nState)]
Drift Detection
Drift is the divergence between the desired state (Terraform code) and the actual state of the infrastructure. It can occur when changes are made manually outside of Terraform.
flowchart TD
DESIRED[Desired state\nmain.tf + state]
ACTUAL[Actual state\nAWS Infrastructure]
DESIRED -->|terraform plan| COMPARE{Diff?}
ACTUAL --> COMPARE
COMPARE -->|No diff| OK[✅ No drift]
COMPARE -->|Diff detected| DRIFT[⚠️ Drift detected!]
DRIFT --> NOTIFY[Notification / Alert]
NOTIFY --> ACTION{Action}
ACTION -->|Fix infrastructure| APPLY[terraform apply]
ACTION -->|Fix code| UPDATE[Update main.tf]
ACTION -->|Ignore| IGNORE[terraform import or lifecycle ignore]
Drift detection strategies:
- HCP Terraform: Automatic periodic planning per workspace
- GitHub Actions: Scheduled job (
schedule: cron) that runsterraform planand alerts on detected changes - atlantis: Drift checks via webhooks and automatic runs
7. Reference — Terraform Automation Tools
Local Validation Tools
| Tool | Command | Description |
|---|---|---|
| terraform fmt | terraform fmt -recursive | Formats .tf files according to canonical HCL style |
| terraform validate | terraform validate | Checks syntax and logical consistency |
| TFLint | tflint --recursive | Advanced linting with provider-specific rules |
| terraform-docs | terraform-docs markdown table . | Generates documentation from variables/outputs |
Security Analysis Tools
| Tool | Publisher | Focus |
|---|---|---|
| Trivy | Aqua Security | IaC misconfigurations, CVEs, secrets |
| Checkov | Bridgecrew/Palo Alto | IaC security policies |
| KICS | Checkmarx | Multi-IaC (Terraform, K8s, Docker, etc.) |
| tfsec | Aqua Security | Terraform security scanning (legacy → Trivy) |
Planning and Summary Tools
| Tool | Role |
|---|---|
| tf-summarize | Human-readable (Markdown) Terraform plan summary |
| terraform show -json | Export plan in JSON format for programmatic analysis |
| infracost | Infrastructure change cost estimation |
| terragrunt | Terraform wrapper for DRY configs and multi-module orchestration |
IaC Automation Platforms
| Platform | Type | VCS Integration | Policy Engine | State Management |
|---|---|---|---|---|
| HCP Terraform | SaaS (HashiCorp) | ✅ GitHub/GitLab/Bitbucket/Azure | ✅ Sentinel / OPA | ✅ Built-in |
| Terraform Enterprise | Self-hosted (HashiCorp) | ✅ | ✅ Sentinel / OPA | ✅ |
| Atlantis | Self-hosted (Open source) | ✅ GitHub/GitLab/Bitbucket | ❌ (via hooks) | ❌ (external) |
| Spacelift | SaaS | ✅ | ✅ OPA | ✅ |
| Env0 | SaaS | ✅ | ✅ OPA | ✅ |
| Scalr | SaaS / Self-hosted | ✅ | ✅ OPA | ✅ |
| GitHub Actions | Generic CI/CD | ✅ Native GitHub | Via plugins | ❌ (external S3/GCS) |
Terraform Environment Variables
| Variable | Example Value | Effect |
|---|---|---|
TF_IN_AUTOMATION | true | Reduces interactive output for pipelines |
TF_LOG | INFO, DEBUG, WARN, ERROR | Log verbosity level |
TF_INPUT | false | Disables interactive prompts |
TF_PLUGIN_CACHE_DIR | /opt/tf-cache | Local provider plugin cache |
TF_VAR_<name> | TF_VAR_region=us-east-1 | Defines a Terraform variable via env var |
TF_CLI_ARGS_plan | -parallelism=30 | Default CLI arguments for a sub-command |
8. Project File Structure
Base Terraform Configuration (taco-wagon-app)
main.tf
# VPC Resources
data "aws_availability_zones" "available" {
state = "available"
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "6.4.0"
name = "${var.prefix}-${var.environment}-vpc"
cidr = var.vpc_address_range
azs = slice(data.aws_availability_zones.available.names, 0, length(var.vpc_public_subnet_ranges))
public_subnets = var.vpc_public_subnet_ranges
enable_nat_gateway = false
enable_vpn_gateway = false
enable_dns_hostnames = true
map_public_ip_on_launch = true
}
variables.tf
variable "environment" {
type = string
description = "(Required) Environment of all resources"
}
variable "prefix" {
type = string
description = "(Required) Prefix to use for all resources in this module."
}
variable "region" {
type = string
description = "(Optional) AWS Region to deploy in. Defaults to us-west-2."
default = "us-west-2"
}
variable "vpc_address_range" {
type = string
description = "Address range for the VPC"
}
variable "vpc_public_subnet_ranges" {
type = list(string)
description = "List of public subnet CIDR ranges for the VPC"
}
providers.tf
provider "aws" {
region = var.region
default_tags {
tags = {
owner = "taco-wagon-team"
environment = var.environment
}
}
}
terraform.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~>6.0"
}
}
backend "s3" {
# Use partial backend: terraform init -backend-config="./backends/dev.hcl"
}
}
backends/dev.hcl
key = "tw-dev.tfstate"
bucket = "my-terraform-state-bucket"
region = "us-east-1"
Complete Workflow Commands
# ─── LOCAL SETUP ───────────────────────────────────────────────
# Install pre-commit
pip install pre-commit
pre-commit install
# Initialize Terraform for the dev environment
terraform init -backend-config="./backends/dev.hcl"
# ─── DEVELOPMENT ───────────────────────────────────────────────
# Create a new feature branch
git checkout -b feature/add-new-subnet
# Make changes...
# Format and validate locally
terraform fmt -recursive
terraform validate
tflint --recursive
# Commit (triggers pre-commit hooks)
git add .
git commit -m "feat: add new subnet configuration"
git push origin feature/add-new-subnet
# ─── CI/CD (automatic via GitHub Actions) ──────────────────────
# PR created → CI starts automatically
# Plan generated → comment added to PR
# ─── MERGE AND DEPLOYMENT ──────────────────────────────────────
# Merge via GitHub UI → CD starts automatically
# DEV → STAGING (sequential)
# ─── PRODUCTION RELEASE ────────────────────────────────────────
git tag v1.2.0
git push --tags
# Release pipeline starts → PROD-EAST || PROD-WEST (parallel)
# ─── USEFUL TERRAFORM COMMANDS ─────────────────────────────────
# Plan with tfvars
terraform plan -var-file="./environments/dev.tfvars" -out=tf.plan
# Apply from a saved plan
terraform apply tf.plan
# Export plan as JSON for analysis
terraform show -json tf.plan > plan.json
# Destroy an environment
terraform destroy -var-file="./environments/dev.tfvars"
# Workspace management
terraform workspace new staging
terraform workspace list
terraform workspace select dev
Search Terms
terraform · automation · gitops · infrastructure · ci/cd · devops · hcp · configuration · pipeline · tools · workflow · environment · github · architecture · backend · detection · drift · environments · flow · iac · objectives · planning · validation · workflows