Azure Anti-Patterns
Overview¶
This guide presents common Azure anti-patterns and mistakes, along with their correct implementations. Each anti-pattern includes:
- Bad Example: The anti-pattern or mistake
- Good Example: The corrected implementation
- Explanation: Why the anti-pattern is problematic and how the correction improves it
Security Anti-Patterns¶
Public Network Access on PaaS Services¶
Bad: Exposing PaaS services to the public internet
resource "azurerm_key_vault" "bad" {
name = "kv-webapp-prod"
location = var.location
resource_group_name = var.resource_group_name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
# No network restrictions - accessible from anywhere
}
resource "azurerm_storage_account" "bad" {
name = "stwebappprod001"
resource_group_name = var.resource_group_name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS"
# Public blob access enabled
allow_nested_items_to_be_public = true
}
resource "azurerm_mssql_server" "bad" {
name = "sql-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.sql_password
# Public network access enabled by default
}
# Firewall rule allowing all Azure services AND public internet
resource "azurerm_mssql_firewall_rule" "bad" {
name = "AllowAll"
server_id = azurerm_mssql_server.bad.id
start_ip_address = "0.0.0.0"
end_ip_address = "255.255.255.255"
}
Good: Use private endpoints and disable public access
resource "azurerm_key_vault" "good" {
name = "kv-webapp-prod"
location = var.location
resource_group_name = var.resource_group_name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
# Disable public access
public_network_access_enabled = false
# Network ACLs as defense in depth
network_acls {
default_action = "Deny"
bypass = "AzureServices"
}
# Enable purge protection for production
purge_protection_enabled = true
soft_delete_retention_days = 90
}
resource "azurerm_storage_account" "good" {
name = "stwebappprod001"
resource_group_name = var.resource_group_name
location = var.location
account_tier = "Standard"
account_replication_type = "GRS"
# Security settings
allow_nested_items_to_be_public = false
public_network_access_enabled = false
min_tls_version = "TLS1_2"
network_rules {
default_action = "Deny"
bypass = ["AzureServices"]
}
}
resource "azurerm_mssql_server" "good" {
name = "sql-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.sql_password
minimum_tls_version = "1.2"
public_network_access_enabled = false
azuread_administrator {
login_username = var.sql_aad_admin
object_id = var.sql_aad_admin_object_id
azuread_authentication_only = true
}
}
# Private endpoint for SQL Server
resource "azurerm_private_endpoint" "sql" {
name = "pep-sql-webapp-prod"
location = var.location
resource_group_name = var.resource_group_name
subnet_id = var.data_subnet_id
private_service_connection {
name = "psc-sql"
private_connection_resource_id = azurerm_mssql_server.good.id
subresource_names = ["sqlServer"]
is_manual_connection = false
}
private_dns_zone_group {
name = "default"
private_dns_zone_ids = [var.sql_private_dns_zone_id]
}
}
Why: Public endpoints expose services to internet-based attacks. Private endpoints ensure traffic stays within Azure's backbone network and your VNet.
Hardcoded Secrets in Terraform¶
Bad: Secrets stored in Terraform code or tfvars
resource "azurerm_mssql_server" "bad" {
name = "sql-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = "P@ssw0rd123!" # Hardcoded secret!
}
resource "azurerm_linux_web_app" "bad" {
name = "app-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
service_plan_id = azurerm_service_plan.main.id
app_settings = {
"DATABASE_PASSWORD" = "P@ssw0rd123!" # Secret in plain text!
"API_KEY" = "sk-abc123xyz" # Another hardcoded secret!
}
site_config {
application_stack {
python_version = "3.11"
}
}
}
Good: Use Key Vault and managed identities
# Generate random password
resource "random_password" "sql_admin" {
length = 32
special = true
override_special = "!@#$%^&*"
}
# Store password in Key Vault
resource "azurerm_key_vault_secret" "sql_password" {
name = "sql-admin-password"
value = random_password.sql_admin.result
key_vault_id = azurerm_key_vault.main.id
content_type = "password"
expiration_date = timeadd(timestamp(), "8760h")
}
resource "azurerm_mssql_server" "good" {
name = "sql-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = random_password.sql_admin.result
# Prefer Azure AD authentication
azuread_administrator {
login_username = var.sql_aad_admin
object_id = var.sql_aad_admin_object_id
azuread_authentication_only = true
}
lifecycle {
ignore_changes = [administrator_login_password]
}
}
resource "azurerm_linux_web_app" "good" {
name = "app-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
service_plan_id = azurerm_service_plan.main.id
identity {
type = "SystemAssigned"
}
app_settings = {
# Reference Key Vault - app retrieves secrets at runtime
"KEY_VAULT_URI" = azurerm_key_vault.main.vault_uri
}
site_config {
application_stack {
python_version = "3.11"
}
}
}
# Grant the app access to Key Vault secrets
resource "azurerm_role_assignment" "app_keyvault" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_linux_web_app.good.identity[0].principal_id
}
Why: Hardcoded secrets end up in version control, state files, and logs. Key Vault with managed identity eliminates secret exposure.
Using Service Principal Secrets Instead of Managed Identity¶
Bad: Service principal with client secrets
provider "azurerm" {
features {}
# Service principal authentication with secrets
client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
client_secret = var.client_secret # Secret that expires and must be rotated
tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
resource "azurerm_linux_web_app" "bad" {
name = "app-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
service_plan_id = azurerm_service_plan.main.id
app_settings = {
# Storing credentials for the app to use
"AZURE_CLIENT_ID" = var.app_client_id
"AZURE_CLIENT_SECRET" = var.app_client_secret
"AZURE_TENANT_ID" = var.tenant_id
}
site_config {
application_stack {
python_version = "3.11"
}
}
}
Good: Use OIDC for CI/CD and managed identity for workloads
provider "azurerm" {
features {}
# OIDC authentication for GitHub Actions
use_oidc = true
subscription_id = var.subscription_id
tenant_id = var.tenant_id
}
resource "azurerm_linux_web_app" "good" {
name = "app-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
service_plan_id = azurerm_service_plan.main.id
identity {
type = "SystemAssigned"
}
app_settings = {
# No credentials needed - uses managed identity
"AZURE_CLIENT_ID" = "" # DefaultAzureCredential auto-detects managed identity
}
site_config {
application_stack {
python_version = "3.11"
}
}
}
# Grant specific permissions to the managed identity
resource "azurerm_role_assignment" "app_storage" {
scope = azurerm_storage_account.main.id
role_definition_name = "Storage Blob Data Contributor"
principal_id = azurerm_linux_web_app.good.identity[0].principal_id
}
Why: Service principal secrets must be managed, rotated, and can be leaked. Managed identities are automatically managed by Azure with no secrets to handle.
Overly Permissive RBAC¶
Bad: Assigning Owner or Contributor at subscription scope
# Granting Owner at subscription level
resource "azurerm_role_assignment" "bad_owner" {
scope = "/subscriptions/${data.azurerm_subscription.current.subscription_id}"
role_definition_name = "Owner"
principal_id = var.developer_group_object_id
}
# Granting Contributor to a service principal for everything
resource "azurerm_role_assignment" "bad_contributor" {
scope = "/subscriptions/${data.azurerm_subscription.current.subscription_id}"
role_definition_name = "Contributor"
principal_id = azurerm_user_assigned_identity.app.principal_id
}
Good: Least privilege at the narrowest scope
# Developers get Contributor only on their resource group
resource "azurerm_role_assignment" "dev_contributor" {
scope = azurerm_resource_group.app.id
role_definition_name = "Contributor"
principal_id = var.developer_group_object_id
}
# App identity gets only the specific permissions needed
resource "azurerm_role_assignment" "app_storage_read" {
scope = azurerm_storage_account.main.id
role_definition_name = "Storage Blob Data Reader"
principal_id = azurerm_user_assigned_identity.app.principal_id
}
resource "azurerm_role_assignment" "app_keyvault_secrets" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_user_assigned_identity.app.principal_id
}
# Custom role for specific actions when built-in roles are too broad
resource "azurerm_role_definition" "app_operator" {
name = "Application Operator"
scope = azurerm_resource_group.app.id
description = "Can restart and monitor applications"
permissions {
actions = [
"Microsoft.Web/sites/restart/action",
"Microsoft.Web/sites/slots/restart/action",
"Microsoft.Insights/metrics/read",
"Microsoft.Insights/logs/read"
]
not_actions = []
}
assignable_scopes = [azurerm_resource_group.app.id]
}
Why: Overly permissive roles violate least privilege and increase blast radius of compromised credentials.
Networking Anti-Patterns¶
Flat Network Without Segmentation¶
Bad: Single subnet for all workloads
resource "azurerm_virtual_network" "bad" {
name = "vnet-webapp"
location = var.location
resource_group_name = var.resource_group_name
address_space = ["10.0.0.0/16"]
}
# Single large subnet for everything
resource "azurerm_subnet" "bad" {
name = "default"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.bad.name
address_prefixes = ["10.0.0.0/16"]
}
# No NSG - all traffic allowed
Good: Network segmentation with NSGs
resource "azurerm_virtual_network" "good" {
name = "vnet-webapp-prod"
location = var.location
resource_group_name = var.resource_group_name
address_space = ["10.0.0.0/16"]
}
# Web tier subnet
resource "azurerm_subnet" "web" {
name = "snet-web"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.good.name
address_prefixes = ["10.0.1.0/24"]
}
# Application tier subnet with App Service delegation
resource "azurerm_subnet" "app" {
name = "snet-app"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.good.name
address_prefixes = ["10.0.2.0/24"]
delegation {
name = "app-service"
service_delegation {
name = "Microsoft.Web/serverFarms"
actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
}
}
}
# Data tier subnet for databases
resource "azurerm_subnet" "data" {
name = "snet-data"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.good.name
address_prefixes = ["10.0.3.0/24"]
private_endpoint_network_policies = "Disabled"
}
# NSG for web tier
resource "azurerm_network_security_group" "web" {
name = "nsg-web"
location = var.location
resource_group_name = var.resource_group_name
security_rule {
name = "AllowHTTPS"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
security_rule {
name = "DenyAllInbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
# NSG for data tier - only allow app tier
resource "azurerm_network_security_group" "data" {
name = "nsg-data"
location = var.location
resource_group_name = var.resource_group_name
security_rule {
name = "AllowAppTier"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_ranges = ["1433", "5432"]
source_address_prefix = "10.0.2.0/24"
destination_address_prefix = "*"
}
security_rule {
name = "DenyAllInbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
resource "azurerm_subnet_network_security_group_association" "web" {
subnet_id = azurerm_subnet.web.id
network_security_group_id = azurerm_network_security_group.web.id
}
resource "azurerm_subnet_network_security_group_association" "data" {
subnet_id = azurerm_subnet.data.id
network_security_group_id = azurerm_network_security_group.data.id
}
Why: Flat networks allow lateral movement. Segmentation limits blast radius and enforces network-level access control.
Not Using Private DNS Zones with Private Endpoints¶
Bad: Private endpoints without DNS integration
resource "azurerm_private_endpoint" "bad" {
name = "pep-storage"
location = var.location
resource_group_name = var.resource_group_name
subnet_id = var.subnet_id
private_service_connection {
name = "psc-storage"
private_connection_resource_id = azurerm_storage_account.main.id
subresource_names = ["blob"]
is_manual_connection = false
}
# No DNS zone group - must manually manage DNS or use IP addresses
}
Good: Private endpoints with private DNS zone integration
# Private DNS zone for blob storage
resource "azurerm_private_dns_zone" "blob" {
name = "privatelink.blob.core.windows.net"
resource_group_name = var.resource_group_name
}
# Link DNS zone to VNet
resource "azurerm_private_dns_zone_virtual_network_link" "blob" {
name = "link-${var.vnet_name}"
resource_group_name = var.resource_group_name
private_dns_zone_name = azurerm_private_dns_zone.blob.name
virtual_network_id = var.vnet_id
registration_enabled = false
}
resource "azurerm_private_endpoint" "good" {
name = "pep-storage"
location = var.location
resource_group_name = var.resource_group_name
subnet_id = var.subnet_id
private_service_connection {
name = "psc-storage"
private_connection_resource_id = azurerm_storage_account.main.id
subresource_names = ["blob"]
is_manual_connection = false
}
# Automatic DNS registration
private_dns_zone_group {
name = "default"
private_dns_zone_ids = [azurerm_private_dns_zone.blob.id]
}
}
Why: Without private DNS integration, applications must use private IP addresses directly, breaking portability and complicating configuration.
Cost Anti-Patterns¶
Over-Provisioned Resources¶
Bad: Production sizing for all environments
resource "azurerm_kubernetes_cluster" "bad" {
name = "aks-webapp-dev"
location = var.location
resource_group_name = var.resource_group_name
dns_prefix = "webapp-dev"
default_node_pool {
name = "system"
node_count = 5 # Same as production
vm_size = "Standard_D16s_v5" # Expensive VM size for dev
}
identity {
type = "SystemAssigned"
}
}
resource "azurerm_mssql_database" "bad" {
name = "sqldb-webapp-dev"
server_id = azurerm_mssql_server.main.id
sku_name = "P6" # Premium tier for dev environment
max_size_gb = 500
zone_redundant = true # HA in dev
}
Good: Right-size based on environment
locals {
env_config = {
dev = {
aks_node_count = 2
aks_vm_size = "Standard_B2ms"
sql_sku = "S0"
sql_size_gb = 10
zone_redundant = false
}
staging = {
aks_node_count = 3
aks_vm_size = "Standard_D4s_v5"
sql_sku = "S3"
sql_size_gb = 50
zone_redundant = false
}
prod = {
aks_node_count = 5
aks_vm_size = "Standard_D8s_v5"
sql_sku = "P2"
sql_size_gb = 250
zone_redundant = true
}
}
config = local.env_config[var.environment]
}
resource "azurerm_kubernetes_cluster" "good" {
name = "aks-webapp-${var.environment}"
location = var.location
resource_group_name = var.resource_group_name
dns_prefix = "webapp-${var.environment}"
default_node_pool {
name = "system"
node_count = local.config.aks_node_count
vm_size = local.config.aks_vm_size
auto_scaling_enabled = var.environment == "prod"
min_count = var.environment == "prod" ? 3 : null
max_count = var.environment == "prod" ? 10 : null
}
identity {
type = "SystemAssigned"
}
}
resource "azurerm_mssql_database" "good" {
name = "sqldb-webapp-${var.environment}"
server_id = azurerm_mssql_server.main.id
sku_name = local.config.sql_sku
max_size_gb = local.config.sql_size_gb
zone_redundant = local.config.zone_redundant
}
Why: Development and test environments don't need production capacity. Right-sizing can reduce costs by 60-80%.
No Auto-Shutdown for Dev/Test¶
Bad: Dev VMs running 24/7
resource "azurerm_linux_virtual_machine" "bad" {
name = "vm-dev-001"
resource_group_name = var.resource_group_name
location = var.location
size = "Standard_D4s_v5"
admin_username = "azureuser"
# VM runs 24/7 even though dev team only works 8 hours
admin_ssh_key {
username = "azureuser"
public_key = var.ssh_public_key
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
network_interface_ids = [azurerm_network_interface.main.id]
}
Good: Auto-shutdown for non-production
resource "azurerm_linux_virtual_machine" "good" {
name = "vm-dev-001"
resource_group_name = var.resource_group_name
location = var.location
size = "Standard_D4s_v5"
admin_username = "azureuser"
admin_ssh_key {
username = "azureuser"
public_key = var.ssh_public_key
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS" # Standard for dev
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
network_interface_ids = [azurerm_network_interface.main.id]
tags = {
Environment = "dev"
AutoShutdown = "true"
}
}
# Auto-shutdown schedule
resource "azurerm_dev_test_global_vm_shutdown_schedule" "good" {
virtual_machine_id = azurerm_linux_virtual_machine.good.id
location = var.location
enabled = true
daily_recurrence_time = "1900" # 7 PM local time
timezone = "Eastern Standard Time"
notification_settings {
enabled = true
time_in_minutes = 30
email = var.team_email
}
}
Why: Development VMs idle 16+ hours daily. Auto-shutdown reduces costs by 66% with no impact on developers.
Missing Tags for Cost Allocation¶
Bad: Resources without cost allocation tags
resource "azurerm_resource_group" "bad" {
name = "rg-webapp"
location = var.location
# No tags
}
resource "azurerm_storage_account" "bad" {
name = "stwebapp001"
resource_group_name = azurerm_resource_group.bad.name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS"
# No tags - impossible to track costs
}
Good: Comprehensive tagging strategy
locals {
required_tags = {
Environment = var.environment
Project = var.project_name
CostCenter = var.cost_center
Owner = var.owner_email
ManagedBy = "terraform"
CreatedDate = formatdate("YYYY-MM-DD", timestamp())
}
}
resource "azurerm_resource_group" "good" {
name = "rg-${var.project_name}-${var.environment}"
location = var.location
tags = local.required_tags
}
resource "azurerm_storage_account" "good" {
name = "st${var.project_name}${var.environment}001"
resource_group_name = azurerm_resource_group.good.name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = merge(local.required_tags, {
DataClassification = "Internal"
BackupPolicy = "Daily"
})
}
# Azure Policy to enforce required tags
resource "azurerm_policy_assignment" "require_cost_center" {
name = "require-cost-center"
scope = azurerm_resource_group.good.id
policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/96670d01-0a4d-4649-9c89-2d3abc0a5025"
parameters = jsonencode({
tagName = { value = "CostCenter" }
})
}
Why: Without proper tagging, cost allocation and chargeback are impossible. Finance can't determine which teams are responsible for spending.
State Management Anti-Patterns¶
Local State for Team Projects¶
Bad: Terraform state stored locally
terraform {
required_version = ">= 1.6.0"
# No backend configured - state stored locally
# - Can't collaborate with team
# - No state locking
# - State can be lost
# - Secrets in state file on developer machines
}
Good: Remote state with Azure Storage
terraform {
required_version = ">= 1.6.0"
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "sttfstateprod001"
container_name = "tfstate"
key = "webapp/prod/terraform.tfstate"
use_oidc = true
}
}
With secure storage account:
resource "azurerm_storage_account" "tfstate" {
name = "sttfstateprod001"
resource_group_name = var.state_resource_group
location = var.location
account_tier = "Standard"
account_replication_type = "GRS"
# Security settings
min_tls_version = "TLS1_2"
allow_nested_items_to_be_public = false
public_network_access_enabled = false
# Enable versioning for state file recovery
blob_properties {
versioning_enabled = true
delete_retention_policy {
days = 365
}
}
# Restrict access
network_rules {
default_action = "Deny"
bypass = ["AzureServices"]
ip_rules = var.allowed_ip_ranges
}
}
Why: Local state prevents collaboration, has no locking for concurrent operations, and can be accidentally deleted or corrupted.
Operational Anti-Patterns¶
No Diagnostic Settings¶
Bad: Resources without logging
resource "azurerm_key_vault" "bad" {
name = "kv-webapp-prod"
location = var.location
resource_group_name = var.resource_group_name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
# No diagnostic settings - can't audit access or troubleshoot issues
}
resource "azurerm_mssql_server" "bad" {
name = "sql-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.sql_password
# No auditing - compliance risk
}
Good: Comprehensive diagnostic settings
resource "azurerm_key_vault" "good" {
name = "kv-webapp-prod"
location = var.location
resource_group_name = var.resource_group_name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
}
resource "azurerm_monitor_diagnostic_setting" "keyvault" {
name = "diag-kv-webapp-prod"
target_resource_id = azurerm_key_vault.good.id
log_analytics_workspace_id = var.log_analytics_workspace_id
enabled_log {
category = "AuditEvent"
}
enabled_log {
category = "AzurePolicyEvaluationDetails"
}
metric {
category = "AllMetrics"
}
}
resource "azurerm_mssql_server" "good" {
name = "sql-webapp-prod"
resource_group_name = var.resource_group_name
location = var.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.sql_password
}
resource "azurerm_mssql_server_extended_auditing_policy" "good" {
server_id = azurerm_mssql_server.good.id
storage_endpoint = var.audit_storage_endpoint
storage_account_access_key = var.audit_storage_key
storage_account_access_key_is_secondary = false
retention_in_days = 90
log_monitoring_enabled = true
}
resource "azurerm_mssql_server_security_alert_policy" "good" {
server_id = azurerm_mssql_server.good.id
state = "Enabled"
email_addresses = [var.security_email]
email_account_admins = true
retention_days = 30
storage_endpoint = var.audit_storage_endpoint
storage_account_access_key = var.audit_storage_key
}
Why: Without diagnostic settings, you can't audit access, detect security incidents, or troubleshoot issues effectively.
No Backup Strategy¶
Bad: Production resources without backup
resource "azurerm_mssql_database" "bad" {
name = "sqldb-webapp-prod"
server_id = azurerm_mssql_server.main.id
sku_name = "S3"
# Default 7-day retention only
# No geo-redundant backup
# No long-term retention
}
resource "azurerm_storage_account" "bad" {
name = "stwebappprod001"
resource_group_name = var.resource_group_name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS" # Single region - data loss risk
# No soft delete
# No versioning
}
Good: Comprehensive backup and recovery
resource "azurerm_mssql_database" "good" {
name = "sqldb-webapp-prod"
server_id = azurerm_mssql_server.main.id
sku_name = "S3"
zone_redundant = true
# Enhanced backup retention
short_term_retention_policy {
retention_days = 35
backup_interval_in_hours = 12
}
# Long-term retention for compliance
long_term_retention_policy {
weekly_retention = "P4W" # 4 weeks
monthly_retention = "P12M" # 12 months
yearly_retention = "P5Y" # 5 years
week_of_year = 1
}
}
resource "azurerm_storage_account" "good" {
name = "stwebappprod001"
resource_group_name = var.resource_group_name
location = var.location
account_tier = "Standard"
account_replication_type = "GRS" # Geo-redundant
# Enable soft delete and versioning
blob_properties {
versioning_enabled = true
change_feed_enabled = true
last_access_time_enabled = true
delete_retention_policy {
days = 30
}
container_delete_retention_policy {
days = 30
}
}
}
# Backup vault for VMs
resource "azurerm_recovery_services_vault" "good" {
name = "rsv-webapp-prod"
location = var.location
resource_group_name = var.resource_group_name
sku = "Standard"
soft_delete_enabled = true
cross_region_restore_enabled = true
}
resource "azurerm_backup_policy_vm" "good" {
name = "policy-vm-daily"
resource_group_name = var.resource_group_name
recovery_vault_name = azurerm_recovery_services_vault.good.name
backup {
frequency = "Daily"
time = "02:00"
}
retention_daily {
count = 30
}
retention_weekly {
count = 12
weekdays = ["Sunday"]
}
retention_monthly {
count = 12
weekdays = ["Sunday"]
weeks = ["First"]
}
retention_yearly {
count = 5
weekdays = ["Sunday"]
weeks = ["First"]
months = ["January"]
}
}
Why: Without proper backup, data loss from human error, ransomware, or disasters is unrecoverable.