본문 바로가기
MegazoneCloud
← 전체 글
AWS 미분류

n8n·PostgreSQL·Grafana·Python 기반 크롤링 자동화

엔터프라이즈에서 외부 포털·공시·물류 데이터를 주기적으로 수집할 때, 단일 스크립트의 “한 번 돌아감”보다 자주 깨지는 것은 트리거·실행·결과 적재·비용 가시화가 서로 다른 도구와 수동 절차로 흩어져 있다는 점이다. 스케줄은 cron, 실행은 개발 PC, 결과는 스프레드시트, 장애는 Slack 스레드 — 이런 구조에서는 재현 가능한 감사 추적(audit trail)과 테넌트별 격리가 동시에 만족되기 어렵다.

본 글은 실제 배포 테스트 검증한 Webhook → n8n → RDS(PostgreSQL) → ECS Fargate(Python) → Result Webhook → Grafana 패턴을 한사이클을 아키텍처인 부분 관점만 정리한다. 목표는 “크롤러 한 개”가 아니라 정의(Definition)는 DB, 실행(Execution)은 컨테이너, 오케스트레이션은 n8n, SLO·비용은 Grafana로 역할을 고정하는 것이다.

0. E2E 흐름 (n8n → 컨테이너 → DB → Grafana)

n8n worker(장기 ECS 서비스)는 큐·스케줄·워크플로 실행용이고, 브라우저 크롤은 Fargate RunTask로 뜨는 Python crawler 컨테이너에서 돈다. Run WF가 DB에서 잡을 읽고 RunTask를 호출한 뒤, 컨테이너가 종료할 때 Result WF로 POST → lab_execution INSERT → Grafana가 같은 DB를 조회한다.

그림 0. E2E 흐름 (Mermaid). n8n worker 서비스는 WF 실행·큐용, 브라우저 크롤은 별도 RunTask 컨테이너.

0-1. AWS 인프라 구성도

DEV 환경은 Terraform(tenant/dev)으로 VPC 3-tier 위에 ECS Fargate 오케스트레이션(n8n) · 관측 스택(Grafana·Prometheus·Loki·Tempo 등 Monitor 서비스) · 크롤러 RunTask · RDS PostgreSQL · ALB path routing(/webhook · /n8n · /grafana)까지 한 스택으로 구성했다. 아래 그림은 해당 IaC 토폴로지를 AWS Architecture Icons로 정리한 참고도다.

AWS VPC ECS n8n Grafana monitoring stack
그림 0-1. Terraform 기반 DEV VPC — ECS(n8n·Monitor) · RunTask · RDS · CI/CD (고객 접두사 제거·DEV VPC 라벨).
계층역할자동화 관련
EdgeInternet Gateway · External ALB/webhook/lab-run · /webhook/lab-result · /n8n · /grafana
ComputeECS Fargate (2a AZ-locked 일부)n8n system · Monitor(Grafana) · RunTask crawler
DataRDS PostgreSQL · (Valkey)스키마 ops_lab · execution fact · job mapping
DeliveryECR · CodeBuild · S3crawler 이미지 빌드·배포 → RunTask task definition

멀티 AZ·서브넷 CIDR·CI/CD 사이드바 등 상세는 내부 Terraform 정본으로만 관리한다. 블로그·고객 공유 HTML에는 회사명·실제 도메인·B/L·ALB 호스트가 포함된 캡처를 붙이지 않는다.

1. DB 스키마 구성도

매핑 테이블(lab_company_job)과 실행 로그(lab_execution)를 분리하면 n8n Run/Result와 Grafana가 동일 ER을 공유한다.

그림 1. ops_lab ER (Mermaid). Run WF → JOIN · Result WF → lab_execution INSERT · Grafana → VIEW/함수.

2. 파이프라인 개요와 Run/Result 분리

성공적인 자동화 두 개의 워크플로로 쪼개진다. Run은 “무엇을, 누구 명의로 실행할지”를 DB에서 읽고 컨테이너를 기동한다. Result는 워커가 종료할 때 JSON을 POST 받아 실행 이력·소요 시간·성공/실패·파생 지표(예: 추정 비용)를 같은 스키마에 기록한다. Run을 Result와 한 노드 체인에 묶으면 Fargate cold start와 크롤 지연 때문에 n8n execution 타임아웃· webhook 응답 지연이 겹치기 쉽다.

