DevOps n8n Kubernetes Helm DevOps

n8n v1 → v2 업그레이드: Kubernetes에서 메이저 버전 넘기

n8n v1.120.4에서 v2.9.4로 2단계 순차 업그레이드한 기록. 연쇄 CVE 대응, Migration Report 활용, community node emptyDir 트레이드오프, Queue 모드 호환성 튜닝.

n8n을 v1.120.4에서 v2.9.4로 업그레이드했습니다. 2025년 말부터 Critical CVE가 연달아 나오고 있었고 v1.x EOL도 임박한 상황이었습니다. changelog에서 v2의 새로운 기능들도 눈에 들어왔습니다.

환경은 OKE Always Free(ARM64 2노드), Queue 모드(main StatefulSet + worker Deployment), PostgreSQL + Redis 구성입니다. Block Volume 200GB 제한 때문에 Redis와 Worker의 PVC를 emptyDir로 대체한 상태인데, 무료로 운영하려다 보니 이런 부분에서 불편함이 있습니다.


왜 업그레이드해야 했나

n8n은 2024년까지 공개된 CVE가 없었는데, 2025년 말부터 보안 취약점이 연달아 나오기 시작했습니다. 도구가 커지면서 보안 연구자들의 관심도 같이 커진 것으로 보입니다.

시기CVECVSS설명
2025년 12월CVE-2025-686139.9Expression Injection으로 원격 코드 실행
2025년 12월CVE-2025-68668 (N8scape)9.9시스템 명령 실행
2026년 1월CVE-2026-21858 (Ni8mare)10.0인증 없이 원격 코드 실행
2026년 2월 25일CVE-2026-27577CriticalExpression Sandbox 우회하여 코드 실행
CVE-2026-27497CriticalMerge 노드를 통한 원격 코드 실행
CVE-2026-27495CriticalJS Task Runner Sandbox 우회
CVE-2026-27498Critical파일 쓰기 및 Git 명령 실행
CVE-2026-27494CriticalPython Code 노드 Sandbox 우회
CVE-2026-27493High인증 없이 Form에서 Expression 평가
CVE-2026-27578High저장형 XSS

v1.120.4는 CVE-2025-68613 패치 버전이었지만, 그 이후로 Ni8mare와 2/25 일괄 공개까지 계속 쌓였습니다. 2/25에 공개된 7건은 v1.123.22 또는 v2.9.3에서 패치되는데, v1.x 보안 패치 지원이 2026년 3월 중순 종료 예정이라 v2로 가는 편이 나았습니다.


n8n v2, 어디로 가고 있나

CVE를 확인하면서 겸사겸사 v2 changelog를 훑어봤는데, 괜찮은 변화들이 있었습니다.

Secure by Default

v1에서는 기본적으로 모든 게 열려있었습니다. Code 노드는 메인 프로세스에서 그대로 실행되고, ExecuteCommand 노드로 시스템 명령을 실행할 수 있었습니다. 보안이 필요하면 사용자가 알아서 막아야 했습니다.

v2는 반대입니다. Task Runner가 기본 활성화되어 Code 노드가 격리된 환경에서 실행되고, ExecuteCommand 같은 위험 노드가 기본 비활성화됩니다. 막혀있는 상태에서 필요한 것만 여는 방식입니다.

예를 들어 GitHub 자동 백업 워크플로우는 ExecuteCommand 노드로 git clone, git commit, git push를 실행합니다. v1에서는 아무 설정 없이 동작했지만, v2로 올리면 ExecuteCommand가 블록리스트에 걸려서 워크플로우 전체가 멈춥니다. 이걸 다시 쓰려면 NODES_EXCLUDE=""로 명시적으로 해제해야 합니다. 이 부분은 Migration Report에서 미리 잡아냈고, 환경 변수로 사전 대응했습니다.

Breaking Changes가 많지만 방향은 맞아 보입니다.

Save/Publish 분리

v1에서는 Save 버튼을 누르면 바로 운영에 반영되었습니다. 워크플로우를 수정하다 실수로 저장하면 즉시 프로덕션에 영향을 줍니다. v2에서는 Save와 Publish를 분리했습니다. 저장은 초안으로 남고, Publish를 눌러야 운영에 배포됩니다.

v2 Publish 다이얼로그 — 버전 이름과 변경 설명을 입력할 수 있습니다

