작업에서 Nomad 액션 사용하기

작업에서 Nomad 액션 사용하기 (Use Nomad actions in jobs)

이 튜토리얼에서는 작업에서 Nomad 액션(Actions)을 만들고 실행해요. 액션은 작업 작성자가 작성하는 명령이며, 태스크 config와 같은 형태를 가져요.

action "hello-world" {
  command = "echo"
  args    = ["Hello, world!"]
}

이 액션들은 jobspec 내에서 태스크(Task) 수준에 위치하며, 실행 후 변수나 대화형 입력 없이 실행돼요. 다른 실행 가능한 명령과 마찬가지로 스스로 종료되거나 실행 중인 사용자가 수동으로 종료할 때까지 실행될 수 있어요. Nomad는 작업 또는 태스크 컨텍스트에서 액션과 상호작용하기 위한 CLI 명령, API, 웹 UI를 제공해요.

출처: 문서

본문

도전 과제 (Challenge)

이 튜토리얼에서는 Redis 인스턴스를 실행하는 Nomad 작업을 수정하고, 다음과 같은 반복 가능한 Nomad 액션을 만들어요:

  • 데이터베이스에서 항목을 추가하고 제거하는 일반적인 워크플로를 단순화
  • 해당 항목의 속성과 Redis 인스턴스의 지연 시간을 모니터링하고 보고
  • 데이터베이스의 핵심 동작 수정

전제 조건 (Prerequisites)

시작 작업 파일 만들기 (Build the starting job file)

다음 내용으로 redis-actions.nomad.hcl이라는 텍스트 파일을 만들어요.

redis-actions.nomad.hcl

job "redis-actions" {

  group "cache" {
    network {
      port "db" {}
    }

    task "redis" {
      driver = "docker"

      config {
        image   = "redis:7"
        ports   = ["db"]
        command = "/bin/sh"
        args    = ["-c", "redis-server --port ${NOMAD_PORT_db} & /local/db_log.sh"]
      }

      template {
        data        = <<EOF
          #!/bin/sh
          while true; do
            echo "$(date): Current DB Size: $(redis-cli -p ${NOMAD_PORT_db} DBSIZE)"
            sleep 3
          done
EOF
        destination = "local/db_log.sh"
        perms       = "0755"
      }

      resources {
        cpu    = 128
        memory = 128
      }

      service {
        name     = "redis-service"
        port     = "db"
        provider = "nomad"

        check {
          name     = "alive"
          type     = "tcp"
          port     = "db"
          interval = "10s"
          timeout  = "2s"
        }
      }
    }
  }
}

이 작업은 단일 Redis 인스턴스를 만들고, Nomad가 동적으로 할당하는 "db"라는 포트와 Nomad 서비스 헬스 체크를 구성해요. 태스크의 config와 template 블록은 redis 서버를 시작하고 3초마다 현재 데이터베이스 크기를 보고해요.

첫 번째 액션 작성하기 (Write your first Action)

관리 또는 alloc-exec 권한이 있는 사용자라면 실행 중인 인스턴스에 ssh로 접속해 Redis 인스턴스에 데이터를 추가할 수 있어요. 하지만 여기에는 몇 가지 단점이 있어요:

  • 서로 다른 시점에 데이터를 추가하려면 인스턴스에 여러 번 ssh로 접속해야 할 수 있으며, 그 방법을 기억해야 해요. 또는 그 과정에 덜 익숙한 다른 운영자가 해야 할 수도 있어요.
  • 수동 추가에 대한 감사 가능한 기록이 없어요. 워크플로를 반복하거나 확장해야 한다면 수동으로 해야 해요.
  • Nomad 태스크는 관리 시크릿이나 환경 변수를 사용해 Redis에 접근할 수 있지만, 사용자는 접근할 수 없을 수 있어요. Redis에 접근하거나 Nomad 인스턴스에 ssh로 접속하기 위해 자격 증명을 수동으로 전달하면 워크플로 보안에 반복적인 접근 구멍이 생겨요.

박스에 ssh로 접속해 redis-cli SET 명령을 반복해서 실행하는 대신, 이를 jobspec 태스크의 액션으로 커밋해요. 태스크의 service 블록에 다음을 추가해요:

redis-actions.nomad.hcl

