하기 5개 Azure Policy를 정의하고, 테라폼으로 Azure에 배포하는 테스트
Azure Policy 5개
| 정책 종류 | 정책 목적 | 예시 정책 설명 | 효과 유형 | 추천 이유 |
| 1. 리전 통제 | 데이터 주권/법률 규제 대응 | 한국 외 리전 배포 차단 (location notIn) | Deny | 일반적으로 특정 리전에만 배포함 |
| 2. 태그 감시 | 자산 식별 및 비용 분석, 자동화 연계 | 태그 없는 경우 리소스 생성 거부 | Deny | 리소스 추적 및 책임자 관리 용이 |
| 3. 리소스 유형 제한 | 불필요한 리소스 배포 방지 | 허용된 리소스만 배포하도록 화이트리스트 외에는 불허 | Deny | 무분별한 배포 방지 |
| 4. VM SKU 제한 | 비용 제어 및 표준화 | 특정 SKU는 배포 불가능하도록 블랙리스트 불허 | Deny | 무분별한 배포 방지 |
| 5. 삭제 보호 | 주요 리소스 삭제 방지 | 삭제 잠금(Delete Lock) | AuditIfNotExists | Terraform에서 Delete Lock 없을 시 리소스 생성 Deny는 불가 |
0. Terraform init
- vs code에서 Terraform-Policy 폴더생성 후 provider.tf 작성 (ctrl+s로 저장)

terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.0.0"
}
random = {
source = "hashicorp/random"
}
}
}
provider "azurerm" {
features {}
}
- policy 전용 폴더에서 따로 테스트 하지만, 모듈화 구조 채택 하여 아래와 같은 구조를 상정하여 테스트
📁 terraform/
│
├── 📁 modules/
│ ├── 📁 resource_group/ # 리소스 그룹 생성
│ ├── 📁 vnet/ # VNet 및 Subnet 생성 (Hub / Spoke 공통)
│ ├── 📁 .....
│ └── 📁 policy/ # Azure policy 생성 및 json 파일
│
├── 📄 main.tf # 상태파일 저장 위치 (예: Azure Storage)
├── 📄 provider.tf # Azure Provider 정의
└── 📄 variables.tf # 공통 변수 정의
1. 리전통제
- 한국 외 리전 배포 차단 (location notIn)
- Effect : Deny
variable "subscription_id" {
type = string
description = "Azure subscription ID"
default = "실제 구독 ID 입력" ######### 실제 구독 ID로 변경필요 #########
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}
resource "azurerm_policy_definition" "deny_non_korea_locations" {
name = "deny-non-korea-locations"
policy_type = "Custom"
mode = "All"
display_name = "Deny non-Korea locations"
description = "Deny resource deployment outside Korea Central and Korea South."
policy_rule = jsonencode({
"if": {
"not": {
"field": "location",
"in": [
"koreaCentral",
"koreaSouth"
]
}
},
"then": {
"effect": "Deny"
}
})
metadata = jsonencode({
category = "General"
})
}
resource "azurerm_subscription_policy_assignment" "deny_non_korea_locations_assignment" {
name = "deny-non-korea-locations-assignment"
display_name = "Deny non-Korea locations"
description = "This policy denies deployment of resources outside Korea."
policy_definition_id = azurerm_policy_definition.deny_non_korea_locations.id
subscription_id = "/subscriptions/${var.subscription_id}"
}
az login
az account show
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )
정책 생성 확인


정책 거부 확인
- 테스트 : non-korea 리전으로 리소스그룹 배포 시도
- 결과 : 배포 실패(deny), Raw 오류에 보면 거부 기준 policy 확인 가능

2. 태그 통제
- 태그 없는 리소스 생성 거부
- 특이사항 : 모든 리소스에 적용하는 것이 아니라, VM, RG, StorageAccount, NIC, PublicIP에 태그가 없을 시 거부
VNET, VPN, Firewall 추후 검토 필요
resource "azurerm_policy_definition" "deny_missing_tags" {
name = "deny-missing-tags"
policy_type = "Custom"
mode = "All"
display_name = "Deny missing tags for key resource types"
description = "Deny creation of VMs, storage accounts, network resources, and resource groups without tags."
policy_rule = jsonencode({
"if": {
"allOf": [
{
"field": "type",
"in": [ ### VM, RG,StorageAccount, NIC,PublicIP,NSG에 태그가 없을시 거부
"Microsoft.Compute/virtualMachines",
"Microsoft.Resources/subscriptions/resourceGroups",
"Microsoft.Storage/storageAccounts",
"Microsoft.Network/networkInterfaces",
"Microsoft.Network/publicIPAddresses"
]
},
{
"field": "tags",
"exists": false
}
]
},
"then": {
"effect": "Deny"
}
})
metadata = jsonencode({
category = "Tags"
})
}
resource "azurerm_subscription_policy_assignment" "deny_missing_tags_assignment" {
name = "deny-missing-tags-assignment"
display_name = "Deny resources without tags"
description = "This policy denies creation of resources that do not have any tags."
policy_definition_id = azurerm_policy_definition.deny_missing_tags.id
subscription_id = "/subscriptions/${var.subscription_id}"
}
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )
정책생성 확인

