SSD Nodes Learn
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-07-24

Claude API로 Ubuntu VPS에서 로그 분석 앱 만들기

Ubuntu 24.04 VPS에서 Python을 사용하여 Claude API 기반 로그 분석 도구를 구축합니다. API key 보안 관리부터 streaming 응답 처리, 비용 제어 로직, systemd 설정까지 실무에 필요한 핵심 기술을 다룹니다.

구축 목표

새로 설치한 Ubuntu 24.04 VPS에서 실행되는 command-line tool을 구축합니다. 에러 메시지나 로그 일부를 입력하면 journalctl -u nginx -n 50 | explain 형태의 평문 진단 결과를 반환합니다. 코드는 Python으로 약 60줄 내외이며, 실제 Claude API 애플리케이션에 필요한 모든 요소를 포함합니다. 여기에는 적절하게 저장된 key, virtualenv, SDK의 response shapes, streaming, typed exception chain, 그리고 백그라운드 실행을 위한 systemd unit이 포함됩니다.

이 프로젝트를 선정한 이유가 있습니다. 대부분의 "첫 API 앱" 튜토리얼은 다시 사용하지 않을 chatbot을 만드는 데 집중합니다. 반면 로그 분석 도구는 구축 직후부터 서버에서 유용하게 사용됩니다. 또한 초보자가 흔히 실수하는 두 가지 요소인 response object의 올바른 처리와 비용 제어를 학습할 수 있습니다. API 비용은 설정한 한도 외에는 제한이 없으므로, 비용 제어는 사후 고려 사항이 아닌 설계 단계부터 고려해야 할 필수 요소입니다. 이는 이 VPS의 tmux에서 Claude Code를 실행할 때도 동일하게 중요한 원칙입니다.

Console에서 API key 가져오기

API 액세스는 platform.claude.com의 Anthropic Console에서 관리합니다. 회원가입 후 Settings → API Keys에서 키를 생성하십시오 (문서 링크는 platform.claude.com/settings/keys로 바로 연결됩니다). 키는 한 번만 표시되며 sk-ant-로 시작합니다. 키를 다시 확인할 수 없으므로 즉시 복사하거나, 삭제 후 재발급해야 합니다.

비용 관련 안내: 2026년 7월 기준으로 API를 위한 상시 무료 티어는 존재하지 않습니다. Anthropic의 가격 책정 문서에 따르면, 신규 사용자는 테스트를 위한 소량의 무료 크레딧을 받습니다. 정확한 금액은 가입 시 Console에 표시되는 금액을 따릅니다. 크레딧을 모두 사용하면 요청을 성공시키기 위해 계정에 자금을 충전해야 합니다. 이는 claude.ai 구독과는 별개입니다. Pro 또는 Max 플랜에는 API 크레딧이 포함되지 않으며, API key를 사용한다고 해서 채팅 앱을 사용할 수 있는 것도 아닙니다. 구독과 API 중 무엇을 선택할지 고민 중이라면, 다음 주제를 참조하십시오: 실제로 필요한 Claude 플랜.

하나의 프로젝트 또는 서버로 범위를 제한하여 키를 생성하십시오. 키가 유출될 경우(장기적으로 유출 사고는 발생할 수 있습니다), 다른 서비스에 영향을 주지 않고 해당 키만 무효화할 수 있어야 합니다.

.bashrc에 키를 저장하지 마십시오

~/.bashrc에서 export ANTHROPIC_API_KEY=sk-ant-...을(를) 사용하는 방식은 권장하지 않습니다. 여기에는 세 가지 문제가 있습니다.

  • 모든 프로세스가 이를 상속받습니다. 로그인 셸에서 export된 환경 변수는 실행되는 모든 프로세스로 전파됩니다. 웹 애플리케이션, 환경 변수를 버그 리포트에 포함하는 crash reporter, 누군가 활성화해 둔 phpinfo() 페이지 등이 해당됩니다. 키의 노출 범위가 "해당 사용자가 실행하는 모든 것"으로 확대됩니다.
  • 입력 시 ~/.bash_history에 기록됩니다. export 명령어를 직접 입력하면 키가 평문 파일에 영구적으로 저장되며, 홈 디렉터리의 모든 백업본으로 동기화됩니다.
  • systemd가 필요할 때 키를 찾을 수 없습니다. 서비스는 .bashrc을(를) 읽지 않습니다. 따라서 스크립트를 unit으로 전환하는 순간, 보통 새벽 6시에 원인 모를 401 오류가 발생하며 패턴이 실패합니다.