[HTTP POST ?job=<code>]
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Run Workflow (n8n) │
│ ├─ Webhook (Respond Immediately) │
│ ├─ PostgreSQL: company ↔ job 매핑 JOIN │
│ ├─ AWS ECS RunTask (Fargate, awsvpc) │
│ └─ 환경변수: JOB_CODE, WEBHOOK_RESULT_URL, LAB_B64 등 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Python Worker (컨테이너 일회성 Task) │
│ ├─ 시나리오 define 로드 (코드/임베드/DB 중 단계별 수행 ) │
│ ├─ Playwright·HTTP 등으로 대상 사이트 수집 │
│ └─ 종료 시 success|fail + duration → POST Result Webhook │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Result Workflow (n8n) │
│ └─ INSERT lab_execution (+ 선택적 payload 컬럼) │
└─────────────────────────────────────────────────────────────┘
│
▼
[Grafana SQL · company_run_summary() · 대시보드 패널]
n8n code node RunTask payload
그림 2-e. Run WF — Code 노드 1잡 필터·RunTask 준비 (환경변수·ALB URL 등 민감 구간 블러).

3. PostgreSQL: 정의와 실행의 단일 진실 공급원

n8n과 Grafana가 동일한 숫자를 보여주려면 “엑셀에만 있는 잡 목록”이 아니라 런타임 조회 테이블이 필요하다. 대표적인 최소 ER은 다음과 같다.

  • lab_company — 테넌트(법인·사업부) 코드와 표시명
  • lab_crawler_job — 수집 잡 단위(포털, 시세, 공시 등)와 enabled 플래그
  • lab_company_job — N:M 매핑: “어느 테넌트가 어떤 잡을 돌리는가”
  • lab_execution — 실행 1회당 status, duration_sec, started_at, (선택) price_krw·change_pct 등

Run 워크플로의 SQL은 보통 lab_company_job을 중심으로 JOIN한다. Webhook 쿼리 파라미터 ?job=job-alpha 같은 식별자는 잡 코드이며, UI에 보이는 “회사 A · 잡 B”는 JOIN 결과로만 노출해야 멀티 테넌트 데모에서 혼선이 줄어든다.

집계는 애플리케이션 코드보다 뷰·함수에 두는 편이 Grafana와 맞다. 예: company_run_summary(from, to)가 기간별 실행 건수·성공/실패·추정 원화를 반환하면 패널 SQL이 짧아지고 Presale·운영 회의에서 같은 쿼리를 재사용할 수 있다.

Run 노드에서 사용하는 JOIN SQL 예시는 다음과 같다.

SELECT c.code AS company_code, c.name AS company_name,
       j.code AS job_code, j.name AS job_name,
       j.portal_url, j.vcpu, j.mem_gb, j.enabled
  FROM ops_lab.lab_company_job m
  JOIN ops_lab.lab_company c ON c.code = m.company_code
  JOIN ops_lab.lab_crawler_job j ON j.code = m.job_code
 WHERE j.enabled = true
 ORDER BY j.code, c.code;

lab_execution DDL 핵심(실행 1회 = 1행):

CREATE TABLE ops_lab.lab_execution (
  id            bigserial PRIMARY KEY,
  company_code  text NOT NULL REFERENCES ops_lab.lab_company(code),
  job_code      text NOT NULL REFERENCES ops_lab.lab_crawler_job(code),
  started_at    timestamptz NOT NULL,
  finished_at   timestamptz,
  duration_sec  int NOT NULL,
  status        text NOT NULL CHECK (status IN ('success', 'fail')),
  task_arn      text,
  note          text
);

4. n8n: 오케스트레이션 계층의 설계 포인트

3-1. Webhook과 즉시 응답

Run 트리거는 외부 CI·Postman·사내 포털에서 호출된다. Webhook 노드는 Respond Immediately로 HTTP 200을 먼저 반환하고, ECS RunTask는 비동기로 이어진다. 호출자는 “태스크 ARN/execution id” 수준만 받고, 실제 성공 여부는 Result 경로와 DB·Grafana에서 확인하는 모델이 안정적이다.

