Best Practice Workflow for Terraform Deployment to Azure¶
Industry best practice workflow for deploying to Azure using Terraform in a controlled, scalable, and auditable way.
🌳 Branch-Based Development¶
🎯 Objective¶
Isolate infrastructure changes in a dedicated Git branch to enable safe experimentation, clear versioning, and collaborative review.
Why It Matters¶
- Isolation: Keeps your changes separate from production (main) until they’re validated and approved.
- Traceability: Every change is tied to a commit, branch, and pull request—great for audits and rollback.
- Parallel Work: Multiple team members can work on different parts of the infrastructure without stepping on each other.
🛠️ Typical Workflow¶
1. Create a Feature Branch¶
Use a naming convention that reflects the purpose of the change:
Other examples:
- feature/update-vnet
- infra/aks-cluster
- bugfix/incorrect-tags
- refactor/module-storage
✅ Naming conventions help with automation, filtering, and clarity.
2. Create or Modify .tf Files¶
Structure your Terraform code modularly. For example:
✅ Modular design promotes reuse and separation of concerns.
3. Commit Your Changes¶
Use clear, descriptive commit messages:
✅ Good commit hygiene makes PRs easier to review and helps future you understand what you did.
4. Push the Branch¶
Push your branch to GitHub:
This triggers your CI workflow (if configured) to validate and plan the changes.
🧠 Best Practices¶
| Practice | Why It’s Useful | 
|---|---|
| Use small, focused branches | Easier to review and test | 
| Avoid committing .tfstateor.terraform | These should be in .gitignore | 
| Use pre-commit hooks | Enforce formatting and validation before pushing | 
| Keep modules versioned | Enables rollback and reuse across environments | 
🧪 Automated Validation & Formatting¶
🎯 Objective¶
Ensure Terraform code is consistently formatted, syntactically valid, and secure—without provisioning any infrastructure. This step is essential for maintaining code quality and catching issues early during development.
When to Run These Checks¶
- After every local change
- On every commit or pull request
- Before merging into main or deploying
These checks are non-invasive and safe to run anytime. They don’t require cloud credentials or modify infrastructure.
Tools & Commands¶
| tool | purpose | importance | 
|---|---|---|
| terraform fmt | Automatically formats .tffiles to canonical style. | Prevents formatting drift and improves readability across teams. | 
| terraform validate | Validates syntax and internal consistency of Terraform files. | Catches typos, missing arguments, and structural issues before runtime. | 
| TFLint | Lints Terraform code for best practices, deprecated syntax, and provider-specific issues. | Goes beyond validateto enforce style and catch subtle bugs. | 
| Checkov | Static analysis for security misconfigurations and compliance violations. | Identifies insecure defaults, missing encryption, overly permissive IAM policies, etc. | 
| Infracost | Estimates cost impact of infrastructure changes. | Adds financial awareness to infrastructure decisions. | 
What Happens After You Commit¶
- Trigger: A push to any branch except main with changes to .tf files activates the workflow.
- Checkout: The workflow checks out your branch and compares it to origin/main.
- Change Detection: It identifies which .tf files were modified.
- Validation Steps:- terraform fmt ensures formatting.
- terraform validate checks syntax and structure of the root module.
- tflint runs linting on each changed file using --filter.
 
Sample GitHub Actions Workflow¶
name: "Terraform File Commit Reaction"
permissions:
  contents: write
  pull-requests: write
on:
  push:
    branches-ignore:
      - main
    paths:
      - "**/*.tf"
jobs:
  terraform-checks:
    runs-on: ubuntu-latest
    steps:
      # Step 1: Checkout the pushed branch
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      # Step 2: Detect changed .tf files
      - name: Find Changed .tf Files
        id: changed-files
        run: |
          CHANGED_FILES=$(git diff --name-only origin/main...HEAD | grep '\.tf$' || true)
          echo "Changed .tf files:"
          echo "$CHANGED_FILES"
          echo "files=$CHANGED_FILES" >> $GITHUB_OUTPUT
      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v3
      # Step 4: Run Terraform fmt on changed files
      - name: Format Check
        if: steps.changed-files.outputs.files != ''
        run: |
          for file in ${{ steps.changed-files.outputs.files }}; do
            terraform fmt "$file"
          done
      # Step 5: Run Terraform validate (only if root module changed)
      - name: Validate Terraform
        if: steps.changed-files.outputs.files != ''
        continue-on-error: true
        run: |
          if ! terraform validate; then
            echo "::error::❌ Terraform validation failed. Check root module syntax."
          else
            echo "✅ Terraform validation passed."
          fi
      # Step 6: Run TFLint setup
      - name: Run TFLint
        if: steps.changed-files.outputs.files != ''
        uses: terraform-linters/setup-tflint@v4
        with:
          tflint_version: latest
      - name: TFLint Execution
        if: steps.changed-files.outputs.files != ''
        continue-on-error: true
        run: |
          for file in ${{ steps.changed-files.outputs.files }}; do
            if ! tflint --filter="$file"; then
              echo "::error file=$file::❌ TFLint failed on $file. Check syntax and formatting."
            else
              echo "✅ TFLint passed for $file"
            fi
          done
🧠 Best Practices¶
- Use pre-commit hooks to run these checks locally before pushing.
- Fail CI builds if any check fails—this enforces discipline.
- Customize rules via .tflint.hcland.checkov.ymlto suit your environment.
- Run checks recursively to cover all modules and subdirectories.
Would you like me to draft a pre-commit config next, or help you set up .tflint.hcl and .checkov.yml for your environment?