LabHub
배우기 러닝패스 코스

Policy as Code

The exception someone label-patched in a hurry was still there two years later

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

정책 예외를 기한과 담당자가 붙은 등록부로 관리하고, 등록부를 클러스터의 실제 적용 범위로 바꾸는 스크립트와 기준일을 인자로 받는 만료 검사기, 네임스페이스별 위반 집계, 그리고 부채가 늘어나면 실패하는 게이트까지 직접 만듭니다.

왜 중요한가

정책을 켠 팀이 부딪히는 두 번째 벽은 규칙의 품질이 아니라 예외의 수명입니다. 규칙을 켜면 지금 당장 고칠 수 없는 것들이 반드시 나오고, 그때 선택지는 둘뿐입니다 — 정책을 내리거나, 예외를 만들거나. 정책을 내리면 모두가 풀리고 다시는 올라가지 않습니다. 그래서 예외를 만들지만, 예외에 기한과 담당자가 없으면 '다음 스프린트에 고칠게요' 로 만든 예외가 2년 뒤에도 그대로 남아 규칙 전체를 믿을 수 없게 만듭니다. 예외를 만드는 절차보다 지우는 절차를 먼저 설계해야 하는 이유입니다. 그 설계의 핵심은 등록부를 원본으로 두는 것입니다 — 클러스터에 손으로 붙인 표식은 누가 왜 붙였는지 아무도 모르지만, 저장소의 등록부는 리뷰를 거치고 기한이 붙고 지워진 기록까지 남습니다. 클러스터는 그 등록부의 반영일 뿐이고, 등록부에 없는 표식은 설명할 수 없으므로 지워집니다. 그리고 숫자가 있어야 줄일 수 있습니다. 예외 몇 건, 만료 몇 건, 아직 아무도 모르는 무단 위반 몇 건 — 이 세 숫자를 한 화면에 세우고 상한을 걸어 두면, 부채를 늘리는 변경이 사람의 기억이 아니라 파이프라인에서 멈춥니다.