3-2. 자격 증명과 최소 IAM

Postgres credential은 n8n 전용 DB 사용자(스키마 scoped)로 분리한다. ECS RunTask용 AWS credential은 ecs:RunTask, iam:PassRole, (필요 시) 태깅 권한만 부여하고, n8n UI에 장기 키를 남기지 않는 방향(역할 기반·Secrets Manager)으로 적용한다.

3-3. Run / Result 워크플로 분리

Result 워크플로는 Active 상태로 상시 대기하며, Python worker만 POST한다. 운영 체크리스트의 첫 항목은 “Result가 꺼져 있지 않은가”이다. Run만 테스트하면 DB row가 늘지 않아 Grafana가 빈 것처럼 보이는 허위 음성(false negative)이 자주 발생한다.

Result 워크플로는 Webhook → (선택) JSON 정규화 → Postgres INSERT 순으로 단순하게 유지한다.

-- Result WF (Postgres INSERT) 개념
INSERT INTO ops_lab.lab_execution (
  company_code, job_code, started_at, finished_at,
  duration_sec, status, task_arn, note
) VALUES (...);

5. Python on ECS: 워커의 책임 경계

컨테이너는 “비즈니스 규칙 전체”가 아니라 수집·파싱·종료 보고에 집중한다. 초기에는 define을 이미지 내 definitions.py 또는 Base64(LAB_B64)로 넘겨 이미지 재빌드 없이 시나리오를 바꾸기도 한다. 성숙 단계에서는 define row를 RDS에서 읽는 lab3 패턴으로 옮겨, DB INSERT만으로 신규 URL·셀렉터를 추가한다.

종료 시 Result webhook으로 보내는 JSON은 스키마를 고정하는 것이 중요하다. 최소 필드 예: job_code, company_code, status, duration_sec, message(실패 사유), (선택) 구조화된 payload. n8n Result 노드는 이 계약만 검증·INSERT하면 되므로 크롤러 팀과 플랫폼 팀의 병렬 개발이 가능하다.

컨테이너 진입점·Result POST는 아래처럼 환경변수 계약에 묶는다.

JOB_CODE = os.environ.get("JOB_CODE", "job-alpha")
WEBHOOK = os.environ.get("N8N_WEBHOOK_URL", "").rstrip("/")