소프트웨어 개발에서는 당연한 관행이지만, 자동화 도구에는 오랫동안 없던 기능이었습니다. 워크플로우에도 버전 관리가 도입된 셈입니다.

자동화 진입 장벽이 낮아지고 있다

n8n은 노코드를 표방하지만, 실제로 비개발자가 워크플로우를 만들기에는 쉽지 않았습니다. 노드 간 데이터 매핑, JSON 구조 이해, Expression 문법 같은 것들은 결국 개발 지식을 요구합니다.

n8n Cloud에서는 2025년 10월부터 AI Workflow Builder를 제공하고 있습니다. 자연어로 원하는 자동화를 설명하면 워크플로우를 생성해주는 기능입니다.

n8n-mcp 같은 MCP 서버를 Claude Code나 Cursor에 등록하면, 배포 방식과 관계없이 “Slack 메시지가 오면 요약해서 Notion에 저장하는 n8n 워크플로우 만들어줘”라고 요청하는 것만으로 워크플로우를 만들 수 있습니다.

노코드 도구의 한계가 코드를 몰라도 되는 대신 도구 자체를 배워야 한다는 점이었는데, 그 간극이 줄어들고 있습니다.

성장 지표

2023년 v1.0 이후 2년간의 변화:

  • GitHub 스타: 3만 → 16만+
  • 커뮤니티: 6,200명 → 11만 5천명
  • 팀 규모: 30명 → 190명

셀프호스팅을 선택한 입장에서 도구의 지속 가능성은 중요합니다. 커뮤니티와 팀이 이 정도로 성장하고 있다면 당분간 걱정은 없어 보입니다.


업그레이드 전략: 왜 2단계인가

n8n 공식 문서의 권고는 명확합니다: “현재 메이저 버전의 최신 릴리스로 먼저 업데이트한 후, 다음 메이저 버전으로 업데이트하라.”

flowchart LR
A["v1.120.4<br>(현재)"] -->|"Chart 1.16.10"| B["v1.123.5<br>(최신 1.x)"]
B -->|"Migration Report<br>+ 안정화"| C["v2.9.4<br>(Chart 1.16.29<br>+image override)"]

한 번에 건너뛰지 않는 이유가 있습니다.

DB 마이그레이션은 단방향입니다. v2로 올리면 DB 마이그레이션이 실행되고, v1으로 돌아갈 수 없습니다. 중간에 문제가 생기면 pg_dump로 복원해야 합니다. 단계를 나누면 각 단계에서 복원 지점이 생깁니다.

Migration Report라는 안전장치가 있습니다. v1.121.0 이상에서만 사용할 수 있는 기능으로, v2로 전환하기 전에 호환성 문제를 미리 보여줍니다. v1.120.4에서는 이 기능이 없으니, 먼저 v1.123.5로 올려야 합니다.


Phase 0: 백업

DB 마이그레이션이 단방향이므로 세 가지 백업을 확보했습니다.

pg_dump: PostgreSQL 전체 덤프. 30,367줄, 153MB. v2 롤백 시 유일한 복원 수단입니다. Bitnami PostgreSQL 이미지는 패스워드를 파일로 관리해서 PGPASSWORD=$(cat /opt/bitnami/postgresql/secrets/password) 형태로 전달해야 했습니다.

n8n CLI export: 워크플로우 28개 + 자격 증명 5개를 JSON으로 내보냅니다. pg_dump는 PostgreSQL에서만 복원할 수 있지만 JSON은 새 인스턴스에도 임포트할 수 있어서 별도로 확보했습니다.

N8N_ENCRYPTION_KEY: n8n이 자격 증명을 암호화할 때 사용하는 키입니다. 이 키가 없으면 백업한 DB를 복원해도 자격 증명을 복호화할 수 없습니다. Kubernetes Secret에서 추출하여 별도 보관했습니다.


Phase 1: v1.123.5 (최신 1.x)

v1 내 마이너 업그레이드입니다. n8n.yaml 변경 없이 Helm chart만 교체합니다.

Terminal window
helm upgrade n8n ./n8n/1.16.10/n8n -n n8n -f n8n.yaml --atomic --timeout 10m

--atomic 플래그를 쓰면 업그레이드가 실패했을 때 자동으로 이전 revision으로 롤백합니다. v1.x 내 마이너 업그레이드이므로 DB 호환성 문제가 없어 롤백도 안전합니다.

