거버넌스를 도입할 때 가장 흔한 실패 방식은 Deny 정책부터 거는 것입니다. “태그 없으면 리소스 생성 금지”를 걸고 나면 그날부터 배포 파이프라인이 전부 실패하고, 며칠 못 가 정책이 꺼집니다. 그리고 다시는 거버넌스 이야기를 꺼내지 못하게 됩니다.
이 글은 마찰을 최소화하면서 단계적으로 통제를 조이는 순서를 정리합니다. Audit → Modify → Deny 3단계이고, 각 단계에서 무엇을 확인하고 다음으로 넘어갈지가 핵심입니다.
태그를 몇 개로 정할 것인가
정책보다 태그 설계가 먼저입니다. 그리고 적을수록 좋습니다. 태그가 10개면 아무도 다 채우지 않고, 결국 절반이 비어서 쓸모없어집니다.
필수는 4개면 충분합니다. 각각 명확한 용도가 있어야 합니다.
| 태그 | 값 예시 | 이 태그로 답하는 질문 |
|---|---|---|
Owner | oliver@example.com | 이거 누구 건가? 지워도 되나? |
Environment | prod / stage / dev | 운영인가? 꺼도 되나? |
CostCenter | CC-1001 | 어느 부서에 청구하나? |
Project | api-renewal | 어느 프로젝트 예산인가? |
여기에 선택적으로 ExpiryDate를 추가하면 유용합니다. 임시 리소스에 만료일을 적어두고, 지난 것을 주기적으로 조회해 정리하는 용도입니다. 이 태그 하나로 “누가 만들었는지 모를 방치 리소스”가 크게 줄어듭니다.
1단계 — Audit: 현황부터 파악한다
아무것도 막지 않고 규정 위반만 기록하는 단계입니다. 여기서 “우리 구독이 얼마나 엉망인지”를 숫자로 확인합니다.
#!/usr/bin/env bash
set -euo pipefail
MG="/providers/Microsoft.Management/managementGroups/mg-platform"
# 필수 태그 4개에 대해 Audit 정책 할당
for TAG in Owner Environment CostCenter Project; do
az policy assignment create \
--name "audit-tag-$TAG" \
--display-name "Audit: $TAG 태그 누락" \
--scope "$MG" \
--policy "871b6d14-10aa-478d-b590-94f262ecfa99" \
--params "{\"tagName\":{\"value\":\"$TAG\"}}"
done
정책은 할당 즉시 평가되지 않습니다. 최초 평가에 최대 30분, 이후 24시간 주기로 돕니다. 하루 뒤에 결과를 봅니다.
# 규정 준수 현황 요약
az policy state summarize \
--management-group mg-platform \
--query "value[0].results.{nonCompliant:nonCompliantResources, total:resourceDetails[0].count}"
# 위반 리소스 상세 (어디가 문제인지)
az policy state list \
--management-group mg-platform \
--filter "ComplianceState eq 'NonCompliant'" \
--query "[].{resource:resourceId, policy:policyDefinitionName}" \
-o tsv | awk -F'/' '{print $5}' | sort | uniq -c | sort -rn
마지막 명령은 리소스 그룹별로 위반 건수를 집계합니다. 대개 특정 팀이나 특정 파이프라인에 몰려 있어서, 그쪽부터 정리하면 준수율이 빠르게 오릅니다. 전체를 한 번에 고치려 하면 진도가 안 나갑니다.
2단계 — Modify: 사람 대신 정책이 채우게 한다
여기가 핵심입니다. Deny로 강제하기 전에 자동으로 채워주는 단계를 넣으면 마찰 없이 준수율이 올라갑니다.
Azure에는 리소스 그룹의 태그를 하위 리소스에 상속시키는 내장 정책이 있습니다. 리소스 그룹에만 태그를 달면 그 안의 리소스는 자동으로 물려받습니다. 개발자는 아무것도 안 해도 됩니다.
MG="/providers/Microsoft.Management/managementGroups/mg-platform"
for TAG in Owner Environment CostCenter Project; do
az policy assignment create \
--name "inherit-tag-$TAG" \
--display-name "Inherit: $TAG 태그 상속" \
--scope "$MG" \
--policy "cd3aa116-8754-49c9-a813-ad46512ece54" \
--params "{\"tagName\":{\"value\":\"$TAG\"}}" \
--mi-system-assigned \
--location koreacentral \
--role "Contributor"
done
주의할 점이 두 가지 있습니다.
- 관리 ID가 필요합니다. Modify 정책은 리소스를 실제로 고치므로 권한이 있어야 합니다.
--mi-system-assigned와--location이 빠지면 할당은 되지만 동작하지 않습니다. 조용히 실패하는 유형이라 눈치채기 어렵습니다. - 기존 리소스는 자동으로 안 고쳐집니다. Modify 정책은 앞으로 만들어지거나 수정되는 리소스에 적용됩니다. 이미 있는 것들은 재구성(Remediation) 작업을 따로 돌려야 합니다.
# 기존 리소스에 정책을 소급 적용 (재구성 작업)
az policy remediation create \
--name "remediate-owner-tag" \
--management-group mg-platform \
--policy-assignment "inherit-tag-Owner" \
--resource-discovery-mode ReEvaluateCompliance
# 진행 상황 확인
az policy remediation show \
--name "remediate-owner-tag" \
--management-group mg-platform \
--query "{state:provisioningState, done:deploymentSummary.successfulDeployments, failed:deploymentSummary.failedDeployments}"
재구성은 리소스 수에 따라 시간이 걸립니다. 수천 개면 몇 시간 단위입니다. 먼저 리소스 그룹에 태그를 제대로 달아둔 뒤 실행해야 의미가 있습니다. 리소스 그룹에 태그가 없으면 상속할 값도 없습니다.
3단계 — Deny: 이제 막는다
준수율이 충분히 올라온 뒤에야 Deny로 넘어갑니다. 기준을 정해두는 게 좋습니다 — 예를 들어 준수율 95% 이상일 때 전환합니다.
# 리소스 그룹에 Owner 태그가 없으면 생성 거부
az policy assignment create \
--name "deny-rg-without-owner" \
--display-name "Deny: Owner 태그 없는 리소스 그룹" \
--scope "$MG" \
--policy "96475b64-4ba5-4a2d-8a68-6b6e0b6b4b6d" \
--params '{"tagName":{"value":"Owner"}}'
여기서 중요한 판단이 하나 있습니다. Deny는 리소스 그룹 레벨에만 거는 것을 권합니다. 개별 리소스까지 Deny를 걸면 상속 정책과 순서가 엉켜서 배포가 실패하는 경우가 생깁니다. 리소스 그룹에만 강제하고, 그 안은 상속으로 채우는 구조가 안정적입니다.
태그 외에 걸어둘 만한 정책들
| 정책 | 효과 | 권장 모드 |
|---|---|---|
| 허용 리전 제한 | 리전 간 트래픽 요금·관리 누락 방지 | Deny (부작용 적음) |
| 비운영 VM 크기 제한 | 개발 환경 대형 VM 사고 방지 | Deny |
| 공용 IP 생성 제한 | 의도치 않은 인터넷 노출 차단 | Audit → Deny |
| 스토리지 HTTPS 강제 | 평문 전송 차단 | Deny |
| 진단 설정 자동 배포 | 로그 수집 누락 방지 | DeployIfNotExists |
| SQL 투명 데이터 암호화 | 저장 데이터 암호화 | DeployIfNotExists |
이 중 허용 리전 제한을 가장 먼저 Deny로 거는 것을 권합니다. 부작용이 거의 없으면서 효과가 확실합니다. 실수로 다른 리전에 만든 리소스는 나중에 찾기도 어렵고, 리전 간 트래픽 요금이라는 형태로 조용히 비용을 만듭니다.
반대로 진단 설정 자동 배포(DeployIfNotExists)는 신중해야 합니다. 로그 수집이 자동으로 켜지면서 Log Analytics 비용이 예상 밖으로 늘어날 수 있습니다. 어떤 카테고리를 수집할지 명시하고, 켠 뒤 수집량을 확인하세요.
이니셔티브로 묶어서 관리하기
정책이 열 개를 넘어가면 개별 할당 관리가 번거로워집니다. 이니셔티브(Initiative)는 여러 정책을 하나로 묶은 것으로, 할당도 한 번, 준수율 확인도 한 번에 됩니다.
Azure에는 규제 준수용 내장 이니셔티브가 여럿 있습니다. ISO 27001, PCI DSS 같은 것들인데, 바로 할당하지 마시고 먼저 Audit으로만 걸어보세요. 정책이 수백 개 들어 있어서 위반이 폭발적으로 나옵니다. 어디까지 맞출지 정한 뒤 필요한 것만 골라 자체 이니셔티브를 만드는 편이 현실적입니다.
# 내장 이니셔티브 목록에서 필요한 것 찾기
az policy set-definition list \
--query "[?policyType=='BuiltIn'].{name:displayName, id:name}" -o tsv | head -30
# 준수율 확인
az policy state summarize \
--management-group mg-platform \
--query "value[0].policyAssignments[].{name:policyAssignmentId, nonCompliant:results.nonCompliantResources}" \
-o table
실무에서는 내장 이니셔티브를 참고 자료로 쓰고, 실제 할당은 자체 구성하는 경우가 많습니다. 조직에 필요 없는 항목까지 준수율을 떨어뜨리면 지표 자체가 무의미해지기 때문입니다.
예외 처리를 설계에 포함하기
정책을 운영하다 보면 반드시 예외가 필요해집니다. 이때 정책을 끄는 게 아니라 예외를 등록해야 합니다. 정책을 끄면 그 범위 전체가 통제 밖으로 나갑니다.
- 제외(Exclusion) — 할당 시 특정 스코프를 아예 대상에서 뺍니다. 영구적 성격
- 면제(Exemption) — 특정 리소스를 기한부로 면제합니다. 만료일 설정 가능
면제 쪽을 권합니다. 만료일이 있어야 임시 예외가 영구화되지 않습니다. 사유를 함께 기록해두면 나중에 왜 뺐는지 추적이 됩니다.
az policy exemption create \
--name "temp-exempt-legacy-vm" \
--policy-assignment "<ASSIGNMENT_ID>" \
--scope "/subscriptions/<SUB>/resourceGroups/rg-legacy" \
--exemption-category Waiver \
--expires-on 2026-12-31 \
--description "레거시 시스템 마이그레이션 완료 시까지 한시 면제"
태그가 실제로 비용 분석에 쓰이게 하기
태그를 다 붙였는데 비용 분석에서 안 보이는 경우가 있습니다. 두 가지를 확인하세요.
첫째, 모든 리소스 종류가 태그를 지원하지는 않습니다. 네트워크 대역폭 요금처럼 특정 리소스에 귀속되지 않는 항목은 태그로 분류되지 않습니다. 이런 항목은 리소스 그룹 단위 분석으로 보완해야 합니다.
둘째, 태그는 소급 적용되지 않습니다. 오늘 태그를 달아도 지난달 청구 데이터에는 반영되지 않습니다. 비용 분석에서 태그별로 보려면 태그를 단 시점 이후 기간만 유효합니다. 이걸 모르고 “태그를 달았는데 왜 안 나오지”라고 헤매는 경우가 많습니다.
# 태그별 비용 조회 (태그를 단 이후 기간만 유효)
az costmanagement query \
--type ActualCost \
--scope "/subscriptions/<SUB>" \
--timeframe MonthToDate \
--dataset-aggregation '{"totalCost":{"name":"Cost","function":"Sum"}}' \
--dataset-grouping name=CostCenter type=TagKey \
-o table
비용을 부서별로 나눠야 한다면 태그 도입 시점을 회계 기간 시작에 맞추는 것이 깔끔합니다. 월 중간에 시작하면 그 달 데이터가 반쪽이 됩니다.
방치 리소스를 찾아내는 활용
태그가 붙기 시작하면 태그가 없는 리소스가 곧 의심 대상이 됩니다. 이 역발상이 실무에서 꽤 유용합니다.
# 정책 도입 이후에도 Owner 태그가 없는 리소스 = 대개 방치·임시 리소스
az resource list \
--query "[?tags.Owner==null].{name:name, type:type, rg:resourceGroup, loc:location}" \
-o table
# ExpiryDate가 지난 리소스 조회 (임시 리소스 정리용)
az resource list \
--query "[?tags.ExpiryDate != null].{name:name, rg:resourceGroup, expiry:tags.ExpiryDate}" \
-o tsv | awk -v today="$(date +%Y-%m-%d)" '$3 < today {print}'
두 번째 명령을 월 1회 돌려 만료된 임시 리소스를 정리하는 습관을 들이면, 앞서 비용 급증 원인 찾기에서 다룬 “완만한 우상향” 유형의 비용 증가를 상당 부분 막을 수 있습니다.
도입 순서 정리
- 필수 태그를 4개로 제한해 정한다 (많으면 아무도 안 채움)
- Audit 정책으로 현황을 파악하고 위반이 몰린 곳을 찾는다
- 리소스 그룹에 태그를 달고 Modify(상속) 정책으로 자동 채움
- 재구성 작업으로 기존 리소스에 소급 적용
- 준수율 95% 이상이 되면 Deny로 전환 (리소스 그룹 레벨만)
- 허용 리전 제한은 처음부터 Deny로 걸어도 무방하다
거버넌스는 한 번에 완성하는 게 아니라 마찰을 관리하면서 조여가는 과정입니다. Deny부터 시작해서 실패한 경험이 있다면, Audit → Modify를 건너뛴 것이 원인일 가능성이 높습니다.
구독 구조 자체를 정리하고 싶으시면 미니멀 Azure 랜딩 존을, 비용 통제 쪽은 신규 구독 첫 주 점검과 함께 보시면 그림이 맞춰집니다.