단계

  1. 모든 작업은 /root/polexc 에서 합니다(export KUBECONFIG=/root/.kube/config, kubectl config use-context kwok-lab). 먼저 현장을 만드세요 — 네임스페이스 polexc-pay·polexc-legacy·polexc-sandbox 를 만들고 kubectl create deployment 로 여섯 개를 올립니다: polexc-pay/pay-api(registry.internal/pay:1.4), polexc-pay/pay-batch(vendor.example/paybatch:latest), polexc-legacy/billing-api(vendor.example/billing:latest), polexc-legacy/report-gen(vendor.example/report:latest), polexc-legacy/promo-web(vendor.example/promo:latest), polexc-sandbox/scratch-job(vendor.example/scratch:latest). 그다음 /root/polexc/policy.yaml 에 ValidatingAdmissionPolicy no-latest-tag 를 쓰세요 — apps/v1 deploymentsCREATE·UPDATE 를 잡고, 파드 템플릿의 컨테이너 이미지가 :latest 로 끝나면 막되, 그 워크로드의 네임스페이스에 polexc.io/exempt-<워크로드이름> 라벨이 있으면 통과시킵니다(namespaceObject 를 봅니다). /root/polexc/binding.yaml 에는 ValidatingAdmissionPolicyBinding no-latest-deny 를 쓰세요 — validationActions["Deny"], matchResources.namespaceSelector.matchLabelspolexc.io/enforce: "on" 입니다. 둘 다 적용한 뒤 polexc-paypolexc-legacy 에만 polexc.io/enforce=on 라벨을 붙이세요(polexc-sandbox 에는 붙이지 않습니다). 이제 kubectl -n polexc-pay patch deployment pay-batch --type merge -p '{"metadata":{"annotations":{"polexc.io/probe":"1"}}}' --dry-run=server 의 출력을 표준 오류까지 /root/polexc/01-blocked.txt 에 저장하세요. 마지막으로 바인딩만 지우고(kubectl delete -f /root/polexc/binding.yaml) 같은 요청을 pay-batchpolexc-legacy/promo-web 에 한 번씩 보내 두 출력을 /root/polexc/01-off.txt 에 모으고, 바인딩을 다시 적용해 두세요.
  2. /root/polexc/exceptions.txt 에 예외 등록부를 쓰세요. 한 줄이 한 항목이고 칸은 정확히 여섯 개, 구분자는 | 입니다 — namespace|workload|owner|expires|ticket|reason 순서이고 expiresYYYY-MM-DD 입니다. 첫 줄에는 # 로 시작하는 머리글을 두어 사람이 읽을 수 있게 하세요(# 로 시작하는 줄과 빈 줄은 도구가 건너뜁니다). 항목은 세 개입니다 — polexc-pay|pay-batch|team-pay|2026-09-10|OPS-2201|근거, polexc-legacy|billing-api|team-billing|2026-10-20|OPS-2188|근거, polexc-legacy|report-gen|team-report|2027-03-31|OPS-2245|근거. 근거는 네 글자 이상으로 직접 쓰세요. 이어서 /root/polexc/scope.txt 에 이 정책의 적용 범위를 한 줄에 하나씩 적으세요 — polexc-pay, polexc-legacy, polexc-sandbox 세 줄입니다. polexc-sandbox 는 강제 라벨이 없지만 위반을 세는 범위에는 들어갑니다.
  3. /root/polexc/apply-exceptions.sh 를 쓰세요. 인자는 두 개입니다 — <등록부> <적용범위파일>. 하는 일은 둘입니다. (1) 등록부의 각 항목에 대해 그 네임스페이스에 라벨 polexc.io/exempt-<workload>=<ticket> 을 붙이고 GRANT <namespace>/<workload> <ticket> 을 한 줄 출력합니다(등록부에 적힌 순서 그대로). (2) 적용 범위 파일의 네임스페이스들을 훑어 polexc.io/exempt- 로 시작하는 라벨 중 등록부에 없는 것을 지우고 REVOKE <namespace>/<workload> 를 출력합니다(이 줄들은 <namespace>/<workload> 사전순으로 모아서 GRANT 줄 뒤에 냅니다). 먼저 누군가 급하게 손으로 달아 둔 예외를 재현하세요 — kubectl label ns polexc-legacy polexc.io/exempt-promo-web=MANUAL --overwrite. 그다음 bash /root/polexc/apply-exceptions.sh /root/polexc/exceptions.txt /root/polexc/scope.txt 를 돌려 출력을 /root/polexc/03-apply.txt 에 저장하세요. GRANT 세 줄과 REVOKE polexc-legacy/promo-web 한 줄이 나와야 합니다.
  4. 예외가 좁은지 직접 확인합니다. polexc-legacy 에는 지금 billing-api(등록된 예외 있음)와 promo-web(3단계에서 표식이 지워짐)이 함께 있습니다. 두 워크로드에 1단계와 같은 kubectl -n polexc-legacy patch deployment <이름> --type merge -p '{"metadata":{"annotations":{"polexc.io/probe":"4"}}}' --dry-run=server 요청을 차례로 보내 두 출력을 표준 오류까지 /root/polexc/04-narrow.txt 에 모으세요 — billing-api 는 통과하고 promo-web 은 같은 네임스페이스인데도 거부되어야 합니다.
  5. /root/polexc/expiry.sh 를 쓰세요. 인자는 <등록부> <기준일 YYYY-MM-DD> 두 개입니다. 등록부에 적힌 순서 그대로 항목마다 한 줄을 냅니다 — <상태> <namespace>/<workload> <expires> <owner> <ticket>. 상태는 셋입니다: 만료일이 기준일보다 이전이면 EXPIRED, 기준일부터 기준일 +30일까지(양끝 포함)면 DUE, 그보다 뒤면 OK 입니다. 종료 코드는 EXPIRED 가 하나라도 있으면 1, 없으면 0 입니다 — 이 코드가 게이트가 됩니다. bash /root/polexc/expiry.sh /root/polexc/exceptions.txt 2026-10-01 을 돌려 출력을 /root/polexc/05-expiry.txt 에 저장하세요 (EXPIRED 하나, DUE 하나, OK 하나가 나옵니다).
  6. 5단계가 찾아낸 만료 항목을 실제로 거둡니다. 먼저 그 항목(polexc-pay|pay-batch|...) 줄을 /root/polexc/retired.txt 에 덧붙여 남기고, /root/polexc/exceptions.txt 에서는 지우세요 — 지운 기록이 남아야 나중에 '이건 왜 사라졌나' 에 답할 수 있습니다. 그다음 bash /root/polexc/apply-exceptions.sh /root/polexc/exceptions.txt /root/polexc/scope.txt 를 다시 돌려 출력을 /root/polexc/06-apply.txt 에 저장하세요 (REVOKE polexc-pay/pay-batch 가 나와야 합니다). 마지막으로 4단계와 같은 요청을 polexc-pay/pay-batch 에 보내 출력을 표준 오류까지 /root/polexc/06-revoked.txt 에 저장하세요 — 이제 거부되어야 합니다. bash /root/polexc/expiry.sh /root/polexc/exceptions.txt 2026-10-01 의 종료 코드도 0 이 되어야 합니다.
  7. /root/polexc/scan.sh 를 쓰세요. 인자는 <적용범위파일> 하나입니다. 파일에 적힌 순서대로 네임스페이스마다 한 줄을 냅니다 — <namespace> <위반수> <예외수> <무단수>. 위반은 컨테이너 이미지가 :latest 로 끝나는 Deployment 이고, 그중 그 네임스페이스에 polexc.io/exempt-<이름> 라벨이 있는 것이 예외수, 없는 것이 무단수입니다(위반이 없는 네임스페이스도 0 0 0 으로 냅니다). bash /root/polexc/scan.sh /root/polexc/scope.txt 를 돌려 출력을 /root/polexc/scan.txt 에 저장하세요. polexc-pay 1 0 1, polexc-legacy 3 2 1, polexc-sandbox 1 0 1 세 줄이 나옵니다.
  8. /root/polexc/debt.sh 를 쓰세요. 인자는 <등록부> <적용범위파일> <기준일> <부채상한> 네 개이고, 같은 디렉터리의 expiry.shscan.sh 를 불러서 씁니다($(dirname "$0") 로 찾습니다). 출력은 정확히 여섯 줄입니다 — EXCEPTIONS <등록부 항목 수>, EXPIRED <만료 수>, DUE <임박 수>, UNMANAGED <무단 위반 합계>, DEBT <EXCEPTIONS + UNMANAGED>, 그리고 마지막에 GATE OK 또는 GATE FAIL. EXPIRED 가 하나라도 있거나 DEBT 가 상한을 넘으면 GATE FAIL 을 찍고 종료 코드 1 로 끝냅니다. 아니면 GATE OK0 입니다. bash /root/polexc/debt.sh /root/polexc/exceptions.txt /root/polexc/scope.txt 2026-10-01 5 를 돌려 출력을 /root/polexc/debt.txt 에 저장하세요 (DEBT 5, GATE OK, 종료 코드 0 이 나옵니다). 같은 명령을 상한 4 로 한 번 더 돌려 GATE FAIL 과 종료 코드 1 도 눈으로 확인하세요.

