Cloud  /  Terraform

IaC Terraform 50 guides · updated 2026

Infrastructure as code done right — providers, state, reusable modules, and the workflow patterns that keep multi-cloud deployments sane in 2026.

Terraform Version Control Integration

Treating infrastructure code with the same discipline as application code — pull requests, code review, automated testing, and deployment pipelines — is the defining characteristic of a mature DevOps practice. Terraform integrates naturally with Git to make this possible.


What to Commit to Git

project/
├── ✅ main.tf # Commit — resource definitions
├── ✅ variables.tf # Commit — variable declarations
├── ✅ outputs.tf # Commit — output definitions
├── ✅ versions.tf # Commit — provider and TF version constraints
├── ✅ .terraform.lock.hcl # Commit — exact provider versions (crucial!)
├── ✅ terraform.tfvars.example # Commit — template showing required vars (no values)
├── ❌ terraform.tfvars # DO NOT commit — may contain secrets
├── ❌ .terraform/ # DO NOT commit — provider binaries, auto-regenerated
├── ❌ terraform.tfstate # DO NOT commit — use remote backend instead
├── ❌ terraform.tfstate.backup # DO NOT commit
├── ❌ *.auto.tfvars # CAREFUL — may contain secrets
└── ❌ crash.log # DO NOT commit — Terraform crash reports

.gitignore for Terraform

.gitignore
.terraform/
terraform.tfstate
terraform.tfstate.backup
*.tfstate
*.tfstate.*
.terraform.tfvars
terraform.tfvars
override.tf
override.tf.json
*_override.tf
*_override.tf.json
crash.log
crash.*.log
*.tfplan

The .terraform.lock.hcl file is the exception — always commit it so all team members and CI/CD runners use identical provider versions.


Branching Strategy for Terraform

main (protected)
├── Always deployable — matches production state
├── Direct pushes blocked
└── Changes only via pull requests
feature/add-redis-cluster
├── New branch for each change
├── PR triggers automated plan
└── Merged after review + plan approval
hotfix/fix-security-group-rule
└── Emergency changes follow same PR process

Environment Branching (For Multi-Environment)

main → deploys to dev automatically
release/v2.1 → deploys to staging on tag
production → deploys to production after manual approval

GitHub Actions CI/CD Pipeline

A complete, production-grade Terraform pipeline:

.github/workflows/terraform.yml
name: Terraform CI/CD
on:
push:
branches: [main]
paths: ['infrastructure/**']
pull_request:
branches: [main]
paths: ['infrastructure/**']
permissions:
id-token: write # OIDC auth to AWS
contents: read
pull-requests: write
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.8.0"
- name: Init
run: terraform init -backend=false
working-directory: infrastructure/
- name: Validate
run: terraform validate
working-directory: infrastructure/
- name: Format check
run: terraform fmt -check -recursive
working-directory: infrastructure/
plan:
name: Plan
runs-on: ubuntu-latest
needs: validate
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/TerraformPlanRole
aws-region: us-east-1
- uses: hashicorp/setup-terraform@v3
- name: Init
run: terraform init -input=false
working-directory: infrastructure/
- name: Plan
id: plan
run: terraform plan -input=false -no-color -out=tfplan
working-directory: infrastructure/
- name: Upload plan
uses: actions/upload-artifact@v4
with:
name: tfplan
path: infrastructure/tfplan
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
const plan = `${{ steps.plan.outputs.stdout }}`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Terraform Plan\n\`\`\`hcl\n${plan}\n\`\`\``
});
apply:
name: Apply
runs-on: ubuntu-latest
needs: [validate]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: production # Requires manual approval gate
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/TerraformApplyRole
aws-region: us-east-1
- uses: hashicorp/setup-terraform@v3
- name: Init
run: terraform init -input=false
working-directory: infrastructure/
- name: Apply
run: terraform apply -input=false -auto-approve
working-directory: infrastructure/

OIDC Authentication (No Long-Lived AWS Keys)

Instead of storing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as GitHub secrets, use OIDC — GitHub Actions assumes an IAM role directly:

# IAM role for GitHub Actions OIDC
resource "aws_iam_role" "github_actions_terraform" {
name = "github-actions-terraform-plan"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com" }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:my-org/my-repo:*"
}
}
}]
})
}

This eliminates static credentials from CI/CD systems entirely — a major security improvement.


Pre-commit Hooks for Local Quality Gates

.pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.92.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docs
- id: terraform_tflint
- id: terraform_checkov # Security and compliance scanning
Terminal window
pip install pre-commit
pre-commit install
# Now every `git commit` automatically validates and formats Terraform code

Pull Request Checklist for Terraform Changes

Before merging a Terraform PR, reviewers should verify: