LabHub
배우기 러닝패스 코스

정책을 코드로 · 클러스터에 닿기 전에 막는다 · 이론

계획을 정책으로 읽는다 — 파괴·교체·공개 범위를 배포 전에 잡는 자리

LabHub 에서 이어서 보기

한 줄 요약

테라폼과 OpenTofu 의 계획(plan)은 JSON 으로 뽑을 수 있는 구조화된 변경 명세이고, 그 JSON 에 정책을 걸면 자원이 만들어지기 전에 파괴·교체·공개 범위를 잡을 수 있다.

왜 이게 필요했나

어드미션 컨트롤은 강력하지만 이미 만들어진 요청을 본다. 그리고 세상의 위험한 변경 중 상당수는 애초에 쿠버네티스 API 를 지나지 않는다. 보안 그룹을 0.0.0.0/0 으로 여는 일, 데이터베이스를 교체(replace)하는 일, 스토리지 버킷을 공개로 돌리는 일. 이것들은 클라우드 API 로 곧장 간다. 어드미션이 볼 수 있는 자리에는 오지 않는다.

그런데 이 변경들에는 공통점이 있다. 적용되기 전에 계획 단계가 있다. tofu planterraform plan 은 "무엇을 만들고 무엇을 지우고 무엇을 바꿀 것인가"를 먼저 계산한다. 사람이 읽으라고 만든 출력이지만 기계가 읽을 수 있는 형태로도 뽑을 수 있다. 그러면 이 계획은 정책으로 검사할 수 있는 문서가 된다. 어드미션이 클러스터의 문지기라면, 계획 검사는 클라우드 전체의 문지기다. 그리고 훨씬 싸다 — 되돌릴 것이 아직 없기 때문이다.

어떻게 동작하나

절차는 두 줄이다. 계획을 파일로 저장하고, 그것을 JSON 으로 바꾼다.

tofu plan -out=tfplan.binarytofu show -json tfplan.binary > plan.json

테라폼도 같다(terraform show -json). 나오는 JSON 에서 정책이 보는 자리는 대부분 resource_changes 배열이고, 항목마다 address, type, name, 그리고 change 객체가 들어 있다. change 안의 actions 가 판정의 중심이다. 문서가 못 박는 유효한 값은 이렇다.

| actions | 뜻 |
| --- | --- |
| ["no-op"] | 바뀌는 것이 없다 |
| ["create"] | 새로 만든다 |
| ["read"] | 데이터 소스를 읽는다 |
| ["update"] | 제자리에서 고친다 |
| ["delete"] | 지운다 |
| ["delete", "create"] | 교체. 지우고 다시 만든다 |
| ["create", "delete"] | 교체인데 먼저 만들고 나중에 지운다 |

교체가 두 원소짜리 배열로 표현된 것은 의도적이다. 문서는 이렇게 표현해 두면 호출하는 쪽이 목록에 delete 가 들어 있는지만 훑어도 자원이 사라지는 세 경우를 모두 잡을 수 있다고 설명한다. 정책의 첫 규칙이 여기서 나온다. "delete" in actions 이면 사람의 승인을 요구한다. 데이터베이스나 볼륨 같은 상태 있는 자원에 대해서는 특히 그렇다.

물을 수 있는 것은 파괴만이 아니다.

계획에는 아직 모르는 값이 있다. 이것이 계획 정책의 가장 큰 제약이다. 자원 ID, 생성된 ARN, 무작위 접미 같은 값은 적용해 봐야 정해진다. JSON 은 이것을 after_unknown 으로 표현하는데, 문서의 설명은 정확하다. after 와 같은 구조를 가지되 모르는 잎 값은 true 로, 아는 잎 값은 아예 빠진 객체다. 그러니 정책을 쓸 때 규칙은 이렇게 된다.