참고

규칙을 켜자 지금 당장 고칠 수 없는 것들이 막혔다

모든 작업은 /root/polexc 에서 합니다(export KUBECONFIG=/root/.kube/config, kubectl config use-context kwok-lab). 먼저 현장을 만드세요 — 네임스페이스 polexc-pay·polexc-legacy·polexc-sandbox 를 만들고 kubectl create deployment 로 여섯 개를 올립니다: polexc-pay/pay-api(registry.internal/pay:1.4), polexc-pay/pay-batch(vendor.example/paybatch:latest), polexc-legacy/billing-api(vendor.example/billing:latest), polexc-legacy/report-gen(vendor.example/report:latest), polexc-legacy/promo-web(vendor.example/promo:latest), polexc-sandbox/scratch-job(vendor.example/scratch:latest). 그다음 /root/polexc/policy.yaml 에 ValidatingAdmissionPolicy no-latest-tag 를 쓰세요 — apps/v1 deploymentsCREATE·UPDATE 를 잡고, 파드 템플릿의 컨테이너 이미지가 :latest 로 끝나면 막되, 그 워크로드의 네임스페이스에 polexc.io/exempt-<워크로드이름> 라벨이 있으면 통과시킵니다(namespaceObject 를 봅니다). /root/polexc/binding.yaml 에는 ValidatingAdmissionPolicyBinding no-latest-deny 를 쓰세요 — validationActions["Deny"], matchResources.namespaceSelector.matchLabelspolexc.io/enforce: "on" 입니다. 둘 다 적용한 뒤 polexc-paypolexc-legacy 에만 polexc.io/enforce=on 라벨을 붙이세요(polexc-sandbox 에는 붙이지 않습니다). 이제 kubectl -n polexc-pay patch deployment pay-batch --type merge -p '{"metadata":{"annotations":{"polexc.io/probe":"1"}}}' --dry-run=server 의 출력을 표준 오류까지 /root/polexc/01-blocked.txt 에 저장하세요. 마지막으로 바인딩만 지우고(kubectl delete -f /root/polexc/binding.yaml) 같은 요청을 pay-batchpolexc-legacy/promo-web 에 한 번씩 보내 두 출력을 /root/polexc/01-off.txt 에 모으고, 바인딩을 다시 적용해 두세요.