# Adds a specific key-value pair ('hello'/'world') to the Redis database
action "add-key" {
  command = "/bin/sh"
  args    = ["-c", "redis-cli -p ${NOMAD_PORT_db} SET hello world; echo 'Key \"hello\" added with value \"world\"'"]
}

이 액션은 redis-cli 명령을 사용해 키-값 쌍을 설정한 다음 확인 메시지를 출력해요.

이제 작업을 제출해요:

$ nomad job run redis-actions.nomad.hcl

작업은 사용 가능한 새 액션과 함께 업데이트돼요. Nomad CLI를 사용해 작업, 그룹, 태스크, 액션 이름을 제공해 실행해요:

$ nomad action \
  -job=redis-actions \
  -group=cache \
  -task=redis \
add-key

액션에 설명된 출력이 표시되어 키가 추가되었음을 알 수 있어요:

OK
Key "hello" added with value "world"

이제 실행 중인 Nomad 작업의 jobspec에 정의된 명령을 실행한 거예요.

반복 가능한 워크플로 시뮬레이션하기 (Simulate a repeatable workflow)

일정한 상태를 적용하는 액션(예: 캐시를 수동으로 지우거나 사이트를 유지보수 모드로 전환하는 액션)은 유용할 수 있어요. 하지만 이 예제에서는 반복해서 실행하고 싶은 액션을 시뮬레이션해 봐요. 고정된 키/값 대신 랜덤 문자열을 생성하도록 액션을 수정해요. 이 액션을 사용자가 가입할 때 지속적인 아티팩트가 저장되거나 다른 공개-facing 액션이 저장되는 실제 시나리오의 대리자로 생각할 수 있어요.

redis-actions.nomad.hcl

# Adds a random key/value to the Redis database
action "add-random-key" {
  command = "/bin/sh"
  args    = ["-c", "key=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13); value=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13); redis-cli -p ${NOMAD_PORT_db} SET $key $value; echo Key $key added with value $value"]
}

이것은 데이터베이스에 랜덤 키/값을 추가하고 결과를 보고해요. 추가 기능을 설명하기 위해 키에 "temp_" 접두사를 붙인다는 점만 다른 두 번째 액션을 추가할 수 있어요:

redis-actions.nomad.hcl

# Adds a random key/value with a "temp_" prefix to the Redis database
action "add-random-temporary-key" {
  command = "/bin/sh"
  args    = ["-c", "key=temp_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13); value=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13); redis-cli -p ${NOMAD_PORT_db} SET $key $value; echo Key $key added with value $value"]
}

add-random-key 액션과 마찬가지로 이 새 액션은 지속적인 아티팩트를 생성하는 애플리케이션의 시뮬레이션으로 생각할 수 있어요. temp 키의 경우 실제 시나리오로는 사용자 가입 이메일 확인을 나타내는 키가 될 수 있어요. 곧 이러한 랜덤 키를 접두사에 따라 다르게 취급하는 또 다른 액션을 만들 거예요.

이 두 액션은 데이터베이스에 서로 다른 두 종류의 랜덤 데이터를 채워요. 이제 추가한 키를 볼 수 있는 액션을 만들기 위해 다음 코드 블록을 추가해요:

redis-actions.nomad.hcl

# Lists all keys currently stored in the Redis database.
action "list-keys" {
  command = "/bin/sh"
  args    = ["-c", "redis-cli -p ${NOMAD_PORT_db} KEYS '*"]
}

이제 작업을 업데이트해요:

$ nomad job run redis-actions.nomad.hcl

Nomad 웹 UI가 실행 중이라면 작업 페이지에 접속하면 Actions 드롭다운이 표시돼요.

Actions dropdown

액션 중 하나를 선택하면 선택한 액션의 출력이 포함된 플라이아웃(fly-out)이 열려요:

Actions flyout

다음으로 데이터베이스에서 임시 키를 지우는 "안전 밸브(safety valve)" 액션을 만드는 새 액션 블록을 추가해요. 이것은 앞서 만든 add-random-key와 add-random-temporary-key 액션이 생성한 아티팩트를 구분해 사용해요.

redis-actions.nomad.hcl