판정을 파이프라인 결과로 바꾸는 일 — 종료 코드가 늘 진실은 아니다. 여기가 이 모듈에서 가장 실용적인 대목이다. 정책 도구를 CI 에 넣을 때 사람들은 자연스럽게 if ! tool scan ...; then exit 1; fi 를 쓴다. 그런데 위반을 찾고도 0 으로 끝나는 도구가 실제로 있다. 이 실습 환경의 kyverno json scan 이 그렇다. 위반은 사람이 읽는 출력에 FAILED 로 찍히고 --output jsonViolations 에도 남는데, 종료 코드는 위반이 있든 없든 0 이다(실습 이미지의 kyverno CLI 1.13.2 에서 직접 확인했다). 그 사실을 모른 채 종료 코드만 믿으면 파이프라인은 영원히 초록불이다.

그래서 정책 게이트를 붙일 때 순서는 이렇다.

1) 도구를 일부러 실패할 입력으로 돌려 본다2) 종료 코드를 확인한다 (echo $?)3) 0 이면 종료 코드를 쓰지 않고 보고서를 파싱한다4) 파싱 결과가 비어 있지 않은지도 확인한다 (형식이 바뀌면 0건으로 읽힌다)5) 그 판정으로 파이프라인을 세운다

2번을 건너뛰는 것이 사고의 시작이고, 4번을 건너뛰면 도구 판올림 날에 게이트가 조용히 꺼진다.

현장에서 쓰는 엔진은 Conftest(OPA 의 Rego 로 임의 JSON 을 검사한다), OPA 자체, kyverno-json 같은 것들이다. 이 실습 파드에는 conftest 와 opa 가 없다. 대신 kyverno CLI 의 kyverno json scan 으로 같은 일을 한다 — 임의 JSON 문서를 페이로드로 주고 정책으로 판정하는 구조는 동일하다. 도구가 달라도 배우는 것(계획 JSON 의 모양, 모르는 값, 종료 코드의 함정)은 그대로다.

현장에서 만나는 모습

첫째, 승인 게이트가 실제로 멈춘 날. ["delete", "create"] 가 RDS 인스턴스에 붙은 계획이 PR 에 올라왔고, 게이트가 그것을 잡아 사람이 확인했다. 필드 하나를 바꾼 것이 교체를 부르는지는 코드만 봐서는 알 수 없고 계획에만 나온다.

둘째, 모르는 값 때문에 생긴 오탐. 필수 태그 검사를 after.tags 로만 썼는데, 태그를 지역 변수로 계산하는 모듈에서 그 값이 계획 단계에 아직 없어 전부 위반으로 떴다. 며칠 만에 게이트가 꺼졌다. after_unknown 을 함께 보는 규칙으로 고쳤어야 했다.

셋째, 조용히 꺼져 있던 게이트. 도구를 판올렸더니 보고서 JSON 의 키 하나가 바뀌어 파싱 결과가 늘 빈 배열이 됐다. 위반 0건으로 읽혀 몇 주 동안 초록불이었다. "결과가 비어 있지 않은가"를 함께 보는 한 줄이 이것을 막는다.

넷째, 계획은 상태에 의존한다. 같은 코드라도 상태 파일이 다르면 계획이 다르다. PR 에서 만든 계획을 며칠 뒤 그대로 적용하면 그 사이의 변경이 반영되지 않는다. 계획 검사는 배포 직전에 다시 만든 계획으로 하는 것이 옳다.

참고 문서

다음 실습에서 할 것

오프라인 프로바이더로 계획을 만들어 JSON 으로 뽑고, 그 JSON 에 정책을 건다. resource_changes 를 훑어 delete 가 들어 있는 항목을 찾아내고, 교체와 단순 갱신을 구분하고, 필수 태그가 빠진 자원을 잡는 규칙을 쓴다. 값이 계획 단계에 아직 없는 자리를 일부러 만들어 after_unknown 을 보지 않는 규칙이 오탐을 내는 것을 확인한 뒤 고친다. 마지막으로 위반이 분명한 입력을 도구에 넣고 종료 코드를 직접 찍어 보아 0 이 나오는 것을 확인하고, 보고서를 파싱해 파이프라인을 세우는 게이트를 만든다.