정책(무엇을 판정하는가)과 바인딩(어디에 어떤 강도로)은 서로 다른 오브젝트입니다. 그래서 바인딩만 지우면 규칙은 그대로 있는데 아무것도 막히지 않습니다 — 이것이 '정책을 끄면 모두가 풀린다' 의 실체이고, 예외가 필요한 이유입니다. CEL 식에서는 요청 오브젝트를 object, 그 네임스페이스 오브젝트를 namespaceObject 로 읽습니다. 없을 수도 있는 필드는 has(...) 로 먼저 확인하고, 문자열을 이어 붙여 만든 키가 라벨에 있는지는 in 으로 봅니다. --dry-run=server 는 어드미션을 그대로 태우되 오브젝트를 남기지 않습니다. 거부 메시지는 표준 오류로 나오므로 2>&1 이 필요합니다.

예외를 머릿속이 아니라 파일에 적는다

/root/polexc/exceptions.txt 에 예외 등록부를 쓰세요. 한 줄이 한 항목이고 칸은 정확히 여섯 개, 구분자는 | 입니다 — namespace|workload|owner|expires|ticket|reason 순서이고 expiresYYYY-MM-DD 입니다. 첫 줄에는 # 로 시작하는 머리글을 두어 사람이 읽을 수 있게 하세요(# 로 시작하는 줄과 빈 줄은 도구가 건너뜁니다). 항목은 세 개입니다 — polexc-pay|pay-batch|team-pay|2026-09-10|OPS-2201|근거, polexc-legacy|billing-api|team-billing|2026-10-20|OPS-2188|근거, polexc-legacy|report-gen|team-report|2027-03-31|OPS-2245|근거. 근거는 네 글자 이상으로 직접 쓰세요. 이어서 /root/polexc/scope.txt 에 이 정책의 적용 범위를 한 줄에 하나씩 적으세요 — polexc-pay, polexc-legacy, polexc-sandbox 세 줄입니다. polexc-sandbox 는 강제 라벨이 없지만 위반을 세는 범위에는 들어갑니다.

