Awesome-omni-skill terraform-platform-stack
Terraform specialist for Platform as a Service Stack v3.0.0+. Expert in deterministic naming (MD5), RBAC role assignments (uuidv5), feature flag orchestration, time-based RBAC propagation, and anti-pattern detection. Always validates with Terraform Registry MCP before ANY code generation to ensure latest provider schemas and avoid deprecated attributes.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/devops/terraform-platform-stack" ~/.claude/skills/diegosouzapw-awesome-omni-skill-terraform-platform-stack && rm -rf "$T"
skills/devops/terraform-platform-stack/SKILL.mdTerraform Platform Stack Specialist
Overview
This skill provides expert guidance for Terraform infrastructure as code development in the Platform as a Service Stack v3.0.0+ environment. It focuses on deterministic patterns (MD5 naming, uuidv5 RBAC), feature flag orchestration, root-level module coordination, and strict adherence to platform anti-pattern rules.
When to Use This Skill
- Creating new Terraform modules for Platform Stack workloads
- Implementing feature flags with proper dependency validation
- Refactoring code to eliminate anti-patterns (random_string, null checks in count)
- Debugging "depends on resource attributes" errors
- Implementing RBAC with deterministic uuidv5() role assignments
- Adding time_sleep for RBAC propagation (180s delays)
- Orchestrating modules at root main.tf level (no inter-module dependencies)
- Validating Azure Provider 4.x compatibility (deprecated attributes)
- Troubleshooting RBAC propagation failures
- Optimizing Terraform state with boolean flags (not null checks)
MCP Integration
Canonical source: See agent's MCP Tool Usage Protocol. Always validate provider versions and resource schemas via MCP tools before any code generation.
Critical Platform Stack Standards
State Management (Azure Blob with Azure AD)
ALL projects use Azure Blob Storage with Azure AD authentication:
# backend.tf terraform { backend "azurerm" { resource_group_name = "rg-paas" storage_account_name = "storagepaas" container_name = "tfstate" key = "platform.terraform.tfstate" # Override per platform use_azuread_auth = true # MANDATORY (no access keys) } }
Initialization (CLI):
terraform init \ -backend-config="key=myplatform.terraform.tfstate" \ -backend-config="use_azuread_auth=true"
Module Structure (Single Concern)
Standard module layout:
terraform/modules/{domain}/{resource}/ ├── main.tf # Resource + RBAC + network rules ├── variables.tf # name, location, tags, managed_identity_principal_id ├── outputs.tf # IDs, URIs, names (NO secret values)
Workloads include:
└── workloads/ ├── observability/ # Log Analytics + App Insights ├── storage-account/ # RBAC-only storage ├── service-bus/ # Premium namespace ├── event-grid/ # Domain with subscriptions ├── sql/ # SQL Server + Database └── container-apps/ # Container Apps Environment # container-registry → external: tfmodules-as-a-service-stack
Root orchestration ONLY (terraform/main.tf):
module "storage" { count = var.enable_storage ? 1 : 0 source = "./modules/workloads/storage-account" name = module.naming.storage_account location = var.location resource_group_name = module.resource_group.name managed_identity_principal_id = var.enable_managed_identity ? module.managed_identity[0].principal_id : null vnet_subnet_ids = var.enable_vnet ? [module.vnet_spoke[0].default_subnet_id] : [] tags = var.tags }
CRITICAL: Modules NEVER reference other modules - orchestration at root only!
Anti-Patterns
See
— Anti-Pattern Detection section for the full grep validation workflow.terraform-platform-instructions.md
Count Conditions (Boolean Flags ONLY)
# ❌ WRONG - Causes "depends on resource attributes" error count = var.log_analytics_workspace_id != null ? 1 : 0 # ✅ CORRECT - Use boolean flag count = var.enable_observability ? 1 : 0 # Root main.tf passes workspace_id directly (not via count) module "container_apps" { count = var.enable_container_apps ? 1 : 0 log_analytics_workspace_id = var.enable_observability ? module.observability[0].workspace_id : null }
Rule: Count conditions ONLY accept boolean variables. NO null checks, NO string comparisons.
Feature Flag Table
| Flag | Resource | Hard Dependency | Recommended Dependency |
|---|---|---|---|
| Managed Identity | - | Used by all workloads for RBAC |
| VNet Spoke | - | Used by Storage, SQL, Container Apps |
| Log Analytics + App Insights | - | REQUIRED by Container Apps |
| Storage Account | - | Managed Identity (RBAC), VNet |
| Service Bus | - | Managed Identity |
| Event Grid | - | Managed Identity, Service Bus |
| SQL Server | - | Managed Identity, VNet |
| Key Vault | SQL (for password) | Managed Identity |
| Container Registry (ACR) | - | Managed Identity (AcrPush + AcrPull) |
| Container Apps | Observability | VNet, Container Registry + MI |
Container Registry Details:
- Source: External module from
(tfmodules-as-a-service-stack
)git::https://github.com/orafaelferreiraa/tfmodules-as-a-service-stack.git//modules/azurerm_container_registry?ref=1.0.3 - SKU: Controlled by
(string, defaultcontainer_registry_sku
)"Basic" - RBAC: When
, AcrPush + AcrPull roles are auto-assigned to the Managed Identityenable_managed_identity = true - Container Apps Integration: When both
andenable_container_registry
are true, Container Apps receives MI pre-attached + ACRenable_managed_identitylogin_server - Output:
— composite zero-config output bundling MI + ACR login server for Container Appscontainer_app_ready_config
Deterministic Patterns
See
for MD5 naming, uuidv5 RBAC, and 180s time_sleep patterns with ❌/✅ examples.terraform-platform-instructions.md
State Management (CRITICAL)
ALL projects use Azure Blob Storage remote state - NEVER local state
# backend.tf terraform { backend "azurerm" { resource_group_name = "rg-paas" storage_account_name = "storagepaas" container_name = "tfstate" key = "<project>-<tenant>-<environment>.tfstate" # Credentials via environment variables: # ARM_SUBSCRIPTION_ID # ARM_TENANT_ID # ARM_CLIENT_ID # ARM_CLIENT_SECRET } }
State file naming convention:
<project>-<tenant>-<environment>.tfstate
Examples:
aks-na-prod.tfstatenetwork-stack-prod.tfstateworkers-backend-sophie-dev.tfstate
State initialization:
terraform init -backend-config="key=<project>-<tenant>-<environment>.tfstate"
Multi-Tenant Configuration Architecture
Configuration layering (hub-and-spoke pattern):
project-root/ ├── backend.tf ├── providers.tf ├── main.tf ├── variables.tf ├── cluster-config/ │ ├── common/ │ │ └── main.tfvars # Base configuration for ALL tenants │ └── specific/ │ ├── na/ │ │ ├── dev.tfvars # NA dev overrides │ │ ├── qa.tfvars # NA qa overrides │ │ └── prod.tfvars # NA prod overrides │ ├── sophie/ │ │ ├── dev.tfvars │ │ └── prod.tfvars │ └── woopi/ │ └── prod.tfvars └── modules/ └── <custom-modules>/
Usage pattern:
# Apply with layered configs terraform plan \ -var-file="cluster-config/common/main.tfvars" \ -var-file="cluster-config/specific/na/prod.tfvars" terraform apply \ -var-file="cluster-config/common/main.tfvars" \ -var-file="cluster-config/specific/na/prod.tfvars"
Variable naming for multi-tenant:
# Prefix tenant-specific variables variable "na_subscription_id" { description = "North America subscription ID" type = string sensitive = true } variable "sophie_subscription_id" { description = "Sophie tenant subscription ID" type = string sensitive = true } # OR use maps for shared structure variable "subscription_ids" { description = "Subscription IDs per tenant" type = map(string) sensitive = true default = { na = "" sophie = "" woopi = "" } }
Version Constraints (CRITICAL)
ALWAYS pin versions with
constraint:~>
# versions.tf terraform { required_version = ">= 1.9.0" required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.57.0" # Allows patch updates (4.57.x) } azuread = { source = "hashicorp/azuread" version = "~> 3.1.0" } kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.31.0" } } }
Version constraint rules:
- ✅ Use
- allows patch updates (4.57.1, 4.57.2)~> 4.57.0 - ❌ NEVER use
- unpinned, breaks reproducibility>= 3.0 - ❌ NEVER use
- exact pin, too restrictive= 4.57.0 - ❌ NEVER omit version - breaks state compatibility
Before upgrading providers:
- Check latest version:
mcp_hashicorp_ter_get_latest_provider_version - Review CHANGELOG for breaking changes
- Test in dev environment first
- Update all modules to compatible versions
File Structure Standards
Standard project organization:
project-root/ ├── backend.tf # Remote state configuration ├── providers.tf # Provider configurations with aliases ├── versions.tf # Terraform and provider version constraints ├── variables.tf # Input variable declarations ├── locals.tf # Local values and computed variables ├── main.tf # Primary resource definitions ├── network.tf # Networking resources (optional) ├── outputs.tf # Output declarations ├── *.tfvars # Variable values (NOT in git if contains secrets) └── modules/ # Local modules └── <module-name>/ ├── main.tf ├── variables.tf ├── outputs.tf └── versions.tf
File-specific guidelines:
backend.tf:
terraform { backend "azurerm" { resource_group_name = "rg-paas" storage_account_name = "storagepaas" container_name = "tfstate" key = "will-be-overridden-at-runtime.tfstate" } }
providers.tf:
terraform { required_version = ">= 1.9.0" required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.57.0" } } } # Multiple provider configurations with aliases provider "azurerm" { alias = "stefanininam" subscription_id = var.stefanininam_subscription_id tenant_id = var.stefanininam_tenant_id client_id = var.stefanininam_client_id client_secret = var.stefanininam_client_secret features {} } provider "azurerm" { alias = "devops" subscription_id = var.devops_subscription_id tenant_id = var.devops_tenant_id client_id = var.devops_client_id client_secret = var.devops_client_secret features {} }
variables.tf:
variable "environment" { description = "Deployment environment" type = string validation { condition = contains(["dev", "qa", "prod"], var.environment) error_message = "Environment must be dev, qa, or prod." } } variable "tenant" { description = "Tenant identifier" type = string validation { condition = contains(["na", "sophie", "woopi", "dex"], var.tenant) error_message = "Tenant must be na, sophie, woopi, or dex." } } variable "client_secret" { description = "Azure service principal client secret" type = string sensitive = true # ALWAYS mark secrets as sensitive }
locals.tf:
locals { resource_prefix = "${var.tenant}-${var.environment}" common_tags = merge( var.tags, { Environment = var.environment Tenant = var.tenant ManagedBy = "Terraform" Project = var.project_name } ) location_short = { "East US" = "eus" "West Europe" = "weu" } }
outputs.tf:
# Output complete resource object output "resource_group" { description = "Complete resource group object" value = azurerm_resource_group.main } # Output specific attributes output "resource_group_id" { description = "Resource group ID" value = azurerm_resource_group.main.id } # Output JSON summary output "resource_summary" { description = "JSON summary of key attributes" value = jsonencode({ id = azurerm_resource_group.main.id name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location }) } # Sensitive outputs output "connection_string" { description = "Database connection string" value = azurerm_storage_account.main.primary_connection_string sensitive = true # ALWAYS mark secrets as sensitive }
Module Development
See
— Module Structure section. Standard layout:terraform-platform-instructions.md,main.tf,variables.tfwith types, validation, and dual outputs (object + JSON).outputs.tf
Common Implementation Tasks
1. Creating New Resource Configuration
MCP workflow:
- Get latest provider version:
mcp_hashicorp_ter_get_latest_provider_version - Search for resource:
mcp_hashicorp_ter_search_providers - Get resource details:
mcp_hashicorp_ter_get_provider_details - Review workspace patterns:
orgrep_searchsemantic_search
Implementation:
# Variable declarations variable "storage_account_replication" { description = "Storage account replication type" type = string default = "LRS" validation { condition = contains(["LRS", "GRS", "RAGRS", "ZRS"], var.storage_account_replication) error_message = "Must be a valid replication type." } } # Resource with environment-specific logic resource "azurerm_storage_account" "data" { provider = azurerm.stefanininam name = "st${var.tenant}${var.app_name}${var.environment}" resource_group_name = azurerm_resource_group.main.name location = var.location account_tier = "Standard" account_replication_type = var.environment == "prod" ? "GRS" : "LRS" # Dynamic configuration based on environment min_tls_version = "TLS1_2" enable_https_traffic_only = true allow_nested_items_to_be_public = false network_rules { default_action = var.environment == "prod" ? "Deny" : "Allow" ip_rules = var.allowed_ips bypass = ["AzureServices"] } tags = local.common_tags } # Outputs output "storage_account_id" { description = "Storage account resource ID" value = azurerm_storage_account.data.id }
2. Debugging Terraform Issues
State Lock Issues:
Symptoms:
- "Error: Error locking state"
- "Error: state blob is already locked"
MCP workflow:
- Search: "Terraform Azure blob storage state lock troubleshooting"
Resolution:
# Check blob lease status az storage blob show ` --account-name storagepaas ` --container-name tfstate ` --name <project>-<tenant>-<environment>.tfstate ` --query "properties.lease" ` --auth-mode login # If lease is "locked", verify no one is running terraform # Contact team to confirm # Break lease (CAUTION) az storage blob lease break ` --blob-name <project>-<tenant>-<environment>.tfstate ` --container-name tfstate ` --account-name storagepaas ` --auth-mode login
Plan Failures:
Symptoms:
- "Error: Unsupported argument"
- "Error: Invalid value for variable"
- "Error: Cycle" (circular dependency)
MCP workflow:
- Get provider details for resource:
mcp_hashicorp_ter_get_provider_details - Search docs: "Terraform <error-message>"
Debugging steps:
# Enable detailed logging export TF_LOG=DEBUG export TF_LOG_PATH=terraform-debug.log terraform plan # Review log for specific error grep -i "error" terraform-debug.log # Visualize dependencies (for cycle errors) terraform graph | dot -Tpng > graph.png # Validate syntax terraform validate # Check provider versions terraform version terraform providers
Resource Already Exists:
Symptoms:
- "Error: A resource with the ID /subscriptions/... already exists"
Options:
Option 1: Import existing resource
# Get resource ID from Azure az resource show --resource-group <rg> --name <name> --resource-type <type> --query id -o tsv # Import into Terraform state terraform import azurerm_resource_group.main "/subscriptions/<sub-id>/resourceGroups/<rg-name>" # Verify import successful terraform plan # Should show no changes or only minor updates
Option 2: Remove from state (if recreating)
terraform state rm azurerm_resource_group.main
State Drift:
Symptoms:
- Terraform plan shows unexpected changes
- Manual changes were made in Azure Portal
Detection:
# Refresh state from actual resources terraform refresh # Detect drift terraform plan -detailed-exitcode # Exit code 0 = no changes # Exit code 1 = error # Exit code 2 = changes detected (drift) # Show specific drift terraform show # For detailed diff terraform plan -out=plan.tfplan terraform show -json plan.tfplan | jq '.resource_changes'
Resolution:
# Option 1: Accept drift (update code to match reality) # Modify .tf files to match current state # Option 2: Force compliance (apply Terraform configuration) terraform apply # Option 3: Ignore specific attributes resource "azurerm_resource_group" "main" { # ... lifecycle { ignore_changes = [ tags["LastModified"], # Ignore auto-updated tags ] } }
3. State Management Operations
Migrating resources between states:
# Step 1: Remove from current state terraform state rm azurerm_resource_group.example # Step 2: Initialize new backend terraform init -backend-config="key=new-project-na-prod.tfstate" # Step 3: Import into new state terraform import azurerm_resource_group.example "/subscriptions/<sub-id>/resourceGroups/<rg-name>" # Step 4: Verify terraform plan # Should show no changes
Renaming resources in code:
# Move resource within same state (no Azure changes) terraform state mv azurerm_resource_group.old_name azurerm_resource_group.new_name # Update code to match # In main.tf: change resource name from "old_name" to "new_name" # Verify no changes planned terraform plan
4. Testing and Validation
Pre-commit validation:
# Format all Terraform files terraform fmt -recursive # Validate syntax terraform validate # Check for security issues (if tfsec is installed) tfsec . # Validate provider versions terraform version terraform providers # Generate documentation terraform-docs markdown . > README.md