서버에서의 올바른 패턴은 600 권한을 가진 전용 환경 파일을 생성하고, 필요한 프로세스만 이를 로드하도록 하는 것입니다.

sudo mkdir -p /opt/explain
sudo install -m 600 -o root -g root /dev/null /etc/claude-explain.env
printf 'ANTHROPIC_API_KEY=sk-ant-YOUR-KEY-HERE\n' | sudo tee /etc/claude-explain.env >/dev/null

에디터의 swap 파일에 키가 남는 것을 방지하려면 에디터 대신 printf를 사용하여 tee을(를) 생성하십시오. 어떤 방식을 사용하든 ls -l /etc/claude-explain.env을(를) 통해 -rw-------을(를) 읽을 수 있는지, 소유자가 root인지 확인해야 합니다. 대화형 셸은 래퍼(아래 참조)를 통해 실행 시마다 키를 전달받으며, systemd는 EnvironmentFile=을(를) 통해 키를 전달받습니다. root가 권한을 낮추기 전에 파일을 읽으므로, 서비스 사용자는 해당 파일에 대한 읽기 권한이 필요하지 않습니다. 키는 코드, git, ps 출력 또는 셸 히스토리에 절대 나타나지 않습니다.

venv에 SDK 설치하기

Ubuntu 24.04는 PEP 668이 적용된 Python 3.12를 제공합니다. 따라서 시스템 인터프리터에 직접 pip install anthropic를 실행하면 error: externally-managed-environment 오류가 발생합니다. 이 오류는 운영 체제가 의도된 대로 작동하고 있음을 의미합니다. 대신 virtualenv를 사용하십시오.

sudo apt update && sudo apt install -y python3-venv
sudo python3 -m venv /opt/explain/venv
sudo /opt/explain/venv/bin/pip install anthropic

서버에서는 별도의 activation 과정이 필요하지 않습니다. /opt/explain/venv/bin/python를 직접 호출하면 항상 venv의 패키지를 사용합니다.

첫 번째 호출 및 응답을 올바르게 읽는 방법

import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from the environment

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1000,
    messages=[{"role": "user", "content": "Explain what a systemd unit file is in three sentences."}],
)

for block in response.content:
    if block.type == "text":
        print(block.text)

이 12줄의 코드에는 API의 핵심 개념 두 가지가 포함되어 있습니다. 첫째, 인자 없이 사용하는 anthropic.Anthropic()은 환경 변수에서 키를 읽어옵니다. 키를 문자열 리터럴로 직접 전달하지 마십시오. 둘째, response.content은 문자열이 아니라 content blocks의 list입니다. 이를 직접 출력하면 다음과 같은 전형적인 초보자의 출력 결과가 나타납니다.

[TextBlock(citations=None, text='A systemd unit file is...', type='text')]

이는 버그가 아니라 해당 객체의 repr입니다. 응답에는 여러 블록 유형(text, tool calls, thinking)이 포함될 수 있습니다. 따라서 .text을 처리하기 전에 루프를 통해 block.type == "text"을 확인해야 합니다. 처음부터 이 루프 구조를 적용하면 "쓰레기 값이 출력된다"는 혼란을 방지할 수 있습니다.

정확한 model ID인 claude-opus-4-8을 사용하십시오. 최신 세대 ID에는 날짜가 포함되지 않습니다. 날짜 접미사를 붙여야 한다는 습관(또는 오래된 블로그 게시물)을 따르지 마십시오. 날짜를 붙이면 404 오류가 발생하며, 이에 대한 내용은 아래에서 다룹니다.

