# Runbook 에이전트

대시보드에서 **Runner**라고 부르는 **Runbook 에이전트**는 Runbook의 JavaScript, Bash, SSH, kubectl 단계를 **자체 인프라에서** 실행하는 작은 자체 호스팅 프로세스입니다. Status200 Worker는 스크립트를 실행하지 않습니다. Worker는 스크립트를 큐에 넣고, 단계 작성자가 고른 Runner가 각 작업을 가져가 실행한 뒤 결과를 돌려보냅니다. 이 페이지는 Runner를 설치하고 운영하는 사람을 위한 것입니다. Runner를 고르지 않은 단계는 설정해 두었다면 [프로젝트 기본 Runner](#프로젝트-기본-runner)에서 실행되고, 그렇지 않으면 JavaScript 단계는 [글로벌 Runner](#글로벌-runner)에서 실행됩니다. 글로벌 Runner는 Status200이 모든 프로젝트를 위해 실행하는 Runner입니다.

:::cards
- [Runner 설치](#runner-설치): 대시보드에서 연결된 컨테이너까지 다섯 단계.
- [단계에 Runner 지정](#단계에-runner-지정): 단계를 실행할 Runner에 연결합니다.
- [프로젝트 기본 Runner](#프로젝트-기본-runner): Runner를 고르지 않은 단계가 실행되는 Runner입니다.
- [글로벌 Runner](#글로벌-runner): Status200이 모든 프로젝트를 위해 실행하는 Runner와 그 Runner가 실행하는 것입니다.
- [타임아웃](#타임아웃): claim 타임아웃과 실행 타임아웃, 그리고 둘의 관계.
- [환경 변수](#환경-변수): 컨테이너가 시작할 때 읽는 것.
:::

## 작동 방식

```mermaid title="Runner와 Status200 사이에서 네트워크를 오가는 것"
flowchart TB
    subgraph cloud["Status200"]
        direction LR
        worker["Worker가 단계를 큐에 넣음"]
        ingest["Runner API"]
    end
    subgraph yours["자체 인프라"]
        direction LR
        runner["Runner 컨테이너"]
        targets["호스트, 클러스터, 내부 서비스"]
    end
    worker --> ingest
    runner -->|"아웃바운드 HTTPS, Runner ID와 키"| ingest
    ingest -->|"시크릿이 포함된 가져간 작업"| runner
    runner -->|"스크립트, SSH 또는 Kubernetes API"| targets
```

1. Status200에서 Runner를 만듭니다. Status200이 Runner의 ID와 비밀 키를 생성합니다.
2. 자체 인프라의 호스트에서 그 ID, 키, Status200 URL로 Runner 컨테이너를 실행합니다.
3. Runner는 5초마다 Status200에 작업을 요청하고, 60초마다 살아 있다고 보고합니다.
4. JavaScript, Bash, SSH 또는 kubectl 단계를 작성할 때 단계의 **추가 필드**에 있는 드롭다운에서 Runner를 고릅니다. 단계는 그 Runner에 연결됩니다. Runner를 고르지 않은 단계는 설정해 두었다면 프로젝트 기본 Runner에서, 그렇지 않으면 JavaScript 단계는 **글로벌 Runner**에서 실행됩니다.
5. 단계가 실행되면 Worker는 `targetAgentId`를 그 Runner로 설정한 작업을 큐에 넣습니다. 그 Runner만 작업을 가져갈 수 있습니다.
6. Runner는 작업을 로컬에서 실행하고(Bash와 kubectl은 `bash -c <script>`, JavaScript는 `isolated-vm` 샌드박스, SSH는 머신에서 bash로 스크립트를 실행하는 SSH 연결), 결과를 기록하고, 그 안의 단계 시크릿을 가린 뒤 돌려보냅니다. Worker는 그 결과로 Runbook을 이어서 진행합니다.

Runner에는 Status200 인스턴스로 가는 **아웃바운드 HTTPS**만 있으면 됩니다. 인바운드 연결은 받지 않습니다.

Runner가 가진 것은 자신의 ID와 키뿐입니다. 나머지는 모두 가져간 작업과 함께 받습니다. 지정된 [Runbook 변수](/docs/runbooks/variables)가 채워진 스크립트와, SSH 단계의 키나 비밀번호 또는 kubectl 단계의 kubeconfig입니다. 시크릿은 그 시크릿이 허용할 때만 받습니다. 그래서 Runner의 키를 가진 사람은 누구나 그 Runner로 행동할 수 있습니다. 키를 그 Runner가 사용할 수 있는 시크릿처럼 다루세요.

## 스크립트를 Runner에서 실행하는 이유

Status200 Worker에서 스크립트를 실행하는 데에는 두 가지 문제가 있었습니다.

- **신뢰 경계.** Runbook을 작성할 수 있는 사람이면 누구나 Worker가 접근할 수 있는 모든 것에 접근하면서 Worker에서 코드를 실행할 수 있었습니다.
- **도달 범위.** 쓸모 있는 단계 대부분은 Status200이 아니라 _자체_ 인프라에 작용합니다("이 서비스를 다시 시작", "내부 데이터베이스에서 레코드 조회").

Runner를 쓰면 이런 단계가 여러분이 관리하는 호스트에서 실행되고, 그 호스트에 무엇을 허용할지 여러분이 정합니다. HTTP 요청과 AI 단계는 자체 네트워크의 무엇도 필요하지 않으므로 계속 Worker에서 실행됩니다.

## 글로벌 Runner

글로벌 Runner는 Status200이 모든 프로젝트를 위해 실행하는 Runner입니다. 자신의 Runner를 고르지 않은 JavaScript 단계는 프로젝트에 [기본 Runner](#프로젝트-기본-runner)가 없는 한 그중 하나에서 실행되므로, 아무것도 설치하기 전에 단계를 작성할 수 있습니다. **런북 → Runbook 에이전트**는 내 Runner 위의 **글로벌 Runner** 아래에 이들을 나열하며, JavaScript 단계의 **Runner** 드롭다운은 **글로벌 Runner**를 맨 먼저, 또는 프로젝트 기본 Runner 바로 다음에 보여 줍니다. Bash, SSH, kubectl 단계의 드롭다운은 이를 맨 마지막에 흐리게 표시하며 **JavaScript 단계만 실행합니다.** 라고 알려 줍니다. 이런 단계는 모든 프로젝트가 함께 쓰는 Runner가 아니라 네트워크 안에 있는 내 Runner에서 실행됩니다.

| | 글로벌 Runner | 내 Runner |
| --- | --- | --- |
| 실행하는 단계 | JavaScript | JavaScript, Bash, SSH, kubectl |
| 단계가 접근할 수 있는 곳 | 공용 인터넷 주소 | 호스트가 접근할 수 있는 곳 |
| 단계의 최대 실행 시간 | 5분 | 1시간 |
| 시크릿 변수 | 글로벌 Runner에 사용을 허용한 것 | 그 Runner를 지정한 것 |

글로벌 Runner는 여러 프로젝트의 단계를 실행하므로, 각 단계는 다른 모든 단계와 분리됩니다.

- 각 단계는 그 단계를 위해 만들어지고 끝나면 버려지는 자체 `isolated-vm` 샌드박스에서 실행됩니다. 파일도, 프로세스도, 환경 변수도 없으며, 한 단계에서 다음 단계로 아무것도 남지 않습니다.
- HTTP 요청은 공용 인터넷 주소에만 닿으며, 사설, 루프백, 링크 로컬 주소나 클라우드 메타데이터 엔드포인트에는 절대 닿지 않습니다. 그래서 글로벌 Runner는 여러분의 네트워크에 절대 닿지 않습니다.
- 시크릿 변수는 그 변수의 **글로벌 Runner가 사용할 수 있음** 스위치가 켜져 있을 때만, 그리고 변수가 속한 프로젝트의 단계에 대해서만 받습니다.
- 한 프로젝트가 글로벌 Runner에서 동시에 실행하는 단계는 최대 세 개이므로, 어떤 프로젝트도 글로벌 Runner를 독차지하지 못합니다.

여러분의 네트워크에 접근해야 하는 단계나, 직접 관리하는 호스트에 두고 싶은 단계에는 내 Runner 중 하나를 고르세요. 모든 단계를 내 Runner에서 실행하려면 **런북 → Runbook 에이전트** 맨 아래의 **글로벌 Runner 사용**을 끕니다. 그러면 Runner를 고르지 않은 JavaScript 단계는 프로젝트 기본 Runner에서 실행되거나, 기본 Runner가 없으면 내 Runner 중 하나를 고를 때까지 그 이유를 알리며 실패합니다. 내 Runner를 고른 단계는 어느 경우든 그 Runner를 유지합니다. 코드의 설정에서 Runner를 선택하지 않은 코드 저장소도 글로벌 Runner에서 작업하며, 이 스위치가 꺼져 있는 동안에는 내 Runner가 필요합니다. [코드](/docs/ai/ai-agent#저장소의-runner-선택하기)를 참조하세요.

### 글로벌 Runner의 출처

Status200 Cloud에서는 Status200이 글로벌 Runner를 실행합니다. 자체 호스팅 설치는 자체 글로벌 프로브를 실행하는 것처럼 기본적으로 자체 글로벌 Runner를 하나 실행합니다. Docker Compose의 `global-runner` 서비스와 Helm 차트의 `globalRunner` 배포입니다. 이 Runner는 자체 키인 `GLOBAL_RUNNER_KEY`를 가지며, 서버의 다른 것은 아무것도 갖지 않습니다.

마스터 관리자는 다른 호스트에서 단계를 실행하도록 글로벌 Runner를 더 추가할 수 있습니다. Admin Dashboard에서 **설정**, 이어서 **글로벌 Runner**를 열고 하나를 만든 다음, 그 **ID 및 키 표시**가 알려 주는 명령을 인터넷에 접근할 수 있는 호스트에서 실행합니다.

함께 제공되는 글로벌 Runner를 끄려면 Docker Compose의 `config.env`에서 `GLOBAL_RUNNER_REPLICAS=0`을 설정하거나, Helm 차트에서 `globalRunner.enabled`를 `false`로 설정합니다. 글로벌 Runner가 전혀 없으면 **런북 → Runbook 에이전트**에는 내 Runner만 표시되며, 모든 JavaScript 단계는 그중 하나를 고릅니다.

## 시작하기 전에

- 자체 인프라에 있는 **Docker가 설치된 호스트**. HTTPS로 Status200 URL에 접근할 수 있고, 단계가 작업하는 시스템에도 접근할 수 있어야 합니다.
- **Runner를 만들 수 있는 역할.** Project Owner, Project Admin, Project Member, Runbook Admin이 만들 수 있습니다. 설정 명령에 포함된 Runner의 키는 Project Owner, Project Admin, Runbook Admin만 볼 수 있습니다.

## Runner 설치

### 1. 에이전트 레코드 만들기

**런북 → Runbook 에이전트**로 가서 새 에이전트를 만듭니다. **Runner 만들기**를 클릭하고 한 페이지에 있는 필드를 채웁니다.

| 필드 | 위치 | 참고 |
| --- | --- | --- |
| **이름** | 페이지에 | 알아보기 쉬운 이름으로, 보통 실행 위치와 접근 범위를 나타냅니다(예: `prod-eu-west-1`). 단계를 작성할 때 고르는 이름입니다. |
| **설명** | 페이지에 | 선택 사항. 이 호스트가 무엇에 접근할 수 있는지 한 문장으로. |
| **런북 실행** | **추가 필드** 아래 | 기본적으로 켜져 있습니다. 이 Runner가 Runbook 단계를 가져갈 수 있게 합니다. |
| **레이블** | **추가 필드** 아래 | 선택 사항. |

Runner는 기능 변경을 다음 하트비트 때 반영하므로 다시 시작할 필요가 없습니다. AI는 기능이 아닙니다. 코드 저장소가 코드의 설정에서 AI가 그 저장소에서 작업할 Runner를 선택하며, Runner 페이지에는 그 Runner를 선택한 저장소가 나열됩니다. AI 규칙은 Status200 AI가 명령을 실행할 Runner를 선택합니다. 거기서 Runner를 선택해야 Status200 AI의 명령이 그 Runner에서 실행됩니다.

### 2. 설치 명령 복사

Runner 행에서 **설정 지침 표시**를 클릭합니다. **런북 에이전트 설정** 대화 상자에 이 Runner의 ID와 키가 이미 들어 있는 `docker run` 명령이 표시됩니다. 같은 명령이 Runner 자체 페이지의 **설정 지침** 아래에도 있습니다.

키를 읽을 수 있는 사람은 Project Owner, Project Admin, Runbook Admin뿐입니다. 다른 모든 사람에게는 명령 대신 "이 런북 에이전트의 키를 볼 수 있는 권한이 없습니다"가 표시됩니다.

### 3. 자체 인프라의 호스트에서 실행

다음을 할 수 있는 환경의 호스트에서 명령을 실행합니다.

- HTTPS로 Status200 인스턴스에 접근할 수 있고,
- 단계에 필요한 일, 예를 들어 SSH로 다른 호스트에 접근하거나, 클러스터의 API 서버를 호출하거나, 데이터베이스와 통신할 수 있습니다.

```bash
docker run --name status200-runner --restart unless-stopped \
  -e STATUS200_RUNNER_ID=<runner-id> \
  -e STATUS200_RUNNER_KEY=<runner-key> \
  -e STATUS200_URL=https://status200.yourdomain.com \
  -d status200/runner:release
```

### 4. 에이전트가 연결되었는지 확인

**런북 → Runbook 에이전트**로 돌아갑니다. 컨테이너가 시작된 뒤 1분 안에 Runner의 **상태**가 **연결됨**으로 바뀌고 **마지막 확인**이 새로 고쳐져야 합니다. Runner 자체 페이지의 **런북 에이전트 상태** 카드에는 **런북 에이전트 버전**과 **호스트**가 표시됩니다. 계속 **연결된 적 없음** 또는 **연결 끊김**이 표시되면 [문제 해결](#문제-해결)을 참조하세요.

### 5. 에이전트 최신 상태 유지

에이전트가 Status200보다 오래된 버전으로 실행 중이면, 에이전트 페이지의 **런북 에이전트 버전** 옆에 경고 표시가 나타납니다. 그것을 선택하면 업그레이드 방법이 표시됩니다. 새 이미지를 받고 컨테이너를 제거한 다음 2단계의 설치 명령을 다시 실행합니다. Kubernetes 에이전트 차트가 설치한 에이전트는 차트로 업그레이드합니다.

```bash
docker pull status200/runner:release
docker rm -f status200-runner
```

## 단계에 Runner 지정

:::steps
### Runner에서 실행되는 단계 추가

Runbook의 **단계**에 JavaScript, Bash, SSH 또는 kubectl 단계를 추가합니다.

### Runner 선택

단계의 **추가 필드**를 여세요. 그 안의 **Runner** 드롭다운에는 프로젝트의 모든 Runner와 각 Runner의 연결 여부가 표시되며, 접힌 줄에는 단계가 실행되는 Runner가 표시됩니다. 프로젝트에 아직 Runner가 없으면 단계가 그렇게 알려 주고 **Runbooks › Runners**를 안내합니다. 첫 번째 선택지는 Runner를 고르지 않은 단계가 실행되는 곳입니다. **프로젝트 기본값** 표시가 붙은 [프로젝트 기본 Runner](#프로젝트-기본-runner)이거나, 그것이 없으면 JavaScript 단계의 경우 **글로벌 Runner**입니다.

### 단계 저장

**Save Steps**를 클릭합니다. 실행이 그 단계에 도달하면 Worker는 그 Runner의 ID로 작업을 큐에 넣고, 그 Runner만 작업을 가져갈 수 있습니다. **글로벌 Runner**로 지정된 단계는 비어 있는 글로벌 Runner를 기다립니다.
:::

Bash는 `bash -c`로 실행됩니다. JavaScript는 Runner의 `isolated-vm` 샌드박스에서 파일 시스템이나 프로세스에 접근하지 않고 실행됩니다. `axios`로 공개 HTTP API를 호출할 수 있지만 사설 네트워크의 주소는 호출할 수 없습니다. SSH와 kubectl 단계는 시크릿 [Runbook 변수](/docs/runbooks/variables)로 로그인하며, 그 변수의 **사용할 수 있는 Runner**에 단계의 Runner가 있어야 합니다.

Runner가 둘 이상 필요한가요? 각각 만들고 각 단계에 알맞은 Runner를 지정하세요. 이중화를 위해서는 Runner를 하나 더 실행해 단계를 나누거나, 단계가 다른 Runner를 가리키는 예비 Runbook을 마련하세요.

## 프로젝트 기본 Runner

프로젝트는 Twilio 설정 하나를 기본값으로 삼듯이, 내 Runner 중 하나를 기본값으로 삼을 수 있습니다. 그러면 Runner를 고르지 않은 모든 Runbook 단계가 그 Runner에서 실행됩니다. Bash, SSH, kubectl 단계는 물론, 기본값이 없으면 글로벌 Runner에서 실행되었을 JavaScript 단계도 마찬가지입니다.

Runner를 기본값으로 삼으려면 **런북 → Runbook 에이전트**로 가서 그 Runner 행의 **⋯** 메뉴를 열고 **프로젝트 기본값으로 설정**을 클릭한 뒤 **프로젝트 기본값으로 지정**으로 확인합니다. 또는 Runner를 열고 **프로젝트 기본값** 카드에서 **프로젝트 기본값으로 사용**을 켭니다. 목록은 기본 Runner의 이름 옆에 **프로젝트 기본값**을 표시하며, 단계의 **Runner** 드롭다운도 첫 번째 선택지에 같은 표시를 합니다. 프로젝트의 기본 Runner는 하나뿐이므로, 다른 Runner를 기본값으로 삼으면 기존 것이 바뀝니다. 같은 메뉴의 **프로젝트 기본값으로 사용 중지**나 Runner 페이지의 스위치로 사용을 멈출 수 있습니다.

Runner를 고르지 않은 단계가 실행되는 곳:

| 프로젝트 | JavaScript 단계 | Bash, SSH, kubectl 단계 |
| --- | --- | --- |
| 기본 Runner가 있음 | 기본 Runner | 기본 Runner |
| 기본 Runner가 없고 글로벌 Runner를 사용함 | 글로벌 Runner | 어디에서도 실행되지 않음: Runner가 없다고 알리며 실패 |
| 기본 Runner가 없고 글로벌 Runner를 사용하지 않음 | 어디에서도 실행되지 않음: 그 이유를 알리며 실패 | 어디에서도 실행되지 않음: Runner가 없다고 알리며 실패 |

Runner를 고른 단계는 기본값이 무엇이든 그 Runner를 유지하며, JavaScript 단계는 여전히 직접 **글로벌 Runner**를 고를 수 있습니다. 기본값을 설정해도 저장된 단계는 바뀌지 않습니다.

기본값이 될 수 있는 것은 Runbook 단계를 실행하는 프로젝트 자체의 Runner뿐입니다. 글로벌 Runner, Kubernetes 에이전트가 설치한 클러스터 내 Runner, **런북 실행**이 꺼진 Runner는 기본값이 될 수 없습니다. Runner가 기본값인 동안에는 그 **런북 실행**이 켜진 상태로 유지됩니다. 기본 Runner가 연결되어 있지 않으면 단계의 드롭다운이 그렇게 알려 주며, 그 Runner를 쓰는 단계는 다른 Runner와 마찬가지로 claim timeout이 지날 때까지 기다립니다.

기본값은 Runbook 단계만 사용합니다. 코드 저장소는 코드의 설정에서 자신의 Runner를 선택하고, AI 규칙도 자신의 Runner를 선택합니다.

## 운영 참고 사항

### 타임아웃

Runner에서 실행되는 모든 단계에는 두 가지 타임아웃이 적용됩니다.

| 타임아웃 | 기본값 | 제어하는 것 |
| --- | --- | --- |
| **가져가기 제한 시간** | 2분 | Worker가 선택한 Runner가 작업을 가져가기를 기다리는 시간입니다. Runner가 제시간에 가져가지 않으면 단계는 시간 초과로 실패하고, Runbook은 계속 진행합니다(**실패 시 계속**에 따라 멈출 수도 있습니다). |
| **실행 제한 시간** | 30초 | Runner가 단계를 멈추기 전까지 실행하게 두는 시간입니다. Bash에는 `SIGKILL`이 보내지고, JavaScript 샌드박스는 정리됩니다. |

둘 다 단계마다 설정할 수 있습니다. **Runbooks › 해당 Runbook › 단계**를 열고 단계를 펼친 다음, **추가 필드**에서 **실행 제한 시간**과 **가져가기 제한 시간**(초 단위)을 지정하세요. 기본값을 쓰려면 비워 둡니다. 각각 1초에서 1시간까지 받으며, 범위를 벗어난 값은 단계가 실행될 때 범위 안으로 조정됩니다.

Worker의 전체 대기 시간은 `claim timeout + execution timeout + a few seconds`입니다. 단계에 맞는 값을 고르세요.

claim timeout을 줄일 때 기억할 두 가지:

- Runner는 폴링 주기(`STATUS200_RUNNER_POLL_INTERVAL_MS`, 기본 5초)마다 작업을 요청합니다. 한 주기보다 짧은 claim timeout은 완전히 정상인 Runner가 작업을 보기도 전에 만료될 수 있으며, 그러면 단계는 오프라인 Runner와 같은 메시지로 실패합니다.
- Runner는 기본적으로 한 번에 작업 하나를 실행합니다(`STATUS200_RUNNER_CONCURRENCY`). 긴 단계가 Runner를 차지하는 동안, 같은 Runner를 가리키는 다른 단계는 각자의 claim timeout이 끝날 때까지 기다립니다. execution timeout을 몇 분으로 늘린다면, 그 Runner를 공유하는 단계의 claim timeout도 그만큼 늘리거나 다른 Runner를 주세요.

### 리스와 하트비트

```mermaid title="Status200과 Runner 사이의 작업"
sequenceDiagram
    participant O as Status200
    participant R as Runner
    O->>O: 이 Runner를 위한 작업을 큐에 넣음
    R->>O: 다음 작업 가져가기
    O-->>R: 30초 리스가 붙은 작업
    loop 실행되는 동안 10초마다
        R->>O: 리스 갱신
    end
    R->>O: 결과 보내기
    O->>O: 출력을 기록하고 실행 계속
```

Runner가 작업을 가져가면 짧은 리스(기본 30초)를 받습니다. 단계가 실행되는 동안 Runner는 10초마다 리스를 갱신합니다. 스크립트 도중 Runner가 죽거나 네트워크가 끊기면 리스가 만료되고, Worker는 영원히 기다리는 대신 작업을 `TimedOut`으로 표시합니다.

리스가 만료되어도 Bash 하위 프로세스는 자동으로 취소되지 **않지만**(JavaScript 샌드박스도 끝난다면 끝까지 실행됩니다), Worker는 더 이상 기다리지 않으며, 다른 쪽이 작업을 넘겨받은 뒤에는 Runner가 결과를 제출할 수 없습니다. 정확히 한 번만 실행되는 것이 중요하다면 다시 실행해도 안전하도록 스크립트를 설계하세요.

### 단계 도중 Status200 Worker가 다시 시작되면

Runbook 실행은 처음부터 끝까지 하나의 Worker에서 실행되므로, 배포나 충돌이 단계 도중에 실행을 중단시킬 수 있습니다. 그다음 일은 실행을 다시 가져가는지에 따라 달라집니다.

- **재개되는 경우.** 실행을 가져간 Worker는 단계가 이미 만든 작업을 찾아 **그 작업에 다시 연결합니다**. Runner에 스크립트 사본을 하나 더 보내는 대신 그 작업을 기다립니다. Runner가 이미 끝냈다면 기록된 결과를 그대로 씁니다. 단계는 실행 한 번당 Runner에 최대 한 번만 보내집니다.
- **재개되지 않는 경우.** 실행을 다시 가져가지 않으면, 현재 단계의 claim 및 실행 시간 창을 지났을 때 정리 작업이 단계 이름을 담은 메시지와 함께 실행을 `Failed`로 표시합니다. 실행이 `Running`에 멈춰 있는 일은 없습니다.

알 수 없는 것은 Worker가 사라지기 전에 스크립트가 어디까지 진행했는지뿐입니다. 실행 도중이던 단계는 일부만 실행되었을 수 있다는 메모와 함께 실패로 보고됩니다. Runbook을 다시 실행하기 전에 대상 시스템을 확인하세요.

### 온라인 에이전트가 없음

단계가 실행될 때 선택한 Runner가 오프라인이면, 작업은 claim timeout이 지날 때까지 `Pending`으로 기다린 뒤 "No runbook agent picked up this step before the wait window expired."와 함께 단계가 실패합니다. 실제로 Runbook을 실행하기 전에 **Runbook 에이전트** 페이지에서 커버리지를 확인하세요. 글로벌 Runner의 단계는 대신 "No global Runner picked up this step before the wait window expired."와 함께 실패합니다.

### 출력 한도

stdout과 stderr를 합쳐 단계당 **50 KB**로 제한됩니다. 더 긴 출력은 표시와 함께 잘립니다. 전체 로그가 필요하면 스크립트에서 로그 저장소나 객체 저장소에 쓰고 `echo`로 URL을 출력하세요.

### 취소

실행 페이지나 API에서 Runbook 실행을 취소하면 `Pending`, `Claimed`, `Running` 상태의 모든 작업이 즉시 `Cancelled`로 표시됩니다. 이미 스크립트를 실행 중인 Runner는 작업을 끝까지 하지만, 서버는 결과를 받아들이지 않으며 Runbook의 이후 단계는 보내지지 않습니다.

### 동시 실행

각 Runner는 기본적으로 한 번에 작업 하나를 실행합니다. 더 허용하려면 컨테이너에 `STATUS200_RUNNER_CONCURRENCY`를 설정하되, Runner가 그 호스트에서 실행되는 다른 모든 것과 호스트를 공유한다는 점을 기억하세요.

## 환경 변수

Runner는 시작할 때 다음을 읽습니다.

| 변수 | 필수 | 기본값 | 참고 |
| --- | --- | --- | --- |
| `STATUS200_URL` | 예 | — | Status200 인스턴스의 기본 URL입니다(예: `https://status200.yourdomain.com`). |
| `STATUS200_RUNNER_ID` | 예 | — | 설정 명령에 들어 있는 Runner의 ID입니다. |
| `STATUS200_RUNNER_KEY` | 예 | — | 설정 명령에 들어 있는 Runner의 비밀 키입니다. |
| `STATUS200_RUNNER_POLL_INTERVAL_MS` | 아니요 | `5000` | Runner가 새 작업을 요청하는 간격입니다. `1000` 미만의 값은 기본값으로 돌아갑니다. |
| `STATUS200_RUNNER_HEARTBEAT_INTERVAL_MS` | 아니요 | `60000` | Runner가 살아 있다고 보고하는 간격입니다. `5000` 미만의 값은 기본값으로 돌아갑니다. |
| `STATUS200_RUNNER_JOB_HEARTBEAT_INTERVAL_MS` | 아니요 | `10000` | Runner가 실행 중인 작업의 리스를 갱신하는 간격입니다. `1000` 미만의 값은 기본값으로 돌아갑니다. |
| `STATUS200_RUNNER_CONCURRENCY` | 아니요 | `1` | 이 Runner에서 동시에 실행할 수 있는 최대 작업 수입니다. |
| `STATUS200_RUNNER_ENABLE_RUNBOOKS` | 아니요 | — | `false`로 설정하면 대시보드 설정과 관계없이 이 Runner가 Runbook 단계를 가져가지 않습니다. 이 변수는 기능을 끄기만 할 수 있습니다. |
| `STATUS200_RUNNER_ENABLE_CODE_FIXES` | 아니요 | — | `false`로 설정하면 이 Runner를 선택한 코드 저장소에 대해서도 이 Runner가 AI 코드 수정을 가져가지 않습니다. |
| `STATUS200_RUNNER_ENABLE_AI_COMMANDS` | 아니요 | — | `false`로 설정하면 대시보드 설정과 관계없이 이 Runner가 AI 해결 명령을 실행하지 않습니다. |

## 에이전트 키 교체

키가 유출되면 재설정하세요. 이전 키는 즉시 작동하지 않습니다.

:::steps
### 키 재설정

**런북 → Runbook 에이전트**에서 Runner를 열고 **런북 에이전트 키 재설정**을 클릭한 다음 확인합니다. Runner는 새 키를 받을 때까지 연결하지 못합니다.

### 새 키로 컨테이너 실행

Runner의 **설정 지침**에서 새 명령을 복사하고, 이전 컨테이너를 제거한 다음 같은 호스트에서 새 명령을 실행합니다.

```bash
docker rm -f status200-runner
```

### 다시 연결되는지 확인

**런북 → Runbook 에이전트**에서 Runner의 **상태**가 1분 안에 **연결됨**으로 돌아옵니다.
:::

## 권한

에이전트 관리는 기존 Runbooks 권한 그룹에 있습니다.

- `CreateRunner`, `EditRunner`, `DeleteRunner`, `ReadRunner` — 에이전트 레코드를 관리합니다.
- `RunbookAdmin`, `RunbookMember`, `RunbookViewer`(역할) — `RunbookAdmin`은 Runbook, 그 규칙, 그것이 실행되는 Runner를 구성하고 Runbook을 실행합니다. `RunbookMember`는 Runbook과 그 실행을 열고 Runbook을 실행합니다(실행을 시작하고, 단계를 완료하거나 건너뛰고, 실행을 취소합니다). 하지만 Runbook이나 Runner를 만들거나 바꾸거나 삭제하지는 않습니다. `RunbookViewer`는 Runbook과 그 실행을 읽기만 하고 아무것도 실행하지 않습니다. `RunbookAdmin`은 위의 세분화된 권한을 모두 묶은 것입니다.
- 글로벌 Runner는 어떤 프로젝트에도 속하지 않습니다. 글로벌 Runner를 추가, 변경, 삭제하는 것은 Admin Dashboard에서 마스터 관리자만 할 수 있습니다. 프로젝트의 Runner를 읽을 수 있는 사람은 모두 글로벌 Runner를 볼 수 있으며, **글로벌 Runner 사용**을 끄려면 프로젝트를 편집할 권한이 필요합니다.

Runbook을 트리거하려면(즉 단계를 Runner에 보내려면) Runbook을 실행하는 역할(`ProjectOwner`, `ProjectAdmin`, `ProjectMember`, `RunbookAdmin`, `RunbookMember`)이나 `CreateRunbookExecution`이 필요합니다. 실행을 완료, 건너뛰기, 취소할 때는 `EditRunbookExecution`도 받아들입니다. 역할은 그 범위가 미치는 Runbook만 실행합니다.

Runner의 키는 Project Owner, Project Admin, Runbook Admin만 읽을 수 있습니다.

## 에이전트용 API

참고로, Runner는 `/runner-ingest` 아래에 마운트된 다음 엔드포인트를 사용합니다. 통합 이전 경로인 `/runbook-agent-ingest`도 아직 다시 배포되지 않은 에이전트를 위해 계속 제공되므로, 서버를 업그레이드해도 에이전트가 깨지지 않습니다. 인증은 JSON 본문(`agentId`와 `agentKey`) 또는 헤더 `x-agent-id`와 `x-agent-key`에 담은 Runner의 ID와 키로 합니다.

| 엔드포인트 | 용도 |
| --- | --- |
| `POST /heartbeat` | 생존 신호입니다. Runner의 마지막 확인 시각, 버전, 호스트 정보를 갱신하고 프로젝트가 부여한 기능을 돌려줍니다. |
| `POST /claim-next-job` | 이 Runner의 ID를 대상으로 하는 `Pending` 작업 중 가장 오래된 것을 원자적으로 가져갑니다. 할 일이 없으면 `{ job: null }`을 돌려줍니다. 글로벌 Runner는 대신 아무 글로벌 Runner나 기다리는 가장 오래된 단계를 가져갑니다. |
| `POST /job/:jobId/heartbeat` | 작업의 리스를 갱신합니다. 리스가 만료되었거나 작업이 끝났으면 404를 돌려줍니다. |
| `POST /job/:jobId/result` | 최종 결과를 제출합니다. 리스가 이미 넘어갔으면 무시됩니다. |
| `POST /disconnect` | 정상 종료 시 연결을 끊습니다. |

직접 호출할 필요는 없습니다. 함께 제공되는 Runner가 호출합니다. 여기에 문서화한 것은, 우리 에이전트가 맞지 않는 제약이 있을 때 직접 에이전트를 만들 수 있도록 하기 위해서입니다.

## 문제 해결

:::details Runner가 계속 연결된 적 없음 또는 연결 끊김으로 표시됨
- `docker logs status200-runner`로 컨테이너 로그에서 인증 또는 네트워크 오류를 확인합니다.
- 예를 들어 `curl`로 호스트가 Status200 URL에 접근할 수 있는지 확인합니다.
- ID와 키가 공백 없이 복사되었는지, `STATUS200_URL`이 Status200을 여는 주소와 같은지 확인합니다.

**연결된 적 없음**은 Runner가 한 번도 보고하지 않았다는 뜻입니다. **연결 끊김**은 보고한 적은 있지만 최근 5분 안에는 보고하지 않았다는 뜻입니다.
:::

:::details 단계가 "No runbook agent picked up this step before the wait window expired."로 실패함
단계의 Runner가 claim timeout 안에 작업을 가져가지 않았습니다. Runner가 **연결됨**인지, 그 Runner에서 **런북 실행**이 켜져 있는지, 긴 단계에 묶여 있지 않은지 확인하세요. `STATUS200_RUNNER_CONCURRENCY`를 늘리지 않는 한 Runner는 한 번에 작업 하나를 실행합니다. 폴링 간격보다 짧은 claim timeout도 같은 방식으로 실패합니다.
:::

:::details 단계가 "The runbook agent stopped responding while this step was running."으로 실패함
Runner가 작업을 가져간 뒤 리스 갱신을 멈췄습니다. 충돌했거나, 다시 시작했거나, 네트워크가 끊겼습니다. Runner가 온라인인지 확인한 다음, Runbook을 다시 실행하기 전에 대상 시스템을 확인하세요.
:::

:::details Runner 로그에 "No capability is enabled"가 나옴
이 Runner의 모든 기능이 꺼져 있습니다. Status200의 Runner 페이지에서 **런북 실행**을 켜세요. Runner는 다음 하트비트 때 변경을 반영합니다.
:::

:::details Runner가 시크릿 변수를 사용할 수 없어 단계가 실패하는 경우
단계가 그 Runner의 사용을 허용하지 않는 시크릿 변수를 지정했기 때문에 Runner에 전달되지 않았고 단계는 실행되지 않았습니다. 오류에는 시크릿과 바꿔야 할 점이 나옵니다. **런북 → 설정 → 전역 변수** 또는 Runbook의 **설정 → 변수**에서 **사용할 수 있는 Runner**에 그 Runner를 추가하거나, 글로벌 Runner의 단계라면 **글로벌 Runner가 사용할 수 있음**을 켜세요. 또는 시크릿이 허용하는 Runner를 고르세요.
:::

:::details Runner가 너무 오래되어 kubectl 단계가 실패하는 경우
단계가 "This kubectl step runs a script, and this Runner is too old to run one, so it was not run."로 실패했습니다. kubectl 스크립트보다 오래된 Runner는 스크립트를 실행할 수 없습니다. 업그레이드할 때 kubectl 스크립트로 옮겨진 Kubernetes 단계는 스크립트나 kubeconfig가 바뀌기 전까지 작성될 때의 동작으로 그 Runner에서 계속 실행됩니다. [에이전트를 최신 상태로 유지](#5-에이전트-최신-상태-유지)에 나온 대로 Runner를 업데이트한 다음 Runbook을 다시 실행하세요.
:::

## 다음 단계

:::cards
- [Runbook 작성](/docs/runbooks/authoring): Runner에서 실행되는 단계를 작성합니다.
- [Runbook 변수](/docs/runbooks/variables): 단계가 사용하는 값과 시크릿, 그리고 그것을 받는 Runner.
- [Runbook 설정과 안전성](/docs/runbooks/configuration): 한도, 권한, 보안 강화.
:::
