본문 바로가기

Cloud/AZURE

Terraform on Azure 정리 (9) - Azure 배포 테스트 : Azure Policy

하기 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 이므로, 별도 알림이 울리거나 리소스 생성 제한되지는 않음. (알림 필요할경우 추가 설정 필요)