Sentinel 정책에서 http import 사용하기

Sentinel 정책에서 http import 사용하기

Sentinel의 http import를 사용해 노마드 외부의 사용자 정의 검증 서비스로 Sentinel 정책 시행을 확장해요. Sentinel 범위의 객체를 JSON 인코딩된 HTTP 요청으로 여러분이 만들고 배포하는 서비스로 보낼 수 있어요.

이 가이드에서는 http import를 사용해 정책을 만들고 예시 검증 서비스로 시행해요. 또한 노마드의 내장 nomad_var 플러그인을 http import와 함께 사용해 정책 평가에 자격 증명을 안전하게 제공하는 방법도 배워요.

엔터프라이즈: 이 기능은 노마드 엔터프라이즈를 필요로 해요.

출처: 문서

본문

사전 요구사항

다음 예시는 http import를 사용해 Sentinel 정책을 안전하게 구축하는 방법을 보여줘요.

노마드 설치가 다음 사전 요구사항을 충족하는지 확인해 주세요:

  • CNI 참조 플러그인 설치. 자세한 내용은 install CNI reference plugins 문서를 참고해 주세요.
  • ACL 부트스트랩. 지침은 ACL 가이드를 참고해 주세요.

추가 요구사항:

  • 노마드 환경에 Docker를 설치했어요.
  • NOMAD_TOKEN 환경 변수를 만들고 값을 노마드 ACL management 토큰으로 설정했어요.

http 모듈 허용

내장 Sentinel http import는 기본적으로 활성화되지 않아요. 노마드 에이전트 구성에 sentinel 블록을 추가해요.

sentinel {
  additional_enabled_modules = ["http"]
}

새 Sentinel 구성을 로드하려면 에이전트를 다시 시작해요.

검증 서비스 배포

검증 서비스는 정책이 보내는 JSON 인코딩 요청을 수용할 수 있는 모든 HTTP 서비스예요. Sentinel 정책 작성자로서 정책 범위의 객체로 요청 본문을 정의해요. 예를 들어 submit-job Sentinel 범위에서 job.task_groups를 통해 제출된 작업의 태스크 그룹 목록을 참조할 수 있어요.

이 예시 검증기에서 정책은 "dev" 네임스페이스의 태스크 그룹이 count를 1로 갖도록 허용하고, 다른 네임스페이스는 무시해요. validator.nomad.hcl 파일에 다음 작업 명세를 만들어요.

검증기 서비스 작업 명세

job "validator" {

  group "group" {

    network {
      mode = "bridge"
      port "www" {
        to = 8001
      }
    }

    service {
      name     = "validator"
      provider = "nomad"
      port     = "www"
    }

    task "http" {

      driver = "docker"

      config {
        image   = "python:3.11-alpine"
        command = "python"
        args    = ["local/server.py"]
        ports   = ["www"]
      }

      template {
        destination = "/secret/token"
        env         = true
        data        = <<EOT
TOKEN={{ with nomadVar "nomad/jobs/validator" }}{{ .token }}{{ end }}
EOT

      }

      template {
        destination = "local/server.py"
        once        = true
        data        = <<EOT
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import os

token = os.getenv("TOKEN")

class RequestHandler(BaseHTTPRequestHandler):
    def reply(self, code, msg):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(msg)

    def do_POST(self):
        if self.path != "/validate.json":
            self.reply(404, b'{"error": "not found"}')
            return

        if self.headers.get("Authorization", "") != f"token {token}":
            self.reply(403, b'{"error": "forbidden"}')
            return

        content_length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(content_length)

        try:
            job = json.loads(body)
            if job["namespace"] == "dev":
                for tg in job["task_groups"]:
                    if tg["count"] != 1:
                        self.reply(400, b'{"message": "invalid task group count in dev"}')
                        return
        except (json.JSONDecodeError, KeyError):
            self.reply(400, b'{"error": "invalid request"}')
            return

        self.reply(200, b'{"is_valid": true}')

if __name__ == "__main__":
    server_address = ("0.0.0.0", 8001)
    httpd = HTTPServer(server_address, RequestHandler)
    httpd.serve_forever()

        EOT

      }

      resources {
        cpu    = 100
        memory = 100
      }
    }
  }

}