# Deletes all keys with a 'temp_' prefix
action "flush-temp-keys" {
  command = "/bin/sh"
  args    = ["-c", <<EOF
    keys_to_delete=$(redis-cli -p ${NOMAD_PORT_db} --scan --pattern 'temp_*')
    if [ -n "$keys_to_delete" ]; then
      # Count the number of keys to delete
      deleted_count=$(echo "$keys_to_delete" | wc -l)
      # Execute the delete command
      echo "$keys_to_delete" | xargs redis-cli -p ${NOMAD_PORT_db} DEL
    else
      deleted_count=0
    fi
    remaining_keys=$(redis-cli -p ${NOMAD_PORT_db} DBSIZE)
    echo "$deleted_count temporary keys removed; $remaining_keys keys remaining in database"
EOF
  ]
}

실제 시나리오에서는 예를 들어 이런 액션이 자동으로 추가된 항목을 필터링해 지우고, 남은 키 수나 삭제에 걸린 시간을 보고할 수 있어요.

이 작업은 명령줄에서 두 가지 방법으로 실행할 수 있어요:

1: 태스크가 단일 할당에서 실행 중일 때, 또는 작업을 실행하는 임의의 할당에서 액션을 수행하려는 경우:

$ nomad action \
  -group=cache \
  -task=redis \
  -job=redis-actions \
flush-temp-keys

2: 특정한 알려진 할당에서 액션을 수행하려는 경우, 먼저 할당 ID를 가져와요:

$ nomad job status redis-actions

Nomad CLI는 jobspec에 대한 정보를 표시하며, 다음이 포함돼요:

ID            = redis-actions
...
Allocations
ID        Node ID   Task Group  Version  Desired  Status   Created   Modified
d841c716  03a56d12  cache       0        run      running  5m4s ago  4m48s ago

표시된 할당에서 ID를 복사하고 다음을 실행해 flush-temp-keys 액션을 수행해요:

$ nomad action \
  -alloc=d841c716 \
  -job=redis-actions \
flush-temp-keys

실행 중인 액션이 할당에 의존하지 않으면 첫 번째 방법을 사용해요. 작업이 여러 Redis 인스턴스를 호스팅하고 특정 인스턴스의 캐시를 지워야 한다면 두 번째 방법을 사용해요. 실제 상황에서 선택한 방법은 목표에 따라 달라져요.

액션은 실행 중인 태스크에 영향을 줄 수 있어요 (Actions can impact the running task)

일부 액션은 애플리케이션의 현재 상태에 영향을 주지 않을 수 있어요. 예를 들어 로그 처리, 서버 통계 보고 및 전송, 토큰 해지 등이요. 하지만 이 예제에서 액션은 태스크의 활성 상태에 영향을 줘요. Redis 태스크는 DBSIZE를 db_log.sh에 쓰고 몇 초마다 로그로 기록했어요. 실행 중인 작업을 검사하고 할당 ID를 가져와요. 그런 다음 다음을 실행해요:

nomad alloc logs <alloc-id>

Nomad는 작업의 할당 ID에 대한 로그를 출력해요:

Tue Nov 28 01:23:46 UTC 2023: Current DB Size: 1
Tue Nov 28 01:23:49 UTC 2023: Current DB Size: 1
Tue Nov 28 01:23:52 UTC 2023: Current DB Size: 2
Tue Nov 28 01:23:55 UTC 2023: Current DB Size: 3
Tue Nov 28 01:23:58 UTC 2023: Current DB Size: 3
Tue Nov 28 01:24:01 UTC 2023: Current DB Size: 4
Tue Nov 28 01:24:04 UTC 2023: Current DB Size: 5
Tue Nov 28 01:24:07 UTC 2023: Current DB Size: 8

이제 애플리케이션의 더 낮은 수준의 구성 옵션에 영향을 주는 또 다른 액션을 추가해요. Redis는 디스크로의 영속화(persistence)를 켜고 끌 수 있어요. 이를 확인하고 전환하는 액션을 작성해요. 다음 액션 블록을 추가해요:

redis-actions.nomad.hcl

# Toggles saving to disk (RDB persistence). When enabled, allocation logs will indicate a save every 60 seconds.
action "toggle-save-to-disk" {
  command = "/bin/sh"
  args    = ["-c", <<EOF
    current_config=$(redis-cli -p ${NOMAD_PORT_db} CONFIG GET save | awk 'NR==2');
    if [ -z "$current_config" ]; then
      # Enable saving to disk (example: save after 60 seconds if at least 1 key changed)
      redis-cli -p ${NOMAD_PORT_db} CONFIG SET save "60 1";
      echo "Saving to disk enabled: 60 seconds interval if at least 1 key changed";
    else
      # Disable saving to disk
      redis-cli -p ${NOMAD_PORT_db} CONFIG SET save "";
      echo "Saving to disk disabled";
    fi;
EOF
  ]
}

