K8s OPA Gatekeeper 정책 관리

OPA Gatekeeper란?

OPA(Open Policy Agent) Gatekeeper는 Kubernetes 클러스터에 정책을 코드로 정의하고 강제하는 Admission Controller다. “특정 레이블이 없는 Pod는 배포 불가”, “latest 태그 이미지 사용 금지” 같은 운영 규칙을 Rego 언어로 작성하고, API 서버가 리소스를 생성·수정할 때 자동으로 검증한다.

Gatekeeper는 OPA를 Kubernetes-native하게 감싼 것으로, ConstraintTemplateConstraint CRD를 통해 정책을 선언적으로 관리한다. Pod Security Policy(PSP)가 deprecated된 후, Gatekeeper는 사실상의 표준 정책 엔진이 되었다.

Gatekeeper 설치

# Helm으로 설치
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update

helm install gatekeeper gatekeeper/gatekeeper 
  --namespace gatekeeper-system 
  --create-namespace 
  --set replicas=3 
  --set audit.replicas=1 
  --set audit.interval=60

# 설치 확인
kubectl get pods -n gatekeeper-system
# NAME                                          READY   STATUS
# gatekeeper-audit-7c8d9f5b6-xxxxx             1/1     Running
# gatekeeper-controller-manager-xxx-xxxxx      1/1     Running
# gatekeeper-controller-manager-xxx-yyyyy      1/1     Running
# gatekeeper-controller-manager-xxx-zzzzz      1/1     Running

Controller Manager는 Validating Admission Webhook으로 동작하며, Audit 컨트롤러는 기존 리소스가 정책을 위반하는지 주기적으로 스캔한다.

핵심 개념: ConstraintTemplate + Constraint

Gatekeeper의 정책은 두 계층으로 나뉜다.

구성 요소 역할 비유
ConstraintTemplate 정책 로직 정의 (Rego) 클래스 (틀)
Constraint 정책 적용 범위·파라미터 설정 인스턴스 (적용)

하나의 ConstraintTemplate으로 여러 Constraint를 만들 수 있다. 예를 들어 “필수 레이블 검사” 템플릿으로 “app 레이블 필수”, “team 레이블 필수” 등 여러 정책을 생성한다.

실전 1: 필수 레이블 강제

# ConstraintTemplate: 필수 레이블 검사 로직
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{"msg": msg}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("필수 레이블 누락: %v", [missing])
        }
---
# Constraint: 모든 Namespace에 app, team 레이블 필수
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: ns-must-have-labels
spec:
  enforcementAction: deny    # deny | dryrun | warn
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels:
      - "app"
      - "team"
# 테스트: 레이블 없이 Namespace 생성 시도
$ kubectl create namespace test-no-labels
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request:
[ns-must-have-labels] 필수 레이블 누락: {"app", "team"}

# 레이블 포함 시 성공
$ kubectl create namespace test-ok --dry-run=server -o yaml 
  -l app=myapp,team=backend
namespace/test-ok created (server dry run)