이 작업은 단일 Docker 작업으로 배포된 Python 스크립트예요. 성공 또는 실패와 진단 메시지를 나타내는 JSON 응답 본문을 반환해요.

⚠️ 참고: 노마드는 http import가 있는 정책이 요구할 때마다 Sentinel 검증 서비스로 요청을 보내요. 검증 서비스가 사용 불가능하면 작업이 노마드에 제출되지 못해요. 프로덕션 환경에서는 검증기를 여러 인스턴스로 배포하고, 노마드 서버와 같은 호스트의 로컬 서비스로 배포할지 고려해야 해요.

검증기 작업은 환경에 API 토큰이 필요하며, template 블록을 사용해 노마드 변수에서 환경으로 읽어요. 임의의 토큰을 만들어 노마드 변수에 써요.

$ nomad var put nomad/jobs/validator token=xyzzy

작업을 배포해요.

$ nomad job run ./validator.nomad.hcl

작업의 주소를 찾아 셸 변수에 써요.

$ ADDR=$(nomad service info -t '{{ (index . 0).Address }}:{{ (index . 0).Port}}' validator)

정책 만들고 설치하기

job-policy.sentinel이라는 이름의 Sentinel 정책 파일을 만들어요.

import "http"
import "json"
import "nomad_var"

# credentials
validator = nomad_var.get("nomad/sentinel/validator@" + namespace.name)
auth_token = validator.token
url = validator.url

# send to validator service
reqBody = { "task_groups": job.task_groups, "namespace": namespace.name }
req = http.request(url).
    with_body(json.marshal(reqBody)).
    with_header("Authorization", "token "+auth_token)

resp = http.with_timeout(1).with_retries(3).post(req)
body = json.unmarshal(resp.body)

main = rule {
  body["is_valid"] is defined and
  body["is_valid"] is true }

정책의 자격 증명 섹션은 내장 nomad_var 플러그인을 사용해 요청의 네임스페이스에서 nomad/sentinel/validator 경로 아래의 노마드 변수를 읽어요. 네임스페이스를 무시하고 @ 구분자나 네임스페이스 추가 없이 경로를 "nomad/sentinel/validator"로 지정해 모든 정책 평가가 기본 네임스페이스의 변수를 사용하게 할 수도 있어요.

다음으로 정책은 작업의 태스크 그룹과 네임스페이스로 구성된 요청 본문을 만들어요. 이 본문은 HTTP POST로 전송되며, 타임아웃 1초, 요청 실패 시 3회 재시도해요.

⚠️ 참고: 정책의 범위에는 정책 작성자가 아닌 작업 작성자가 제어하는 객체가 포함돼요. 예를 들어 url + "?job=" + job.id처럼 작업 ID를 연결해 URL을 구성하면, 작업 작성자가 작업 ID에 &를 추가해 URL에 임의의 쿼리 파라미터를 추가할 수 있어요. 클러스터 관리자가 네임스페이스를 만들기 때문에 네임스페이스 이름만 안전한 것으로 간주해야 해요. 노마드는 Sentinel 정책 평가가 발생하기 전에 네임스페이스에 대한 요청을 인증하고 승인해요.

정책을 설치하기 전에 자격 증명을 노마드 변수로 설치해야 해요. 먼저 "dev" 네임스페이스를 만들어요.

$ nomad namespace apply dev
Successfully applied namespace "dev"!

"dev" 네임스페이스에 자격 증명을 만들어요.

$ nomad var put -namespace dev nomad/sentinel/validator \
    token=xyzzy url="http://${ADDR}/validate.json"

기본 네임스페이스에 자격 증명을 만들어요.

$ nomad var put nomad/sentinel/validator \
    token=xyzzy url="http://${ADDR}/validate.json"

실패 시 경고를 발행하는 advisory 수준으로 정책을 설치해요.

$ nomad sentinel apply -level=advisory -scope=submit-job test job-policy.sentinel
Successfully wrote "test" Sentinel policy!

Sentinel 정책은 항상 advisory 수준으로 먼저 설치하고, 정책을 테스트한 뒤에만 soft-mandatory나 hard-mandatory로 높여야 해요.

정책 테스트하기

다음 내용으로 새 작업 명세 파일 example.nomad.hcl을 만들어요.