예외 등록부가 갖춰야 할 것은 '무엇을 푸는가' 만이 아닙니다. 담당자가 없으면 물어볼 사람이 없고, 만료일이 없으면 지울 근거가 없고, 추적 번호가 없으면 왜 풀었는지 되짚을 수 없습니다. 이 네 가지가 있어야 예외가 스위치가 아니라 부채 목록이 됩니다. 형식은 사람이 읽기 좋으면서 awk -F'|'while IFS='|' read 로 한 번에 갈라지는 것이 좋습니다. 적용 범위를 따로 적는 이유는 뒤에서 드러납니다 — 등록부에 없는 표식을 지우려면 '어디까지 훑을 것인가' 가 정해져 있어야 합니다.

손으로 달아 둔 예외가 등록부 앞에서 지워졌다

/root/polexc/apply-exceptions.sh 를 쓰세요. 인자는 두 개입니다 — <등록부> <적용범위파일>. 하는 일은 둘입니다. (1) 등록부의 각 항목에 대해 그 네임스페이스에 라벨 polexc.io/exempt-<workload>=<ticket> 을 붙이고 GRANT <namespace>/<workload> <ticket> 을 한 줄 출력합니다(등록부에 적힌 순서 그대로). (2) 적용 범위 파일의 네임스페이스들을 훑어 polexc.io/exempt- 로 시작하는 라벨 중 등록부에 없는 것을 지우고 REVOKE <namespace>/<workload> 를 출력합니다(이 줄들은 <namespace>/<workload> 사전순으로 모아서 GRANT 줄 뒤에 냅니다). 먼저 누군가 급하게 손으로 달아 둔 예외를 재현하세요 — kubectl label ns polexc-legacy polexc.io/exempt-promo-web=MANUAL --overwrite. 그다음 bash /root/polexc/apply-exceptions.sh /root/polexc/exceptions.txt /root/polexc/scope.txt 를 돌려 출력을 /root/polexc/03-apply.txt 에 저장하세요. GRANT 세 줄과 REVOKE polexc-legacy/promo-web 한 줄이 나와야 합니다.

여기서 정하는 것은 기술이 아니라 어느 쪽이 원본인가 입니다. 등록부가 원본이면 클러스터는 그 반영일 뿐이라, 등록부에 없는 표식은 설명할 수 없는 것이므로 지워야 합니다. 반대로 클러스터를 원본으로 두면 누가 언제 왜 풀었는지 아무도 모르게 됩니다. 라벨 값에 추적 번호를 넣어 두면 클러스터만 보고도 근거를 되짚을 수 있습니다. 라벨을 지울 때는 키 뒤에 - 를 붙입니다. 네임스페이스의 라벨 키 목록은 kubectl get ns <n> -o jsonjq 로 훑으면 얻을 수 있습니다. 스크립트를 두 번 돌려도 결과가 같아야 합니다.

예외가 네임스페이스 전체를 풀지 않는지 확인한다

예외가 좁은지 직접 확인합니다. polexc-legacy 에는 지금 billing-api(등록된 예외 있음)와 promo-web(3단계에서 표식이 지워짐)이 함께 있습니다. 두 워크로드에 1단계와 같은 kubectl -n polexc-legacy patch deployment <이름> --type merge -p '{"metadata":{"annotations":{"polexc.io/probe":"4"}}}' --dry-run=server 요청을 차례로 보내 두 출력을 표준 오류까지 /root/polexc/04-narrow.txt 에 모으세요 — billing-api 는 통과하고 promo-web 은 같은 네임스페이스인데도 거부되어야 합니다.

예외 범위가 넓어지는 사고는 대부분 조용합니다. 네임스페이스 단위로 푼 예외는 그 안에서 앞으로 새로 만들어지는 워크로드까지 영구히 규칙 밖에 놓는데, 아무 오류도 나지 않으니 몇 달 뒤에야 드러납니다. 그래서 예외를 만든 직후에 풀리지 않아야 할 이웃을 한 번 찔러 보는 것이 절차의 일부여야 합니다. 두 출력이 한 파일에 들어가야 하므로 묶어서 넘기거나 두 번째를 덧붙여 씁니다.