업그레이드는 깔끔하게 통과했지만, Warning이 하나 떴습니다. GENERIC_TIMEZONEWEBHOOK_URL이 chart 기본값과 extraEnvVars에서 중복 설정되어 있었습니다. 마지막 값이 적용되므로 동작에는 문제없지만 정리가 필요한 부분입니다.

Migration Report

v1.123.5에서 Migration Report를 실행했습니다. 28개 워크플로우 중 21개가 v2.0 호환, 7개에서 이슈가 발견되었습니다.

Migration Report — Instance issues 탭. Critical 0건, Medium 2건, Low 3건

Workflow issues (3종류):

  • ExecuteCommand/LocalFileTrigger 비활성화 → 4개 워크플로우
  • File Access Restrictions → 2개 워크플로우
  • Sub-workflow waiting node 동작 변경 → 1개 (비활성 워크플로우, 무시 가능)

Instance issues (5건): OAuth 콜백 인증 강화(Medium), Task Runner 이미지 분리(Medium), 나머지 Low 3건.

Critical 0건. 모든 이슈가 환경 변수 설정으로 대응 가능한 범위여서 Phase 2로 넘어갔습니다.


Phase 2: v2.9.4

메이저 업그레이드입니다. 이번엔 n8n.yaml을 수정해야 합니다.

image.tag 오버라이드

Helm chart 1.16.29의 기본 앱 버전은 2.8.3입니다. CVE 패치가 적용된 2.9.4를 쓰려면 chart를 수정할 필요 없이 image.tag만 오버라이드하면 됩니다.

image:
repository: n8nio/n8n
tag: "2.9.4" # Chart 기본값 2.8.3 대신 CVE 패치 버전

v2 Breaking Changes 대응

Migration Report 결과 + v2 기본값 변경에 대응하는 환경 변수 5개를 추가했습니다. main과 worker 모두에 동일하게 적용해야 합니다.

extraEnvVars:
# ExecuteCommand 노드 재활성화 — v2 기본 블록리스트 초기화
NODES_EXCLUDE: ""
# 파일 접근 경로 확장 — v2 기본값은 ~/.n8n-files만 허용
N8N_RESTRICT_FILE_ACCESS_TO: "/home/node/.n8n-files:/home/node/.n8n:/tmp"
# Queue 안정성 튜닝 — v2에서 stall 재시도 로직이 제거됨
QUEUE_WORKER_LOCK_DURATION: "3600000"
QUEUE_WORKER_LOCK_RENEW_TIME: "120000"
QUEUE_WORKER_STALLED_INTERVAL: "240000"

NODES_EXCLUDE="": 앞서 설명한 Secure by Default 대응입니다. v2 기본 블록리스트에는 두 노드가 포함되어 있습니다.

  • ExecuteCommand: 임의 시스템 명령 실행 가능
  • LocalFileTrigger: 호스트 파일 시스템 접근 가능

둘 다 컨테이너 탈출이나 파일 시스템 노출 리스크 때문에 v2에서 기본 차단된 것입니다. NODES_EXCLUDE=""로 설정하면 이 블록리스트를 비워서 둘 다 사용 가능하게 됩니다. 셀프호스트 환경에서 본인만 사용하니 문제없습니다.

초기 스펙에서는 NODES_INCLUDE로 특정 노드만 허용하려 했는데, n8n의 실제 동작이 블록리스트 방식이라는 걸 GitHub Issue #23439에서 확인하고 수정했습니다.

Queue 튜닝: v2에서 QUEUE_WORKER_MAX_STALLED_COUNT가 제거되었습니다. stall 상태의 작업을 자동 재시도하는 로직이 사라진 것입니다. 대신 lock 시간을 넉넉하게 잡아서 stall 자체를 방지하는 전략입니다. lock duration 1시간, 갱신 주기 2분, stall 감지 주기 4분.

helm upgrade

Terminal window
helm upgrade n8n ./n8n/1.16.29/n8n -n n8n -f n8n.yaml --timeout 10m

Phase 1에서는 --atomic을 사용했지만, 여기서는 뺐습니다. --atomic은 업그레이드 실패 시 이전 revision으로 자동 롤백하는데, v2 DB 마이그레이션이 이미 실행된 상태에서 Helm이 v1 리소스로 롤백하면 v1 앱이 v2 스키마를 읽으려는 상태가 됩니다. DB 마이그레이션이 단방향인 메이저 업그레이드에서는 자동 롤백이 오히려 위험합니다. 문제가 생기면 pg_dump로 복원하는 것이 유일한 방법입니다.

