Skip to content

GitHub Actions IaC CI/CD ArchitectureΒΆ

This CI/CD system is built around modular GitHub Actions & Workflows that are centralized to allow any repository to use them.


It provides:

  • Automatic detection of changed main.tf files
  • Matrix processing for parallel validation of multiple files
  • Complete Terraform validation pipeline:
    • Format checking, linting, validation
    • Planning with cost estimation
    • Security scanning with Checkov
    • Slack notifications
  • Repository context awareness (PROD/DEV/STAGING/DEFAULT)
  • Centralized action management with cross-repository compatibility

GitHub StructureΒΆ

Central Organization Wide Repository

This repository hosts the centralized workflows and actions files consumed by the subscription repositories. This means you can modify or change one action file and all the deployments will use that change the next time they run. This is far easier than manually editing the same file multiple times which would be the case if you were to keep the workflows and action files on a per repository basis.

🏒
πŸ“ CENTRALIZED REPOSITORY: grinntec-terraform-deployments-azure/.github
β”œβ”€β”€ πŸ“„ README.md
└── πŸ“ .github/
    β”œβ”€β”€ πŸ“ actions/                                         # Reusable Actions Library
    β”‚   β”œβ”€β”€ πŸ“ cache-terraform-providers/                   # Cache Terraform providers
    β”‚   β”œβ”€β”€ πŸ“ calculate-file/                              # Extract file paths & directories
    β”‚   β”œβ”€β”€ πŸ“ checkov-security-scan/                       # Security scanning with Checkov
    β”‚   β”œβ”€β”€ πŸ“ configure-git-private-modules/               # Setup Git for private modules
    β”‚   β”œβ”€β”€ πŸ“ create-terraform-backend-config-file/        # Generate backend config
    β”‚   β”œβ”€β”€ πŸ“ ensure-tf-lockfile-exists/                   # Ensure .terraform.lock.hcl exists
    β”‚   β”œβ”€β”€ πŸ“ extract-tfvars/                              # Parse .tfvars files
    β”‚   β”œβ”€β”€ πŸ“ read-ci-mode/                                # Determine plan/apply/destroy mode
    β”‚   β”œβ”€β”€ πŸ“ slack-notify/                                # Send Slack notifications
    β”‚   β”œβ”€β”€ πŸ“ terraform-apply/                             # Apply Terraform changes
    β”‚   β”œβ”€β”€ πŸ“ terraform-cost-estimate/                     # Cost estimation with Infracost
    β”‚   β”œβ”€β”€ πŸ“ terraform-destroy/                           # Destroy Terraform resources
    β”‚   β”œβ”€β”€ πŸ“ terraform-fmt/                               # Format Terraform code
    β”‚   β”œβ”€β”€ πŸ“ terraform-plan/                              # Plan Terraform changes
    β”‚   β”œβ”€β”€ πŸ“ terraform-validate/                          # Validate Terraform syntax
    β”‚   β”œβ”€β”€ πŸ“ tflint/                                      # Lint Terraform code
    β”‚   └── πŸ“ archive/                                     # Archived/deprecated actions
    └── πŸ“ workflows/                                       # Reusable Workflows
        β”œβ”€β”€ πŸ“„ detect-changed-tfvars-files-multi-repo.yaml  # Main orchestrator
        β”œβ”€β”€ πŸ“„ process-changed-main-files.yaml              # Matrix processor

Azure Subscription Specific Repository

This is an example of a subscription repository. As the name suggests, it's targeted for a specific Azure subscription and is meant host IaC only for resources within that subscription. Credentials are created per subscription and applied at this level to limit the scope of access the GitHub Actions can perform. To use the centralized repository it requires a single Workflow that when triggered consumes the workflow and action files from the centralized repository.

πŸ—οΈ
πŸ“ SUBSCRIPTION REPOSITORY: grinntec-[environment]-subscription
β”œβ”€β”€ πŸ“„ README.md
β”œβ”€β”€ πŸ“„ .gitignore
β”œβ”€β”€ πŸ“ .github/
β”‚   └── πŸ“ workflows/
β”‚       └── πŸ“„ terraform-validation.yaml                    # Calls centralized workflow
β”œβ”€β”€ πŸ“ [app-folder-0]/                                      # Azure Resource Groups
β”‚   └── πŸ“„ main.tf                                          # Terraform configuration
β”œβ”€β”€ πŸ“ [app-folder-1]/
β”‚   └── πŸ“„ main.tf
β”œβ”€β”€ πŸ“ [app-folder-2]/
β”‚   └── πŸ“„ main.tf
└── πŸ“ [app-folder-x]/
    └── πŸ“„ main.tf