정책 테스트
- 테스트 : VM 생성시 태그 없이 [검토+만들기]
- 결과 : Policy로 인해 VM 생성 전 유효성 검사 불가로, VM 생성 거부됨


3. 리소스 유형 제한
- 화이트리스트를 제외한 리소스 생성 제한
- 화이트 리스트 :
- Compute
├─ Virtual Machines (VM)
└─ Disks
- Network
├─ Virtual Networks (VNet)
│ └─ Subnets
├─ Public IP Addresses
├─ Network Interfaces (NIC)
├─ Network Security Groups (NSG)
├─ Route Tables
│ └─ Routes
├─ VPN Gateways
├─ Local Network Gateways
├─ ExpressRoute
│ ├─ ExpressRoute Gateways
│ └─ ExpressRoute Circuits
├─ Application Gateway
│ └─ WAF Policies (Web Application Firewall)
├─ Azure Firewall
├─ Firewall Policies
├─ IP Groups
└─ DNS
└─ DNS Zones
- Storage
└─ Storage Accounts
- Web
├─ App Service Plans (Server Farms)
└─ App Services (Websites)
- Database (PaaS)
├─ SQL Servers
├─ SQL Databases
├─ MySQL Servers
├─ MySQL Databases
├─ PostgreSQL Servers
├─ PostgreSQL Databases
├─ MariaDB Servers
├─ MariaDB Databases
└─ Cosmos DB Accounts
- Security & Identity
└─ Key Vaults
- Monitoring & Insights
└─ Log Analytics Workspaces (Operational Insights)
- Management & Governance
├─ Resource Groups
└─ Policy Definitions (Microsoft.Authorization/policyDefinitions)
resource "azurerm_policy_definition" "deny_non_whitelisted_types" {
name = "deny-non-whitelisted-resource-types"
policy_type = "Custom"
mode = "All"
display_name = "Deny resources not in the allowed list"
description = "Prevents creation of any resource types not explicitly allowed."
policy_rule = jsonencode({
"if": {
"not": {
"field": "type",
"in": [
"Microsoft.Compute/virtualMachines",
"Microsoft.Compute/disks",
"Microsoft.Network/publicIPAddresses",
"Microsoft.Network/networkInterfaces",
"Microsoft.Network/networkSecurityGroups",
"Microsoft.Network/networkSecurityGroups/securityRules", #NSG뿐만아니라 rules도 허용
"Microsoft.Network/virtualNetworks",
"Microsoft.Network/virtualNetworks/subnets",
"Microsoft.Network/routeTables",
"Microsoft.Network/routeTables/routes",
"Microsoft.Network/vpnGateways",
"Microsoft.Network/localNetworkGateways",
"Microsoft.Network/expressRouteGateways",
"Microsoft.Network/expressRouteCircuits",
"Microsoft.Network/applicationGateways",
"Microsoft.Network/applicationGatewayWebApplicationFirewallPolicies",
"Microsoft.Network/dnsZones",
"Microsoft.Network/azureFirewalls",
"Microsoft.Network/firewallPolicies",
"Microsoft.Network/ipGroups",
"Microsoft.Resources/resourceGroups",
"Microsoft.Sql/servers",
"Microsoft.Sql/servers/databases",
"Microsoft.DBforMySQL/servers",
"Microsoft.DBforMySQL/servers/databases",
"Microsoft.DBforPostgreSQL/servers",
"Microsoft.DBforPostgreSQL/servers/databases",
"Microsoft.DBforMariaDB/servers",
"Microsoft.DBforMariaDB/servers/databases",
"Microsoft.DocumentDB/databaseAccounts",
"Microsoft.Web/serverfarms",
"Microsoft.Web/sites",
"Microsoft.Storage/storageAccounts",
"Microsoft.KeyVault/vaults",
"Microsoft.OperationalInsights/workspaces",
"Microsoft.Authorization/policyDefinitions", # 추후 azure policy 생성 위해 추가
"Microsoft.Authorization/policyAssignments"
]
}
},
"then": {
"effect": "Deny"
}
})
metadata = jsonencode({
category = "General"
})
}
resource "azurerm_subscription_policy_assignment" "deny_non_whitelisted_types_assignment" {
name = "deny-non-whitelisted-types-assignment"
display_name = "Deny non-whitelisted resource types"
description = "This policy denies creation of resources not included in the approved list."
policy_definition_id = azurerm_policy_definition.deny_non_whitelisted_types.id
subscription_id = "/subscriptions/${var.subscription_id}"
}
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )
정책생성 확인