실전 2: latest 태그 이미지 금지

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sdisallowedtags
spec:
  crd:
    spec:
      names:
        kind: K8sDisallowedTags
      validation:
        openAPIV3Schema:
          type: object
          properties:
            tags:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sdisallowedtags

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          tag := _get_tag(container.image)
          forbidden := {t | t := input.parameters.tags[_]}
          forbidden[tag]
          msg := sprintf("컨테이너 '%v'에 금지된 태그 '%v' 사용: %v",
            [container.name, tag, container.image])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not contains(container.image, ":")
          msg := sprintf("컨테이너 '%v'에 태그 미지정 (latest 암시): %v",
            [container.name, container.image])
        }

        _get_tag(image) = tag {
          parts := split(image, ":")
          tag := parts[count(parts) - 1]
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDisallowedTags
metadata:
  name: no-latest-tag
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment", "StatefulSet", "DaemonSet"]
    namespaces:
      - "production"
      - "staging"
    excludedNamespaces:
      - "kube-system"
  parameters:
    tags: ["latest"]

match 필드로 정책이 적용될 리소스 종류와 네임스페이스를 세밀하게 제어할 수 있다. excludedNamespaces로 시스템 네임스페이스는 제외한다.

실전 3: 리소스 Requests/Limits 강제

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8scontainerlimits
spec:
  crd:
    spec:
      names:
        kind: K8sContainerLimits
      validation:
        openAPIV3Schema:
          type: object
          properties:
            requireRequests:
              type: boolean
            requireLimits:
              type: boolean
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8scontainerlimits

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          input.parameters.requireRequests
          not container.resources.requests
          msg := sprintf("컨테이너 '%v'에 resources.requests 미설정", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          input.parameters.requireLimits
          not container.resources.limits
          msg := sprintf("컨테이너 '%v'에 resources.limits 미설정", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          input.parameters.requireRequests
          container.resources.requests
          not container.resources.requests.cpu
          msg := sprintf("컨테이너 '%v'에 CPU requests 미설정", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          input.parameters.requireRequests
          container.resources.requests
          not container.resources.requests.memory
          msg := sprintf("컨테이너 '%v'에 Memory requests 미설정", [container.name])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sContainerLimits
metadata:
  name: must-have-resource-limits
spec:
  enforcementAction: deny
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds: ["Deployment"]
  parameters:
    requireRequests: true
    requireLimits: true

리소스 관련 정책은 K8s ResourceQuota·LimitRange 운영 글의 LimitRange와 함께 사용하면 더 강력한 리소스 거버넌스를 구축할 수 있다.

enforcementAction: 단계적 적용

모드 동작 사용 시점
dryrun 위반 기록만, 차단 안 함 정책 도입 초기, 영향 파악
warn 경고 메시지 반환, 차단 안 함 팀에 인지시키는 전환 기간
deny 요청 거부 프로덕션 강제 적용
# 권장 적용 순서
# 1단계: dryrun으로 기존 위반 사항 파악
spec:
  enforcementAction: dryrun

# Audit 결과 확인
kubectl get k8srequiredlabels ns-must-have-labels -o yaml
# status:
#   totalViolations: 5
#   violations:
#     - name: default
#       message: "필수 레이블 누락: {"app", "team"}"

# 2단계: warn으로 전환 (팀에 알림)
spec:
  enforcementAction: warn

# 3단계: 충분한 전환 기간 후 deny 적용
spec:
  enforcementAction: deny

Gatekeeper Library 활용

OPA 커뮤니티에서 제공하는 gatekeeper-library에는 실무에서 자주 쓰는 ConstraintTemplate이 이미 준비되어 있다.

# gatekeeper-library 설치
git clone https://github.com/open-policy-agent/gatekeeper-library.git
cd gatekeeper-library

# 주요 템플릿 예시
library/
├── general/
│   ├── allowedrepos/          # 허용된 컨테이너 레지스트리만 사용
│   ├── containerlimits/       # 리소스 제한 필수
│   ├── disallowedtags/        # 특정 태그 금지
│   ├── requiredlabels/        # 필수 레이블
│   └── uniqueserviceselector/ # Service selector 중복 방지
├── pod-security-policy/
│   ├── host-network-ports/    # hostNetwork 사용 금지
│   ├── privileged-containers/ # 특권 컨테이너 금지
│   ├── read-only-root-fs/     # 읽기 전용 루트 파일시스템
│   └── volumes/               # 허용 볼륨 타입 제한

# 필요한 템플릿만 적용
kubectl apply -f library/general/requiredlabels/template.yaml
kubectl apply -f library/pod-security-policy/privileged-containers/template.yaml

Mutation: 리소스 자동 수정

Gatekeeper 3.10+부터 Mutation 기능으로 리소스를 자동 수정할 수 있다. 검증(Validation)만이 아니라 기본값 주입도 가능하다.

# Assign: 기본 리소스 limits 자동 주입
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
  name: default-memory-limit
spec:
  applyTo:
    - groups: [""]
      kinds: ["Pod"]
      versions: ["v1"]
  match:
    scope: Namespaced
    kinds:
      - apiGroups: ["*"]
        kinds: ["Pod"]
  location: "spec.containers[name:*].resources.limits.memory"
  parameters:
    assign:
      value: "512Mi"
    pathTests:
      - subPath: "spec.containers[name:*].resources.limits.memory"
        condition: MustNotExist  # 이미 설정된 경우 덮어쓰지 않음
---
# AssignMetadata: 기본 레이블 자동 추가
apiVersion: mutations.gatekeeper.sh/v1
kind: AssignMetadata
metadata:
  name: add-env-label
spec:
  match:
    scope: Namespaced
    kinds:
      - apiGroups: ["*"]
        kinds: ["Pod"]
    namespaces: ["production"]
  location: "metadata.labels.env"
  parameters:
    assign:
      value: "production"

pathTestsMustNotExist 조건은 이미 값이 설정된 리소스를 덮어쓰지 않도록 보호한다.

Audit와 모니터링

# 전체 위반 사항 조회
kubectl get constraints -o wide

# 특정 Constraint의 위반 상세
kubectl get k8sdisallowedtags no-latest-tag -o json | 
  jq '.status.violations[]'

# Prometheus 메트릭 연동
# Gatekeeper는 기본적으로 /metrics 엔드포인트 제공
# 주요 메트릭:
# - gatekeeper_violations{constraint_kind, constraint_name}
# - gatekeeper_request_duration_seconds
# - gatekeeper_constraint_template_status

# Grafana 대시보드 설정
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: gatekeeper-metrics
  namespace: gatekeeper-system
spec:
  selector:
    matchLabels:
      gatekeeper.sh/system: "yes"
  endpoints:
    - port: metrics
      interval: 30s

Prometheus + Grafana 연동에 대한 자세한 설정은 K8s Prometheus 모니터링 운영 글을 참고하자.

CI/CD 파이프라인 통합

# GitHub Actions에서 배포 전 정책 검증
name: Policy Check
on: [pull_request]

jobs:
  gatekeeper-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # gator CLI로 로컬 검증 (클러스터 불필요)
      - name: Install gator
        run: |
          curl -L https://github.com/open-policy-agent/gatekeeper/releases/download/v3.16.0/gator-v3.16.0-linux-amd64.tar.gz | tar xz
          sudo mv gator /usr/local/bin/

      # ConstraintTemplate + Constraint + 테스트 리소스 검증
      - name: Verify policies
        run: |
          gator verify ./policies/...

      # 특정 매니페스트가 정책을 통과하는지 테스트
      - name: Test manifests against policies
        run: |
          gator test 
            --filename=./policies/ 
            --filename=./k8s/manifests/
# gator test용 테스트 스위트 작성
# policies/tests/required-labels/suite.yaml
apiVersion: test.gatekeeper.sh/v1alpha1
kind: Suite
tests:
  - name: "레이블 없는 Pod는 거부"
    template: ../../templates/required-labels.yaml
    constraint: ../../constraints/must-have-app-label.yaml
    cases:
      - name: "app 레이블 있으면 통과"
        object: allowed-pod.yaml
        assertions:
          - violations: "no"
      - name: "app 레이블 없으면 거부"
        object: denied-pod.yaml
        assertions:
          - violations: "yes"
            message: "필수 레이블 누락"

gator CLI를 사용하면 클러스터 없이 로컬에서 정책을 테스트할 수 있어, PR 단계에서 정책 위반을 사전에 잡을 수 있다.

운영 팁과 주의사항

# 1. Webhook 장애 대비: failurePolicy 설정
# Gatekeeper가 다운되면 모든 배포가 차단될 수 있음
# Helm values에서 설정:
controllerManager:
  webhook:
    failurePolicy: Ignore  # Fail(기본) → Ignore로 변경 시 장애 시 허용

# 2. 시스템 네임스페이스 제외 (필수!)
# Config 리소스로 글로벌 제외 설정
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
  name: config
  namespace: gatekeeper-system
spec:
  match:
    - excludedNamespaces:
        - "kube-system"
        - "gatekeeper-system"
        - "cert-manager"
      processes: ["*"]

# 3. Constraint 상태 확인 자동화
kubectl get constraints -A -o custom-columns=
  NAME:.metadata.name,
  ACTION:.spec.enforcementAction,
  VIOLATIONS:.status.totalViolations

마무리

OPA Gatekeeper는 Kubernetes 클러스터의 정책 관리를 자동화하고 표준화하는 핵심 도구다. ConstraintTemplate으로 재사용 가능한 정책 로직을 정의하고, Constraint로 적용 범위를 제어하며, Mutation으로 기본값까지 자동 주입할 수 있다. dryrun → warn → deny 순서로 단계적 적용하고, gator CLI로 CI/CD 파이프라인에 통합하면, 인적 실수 없이 일관된 보안·운영 정책을 강제할 수 있다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux