Martin Rylko
  • Services
  • Blog
  • About
  • Contact
  • Get in Touch
Martin Rylko

Senior Cloud Architect & DevOps Engineer. Specializing in Microsoft Azure, IaC, Cloud Security and AI.

Navigation

  • Services
  • Blog
  • About
  • Contact

Collaboration

Looking for an experienced architect for your Azure project? Get in touch.

rylko@cloudmasters.cz

© 2026 Martin Rylko. All rights reserved.

Built in the cloud. Deployed via Azure Static Web Apps.

Home/Blog/Terraform Azure Best Practices: Modules & CI/CD
All articlesČíst česky

Terraform Azure Best Practices: Modules & CI/CD

6/1/2025 2 min
#Terraform#Azure#IaC#DevOps

After three years of intensive work with Terraform in enterprise environments (from H&M Group to smaller FinTech companies), I've identified five key principles that separate amateur IaC code from production-hardened infrastructure.

1. Remote State with Locking – No Exceptions

Never, I mean never, use a local terraform.tfstate in a team setting. An Azure Storage Account with a blob container and state locking is the foundation:

terraform {
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "stterraformstprod"
    container_name       = "tfstate"
    key                  = "landing-zone/prod.tfstate"
    use_oidc             = true
  }
}

Why use_oidc = true? Because Managed Identity + OIDC federation eliminates the need to store service principal secrets in pipeline variables. More secure, simpler.

2. Modularization from Day One

The biggest mistake I see at client sites: a massive monolithic main.tf with 2,000+ lines. The solution? Terraform modules:

infrastructure/
├── modules/
│   ├── networking/          # VNet, NSG, Route Tables
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── compute/             # VM, VMSS, AKS
│   ├── security/            # Key Vault, Defender
│   └── monitoring/          # Log Analytics, Alerts
├── environments/
│   ├── dev/
│   │   └── main.tf          # Calls modules with dev parameters
│   ├── staging/
│   └── prod/
└── shared/
    └── backend.tf

Each module has a clear responsibility. A change in the networking module doesn't affect compute. You test in isolation, deploy safely.

3. Drift Detection in CI/CD Pipeline

Infrastructure changes over time – someone clicks in the portal, someone runs a script. Drift is the silent killer of IaC consistency. The solution:

# Azure DevOps pipeline - nightly drift check
schedules:
  - cron: "0 3 * * *"
    displayName: "Nightly drift detection"
 
steps:
  - task: TerraformCLI@2
    inputs:
      command: 'plan'
      workingDirectory: 'infrastructure/environments/prod'
      commandOptions: '-detailed-exitcode'
    continueOnError: true
 
  - script: |
      if [ $TERRAFORM_EXITCODE -eq 2 ]; then
        echo "##vso[task.logissue type=warning]DRIFT DETECTED!"
        # Send notification to Teams/Slack
      fi
    displayName: 'Evaluate drift'

4. Resource Naming via a Naming Convention Module

Consistent naming is the SEO of infrastructure – without it, you'll get lost:

module "naming" {
  source = "Azure/naming/azurerm"
  suffix = [var.environment, var.location_short]
}
 
resource "azurerm_resource_group" "main" {
  name     = module.naming.resource_group.name
  location = var.location
}
 
resource "azurerm_virtual_network" "hub" {
  name                = module.naming.virtual_network.name
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  address_space       = ["10.0.0.0/16"]
}

5. Infrastructure Testing – Terratest or terraform validate

Yes, infrastructure needs to be tested. At a minimum:

  • terraform validate – syntax check
  • terraform plan in a PR pipeline – review changes before merge
  • tflint – static analysis for best practices
  • Terratest (Go) – integration tests for critical modules

Conclusion

Terraform is a powerful tool, but without discipline it becomes a nightmare. Start with remote state and modularization, add drift detection and naming conventions. Your future self will thank you. If you're using Bicep instead of Terraform, see our Bicep CI/CD pipeline guide.

Interested in how to set up a complete Terraform CI/CD pipeline in Azure DevOps? I'm preparing a detailed guide on that topic. Learn more about our Infrastructure as Code consulting.

Tags:#Terraform#Azure#IaC#DevOps
LinkedInX / Twitter

About the author

Martin Rylko

Martin Rylko

Senior Cloud Architect & DevOps Engineer

14+ years in IT – from on-premises datacenters and Hyper-V clustering to cloud infrastructure on Microsoft Azure. I specialize in Landing Zones, IaC automation, Kubernetes and security compliance.

Email LinkedInFull profile

Frequently Asked Questions

How do I secure Terraform state files in Azure?▾
Store state in an Azure Storage Account with blob versioning enabled, container-level RBAC (not storage account keys), and a state locking mechanism via Azure Blob lease. Enable soft delete with 30-day retention. The storage account should be in a dedicated management subscription with network rules restricting access to your CI/CD runner IP ranges only.
When should I choose Terraform over Bicep for Azure infrastructure?▾
Choose Terraform when you manage multi-cloud resources (Azure + AWS/GCP), when your team already has Terraform expertise, or when you need the mature module ecosystem from the Terraform Registry. Choose Bicep for Azure-only environments where you want zero state management overhead and native ARM integration. In practice, many enterprises use both -- Bicep for Landing Zone platform and Terraform for application workloads.
How should I version Terraform modules in production?▾
Use semantic versioning (semver) with Git tags -- v1.0.0 for stable releases, v1.1.0 for backward-compatible features, v2.0.0 for breaking changes. Pin module versions in root configurations using exact version constraints (version = "1.2.3") rather than ranges. Never use unversioned module references (source = "git::...") in production code.
What is drift detection and how do I implement it?▾
Drift detection identifies differences between your Terraform state and the actual Azure infrastructure -- for example, someone manually changed a firewall rule in the portal. Implement it by scheduling terraform plan in your CI/CD pipeline (e.g., daily cron job) and alerting on non-empty plan output. Tools like Spacelift and env0 have built-in drift detection dashboards.

You might also like

Terraform Azure Modules: Private Registry and Testing

Build reusable Terraform modules for Azure with private registry publishing, automated testing with Terratest, and versioned module consumption in production.

Read

Bicep CI/CD: GitHub Actions Pipeline for Azure

Build a production Bicep deployment pipeline with GitHub Actions. Covers what-if previews, environment approvals, OIDC authentication, and rollback strategies.

Read

AKS Breaking Changes: What Is Retiring in March 2026 and How to Migrate

Windows Server 2019, Azure Linux 2.0, and kubelet certificate rotation – three AKS retirements with March 2026 deadlines. Practical migration guide with CLI commands and Bicep templates.

Read