실제 도구: 설명

다음은 전체 프로그램입니다. stdin을 입력받고, 진단 결과는 스트림으로 출력하며, 오류를 처리합니다:

#!/usr/bin/env python3
"""explain: pipe an error or log excerpt in, get a diagnosis out."""
import sys
import anthropic

MODEL = "claude-opus-4-8"

def main() -> int:
    text = sys.stdin.read().strip()
    if not text:
        print("usage: journalctl -u nginx -n 50 | explain", file=sys.stderr)
        return 1

    client = anthropic.Anthropic()
    try:
        with client.messages.stream(
            model=MODEL,
            max_tokens=1500,
            system=(
                "You are a senior Linux sysadmin. The user pipes you server "
                "logs or error output. Name the most likely cause outright, "
                "then give the commands to confirm and fix it. Be terse."
            ),
            messages=[{"role": "user", "content": text}],
        ) as stream:
            for chunk in stream.text_stream:
                print(chunk, end="", flush=True)
        print()
    except anthropic.RateLimitError as e:
        retry_after = e.response.headers.get("retry-after", "60")
        print(f"rate limited; retry in {retry_after}s", file=sys.stderr)
        return 2
    except anthropic.APIStatusError as e:
        print(f"API error {e.status_code}: {e.message}", file=sys.stderr)
        return 2
    except anthropic.APIConnectionError:
        print("network error reaching the API", file=sys.stderr)
        return 2
    return 0

if __name__ == "__main__":
    sys.exit(main())

이 파일을 /opt/explain/explain.py로 저장하십시오. 그 다음, 대화형 사용을 위해 키를 로드하는 wrapper를 추가하십시오:

sudo tee /usr/local/bin/explain >/dev/null <<'EOF'
#!/bin/sh
set -a; . /etc/claude-explain.env; set +a
exec /opt/explain/venv/bin/python /opt/explain/explain.py "$@"
EOF
sudo chmod 755 /usr/local/bin/explain

(wrapper는 sudo를 통해 실행되어야 합니다. 또는 env 파일의 그룹이 관리자 사용자 그룹에 포함되어야 합니다. 파일 권한을 644로 완화하는 대신 이 중 하나를 선택하십시오.)

스트리밍을 사용하는 이유. client.messages.stream은 전체 생성이 완료될 때까지 대기하지 않고 토큰이 도착하는 즉시 출력합니다. 또한 긴 출력 시 발생하는 HTTP timeout 문제를 방지합니다. SDK는 바로 이 이유 때문에 non-streaming 호출 시 매우 큰 max_tokens 값을 거부합니다. 생성된 객체가 나중에 필요한 경우, with 블록 내부에서 stream.get_final_message()를 호출하십시오.

예외 순서가 이와 같은 이유. SDK는 가장 구체적인 예외를 먼저 발생시킵니다: RateLimitError은 429 오류이며, 대기 시간을 알려주는 retry-after 헤더를 포함합니다; APIStatusError은 기타 non-2xx 응답을 처리합니다 (서버 측 문제는 e.status_code >= 500를 확인하십시오); APIConnectionError은 요청에 대한 응답이 아예 없음을 의미합니다. 재시도 루프를 만들기 전에 주의하십시오: SDK는 이미 429 및 5xx 오류에 대해 자체적으로 재시도를 수행합니다. 기본적으로 exponential backoff를 사용하여 두 번 재시도합니다 (클라이언트 측 max_retries). except이 실행될 시점에는 이미 재시도가 모두 소진된 상태입니다. 따라서 CLI에서는 대기하며 계속 요청을 보내는 대신, 오류를 보고하고 종료하는 것이 올바른 방법입니다.

Cost control

API에는 설정된 값 외에 내장된 월간 한도가 없습니다. 실수가 발생하면 비용이 조용히 누적되므로 별도의 섹션으로 다룹니다.