def post_result(payload: dict) -> None:
    data = json.dumps(payload, ensure_ascii=False).encode()
    req = urllib.request.Request(
        WEBHOOK, data=data, method="POST",
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        print(f"webhook {resp.status}")

async def run_job() -> dict:
    async with async_playwright() as pw:
        browser = await pw.chromium.connect_over_cdp(CDP_URL)
        page = await context.new_page()
        item = await probe(page, sample_ref)  # 도메인별 probe
    return {"success": bool(item.get("success")), ...}

4-1. CAPTCHA·CDP (Playwright)

포털 보호 구간은 n8n이 아니라 Python worker + CDP에서 처리한다. iframe 슬라이더·rotate widget 등은 probe plan의 depth 단계로 기록하고, 통과 실패 시에도 fail status와 note를 Result webhook으로 보내 Grafana 집계에 반영한다.

n8n Postgres node JOIN jobs
그림 3-a. Run WF — DB 잡 조회 Postgres JOIN (credential·호스트 블러).
n8n HTTP ECS RunTask node
그림 3-b. Run WF — ECS RunTask HTTP (ecs.ap-northeast-2, AWS credential 블러).

CAPTCHA·CDP 동작은 아래 Python 코드 경로에서 처리; UI는 포털 측이라 증빙은 실행 로그·lab_execution.note로 남긴다.

async def agent_act_datadome(page) -> dict:
    """captcha-delivery iframe / 별도 탭에서 slider drag."""
    for ctx in await _datadome_targets(page):
        if await _try_datadome_drag_in_context(page, ctx):
            return {"op": "agent_act_datadome", "ok": True, "mode": "wobble"}
    ok = await solve_datadome_slider(page, timeout_ms=35000)
    return {"op": "agent_act_datadome", "ok": ok, "mode": "solve_datadome_slider"}

async def agent_act_slider_to_target(page, *, x0, y0, x1, y1, steps=28):
    await page.mouse.move(x0, y0)
    await page.mouse.down()
    for i in range(1, steps + 1):
        u = i / steps
        e = 3 * u * u - 2 * u * u * u  # ease
        await page.mouse.move(x0 + (x1 - x0) * e, y0 + (y1 - y0) * e)
        await asyncio.sleep(0.018)
    await page.mouse.up()

6. Grafana: 실행·비용·품질의 관측면

Grafana는 “또 다른 대시보드”가 아니라 DB 함수와 동일 소스를 시각화하는 계층이다. 대표 패널 구성은 다음과 같다.

  • 기간 합계: 실행 건수, success/fail, 추정 Fargate 비용(원화 환산)
  • 테넌트 × 잡 코드 pivot: 데모에서 멀티 테넌트 혼합 실행을 한 화면에 증명
  • 최근 execution 테이블: status badge, duration, (도메인별) 수집 필드 — 예: 시세 현재가·등락률

관측의 기준점은 Grafana UI가 아니라 PostgreSQL 집계 함수·VIEW다. Stat·pivot·실행명세 패널은 모두 lab_execution 계열과 동일 SQL을 쓰므로, n8n Result WF가 row를 넣는 순간부터 ALB /grafana에서도 같은 건수·비용·success/fail이 따라와야 한다.

대시보드 정의는 Git provisioning(JSON)과 런타임 API/수동 편집이 섞이면 패널·uid·datasource 참조가 어긋나 “DB에는 44건인데 화면 Stat은 옛 쿼리” 같은 불일치가 난다. 검증한 Lab2 Cost 보드처럼 dashboard uid · folder uid · Postgres datasource uid를 Terraform 또는 init 스크립트 한 벌로 고정하고, 배포마다 provisioning만 갱신하는 편이 실행·비용·품질 지표의 재현 가능한 관측면을 유지한다.

Grafana Lab2 cost dashboard last 7 days
그림 4. Lab2 Cost — Last 7 days, Fargate 비용 산식, success/fail Stat, 회사·크롤러 pivot·일별 집계 (ALB /grafana 실캡처, 회사명 블러).

패널 SQL 예시:

SELECT * FROM ops_lab.company_run_summary(
  $__timeFrom()::timestamptz,
  $__timeTo()::timestamptz
);

7. 엔터프라이즈 관점의 트레이드오프

MCT 관점에서 강조할 원칙은 “데모 E2E가 곧바로 SLA 있는 프로덕션”이 아니라는 점과 같다. 본 패턴도 다음 균형을 전제한다.

  • 동기 webhook vs 비동기 큐: 트래픽·Rate limit·장시간 크롤이 늘면 Run 트리거만 SQS/EventBridge로 디커플링하고 n8n은 consumer로 전환하는 2단계가 자연스럽다.
  • define 위치: 코드·엑셀·DB 삼분 — 속도, 프로덕션은 DB+버전 컬럼+감사 로그.
  • 멀티 테넌트: company_code를 execution row에 denormalize해 두면 Grafana GROUP BY가 단순해지고, JOIN 누락 시 데이터 유출 위험을 SQL 한 줄로 제한할 수 있다.
  • 관측 SPOF: Result WF 비활성·RDS 연결 풀 고갈·ECS capacity — 각각 알람 규칙을 Grafana Alerting 또는 CloudWatch와 연동한다.


초기에 에러가 많이 발생하지만 에러에 대한 자동화 루틴을 적용하고 AI 등을 활용한 오류부분에
자동화를 적용한다면 비용 절감과 자동화 2가지 일을 획득 할 수 있을 것 같습니다.

📚 참고 개념

  • n8n Webhook · Postgres · AWS ECS 노드 — Run/Result 분리 패턴
  • PostgreSQL — 멀티 테넌트 매핑 테이블, 기간 집계 함수, execution fact 테이블
  • AWS Fargate — awsvpc, 일회성 RunTask, 환경변수 기반 시나리오 주입
  • Grafana — PostgreSQL datasource, SQL 패널, provisioning vs API drift

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다