정책 테스트
- 테스트 : 화이트리스트에 없는 Container Instance 만들기 [검토+만들기]
- 결과 : 유효성 검사 실패로 생성 불가

4. VM SKU 제한
- VM 특정 SKU 시리즈는 배포 불가능하도록 블랙리스트 작성
- 블랙리스트 :
- H 시리즈
- L 시리즈
- F 시리즈
- FX 시리즈
- F 시리즈 v6
- 테라폼 코드
resource "azurerm_policy_definition" "deny_expensive_vm_skus" {
name = "deny-expensive-vm-skus"
policy_type = "Custom"
mode = "All"
display_name = "Deny Expensive VM SKUs"
description = "Deny deployment of virtual machines with expensive SKUs including H, Ls, Mv2, N series and specified F, FX, L, HB, HC, HX SKUs."
policy_rule = jsonencode({
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
{
"anyOf": [
{
"field": "Microsoft.Compute/virtualMachines/sku.name",
"in": [
"Standard_F2als_v6", "Standard_F2ams_v6", "Standard_F2as_v6",
"Standard_F4als_v6", "Standard_F4ams_v6", "Standard_F4as_v6",
"Standard_F8als_v6", "Standard_F8ams_v6", "Standard_F8as_v6",
"Standard_F16als_v6", "Standard_F16ams_v6", "Standard_F16as_v6",
"Standard_F32als_v6", "Standard_F32ams_v6", "Standard_F32as_v6",
"Standard_F2s_v2", "Standard_F4s_v2", "Standard_F8s_v2",
"Standard_F16s_v2", "Standard_F32s_v2",
"Standard_FX2mds_v2", "Standard_FX2ms_v2", "Standard_FX4-2mds_v2",
"Standard_FX4-2ms_v2", "Standard_FX8-2mds_v2", "Standard_FX8-2ms_v2",
"Standard_FX4mds_v2", "Standard_FX4ms_v2", "Standard_FX8-4mds_v2",
"Standard_FX8-4ms_v2", "Standard_FX16-4mds_v2", "Standard_FX16-4ms_v2",
"Standard_FX12-6mds_v2", "Standard_FX12-6ms_v2", "Standard_FX24-6mds_v2",
"Standard_FX24-6ms_v2", "Standard_FX8mds_v2", "Standard_FX8ms_v2",
"Standard_FX16-8mds_v2", "Standard_FX16-8ms_v2", "Standard_FX32-8mds_v2",
"Standard_FX32-8ms_v2", "Standard_FX12mds_v2", "Standard_FX12ms_v2",
"Standard_FX24-12mds_v2", "Standard_FX24-12ms_v2", "Standard_FX16mds_v2",
"Standard_FX16ms_v2", "Standard_FX32-16mds_v2", "Standard_FX32-16ms_v2",
"Standard_FX24mds_v2", "Standard_FX24ms_v2", "Standard_FX32mds_v2",
"Standard_FX32ms_v2",
"Standard_L2s_v4", "Standard_L4s_v4", "Standard_L8s_v4",
"Standard_L16s_v4", "Standard_L32s_v4",
"Standard_L8as_v3", "Standard_L16as_v3", "Standard_L32as_v3",
"Standard_HB176-24rs_v4", "Standard_HB176-48rs_v4", "Standard_HB176-96rs_v4",
"Standard_HB176-144rs_v4", "Standard_HB176rs_v4",
"Standard_HC44-16rs", "Standard_HC44-32rs", "Standard_HC44rs",
"Standard_HX176-24rs", "Standard_HX176-48rs", "Standard_HX176-96rs",
"Standard_HX176-144rs", "Standard_HX176rs"
]
}
]
}
]
},
"then": {
"effect": "deny"
}
})
metadata = jsonencode({
category = "Compute"
})
}
resource "azurerm_subscription_policy_assignment" "deny_expensive_vm_skus_assignment" {
name = "deny-expensive-vm-skus-assignment"
display_name = "Deny Expensive VM SKUs Assignment"
description = "Policy to deny expensive VM SKUs (H, Ls, Mv2, N, F, FX series)."
policy_definition_id = azurerm_policy_definition.deny_expensive_vm_skus.id
subscription_id = "/subscriptions/${var.subscription_id}"
}
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )
정책생성 확인