max_tokens은 호출당 지출 상한선입니다. Output token은 비용이 많이 발생합니다. Opus 4.8의 경우 input 가격의 5배입니다. max_tokens은 모델이 생성할 수 있는 최대 토큰 수에 대한 하드 캡(hard cap)입니다. 잘못된 프롬프트로 인해 허용된 출력량 이상의 비용이 발생하지 않습니다. 작업에 맞춰 크기를 설정하십시오. 로그 진단에는 1,500이면 충분하며, 분류 작업에는 100이 필요합니다. stop_reason: "max_tokens"과 함께 응답이 문장 중간에 끊긴다면 설정값이 너무 낮은 것입니다. 처음부터 크게 설정하기보다 필요에 따라 의도적으로 높이십시오.

전송 전에 개수를 확인하십시오. Input도 비용이 발생하며, 로그는 용량이 큽니다. API는 무료로 사용할 수 있는 counting endpoint를 제공합니다(메시지 생성과는 별개의 rate limit이 적용됩니다).

count = client.messages.count_tokens(
    model="claude-opus-4-8",
    messages=[{"role": "user", "content": big_log_text}],
)
print(count.input_tokens)

이 기능을 사용하여 2 GB 로그를 실수로 도구에 전달하는 것을 방지하십시오. 이 용도로 tiktoken을 사용하지 마십시오. 이는 OpenAI의 tokenizer이며, 일반 텍스트에서 Claude 토큰을 약 15–20% 적게 계산하고, 코드에서는 그보다 더 적게 계산합니다.

충성도가 아닌 작업에 따라 모델을 선택하십시오. 2026년 7월 기준, Opus 4.8 (claude-opus-4-8)은 input 100만 토큰당 $5, output 100만 토큰당 $25입니다. Haiku 4.5 (claude-haiku-4-5)은 200K context 기준 $1/$5입니다. Sonnet 5 (claude-sonnet-5)은 $3/$15이며, 2026년 8월 31일까지 $2/$10의 도입 가격이 적용됩니다. 구체적인 예로, 2,000-token 로그 발췌본과 500-token 답변은 Opus에서 약 $0.0225, Haiku에서 $0.0045가 소요됩니다. 출력 품질을 판단할 때는 Opus로 시작한 다음, 동일한 프롬프트를 Haiku에서 테스트하십시오. 대량의 단순 변환 작업에서는 Haiku가 5분의 1 가격으로 거의 동일한 결과를 제공합니다. 예산을 확정하기 전에 pricing page에서 현재 수치를 확인하십시오.

대기 가능한 작업은 Batches를 사용하십시오. Batches API는 표준 가격의 50%로 요청을 비동기 처리하며, 대부분의 배치 작업은 1시간 이내에 완료됩니다. 야간 요약, 데이터 백필, 대량 분류 등 사람이 기다릴 필요가 없는 모든 작업에 적합합니다.

반복되는 context에는 Prompt caching을 사용하십시오. 모든 호출에서 동일한 대형 system prompt나 runbook을 다시 전송하는 경우, 캐시 가능하도록 설정하십시오.

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1000,
    system=[{
        "type": "text",
        "text": RUNBOOK_TEXT,  # the same 30K tokens on every call
        "cache_control": {"type": "ephemeral"},
    }],
    messages=[{"role": "user", "content": question}],
)
print(response.usage.cache_read_input_tokens)  # non-zero from the second call on

캐시 쓰기(cache writes) 비용은 input 가격의 약 1.25배이며, 캐시 읽기(cache reads)는 5분 TTL 기준 약 0.1배입니다. 따라서 5분 이내의 두 번째 호출은 이미 첫 번째 호출 비용을 지불한 셈입니다. 주의사항이 두 가지 있습니다. 캐시된 prefix는 모델별 최소 기준(Opus의 경우 수천 토큰)을 충족해야 합니다. 짧은 system prompt는 캐시되지 않을 수 있습니다. 또한, 동일한 호출에서 cache_read_input_tokens이 계속 0이라면, prefix의 일부가 매 요청마다 변경되고 있는 것입니다(일반적으로 timestamp가 원인입니다).