위 방식으로 RDB 스냅샷을 활성화하면 애플리케이션 로그의 출력도 수정돼요.

$ nomad action \
  -group=cache \
  -task=redis \
  -job=redis-actions \
toggle-save-to-disk

Nomad는 액션 실행과 디스크 저장 활성화에 대한 확인을 반환해요.

OK
Saving to disk enabled: 60 seconds interval if at least 1 key changed

서버 로그에 접속해 Redis가 스냅샷을 저장하는 줄을 찾아보세요:

Tue Nov 28 01:31:14 UTC 2023: Current DB Size: 12
28 Nov 01:31:17.800 * 2 changes in 60 seconds. Saving...
28 Nov 01:31:17.800 * Background saving started by pid 36652
28 Nov 01:31:17.810 * DB saved on disk
28 Nov 01:31:17.810 * RDB: 0 MB of memory used by copy-on-write
28 Nov 01:31:17.902 * Background saving terminated with success
Tue Nov 28 01:31:17 UTC 2023: Current DB Size: 12

Nomad 액션은 액션이 실행되는 바로 그 태스크의 상태와 동작에 영향을 줄 수 있어요. 이를 염두에 두면 개발자와 플랫폼 팀이 애플리케이션에서 비즈니스 로직과 운영 로직을 분리하는 데 도움이 돼요.

무기한 및 자체 종료 액션 (Indefinite and self-terminating actions)

지금까지의 모든 액션은 자체 종료형이었어요. 완료되어 완료를 알리는 명령을 실행했어요. 하지만 액션은 원하는 태스크의 완료를 기다리며, Nomad API와 웹 UI는 이를 위해 웹소켓을 사용해요.

Redis 인스턴스의 지연 시간을 관찰하기 위해 ctrl + c 같은 신호 인터럽션으로 직접 중지할 때까지 실행되는 액션을 추가해요:

redis-actions.nomad.hcl

# Performs a latency check of the Redis server.
# This action is a non-terminating action, meaning it will run indefinitely until it is stopped.
# Pass a signal interruption (Ctrl-C) to stop the action.
action "health-check" {
  command = "/bin/sh"
  args    = ["-c", "redis-cli -p ${NOMAD_PORT_db} --latency"]
}

작업을 제출하고 다음으로 액션을 실행해요:

$ nomad action \
  -group=cache \
  -task=redis \
  -job=redis-actions \
  -t=true \
health-check

출력은 Redis 인스턴스의 최소, 최대, 평균 지연 시간(ms)을 나타내야 해요. 신호를 인터럽트하거나 웹소켓을 닫으면 액션 실행이 종료돼요.

Actions health check

마무리 (Wrap-up)

액션이 포함된(몇 가지 추가 사항이 들어있는) 완전한 Redis 작업은 아래에서 찾을 수 있어요:

redis-actions.nomad.hcl