만료일을 적어 두고 아무도 읽지 않으면 없는 것과 같다

/root/polexc/expiry.sh 를 쓰세요. 인자는 <등록부> <기준일 YYYY-MM-DD> 두 개입니다. 등록부에 적힌 순서 그대로 항목마다 한 줄을 냅니다 — <상태> <namespace>/<workload> <expires> <owner> <ticket>. 상태는 셋입니다: 만료일이 기준일보다 이전이면 EXPIRED, 기준일부터 기준일 +30일까지(양끝 포함)면 DUE, 그보다 뒤면 OK 입니다. 종료 코드는 EXPIRED 가 하나라도 있으면 1, 없으면 0 입니다 — 이 코드가 게이트가 됩니다. bash /root/polexc/expiry.sh /root/polexc/exceptions.txt 2026-10-01 을 돌려 출력을 /root/polexc/05-expiry.txt 에 저장하세요 (EXPIRED 하나, DUE 하나, OK 하나가 나옵니다).

기준일을 인자로 받는 것이 이 단계의 핵심입니다. 오늘 날짜를 스크립트 안에서 읽으면 같은 등록부가 어느 날 갑자기 다르게 판정되고, 과거 상태를 되짚거나 다음 분기를 미리 시험해 볼 수도 없습니다. 기준일을 밖에서 주면 '지난달 기준으로는 몇 건이었나' 도 답할 수 있습니다. 날짜 비교는 date -d '<날짜>' +%s 로 초로 바꿔 놓고 정수로 견주면 자릿수·월말 문제가 사라집니다. 30일은 30*86400 입니다. 이 파드에서는 시스템 시각을 바꿀 수 없습니다(date -s 는 권한이 없어 실패합니다). 그래서 기준일 인자가 유일한 시험 방법입니다.

만료된 예외를 실제로 거두자 그 워크로드가 다시 막혔다

5단계가 찾아낸 만료 항목을 실제로 거둡니다. 먼저 그 항목(polexc-pay|pay-batch|...) 줄을 /root/polexc/retired.txt 에 덧붙여 남기고, /root/polexc/exceptions.txt 에서는 지우세요 — 지운 기록이 남아야 나중에 '이건 왜 사라졌나' 에 답할 수 있습니다. 그다음 bash /root/polexc/apply-exceptions.sh /root/polexc/exceptions.txt /root/polexc/scope.txt 를 다시 돌려 출력을 /root/polexc/06-apply.txt 에 저장하세요 (REVOKE polexc-pay/pay-batch 가 나와야 합니다). 마지막으로 4단계와 같은 요청을 polexc-pay/pay-batch 에 보내 출력을 표준 오류까지 /root/polexc/06-revoked.txt 에 저장하세요 — 이제 거부되어야 합니다. bash /root/polexc/expiry.sh /root/polexc/exceptions.txt 2026-10-01 의 종료 코드도 0 이 되어야 합니다.

예외를 만드는 절차보다 예외를 지우는 절차가 어렵고 중요합니다. 지우는 쪽이 자동으로 돌지 않으면 예외는 조용히 영구화되고, 그때부터 그 규칙은 '켜져 있지만 아무도 믿지 않는 규칙' 이 됩니다. 3단계에서 등록부를 원본으로 삼아 둔 덕에 여기서 할 일은 등록부에서 한 줄을 지우고 같은 스크립트를 다시 돌리는 것뿐입니다 — 회수 로직을 따로 쓰지 않습니다. 등록부를 제자리에서 고칠 때는 임시 파일에 쓴 뒤 옮기세요. 이 단계를 두 번 돌려도 retired.txt 에 같은 줄이 두 번 들어가면 안 됩니다.

위반을 세어 보니 예외보다 무단 쪽이 많았다