Input에 포함되는 항목을 숙지하십시오. System prompts, tool definitions, 그리고 멀티턴 대화에서의 전체 대화 이력은 모두 input token으로 청구됩니다. 이력을 삭제하지 않는 채팅 루프는 비용이 기하급수적으로 증가합니다. 대화형 서비스를 구축하기 전에 Claude 토큰 사용량 및 과금 방식을 이해하는 것이 중요합니다.

systemd에서 실행하기

environment-file 방식을 사용하면 매일 아침 어제의 오류를 요약하여 알려주는 timer를 사용할 수 있습니다.

# /etc/systemd/system/log-digest.service
[Unit]
Description=Daily error-log digest via the Claude API

[Service]
Type=oneshot
User=explain
Group=systemd-journal
EnvironmentFile=/etc/claude-explain.env
ExecStart=/bin/sh -c 'journalctl -p err --since yesterday | /opt/explain/venv/bin/python /opt/explain/explain.py >> /var/log/log-digest.txt'
# /etc/systemd/system/log-digest.timer
[Unit]
Description=Run the log digest every morning

[Timer]
OnCalendar=06:15
Persistent=true

[Install]
WantedBy=timers.target
sudo useradd -r -s /usr/sbin/nologin explain
sudo touch /var/log/log-digest.txt && sudo chown explain /var/log/log-digest.txt
sudo systemctl daemon-reload
sudo systemctl enable --now log-digest.timer
sudo systemctl start log-digest.service   # test it once, right now

EnvironmentFile=의 이점을 확인하십시오. systemd는 권한이 없는 explain 사용자로 전환하기 전에 root 소유의 mode-600 파일을 읽습니다. 따라서 프로세스는 변수를 가져오지만, 사용자는 키 파일을 읽을 수 없습니다. systemd-journal 그룹은 로그 접근 권한을 부여합니다. 수동으로 systemctl start를 실행하여 journalctl -u log-digest.service를 확인하십시오. 오타를 확인하기 위해 06:15까지 기다리지 마십시오. 이 패턴이 shell pipeline보다 복잡해지면, 동일한 환경에서 Claude 기반 n8n workflow에도 동일한 env-file 방식을 적용할 수 있습니다.

Failure modes, with the strings you will see

401 on a working key. 예외 메시지는 다음과 같습니다:

anthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'invalid x-api-key'}, 'request_id': 'req_011CSHoEeqs5C35K2UUqR7Fy'}

Shell에서는 키가 작동하지만 서비스에서 401 오류가 발생한다면, 서비스가 키를 전달받지 못한 것입니다. systemd는 .bashrc을 읽지 않습니다. EnvironmentFile=가 올바른 경로를 가리키는지 확인하십시오. 다른 원인으로는 env 파일에 따옴표를 붙여 붙여넣은 경우(ANTHROPIC_API_KEY="sk-ant-..." — systemd는 따옴표를 제거하지만, shell wrapper의 . file는 따옴표를 포함할 수 있음), 끝에 공백이 있는 경우, 또는 지난주에 Console에서 취소한 키를 사용하는 경우가 있습니다.

404 from a model typo. 가장 흔한 사례는 현재 모델 ID 뒤에 날짜 접미사를 붙이는 것입니다:

anthropic.NotFoundError: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'model: claude-opus-4-8-20260115'}, 'request_id': 'req_011CSJqymAvNw4bT3qmDdMbA'}

현재 세대 ID는 작성된 그대로 정확해야 합니다 — claude-opus-4-8, claude-haiku-4-5, claude-sonnet-5. 모델 문서에서 직접 복사하여 사용하십시오. 기억에 의존하거나 오래된 튜토리얼을 참조하지 마십시오.

429 rate_limit_error. 오류 유형 문자열은 rate_limit_error이며, 응답에는 대기해야 할 초 단위 시간이 포함된 retry-after 헤더가 포함됩니다. SDK는 예외가 발생하기 전에 이미 backoff를 적용하여 두 번 재시도했습니다. 지속적인 429 오류는 현재 요청 속도가 할당된 tier를 초과했음을 의미합니다. 작업을 배치(batch)로 처리하거나 간격을 넓히십시오. 재시도 루프를 좁히지 마십시오.