EXAMPLE SUBSCRIPTION REPOSITORIES NAMES:

πŸ“ grinntec-prod-subscription               # 🟒 Production Environment
πŸ“ grinntec-dev-subscription                # 🟑 Development Environment  
πŸ“ grinntec-staging-subscription            # 🟠 Staging Environment
πŸ“ grinntec-testing-subscription            # πŸ”΅ Testing/Sandbox Environment

WORKFLOW EXECUTION FLOW:ΒΆ

  1. Developer commits to subscription repo (e.g., grinntec-prod-subscription)
  2. terraform-validation.yaml triggers in subscription repo
  3. Calls centralized detect-changed-tfvars-files-multi-repo.yaml
  4. Detects changed main.tf files and repository context
  5. Creates matrix for parallel processing of changed files
  6. Each matrix job calls process-changed-main-files.yaml
  7. Executes complete validation pipeline using centralized actions:
   β”œβ”€β”€ πŸ”’ Azure Authentication (OIDC)
   β”œβ”€β”€ πŸ“ File & Directory Calculation  
   β”œβ”€β”€ 🎯 CI Mode Detection (plan/apply/destroy)
   β”œβ”€β”€ πŸ” Git Configuration for Private Modules
   β”œβ”€β”€ ⚑ Terraform Provider Caching
   β”œβ”€β”€ πŸ”§ Backend Configuration Generation
   β”œβ”€β”€ ✨ Terraform Format Check
   β”œβ”€β”€ 🧹 TFLint Analysis
   β”œβ”€β”€ βœ… Terraform Validation
   β”œβ”€β”€ πŸ“‹ Terraform Plan Generation
   β”œβ”€β”€ πŸ›‘οΈ Checkov Security Scan
   β”œβ”€β”€ πŸ’° Infracost Cost Estimation
   β”œβ”€β”€ πŸš€ Terraform Apply (if applicable)
   └── πŸ“’ Slack Notifications

SECRETS ARCHITECTURE:ΒΆ

Why this split?

  • Organization secrets: Same values across all subscription repositories
  • Repository secrets: Subscription-specific values that isolate environments
  • Security: Each repo can only deploy to its designated subscription
  • Maintenance: Update tenant/slack once vs per-repo updates

Security Model

  • OIDC Authentication – No static Azure credentials.
  • Scoped Secrets – Only required secrets passed to jobs.
  • Concurrency Control – Prevents overlapping runs for the same file.
  • State Isolation – Backend config ensures environment separation.
🏒 ORGANIZATION-LEVEL (shared across all repos):
   β”œβ”€β”€ AZURE_TENANT_ID              # Azure AD tenant
   β”œβ”€β”€ AZURE_DEPLOY_TO_MODULE_RO    # GitHub token for private modules
   β”œβ”€β”€ INFRACOST_API_KEY            # Cost estimation API
   └── SLACK_WEBHOOK_URL            # Team notifications
πŸ—οΈ REPOSITORY-LEVEL (subscription-specific):
   β”œβ”€β”€ AZURE_CLIENT_ID              # Service principal for subscription
   └── AZURE_SUBSCRIPTION_ID        # Target Azure subscription

ENVIRONMENT DETECTION:ΒΆ

The centralized workflow automatically determines which Azure environment it's operating in based on the repository name, enabling environment-specific behavior without manual configuration.

Repository Name Pattern β†’ Environment Mapping:
β”œβ”€β”€ *prod*              # PROD
β”œβ”€β”€ *dev*               # DEV  
β”œβ”€β”€ *staging*|*stage*   # STAGING
└── other               # DEFAULT

ENVIRONMENT-SPECIFIC SECURITY:

  • PROD: Could require additional approvals
  • DEV: Allow auto-apply for faster iteration
  • STAGING: Require manual approval before apply
  • DEFAULT: Safe sandbox mode (plan-only)

DIFFERENTIATED WORKFLOWS:

  • PROD: Full security scans + cost alerts
  • DEV: Faster validation, skip expensive checks
  • STAGING: Production-like validation
  • DEFAULT: Basic validation for testing

TARGETED NOTIFICATIONS:

  • PROD: Alert leadership + ops team
  • DEV: Notify development team only
  • STAGING: QA team notifications
  • DEFAULT: Individual developer alerts

COST MANAGEMENT:

  • PROD: Strict cost thresholds & alerts
  • DEV: Relaxed limits for experimentation
  • STAGING: Medium cost controls
  • DEFAULT: Basic cost awareness