약 20개의 DB 마이그레이션이 자동 실행되었습니다. n8n-0과 worker에서 각 1회 재시작이 있었는데, DB와 Redis가 준비되기 전에 앱이 먼저 뜨려고 시도한 것이었습니다. 잠시 후 모든 Pod이 안정화되었습니다.

로그에서 N8N_RUNNERS_ENABLED 폐기 경고가 보였습니다. v2에서는 Task Runner가 기본 활성화이므로 이 환경 변수가 더 이상 필요하지 않습니다. n8n.yaml에서 제거했습니다.

community node가 사라진 이유

v2 기동 후 워크플로우를 확인하는 과정에서 MCP Client Tool 노드를 사용하는 워크플로우가 Unrecognized node type: n8n-nodes-mcp.mcpClient 에러를 내고 있었습니다.

/home/node/.n8n/nodes/package.json을 확인했더니 dependencies가 비어있었습니다. community node 패키지가 사라진 상태였습니다.

원인을 추적해보니 Helm chart 템플릿의 볼륨 마운트 구조 때문이었습니다. chart가 /home/node/.n8n/nodes/community-node-modules라는 이름의 emptyDir로 마운트합니다. 이 디렉토리는 PVC(/home/node/.n8n/)의 하위 경로인데, emptyDir가 그 위에 오버레이됩니다.

/home/node/.n8n/ ← PVC (영구 저장)
/home/node/.n8n/nodes/ ← emptyDir (Pod 재시작 시 초기화)

PVC에 저장된 community node 파일이 emptyDir 오버레이에 가려져서 보이지 않게 됩니다. Pod이 재시작될 때마다 emptyDir는 빈 상태로 초기화되므로, community node가 매번 유실됩니다.

이전에 OCI Always Free 비용 최적화를 하면서 Worker의 PVC를 emptyDir로 바꾼 적이 있는데, 그때 community node 경로도 emptyDir에 포함된다는 걸 신경 쓰지 못했습니다.

컨테이너 안에서 npm install n8n-nodes-mcp n8n-nodes-notionmd를 실행하고(498개 패키지 설치) kubectl rollout restart로 Pod을 재시작하니 정상 로드되었습니다. 다만 이건 임시 대응이고, 다음 Pod 재시작 때 다시 유실됩니다. 근본 해결은 chart 템플릿을 수정하여 community-node-modules를 PVC 하위가 아닌 별도 경로에 매핑하거나, initContainer에서 매번 설치하는 방식이 필요합니다. OCI Always Free 200GB Block Volume 제약으로 PVC를 아끼려는 선택의 트레이드오프입니다.

Save/Publish 전환

v1에서 active였던 워크플로우는 자동으로 published 상태로 전환되었습니다. 별도 작업 없이 기존 워크플로우가 그대로 운영에 유지되었습니다.

검증

업그레이드 후 24시간 동안 Worker + Main 로그를 모니터링했습니다. 실행 에러나 stall 없이 정상 동작하고 있었고, 모든 Pod 재시작 0회로 안정화되었습니다.


핵심 정리

n8n v1 → v2 업그레이드 체크리스트

업그레이드 전:

  • pg_dump + n8n CLI export + encryption key 세 가지 백업
  • 최신 v1.x로 먼저 업그레이드 → Migration Report 실행
  • Critical 이슈 0건 확인 후 v2 진행

n8n.yaml 변경 (v2 필수):

  • NODES_EXCLUDE: ExecuteCommand 사용 시 빈 문자열로 설정
  • N8N_RESTRICT_FILE_ACCESS_TO: 파일 접근 경로 명시
  • Queue 튜닝 3종: LOCK_DURATION, LOCK_RENEW_TIME, STALLED_INTERVAL
  • image.tag: Chart 기본 버전과 CVE 패치 버전이 다를 수 있음
  • main과 worker 모두에 동일 환경 변수 적용

주의 사항:

  • community node는 emptyDir 마운트 여부 확인 — Pod 재시작 시 유실
  • DB 마이그레이션은 단방향 — v2에서 v1으로 롤백 시 pg_dump 복원 필요
  • v2에서 Save/Publish 분리 — 기존 active 워크플로우는 자동으로 published 전환

관련 콘텐츠

댓글