정책 테스트
- 테스트 내용 : F 시리즈 VM 생성
- 테스트 결과 : 정책에 의해 유효성 검사 실패


5. 삭제 보호
- 화이트리스트 리소스들에 삭제잠금이 없으면 AuditIfNotExists
** 참고로 terraform state로 관리되는 리소스는 Lock 걸면 배포 방해 되니 최소화 권장
** 과도한 운영 부담이 되지 않도록, 현실적인 Delete Lock 강제 가이드라인 필요
** Resource Group 전체에 Delete Lock을 걸 수는 있지만, 운형 비효율
- 삭제 잠금을 강제할 리소스 리스트 :
| 리소스 | 이유 |
| Virtual Machine | 고객 서비스 중단 방지 |
| Managed Disk (OS/Data) | 데이터 손실 방지 |
| Public IP (고정형) | 재할당 시 서비스 중단 |
| Azure SQL / Azure Database for MySQL / Storage Account | 데이터 손실 방지 |
- 테라폼 코드
** policy 생성 전에 이미 배포되어있던 리소스에 대해서는 Audit을 수행하지 않음!
** 다만 자동 스캔(Azure Policy는 24시간마다 한 번씩 자동으로 규정 준수 스캔을 수행하여 기존 리소스의 상태를 평가)에 따라, 정책을 배포한 후 하루 정도 기다리면 기존 리소스의 규정 준수 상태가 업데이트될 수 있다고 함.
resource "azurerm_policy_definition" "audit_critical_resources_delete_lock" {
name = "audit-critical-resources-delete-lock"
policy_type = "Custom"
mode = "Indexed"
display_name = "Audit Critical Resources without Delete Lock"
description = "Audit VM, Disk, Public IP, SQL, MySQL, and Storage without Delete Lock"
policy_rule = jsonencode({
"if": {
"anyOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{ "field": "type", "equals": "Microsoft.Compute/disks" },
{
"allOf": [
{ "field": "type", "equals": "Microsoft.Network/publicIPAddresses" },
{ "field": "Microsoft.Network/publicIPAddresses/publicIPAllocationMethod", "equals": "Static" }
]
},
{ "field": "type", "equals": "Microsoft.Sql/servers" },
{ "field": "type", "equals": "Microsoft.DBforMySQL/servers" },
{ "field": "type", "equals": "Microsoft.Storage/storageAccounts" }
]
},
"then": {
"effect": "AuditIfNotExists",
"details": {
"type": "Microsoft.Authorization/locks",
"existenceCondition": {
"field": "Microsoft.Authorization/locks/level",
"equals": "CanNotDelete"
}
}
}
})
metadata = jsonencode({
category = "Security"
})
}
resource "azurerm_subscription_policy_assignment" "audit_critical_resources_delete_lock_assignment" {
name = "audit-critical-resources-delete-lock-assignment"
display_name = "Audit Critical Resources without Delete Lock Assignment"
description = "Assignment of custom policy to audit critical resources without delete lock"
policy_definition_id = azurerm_policy_definition.audit_critical_resources_delete_lock.id
subscription_id = "/subscriptions/${var.subscription_id}"
}
terraform validate를 통해 설정 유효성 검사
terraform plan을 통해 리소스 생성 계획 확인
terraform apply -auto-approve (yes 자동 입력 )
정책 생성 확인

정책 테스트
- 구독 > 정책 > 할당(Assignment) > 할당한 정책 선택 > 준수 보기
- Delete Lock 상태에 따라, 리소스별로 '준수', '미준수' 표기 됨
- AuditIfNotExists 이므로, 별도 알림이 울리거나 리소스 생성 제한되지는 않음. (알림 필요할경우 추가 설정 필요)

'Cloud > AZURE' 카테고리의 다른 글
| Azure Policy 정리 (1) | 2025.06.10 |
|---|---|
| Terraform on Azure [모듈화] - (6) Firewall & UDR 모듈 (1) | 2025.05.22 |
| Terraform on Azure [모듈화] - (5) VPN 모듈 (0) | 2025.05.22 |
| Terraform on Azure [모듈화] - (4) VM & NSG 모듈 (1) | 2025.05.22 |
| Terraform on Azure [모듈화] - (3) network 모듈 (1) | 2025.05.22 |