job "redis-actions" {

  group "cache" {
    network {
      port "db" {}
    }

    task "redis" {
      driver = "docker"

      config {
        image   = "redis:7"
        ports   = ["db"]
        command = "/bin/sh"
        args    = ["-c", "redis-server --port ${NOMAD_PORT_db} & /local/db_log.sh"]
      }

      template {
        data        = <<EOF
#!/bin/sh
while true; do
  echo "$(date): Current DB Size: $(redis-cli -p ${NOMAD_PORT_db} DBSIZE)"
  sleep 3
done
EOF
        destination = "local/db_log.sh"
        perms       = "0755"
      }

      resources {
        cpu    = 128
        memory = 128
      }

      service {
        name     = "redis-service"
        port     = "db"
        provider = "nomad"

        check {
          name     = "alive"
          type     = "tcp"
          port     = "db"
          interval = "10s"
          timeout  = "2s"
        }
      }

      # Adds a random key/value to the Redis database
      action "add-random-key" {
        command = "/bin/sh"
        args    = ["-c", "key=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13); value=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13); redis-cli -p ${NOMAD_PORT_db} SET $key $value; echo Key $key added with value $value"]
      }

      # Adds a random key/value with a "temp_" prefix to the Redis database
      action "add-random-temporary-key" {
        command = "/bin/sh"
        args    = ["-c", "key=temp_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13); value=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13); redis-cli -p ${NOMAD_PORT_db} SET $key $value; echo Key $key added with value $value"]
      }

      # Lists all keys currently stored in the Redis database.
      action "list-keys" {
        command = "/bin/sh"
        args    = ["-c", "redis-cli -p ${NOMAD_PORT_db} KEYS '*"]
      }

      # Performs a latency check of the Redis server.
      # This action is a non-terminating action, meaning it will run indefinitely until it is stopped.
      # Pass a signal interruption (Ctrl-C) to stop the action.
      action "health-check" {
        command = "/bin/sh"
        args    = ["-c", "redis-cli -p ${NOMAD_PORT_db} --latency"]
      }

      # Deletes all keys with a 'temp_' prefix
      action "flush-temp-keys" {
        command = "/bin/sh"
        args    = ["-c", <<EOF
keys_to_delete=$(redis-cli -p ${NOMAD_PORT_db} --scan --pattern 'temp_*')
if [ -n "$keys_to_delete" ]; then
  # Count the number of keys to delete
  deleted_count=$(echo "$keys_to_delete" | wc -l)
  # Execute the delete command
  echo "$keys_to_delete" | xargs redis-cli -p ${NOMAD_PORT_db} DEL
else
  deleted_count=0
fi
remaining_keys=$(redis-cli -p ${NOMAD_PORT_db} DBSIZE)
echo "$deleted_count temporary keys removed; $remaining_keys keys remaining in database"
EOF
        ]
      }

      # Toggles saving to disk (RDB persistence). When enabled, allocation logs will indicate a save every 60 seconds.
      action "toggle-save-to-disk" {
        command = "/bin/sh"
        args    = ["-c", <<EOF
current_config=$(redis-cli -p ${NOMAD_PORT_db} CONFIG GET save | awk 'NR==2');
if [ -z "$current_config" ]; then
  # Enable saving to disk (example: save after 60 seconds if at least 1 key changed)
  redis-cli -p ${NOMAD_PORT_db} CONFIG SET save "60 1";
  echo "Saving to disk enabled: 60 seconds interval if at least 1 key changed";
else
  # Disable saving to disk
  redis-cli -p ${NOMAD_PORT_db} CONFIG SET save "";
  echo "Saving to disk disabled";
fi;
EOF
        ]
      }
    }
  }
}

이 액션들을 복제하고 수정해 Nomad에서 액션 기반 워크플로의 잠재력을 탐색해 보세요.

워크플로에서 액션을 어떻게 활용할 수 있는지 더 탐색하려면 다음을 고려해 보세요:

  • 위 예제들은 대부분 단일 태스크 그룹과 태스크가 있는 작업의 단일 할당에서 격리되어 실행되는 자체 포함형이에요. 여러 그룹과 태스크를 가진 작업을 만들고, 그 액션들이 서비스 디스커버리를 통해 서로 통신할 수 있게 해보세요.
  • GET job actions 엔드포인트를 사용해 작업과 그 그룹 및 태스크에서 사용할 수 있는 액션 목록을 확인해 보세요.
  • Nomad 환경 변수를 활용하는 액션을 작성해 보세요. 예를 들어 다음 액션들은 운영자가 시스템 상태를 파악하기 위한 바로가기를 Nomad 작업에 추가하는 방법을 보여줘요:
action "get-alloc-info" {
  command = "/bin/sh"
  args    = ["-c",
    <<EOT
    nomad alloc status ${NOMAD_ALLOC_ID}
    EOT
  ]
}
action "get-event-stream" {
  command = "/usr/bin/curl"
  args    = ["-s", "localhost:4646/v1/event/stream", " | ", "jq"]
}

정리 (Clean up)

Nomad 액션 작업을 중지하려면 Nomad CLI를 사용해요:

$ nomad job stop redis-actions

작성한 jobspec은 파일시스템에 남지만, 작업 실행에 사용된 리소스는 해제되며 Nomad는 잠시 후 중지된 작업을 자동으로 가비지 컬렉션해요.

더 알아보기 (Learn more)