Terraform 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)
MANDATORY: MCP Integration Workflow
BEFORE writing/modifying ANY Terraform code, you MUST execute this workflow:
Step 1: Check Provider/Module Versions (REQUIRED)
Use mcp_hashicorp_ter_get_latest_provider_version: - Provider: azurerm, azuread, kubernetes, helm, etc. - Purpose: Get current version for ~> constraint - NEVER guess versions Use mcp_hashicorp_ter_get_latest_module_version: - Namespace: <registry namespace> - Name: <module name> - Provider: <provider name> - Purpose: Check latest module version if using registry modules
Example workflow:
1. Get version: azurerm provider 2. Result: "4.51.0" → use "~> 4.51.0" 3. Get version: azuread provider 4. Result: "3.1.0" → use "~> 3.1.0"
Step 2: Consult Provider/Module Documentation (REQUIRED)
Use mcp_hashicorp_ter_search_providers: - Provider name: "azurerm" - Service slug: "<resource-type>" (e.g., "kubernetes_cluster", "storage_account") - Provider namespace: "hashicorp" - Provider document type: "resources" (for creating) or "data-sources" (for reading) - Purpose: Find exact resource documentation Use mcp_hashicorp_ter_get_provider_details: - Provider doc ID: <from search results> - Purpose: Get complete resource schema, arguments, examples - NEVER skip this - it prevents incorrect argument usage
Example workflow:
1. Search: provider="azurerm", service="kubernetes_cluster", type="resources" 2. Get doc ID from results 3. Get details: Complete azurerm_kubernetes_cluster schema 4. Verify: required vs optional arguments 5. Review: official examples
For modules:
Use mcp_hashicorp_ter_search_modules: - Query: "<module purpose>" - Provider: "azure" or specific provider - Purpose: Find existing public registry modules Use mcp_hashicorp_ter_get_module_details: - Module namespace/name/provider: from search - Purpose: Get usage documentation, inputs, outputs
Step 3: Review Workspace Patterns (REQUIRED)
Use grep_search or semantic_search: - Search for: similar resource types in *.tf files - Purpose: Maintain consistency with existing patterns Read relevant workspace files: - backend.tf - State configuration pattern - providers.tf - Provider alias patterns - variables.tf - Variable declaration standards - modules/terraform-azurerm-*/ - Module structure examples
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 (FORBIDDEN)
Detect with grep before committing:
# 1. No random_string/random_uuid (use MD5) grep -r "random_string\|random_uuid" terraform/modules/ # 2. All role assignments MUST have 'name' (use uuidv5) grep -n "azurerm_role_assignment" terraform/modules -r | grep -v "name =" # 3. No null checks in count (use boolean flags) grep -n "!= null\|!= \"\"\|== null\|== \"\"" terraform/ # 4. No inter-module dependencies grep -n "module\\..*\\..*=" terraform/modules/ # 5. No dynamic blocks in Event Grid grep -n "dynamic" terraform/modules/workloads/event-grid/ | grep "service_bus"
If any grep returns results: Fix before commit!
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 |
|---|---|---|---|
enable_managed_identity | Managed Identity | - | Used by all workloads for RBAC |
enable_vnet | VNet Spoke | - | Used by Storage, SQL, Container Apps |
enable_observability | Log Analytics + App Insights | - | REQUIRED by Container Apps |
enable_storage | Storage Account | - | Managed Identity (RBAC), VNet |
enable_service_bus | Service Bus | - | Managed Identity |
enable_event_grid | Event Grid | - | Managed Identity, Service Bus |
enable_sql | SQL Server | - | Managed Identity, VNet |
enable_key_vault | Key Vault | SQL (for password) | Managed Identity |
enable_container_registry | Container Registry (ACR) | - | Managed Identity (AcrPush + AcrPull) |
enable_container_apps | 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.2) - •SKU: Controlled by
container_registry_sku(string, default"Basic") - •RBAC: When
enable_managed_identity = true, AcrPush + AcrPull roles are auto-assigned to the Managed Identity - •Container Apps Integration: When both
enable_container_registryandenable_managed_identityare true, Container Apps receives MI pre-attached + ACRlogin_server - •Output:
container_app_ready_config— composite zero-config output bundling MI + ACR login server for Container Apps
Deterministic Patterns
1. MD5 Naming (NOT random_string):
# ❌ WRONG - Destroys resources on re-apply
resource "random_string" "suffix" {
length = 4
}
locals {
name = "${var.name}-${random_string.suffix.result}" # Changes every apply!
}
# ✅ CORRECT - Deterministic MD5
locals {
md5_suffix = substr(md5(var.name), 0, 4) # Same input = same output
name = "${var.name}-${local.md5_suffix}"
}
# Platform Stack naming locals (from naming module)
locals {
storage_account = "st${replace(local.name, "-", "")}${local.location_abbr}${local.suffix}"
key_vault = "kv-${local.name}-${local.location_abbr}-${local.suffix}"
sql_server = "sql-${local.name}-${local.location_abbr}-${local.suffix}"
container_app_env = "cae-${local.name}-${local.location_abbr}"
container_registry = "cr${local.name}${local.location_abbr}${local.suffix}" # e.g., "crmyplatformeus2abc1"
}
2. uuidv5 Role Assignments (NOT omitting name):
# ❌ WRONG - Azure generates random UUID
resource "azurerm_role_assignment" "example" {
scope = azurerm_resource.main.id
role_definition_name = "Contributor"
principal_id = var.principal_id
# No 'name' = Azure assigns random ID = destroy/recreate cycle
}
# ✅ CORRECT - Deterministic uuidv5
resource "azurerm_role_assignment" "example" {
name = uuidv5("dns", "${azurerm_resource.main.id}-${var.principal_id}-contributor")
scope = azurerm_resource.main.id
role_definition_name = "Contributor"
principal_id = var.principal_id
}
3. RBAC Propagation (180s time_sleep):
# ❌ WRONG - Secret created before RBAC propagates
resource "azurerm_key_vault_secret" "example" {
key_vault_id = azurerm_key_vault.main.id
depends_on = [azurerm_role_assignment.admin] # Not enough!
}
# ✅ CORRECT - Add time_sleep
resource "time_sleep" "wait_for_rbac" {
depends_on = [azurerm_role_assignment.admin]
create_duration = "180s"
triggers = {
role_assignment_id = azurerm_role_assignment.admin.id
}
}
resource "azurerm_key_vault_secret" "example" {
key_vault_id = azurerm_key_vault.main.id
depends_on = [time_sleep.wait_for_rbac] # Wait for propagation
}
State Management (CRITICAL)
ALL projects use Azure Blob Storage remote state - NEVER local state
# backend.tf
terraform {
backend "azurerm" {
resource_group_name = "rg-tfstate"
storage_account_name = "stapplicationsautomation"
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.tfstate - •
network-stack-prod.tfstate - •
workers-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.51.0" # Allows patch updates (4.51.x)
}
azuread = {
source = "hashicorp/azuread"
version = "~> 3.1.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.31.0"
}
}
}
Version constraint rules:
- •✅ Use
~> 4.51.0- allows patch updates (4.51.1, 4.51.2) - •❌ NEVER use
>= 3.0- unpinned, breaks reproducibility - •❌ NEVER use
= 4.51.0- exact pin, too restrictive - •❌ 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-tfstate"
storage_account_name = "stapplicationsautomation"
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.51.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 Standards
Module directory structure:
modules/terraform-azurerm-<resource>/
├── README.md # Module documentation
├── main.tf # Resource logic
├── variables.tf # Input declarations with validation
├── outputs.tf # Output declarations
├── versions.tf # Provider version constraints
├── locals.tf # Local computations (optional)
└── examples/
└── basic/
├── main.tf
└── terraform.tfvars.example
Module best practices:
variables.tf:
variable "resource_group_name" {
description = "Name of the resource group"
type = string
}
variable "location" {
description = "Azure region for resources"
type = string
validation {
condition = contains(["eastus", "westus", "westeurope"], var.location)
error_message = "Location must be a supported region."
}
}
variable "tags" {
description = "Tags to apply to resources"
type = map(string)
default = {}
}
# Provider alias support
variable "providers" {
description = "Provider configuration"
type = object({
azurerm = any
})
default = null
}
main.tf:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.51.0"
configuration_aliases = [azurerm.main] # Support provider aliases
}
}
}
resource "azurerm_storage_account" "main" {
provider = var.providers != null ? var.providers.azurerm : azurerm.main
name = "st${var.name}${var.environment}"
resource_group_name = var.resource_group_name
location = var.location
account_tier = var.account_tier
account_replication_type = var.replication_type
tags = merge(
var.tags,
{
Module = "terraform-azurerm-storage"
}
)
}
outputs.tf:
# Output complete resource object (most flexible)
output "storage_account" {
description = "Complete storage account resource object"
value = azurerm_storage_account.main
}
# Output specific commonly-used attributes
output "id" {
description = "Storage account ID"
value = azurerm_storage_account.main.id
}
output "name" {
description = "Storage account name"
value = azurerm_storage_account.main.name
}
# Output JSON summary
output "summary" {
description = "Storage account summary"
value = jsonencode({
id = azurerm_storage_account.main.id
name = azurerm_storage_account.main.name
primary_endpoint = azurerm_storage_account.main.primary_blob_endpoint
primary_access_key = "***SENSITIVE***"
})
}
# Sensitive outputs
output "primary_connection_string" {
description = "Primary connection string"
value = azurerm_storage_account.main.primary_connection_string
sensitive = true
}
README.md template:
# Terraform Azure <Resource> Module
## Description
Brief description of what this module creates and its purpose.
## Usage
\`\`\`hcl
module "storage" {
source = "./modules/terraform-azurerm-storage"
name = "myapp"
environment = "prod"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
tags = {
Environment = "Production"
}
}
\`\`\`
## Inputs
<!-- Auto-generated with: terraform-docs markdown . > README.md -->
## Outputs
<!-- Auto-generated with: terraform-docs markdown . > README.md -->
## Requirements
- Terraform >= 1.9.0
- azurerm provider ~> 4.51.0
## Examples
See [examples/basic](./examples/basic) for a complete example.
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:
grep_searchorsemantic_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 stapplicationsautomation ` --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 stapplicationsautomation ` --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
Response Format
When providing Terraform solutions:
- •Analysis: Explain what the code accomplishes
- •MCP validation: Show which MCP tools were consulted
- •Code: Complete, properly formatted Terraform with comments
- •File structure: Show which files need to be created/modified
- •Validation commands: How to test (
fmt,validate,plan) - •Documentation: Links to Terraform registry
- •Testing: How to verify implementation works
- •Rollback plan: How to safely undo changes
Key Reminders
- •✅ ALWAYS consult MCP tools before generating code
- •✅ ALWAYS pin provider versions with
~> - •✅ ALWAYS use remote state (Azure Blob Storage)
- •✅ ALWAYS include variable types and descriptions
- •✅ ALWAYS mark sensitive variables and outputs
- •✅ ALWAYS validate with
terraform validateandplan - •✅ ALWAYS follow multi-tenant configuration patterns
- •❌ NEVER use unpinned provider versions
- •❌ NEVER hardcode values that should be variables
- •❌ NEVER skip MCP provider documentation validation
- •❌ NEVER commit .tfvars files with secrets to git