/root/polexc/scan.sh 를 쓰세요. 인자는 <적용범위파일> 하나입니다. 파일에 적힌 순서대로 네임스페이스마다 한 줄을 냅니다 — <namespace> <위반수> <예외수> <무단수>. 위반은 컨테이너 이미지가 :latest 로 끝나는 Deployment 이고, 그중 그 네임스페이스에 polexc.io/exempt-<이름> 라벨이 있는 것이 예외수, 없는 것이 무단수입니다(위반이 없는 네임스페이스도 0 0 0 으로 냅니다). bash /root/polexc/scan.sh /root/polexc/scope.txt 를 돌려 출력을 /root/polexc/scan.txt 에 저장하세요. polexc-pay 1 0 1, polexc-legacy 3 2 1, polexc-sandbox 1 0 1 세 줄이 나옵니다.

어드미션은 앞으로 들어올 것만 봅니다. 이미 클러스터 안에 있는 것은 아무도 다시 검사하지 않으므로, 규칙을 켠 뒤에도 '지금 몇 개가 어기고 있나' 는 따로 훑어야 알 수 있습니다. 그리고 그 숫자는 반드시 둘로 갈라야 의미가 있습니다 — 예외로 덮인 것은 기한이 있는 부채이고, 무단 위반은 아직 아무도 모르는 구멍입니다. 합쳐 놓으면 둘 다 보이지 않게 됩니다. polexc-sandbox 처럼 강제 라벨이 없는 네임스페이스도 세야 합니다 — 강제가 꺼져 있다는 것이 위반이 없다는 뜻은 아닙니다. 이미지 목록은 kubectl get deploy -o jsonjq 로 훑으면 한 번에 얻을 수 있습니다.

부채를 한 화면에 세우고 늘어나면 실패하게 만든다

/root/polexc/debt.sh 를 쓰세요. 인자는 <등록부> <적용범위파일> <기준일> <부채상한> 네 개이고, 같은 디렉터리의 expiry.shscan.sh 를 불러서 씁니다($(dirname "$0") 로 찾습니다). 출력은 정확히 여섯 줄입니다 — EXCEPTIONS <등록부 항목 수>, EXPIRED <만료 수>, DUE <임박 수>, UNMANAGED <무단 위반 합계>, DEBT <EXCEPTIONS + UNMANAGED>, 그리고 마지막에 GATE OK 또는 GATE FAIL. EXPIRED 가 하나라도 있거나 DEBT 가 상한을 넘으면 GATE FAIL 을 찍고 종료 코드 1 로 끝냅니다. 아니면 GATE OK0 입니다. bash /root/polexc/debt.sh /root/polexc/exceptions.txt /root/polexc/scope.txt 2026-10-01 5 를 돌려 출력을 /root/polexc/debt.txt 에 저장하세요 (DEBT 5, GATE OK, 종료 코드 0 이 나옵니다). 같은 명령을 상한 4 로 한 번 더 돌려 GATE FAIL 과 종료 코드 1 도 눈으로 확인하세요.

이 한 화면이 회의에서 답해야 하는 질문 전부입니다 — 예외가 몇 건인가, 지금 기한을 넘긴 것이 있는가, 곧 넘길 것은 몇 건인가, 그리고 아무도 모르는 위반이 몇 건인가. 숫자가 흩어져 있으면 아무도 보지 않고, 한 줄로 합쳐 버리면 무엇을 고쳐야 할지 알 수 없습니다. 상한을 두는 이유는 '줄이자' 는 말이 저절로 지켜지지 않기 때문입니다. 상한이 있으면 부채가 늘어나는 변경이 파이프라인에서 멈춥니다. 앞 단계의 두 스크립트를 다시 쓰는 것이 중요합니다 — 세는 규칙이 두 군데에 적히면 반드시 한쪽이 먼저 낡습니다. expiry.sh 는 만료가 있으면 종료 코드 1 로 끝납니다. 그 출력을 받아 쓰는 쪽이 그 코드에 걸려 죽지 않게 하세요.