홈랩에서 각종 태스크 자동화하기
들어가며
요즘 AI에 꽤 많은 돈과 시간을 들이고 있습니다.
그러다 보니 자연스레 n8n에서도 AI를 써서 각종 운영성 업무같은 것들을 자동화 하고자 하는 욕구가 생겼습니다.
하지만 비싼 Claude 요금제를 사용하면서, API까지 이중 결제해서 사용하고 싶진 않더라고요.
또 로컬에서 로그 확인같은 작업을 수행하고, Claude Skill을 활용하며, 로컬에 있는 파일을 수정하는 등의 작업도 시키고 싶었는데 API를 사용하는 것으론 이런 작업들을 하는데 있어 한계가 분명했습니다.
하지만 N8N은 컨테이너 안에서 실행되고 있어서, 호스트의 shell에 접근이 불가능합니다.
고민을 좀 해보다, ‘그럼 SSH로 접속하면 되지 않을까?’ 하는 생각이 들어 진행해본 작업을 공유합니다.
n8n 설정하기
컨테이너 설정하기
services:
n8n:
image: docker.n8n.io/n8nio/n8n:2.0.1
container_name: n8n
restart: unless-stopped
ports:
- "5678:5678"
env_file:
- .env
environment:
- GENERIC_TIMEZONE=Asia/Seoul
- TZ=Asia/Seoul
volumes:
- /mnt/hdd/data/n8n:/home/node/.n8n
- ~/.ssh/n8n_key:/home/node/.ssh/id_ed25519:ro
- ./ssh_config:/home/node/.ssh/config:ro
extra_hosts:
- "host.docker.internal:host-gateway"
docker compose로 서비스를 구성했습니다.
SSH 접속을 위한 키와, 설정 파일, 그리고 host에 접근하기 위한 extra_hosts 설정을 진행해줬습니다.
execute command 활성화
N8N 최신 버전에선 보안 상의 이유로 Execute command 노드가 비활성화되어 있습니다.
NODES_EXCLUDE=[]
N8N_ENABLE_NODES_DEV=true
N8N_ALLOW_EXECUTE_COMMAND=true
N8N_FILESYSTEM_PATH_WHITELIST=/
HOME=/home/node
위와 같은 환경 변수들을 전달해 주면, 다시 해당 노드가 활성화 됩니다.
Mount한 경로가 있다면, whitelist를 꼭 신중하게 설정해 주세요.
SSH 설정
ssh-keygen -t ed25519 -f ~/.ssh/n8n_key -N ""
Host에서 위 명령어로 ssh key를 생성합니다.
Host n8n-host
HostName host.docker.internal
IdentityFile /home/node/.ssh/id_ed25519
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
LogLevel ERROR
RequestTTY no
다음 위와 같이 host.docker.internal로 붙는 ssh_config 생성해 줍니다.
ssh -o StrictHostKeyChecking=no -o UserKnownHostFile=/dev/null -i /home/node/.ssh/id_25519 user@host
만약 이 과정을 건너뛰면 매번 이렇게 불편하게 SSH 접속을 진행해야 하고, known host에 추가되었단 알림도 log level을 error로 올리지 않으면 지속적으로 발생합니다.
주의: MacOS에서는 keychain 관련 설정 등도 추가로 진행해 줘야 non-interactive shell에서도 claude code를 실행할 수 있습니다.
ssh user@n8n-host "DISABLE_AUTOUPDATER=1 /home/user/.local/bin/claude --print"
이제 상술한 docker-compose.yaml과 같이 SSH 관련 파일들을 마운트해 주면, 문제 없이 SSH 접근을 하고, claude code를 실행할 수 있게 됩니다.
echo {{ JSON.stringify($json.prompt) }} > /tmp/p.txt && scp /tmp/p.txt marshall@n8n-host:/tmp/ && ssh marshall@n8n-host "DISABLE_AUTOUPDATER=1 /home/marshall/.local/bin/claude --print < /tmp/p.txt && rm /tmp/p.txt" && rm /tmp/p.txt
사용하다보니 프롬프트가 길어지는 경우가 꽤 잦았습니다.
단순히 SSH로 전달하면 입력 길이를 초과하는 경우가 잦아, scp를 사용했습니다.
여기까지 세팅하면 이렇게 claude code를 활용해 workflow를 만들 수 있게 됩니다.
위 예제는 제가 매일 아침 RSS 피드들을 요약하는 용도로 만든 workflow입니다.
{
"permissions": {
"allow": [
"Bash(tree:*)"
]
}
}
여기에 더해서, 실행할 명령어 별로 디렉터리들을 나누고 사전에 명령어들을 허용해 주거나, claude skill같은 도구들을 추가해 두거나, 맥락들을 파일로 추가해두면 에이전트를 역할별로 분할하는 것도 가능합니다.
Discord bot 설정하기
자세한 코드는 marshallku/zero-ops-bot 에서 확인하실 수 있습니다.
ssh로 접근까지만 설정 해도 어느정도 정적인 작업들은 자동화할 수 있습니다.
전 여기서 나아가서 좀 더 동적인 작업도 처리해 보고 싶어, Discord 봇을 하나 만들었습니다.
각종 알림을 Discord로 받고 있기도 하고, 제가 쓰는 플랫폼들을 제일 잘 지원해 주기도 해서 선택했는데, Slack이나 Telegram같은 메신저로는 훨씬 간단하게 만들 수 있을 겁니다.
시작하기 앞서 Discord의 slash command는 두 가지 방법으로 등록할 수 있습니다:
- Global 커맨드: 모든 서버에서 사용 가능, 업데이트 전파에 최대 1시간 소요
- Guild 커맨드: 특정 서버에서만 사용 가능, 즉시 업데이트
아무래도 저만 사용할 거기도 하고, 업데이트 전파를 기다릴 시간도 없으니 Guild 커맨드로 작업했습니다.
시작하기 전 간략하게 구상해봤던 데이터 흐름입니다.
디스코드에서 커맨드를 입력하면, Discord bot이 n8n webhook을 trigger하고, n8n에서 제 서버로 SSH 접속을 한 뒤 Claude Code를 실행하고 응답을 반환합니다.
봇에서 SSH로 바로 접속하는 방식도 있겠지만, SSH Key 노출이라거나, 봇을 잘못 관리하면 인프라가 전체로 위험할 수 있고, 복잡도도 증가할 것이라 생각했습니다.
Workflow 로직을 전부 n8n에서 처리하게 되면 봇이 뚫려도 공격자는 webhook trigger밖에 할 수 없고, 코드 수정 없이 workflow 수정이나 추가도 가능해지며, n8n에서 시각적으로 디버깅도 훨씬 편하게 진행할 수 있다는 장점도 있습니다.
굳이 화려한 단어를 덧붙이자면, Defense in Depth 원칙을 따라 각 레이어가 독립적으로 보안을 담당하도록 하고 싶었습니다.
type WebhookPayload struct {
Type string // "command" 또는 "message"
Command string // 커맨드 이름
Content string // 사용자 입력
UserID string // Discord 사용자 ID
UserName string // Discord 사용자명
ChannelID string // 채널 ID
Timestamp string // RFC3339 타임스탬프
Source string // "zero-ops-bot"
}
봇이 전송하는 payload의 타입입니다.
command의 타입과 각종 사용자 정보를 전달하면, 이를 n8n에서 처리하는 구조입니다.
위와 같이, Webhook 이벤트를 수신했을 때, 그에 맞는 커맨드들을 실행하도록 연결해 주면 됩니다.
해본 적은 없는데 scratch로 코딩하면 이런 느낌일까 싶었습니다.
유의사항
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
Discord의 기본 응답 타임아웃은 3초인데, Claude Code가 생각하는 시간은 아무래도 그보다 오래 걸립니다.
위와 같이 작업하면 봇이 생각 중이라는 내용으로 먼저 응답하고, 결과를 받은 뒤 최종적으로 메시지를 업데이트할 수 있습니다.
추가로 Discord에서는 (Slack도 동일) 2,000자 이상의 메시지를 보낼 수 없습니다.
1,800글자 언저리에서 \n을 만나면 새로운 메시지로 발송하게 하는 방향으로 처리할 수 있습니다.
혹은 n8n에서 발송 처리를 한다면, Code 노드에서 해당 작업을 한 뒤 Loop 노드에서 메시지 발송 처리를 하면 됩니다.
결과물
여기까지 작업했다면 위와 같이 디스코드 봇에게 docker, kubectl 등의 명령어로 디버깅을 시키거나, 파일을 직접 수정하게 하는 등의 작업이 가능해 집니다.
물론 여러모로 많이 위험한 작업이니 보안에도, AI의 명령어 실행에도 많은 주의를 기울여야겠지만, 꽤 저렴한 가격에 신입 엔지니어를 굴릴 수 있게 됩니다!
아주 간단하게 SSH로 작업을 처리하는 거다 보니, 굳이 claude가 아니어도 되고, 심지어 결과물을 받아 bash script로 직접 특정 작업들을 처리할 수도 있습니다.
저는 이제 알림을 수신하면 직접 특정 작업들을 수행해서 리포팅을 하거나, PR을 생성하는 등의 작업도 진행해 보려 하는데, 또 다른 참신한 아이디어가 있으시다면 인사이트를 나눠주시면 감사하겠습니다!
댓글을 불러오는 중...