예시 작업 명세

job "example" {
  group "group" {

    count = 2

    task "task" {
      driver = "docker"
      config {
        image   = "busybox:1"
        command = "httpd"
        args    = ["-vv", "-f", "-p", "8001", "-h", "/local"]
      }
    }
  }
}

네임스페이스가 지정되지 않은 채 남겨져 CLI에서 설정할 수 있게 했다는 점, 그리고 태스크 그룹이 count 2를 갖는다는 점을 유의하세요.

기본 네임스페이스에서 작업을 계획해요.

$ nomad job plan ./example.nomad.hcl
+ Job: "example"
+ Task Group: "group" (2 create)
  + Task: "task" (forces create)

Scheduler dry-run:
- All tasks successfully allocated.

Job Modify Index: 0
To submit the job with version verification run:

nomad job run -check-index 0 ./example.nomad.hcl

When running the job with the check-index flag, the job will only be run if the
job modify index given matches the server-side version. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.

dev 네임스페이스에서 작업을 계획해요. 이번에는 요청 실패를 나타내는 작업에 대한 advisory 경고가 보일 거예요.

$ nomad job plan -namespace dev ./example.nomad.hcl
+ Job: "example"
+ Task Group: "group" (2 create)
  + Task: "task" (forces create)

Scheduler dry-run:
- All tasks successfully allocated.

Job Warnings:
1 warning:

* test : Result: false

Error message: test:21:8: error calling function "post": expected status code to be one of [200], got 400

Job Modify Index: 0
To submit the job with version verification run:

nomad job run -check-index 0 -namespace="dev" ./example.nomad.hcl

When running the job with the check-index flag, the job will only be run if the
job modify index given matches the server-side version. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.

작업 작성자를 위한 피드백 제공

기본 경고는 작업 작성자에게 정책이 왜 작업을 거부했는지 알려주지 않아요. 검증 서비스에서 반환된 진단 메시지를 출력해 피드백을 제공할 수 있어요.

HTTP 400 상태 코드를 수용하고 받은 메시지를 출력하도록 job-policy.sentinel 파일을 편집해요.

import "http"
import "json"
import "nomad_var"

# credentials
validator = nomad_var.get("nomad/sentinel/validator@" + namespace.name)
auth_token = validator.token
url = validator.url

# send to validator service
reqBody = { "task_groups": job.task_groups, "namespace": namespace.name }
req = http.request(url).
    with_body(json.marshal(reqBody)).
    with_header("Authorization", "token "+auth_token)

resp = http.with_timeout(1).with_retries(3).
    accept_status_codes([200, 400]).
    post(req)
body = json.unmarshal(resp.body)

if body["message"] is defined {
  print(body["message"])
}

main = rule {
  body["is_valid"] is defined and
  body["is_valid"] is true }

⚠️ 참고: 예시 검증기는 내부 오류와 작업 진단용 메시지를 구분하는 응답을 갖고 있어요. 내부 오류를 작업 작성자에게 노출하지 않도록 주의해 주세요.

업데이트된 정책 파일을 적용해요.

$ nomad sentinel apply -level=advisory -scope=submit-job test job-policy.sentinel
Successfully wrote "test" Sentinel policy!

dev 네임스페이스에서 작업을 다시 계획하고, 검증 서비스 응답에 메시지가 포함되어 있는지 확인해요.

$ nomad job plan -namespace dev ./example.nomad.hcl
+ Job: "example"
+ Task Group: "group" (2 create)
  + Task: "task" (forces create)

Scheduler dry-run:
- All tasks successfully allocated.

Job Warnings:
1 warning:

* test : Result: false

Print messages:

invalid task group count in dev

test:23:1 - Rule "main"
  Value:
    false

Job Modify Index: 0
To submit the job with version verification run:

nomad job run -check-index 0 -namespace="dev" ./example.nomad.hcl

When running the job with the check-index flag, the job will only be run if the
job modify index given matches the server-side version. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.

Sentinel에 대해 더 알아보기

Sentinel 작업에 대한 자세한 내용은 nomad sentinel 하위 명령, HTTP API 문서, Sentinel 정책 참조를 참고해 주세요.

더 알아보기 (Learn more)