정책을 코드로 · 클러스터에 닿기 전에 막는다 · 실습
종료 코드가 0 이라 파이프라인이 몇 주 동안 초록불이었다
목표
진짜 OpenTofu 계획을 JSON 으로 뽑아 구조를 읽고, 그 JSON 에 정책을 걸어 클러스터와 클라우드에 닿기 전에 파괴·교체·빠진 태그를 잡습니다. 그리고 위반을 찾고도 0 으로 끝나는 도구 위에서 믿을 수 있는 게이트를 만듭니다.
왜 중요한가
어드미션 컨트롤은 이미 만들어진 요청을 봅니다. 그런데 데이터베이스 교체나 버킷 공개 같은 위험한 변경은 쿠버네티스 API 를 지나지 않고 클라우드로 곧장 갑니다. 그 변경들에는 공통점이 하나 있습니다 — 적용 전에 계획 단계가 있고, 그 계획은 JSON 으로 뽑을 수 있는 구조화된 문서라는 것입니다. 여기에 정책을 걸면 되돌릴 것이 아직 없을 때 막을 수 있습니다. 다만 계획에는 적용해 봐야 정해지는 값이 섞여 있어서, 그 자리를 모르고 규칙을 쓰면 오탐이 쏟아지고 게이트는 며칠 만에 꺼집니다. 그리고 판정을 도구의 종료 코드에 맡기면, 위반을 찾고도 0 으로 끝나는 도구 하나 때문에 파이프라인이 몇 주 동안 조용히 초록불일 수 있습니다. 그래서 이 실습은 규칙을 쓰는 법만큼이나 '무엇을 근거로 판정하는가'를 다룹니다.
단계
1. /root/tfpolicy/main.tf 에 local·random·null 프로바이더를 요구하고 리소스 넷을 선언하세요. null_resource.api 의 triggers 는 owner = "platform"·env = "dev", null_resource.worker 의 triggers 는 owner = "data"·env = "dev", local_file.config 는 ${path.module}/out/config.txt 에 v1 한 줄(끝에 줄바꿈)을 쓰고, terraform_data.release 는 input 이 v1 입니다. tofu init 과 tofu apply -auto-approve 로 적용한 다음, 변경이 하나도 남지 않은 계획을 /root/tfpolicy/base.tfplan 으로 저장하고 tofu show -json 으로 /root/tfpolicy/base.json 을 만드세요.
2. main.tf 를 이렇게 고치세요. null_resource.worker 블록을 지우고, random_pet.suffix(length = 2)를 더하고, null_resource.cache 를 더합니다(triggers 는 owner = ""·env = "dev"·name = random_pet.suffix.id). local_file.config 의 내용은 v2 한 줄로, terraform_data.release 의 input 은 v2 로 바꿉니다. 적용하지 마세요. 계획을 /root/tfpolicy/change.tfplan 으로 저장하고 /root/tfpolicy/change.json 을 만든 뒤, /root/tfpolicy/changes.txt 에 no-op 이 아닌 자원마다 <주소> <create|update|replace|delete> 를 한 줄씩 적으세요. 삭제와 생성이 한 자원에 함께 들어 있으면 replace 한 줄로 적습니다.
3. /root/tfpolicy/change.json 의 resource_changes[].change.after_unknown 을 읽어 /root/tfpolicy/unknown.txt 를 만드세요. 모르는 잎(값이 true 인 자리)이 하나라도 있는 자원마다 <주소> <모르는 잎 개수> 를 한 줄씩 적습니다. triggers.name 처럼 중첩된 자리도 잎 하나로 셉니다. 모르는 잎이 없는 자원은 적지 않습니다. 줄 순서는 보지 않습니다.
4. /root/tfpolicy/policies/block.yaml 에 apiVersion: json.kyverno.io/v1alpha1·kind: ValidatingPolicy 인 정책을 쓰세요. 규칙 하나를 두고, change.actions 에 delete 가 들어 있는 자원이 하나도 없어야 통과하게 합니다(순수 삭제와 교체가 모두 걸립니다). 그다음 KYVERNO_EXPERIMENTAL=true kyverno json scan --payload /root/tfpolicy/change.json --policy /root/tfpolicy/policies/block.yaml 을 돌리고, 그 출력 전체와 종료 코드를 /root/tfpolicy/scan-exit.txt 에 저장하세요. 종료 코드는 exit=<코드> 꼴로 마지막에 한 줄 덧붙입니다. /root/tfpolicy/base.json 에도 같은 스캔을 돌려 통과하는 것을 눈으로 확인하세요.
5. /root/tfpolicy/gate.sh <계획JSON> 을 만드세요. policies/block.yaml 로 스캔을 돌려 보고서를 파싱합니다. 위반 줄(FAILED 또는 ERROR: 가 든 줄)마다 BLOCK 을 앞에 붙여 한 줄씩 출력하고, 마지막 줄에 RESULT block=<위반 줄 수> 를 출력하세요(뒤에 낱말이 더 붙어도 됩니다). 위반이 없으면 0, 하나라도 있으면 0 이 아닌 값으로 끝냅니다. 보고서에 판정 줄(PASSED·FAILED·ERROR:)이 하나도 없으면 도구 출력이 바뀐 것이므로 2 로 끝내고, 인자로 받은 파일이 없을 때도 2 로 끝냅니다. 채점기는 자기가 만든 깨끗한 계획과 위반 계획으로 이 스크립트를 돌립니다.
6. /root/tfpolicy/policies/block.yaml 에 두 번째 규칙을 더하세요. change.after.triggers 가 있는 자원은 change.after.triggers.owner 가 있고 빈 문자열이 아니어야 통과합니다. 다만 그 값이 계획 단계에 아직 알려지지 않은 자원(change.after_unknown.triggers.owner 가 true)은 위반으로 세지 않습니다. 규칙을 더한 뒤 ./gate.sh /root/tfpolicy/change.json 을 다시 돌려 위반이 두 줄이 되는지 확인하세요. 채점기는 owner 가 빈 계획·owner 가 아직 모르는 값인 계획·깨끗한 계획 셋으로 이 규칙을 시험합니다.
7. /root/tfpolicy/policies/warn.yaml 에 두 번째 정책 파일을 만드세요. 규칙 하나를 두고, 계획 JSON 의 최상위 resource_drift 가 비어 있어야 통과하게 합니다(키 자체가 없는 계획도 통과해야 합니다). 그리고 /root/tfpolicy/gate.sh 를 고쳐 두 정책을 따로 돌리게 하세요. block.yaml 의 위반은 BLOCK 을 붙여 출력하고 종료 코드를 0 이 아니게 만들고, warn.yaml 의 위반은 WARN 을 붙여 출력하되 종료 코드를 바꾸지 않습니다. 마지막 줄은 RESULT block=<차단 위반 수> warn=<경고 위반 수> 로 바꿉니다. 경고 정책 보고서에도 판정 줄이 하나도 없으면 2 로 끝냅니다.
8. /root/tfpolicy/out/config.txt 를 테라폼 밖에서 직접 고치세요(예: printf 'hand-edited\n' > /root/tfpolicy/out/config.txt). 그다음 계획을 /root/tfpolicy/drift.tfplan 으로 저장하고 /root/tfpolicy/drift.json 을 만드세요 — 최상위에 resource_drift 가 생깁니다. 마지막으로 base.json·change.json·drift.json 세 계획에 ./gate.sh 를 돌려 출력을 각각 /root/tfpolicy/reports/base.txt·/root/tfpolicy/reports/change.txt·/root/tfpolicy/reports/drift.txt 에 저장하고, 각 파일 마지막 줄에 EXIT <게이트 종료 코드> 를 덧붙이세요. 채점기는 세 계획에 게이트를 다시 돌려 보고서와 대조합니다.
참고
- 현장에서는 Conftest 나 OPA(Rego)로 계획 JSON 을 검사하는 곳이 많지만 이 파드에는 conftest·opa 가 없습니다. 대신 kyverno CLI 1.13.2 의
kyverno json scan으로 같은 일을 합니다 — 임의 JSON 을 페이로드로 받아 정책으로 판정하는 구조는 같고, 배우는 것(계획 JSON 의 모양, 모르는 값, 판정의 근거)도 같습니다. - 스캔:
KYVERNO_EXPERIMENTAL=true kyverno json scan --payload <계획JSON> --policy <정책YAML>— 환경변수를 빠뜨리면 실험 기능이라 명령 자체가 거부됩니다. - 정책 형식:
apiVersion: json.kyverno.io/v1alpha1·kind: ValidatingPolicy·spec.rules[].assert.all[].check— check 의 열쇠는 괄호로 감싼 JMESPath 식이고 값이 기대값입니다. - 식만 따로 시험하려면
kyverno jp query -i <파일> '<식>'을 쓰세요. JMESPath 의 JSON 리터럴은 역따옴표입니다. - 파드에는 OpenTofu 1.9.0 이
tofu로 들어 있고 미러에는 local·random·null·tls 프로바이더만 있습니다. 클라우드 프로바이더는 없으니 태그에 해당하는 자리는null_resource의triggers맵으로 대신합니다. - 흔한 실수:
if kyverno json scan ...; then으로 판정하는 것. 이 도구는 위반이 있어도 0 으로 끝납니다 — 4단계에서 직접 찍어 확인합니다. - 흔한 실수: 위반이 0 건인 것과 보고서를 한 줄도 못 읽은 것을 구분하지 않는 것. 도구를 판올린 날 게이트가 조용히 꺼집니다.
- 채점기가 읽지 않는 중간 산출물:
/root/tfpolicy/base.tfplan·change.tfplan·drift.tfplan(저장된 계획 파일)과/root/tfpolicy/out/config.txt. - [tofu show](https://opentofu.org/docs/cli/commands/show/) · [tofu plan](https://opentofu.org/docs/cli/commands/plan/) · [계획 JSON 형식](https://developer.hashicorp.com/terraform/internals/json-format) · [kyverno-json](https://kyverno.github.io/kyverno-json/latest/) · [kyverno-json assert](https://kyverno.github.io/kyverno-json/latest/policies/asserts/) · [kyverno CLI 의 json 명령](https://kyverno.io/docs/kyverno-cli/usage/json/) · [Conftest](https://www.conftest.dev/) · [JMESPath 명세](https://jmespath.org/specification.html)
단계 8개
- 계획을 JSON 으로 뽑아 놓는다
- 한 계획에 네 가지 동작이 한꺼번에 들어온다
- 계획이 아직 모르는 값을 세어 둔다
- 규칙을 데이터로 적고 종료 코드를 찍어 본다
- 종료 코드를 버리고 보고서를 센다
- 태그를 요구했더니 아직 모르는 값까지 걸렸다
- 어떤 규칙은 막고 어떤 규칙은 알리기만 한다
- 손으로 고친 파일 하나가 드리프트로 잡힌다