Skip to content

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:

git checkout -b feature/add-resource-group

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:

working-dir/
── resource-group/
        └──  main.tf
── key-vault/
        └──  main.tf        

✅ Modular design promotes reuse and separation of concerns.


3. Commit Your Changes

Use clear, descriptive commit messages:

git add .
git commit -m "Add resource group module for dev environment"

✅ 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:

git push origin feature/add-resource-group

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 .tfstate or .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 .tf files 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 validate to 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

  1. Trigger: A push to any branch except main with changes to .tf files activates the workflow.
  2. Checkout: The workflow checks out your branch and compares it to origin/main.
  3. Change Detection: It identifies which .tf files were modified.
  4. 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.hcl and .checkov.yml to 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?