It prints the object, not the text. 출력 결과가 [TextBlock(citations=None, text='...', type='text')]과 같이 나타납니다. 블록을 반복하여 block.type == "text"인 항목에서 .text를 읽는 대신 response.content을 출력했습니다. 위의 모든 SDK 예제는 이를 올바르게 수행합니다. 해당 루프를 복사하여 사용하십시오.

error: externally-managed-environment. Ubuntu 24.04의 system Python에 대해 pip install을 실행했습니다. venv를 사용하십시오. 중요한 서버에서 --break-system-packages을 실행하지 마십시오.

Truncated answers. response.stop_reason == "max_tokens"은 모델이 생성 도중 출력 제한(output cap)에 도달했음을 의미합니다. 이는 의도된 동작입니다. 제한 값을 의도적으로 높이십시오.

첫 번째 앱이 작동하면, building an AI agent with Claude를 통해 동일한 API 호출을 도구를 사용하는 agent로 전환할 수 있습니다.

FAQ

Claude API의 테스트 비용은 얼마인가요?

이러한 도구의 비용은 매우 저렴합니다. 2026년 7월 기준, Opus 4.8의 비용은 input token 100만 개당 $5, output 100만 개당 $25입니다. 일반적인 로그 진단(input 수천 개, output 수백 개)은 약 2센트이며, Haiku 4.5($1/$5)를 사용하면 0.5센트 미만입니다. 한 달간 매일 요약을 생성해도 커피 한 잔 값보다 적게 듭니다. 위험 요소는 호출당 가격이 아니라 무한 루프와 무제한 max_tokens입니다. 이 가이드에서 두 항목을 명시적으로 설정하는 이유입니다.

Claude API의 무료 티어가 있나요?

2026년 7월 기준으로 지속적인 무료 티어는 없습니다. Anthropic의 가격 문서에 따르면, 신규 사용자는 API 테스트를 위해 소량의 무료 크레딧을 받습니다. 이는 가입 시 Console에 표시되는 금액만큼 제공되는 일회성 체험이며, 이후에는 계정에 비용을 충전해야 합니다. 최첨단 품질보다 요청당 한계 비용이 0인 것이 목표라면, Ollama로 open-weight 모델을 self-host하여 토큰 대신 RAM을 사용하는 방법이 있습니다.

서버에서 API key를 안전하게 관리하는 방법은 무엇인가요?

코드에 포함하지 마십시오. git에 포함하지 마십시오. .bashrc에서 내보내지 마십시오. 히스토리에 남는 쉘에 직접 입력하지 마십시오. 600 권한을 가진 root 소유 파일에 저장하십시오. 대화형 사용을 위한 wrapper script나 systemd를 위한 EnvironmentFile=를 사용하여 프로세스별로 로드하십시오. 유출된 키를 즉시 무효화할 수 있도록 서버나 프로젝트당 하나의 키만 할당하십시오. 키가 paste site나 git commit에 노출되었다면 즉시 Console에서 무효화하십시오. 커밋을 삭제한다고 해서 유출이 해결되지는 않습니다.

어떤 Claude 모델로 시작해야 하나요?

출력 결과가 충분히 좋은지 평가하는 단계라면 claude-opus-4-8로 시작하십시오. 아이디어를 최고 품질로 검증해야 하며, 개인적인 사용 규모에서는 비용 차이가 몇 센트에 불과합니다. 프롬프트가 확정되면 실제 입력값을 claude-haiku-4-5에서 다시 실행하십시오. 요약, 분류, 로그 분류 작업의 경우 claude-haiku-4-5은 가격이 5분의 1 수준이면서 성능은 대등한 경우가 많습니다. 기본 설정이 아닌 측정 결과에 따라 Haiku 또는 Sonnet으로 전환하십시오.