수요일

Docker Compose healthcheck 에러 해결 — depends_on이 기다려주지 않는 이유

Celery Worker 에러 해결 — 태스크가 처리되지 않을 때 완벽 가이드

🔍 검색 키워드: celery worker 에러 해결, celery worker not processing tasks, kombu connection refused, celery django redis 에러, celery worker 작업 안됨, celery OperationalError 해결

증상: 태스크를 보냈는데 Worker가 반응이 없다

Celery 써본 사람이라면 한 번씩 겪어봤을 상황이다. .delay() 또는 .apply_async()로 태스크를 넣었는데 처리가 안 된다. 워커 로그를 보면 아예 조용하거나, 아래 같은 에러가 떠 있다.

kombu.exceptions.OperationalError: [Errno 111] Connection refused

또는 워커 프로세스는 살아 있는데 태스크 큐에 메시지가 쌓이기만 하고 소비가 안 된다. Celery + Django + Redis 조합에서 특히 자주 터진다.

원인 분류

Celery 워커가 태스크를 처리하지 못하는 원인은 크게 세 가지다.

1. 브로커(Redis/RabbitMQ)에 연결이 안 됨

가장 흔한 원인. Redis가 아예 안 떠 있거나, 포트나 URL이 틀렸다.

# Redis 실행 여부 확인 redis-cli ping # PONG 이 나와야 정상 # 포트 확인 netstat -an | grep 6379

Django settings에서 CELERY_BROKER_URL 확인:

# settings.py CELERY_BROKER_URL = 'redis://localhost:6379/0' # 로컬 # Docker 환경이면 CELERY_BROKER_URL = 'redis://redis:6379/0' # 서비스명 주의

Docker Compose 쓸 때 실수가 잦다. 컨테이너 안에서 localhost는 자기 자신을 가리키기 때문에, Redis 컨테이너 이름(서비스명)을 써야 한다.

2. Celery 5.6.x 버전 버그 — Redis Reconnection 후 멈춤

이건 좀 억울한 케이스다. Celery 5.5.0에서 Kombu reconnection 버그를 고쳤는데, 5.6.x에서 같은 문제가 다시 들어왔다. 증상은 이렇다.

  • 워커가 처음엔 잘 돌다가 Redis 재연결(failover, 재시작) 이후 멈춤
  • 태스크가 큐에 들어오는 건 보이는데 워커가 pick-up 안 함
  • 워커 프로세스는 살아 있음 (CPU 거의 0%)
# 현재 celery 버전 확인 pip show celery | grep Version pip show kombu | grep Version

5.6.x라면 다운그레이드하거나 패치 버전을 기다려야 한다.

pip install "celery==5.5.0" "kombu==5.4.0"

3. Worker Concurrency 부족 — 큐는 차는데 처리 속도가 안 따라감

에러는 없는데 태스크가 밀리는 경우다. 기본 concurrency는 CPU 코어 수인데, I/O bound 작업이 많으면 이걸 올려야 한다.

# 현재 워커 상태 확인 celery -A myproject inspect active celery -A myproject inspect reserved celery -A myproject inspect stats
# concurrency 늘려서 실행 celery -A myproject worker --concurrency=16 --loglevel=info # 여러 워커 프로세스 (celery multi) celery multi start 4 -A myproject -Q default,priority --concurrency=8

디버깅 순서: 이 순서대로 확인해라

Step 1. 브로커 연결 직접 확인

# Django shell에서 from celery_app import app # 또는 본인 celery app app.connection().ensure_connection(max_retries=3)

에러 없이 통과하면 브로커 연결은 OK다.

Step 2. 간단한 태스크로 테스트

# tasks.py from celery import shared_task @shared_task def debug_task(): print("task executed!") return "ok" # Django shell에서 from myapp.tasks import debug_task result = debug_task.delay() print(result.get(timeout=10)) # "ok" 나오면 정상

Step 3. 워커 로그 레벨 올려서 확인

celery -A myproject worker --loglevel=DEBUG

[DEBUG/MainProcess] Received task: 라인이 안 뜨면 태스크가 워커까지 도달 자체를 못 하는 것. Received task:는 뜨는데 처리가 안 되면 concurrency 문제이거나 태스크 내부에서 죽는 것.

Step 4. Flower로 실시간 모니터링

pip install flower celery -A myproject flower --port=5555

http://localhost:5555에서 워커 상태, 태스크 큐, 처리 속도를 한눈에 볼 수 있다.

상황별 체크리스트

증상체크 항목해결 방법
kombu connection refusedRedis 실행 여부redis-cli ping → Redis 시작
Docker에서 Redis 연결 실패BROKER_URL 호스트명localhost → 서비스명 변경
워커 재시작 후 멈춤Celery 버전5.5.0으로 다운그레이드
큐 적체, 에러 없음concurrency 설정--concurrency 값 증가
태스크 timeout작업 시간 초과soft_time_limit, time_limit 설정
특정 큐만 처리 안 됨워커 큐 설정-Q 파라미터로 큐 명시

실무 설정 예시

Django settings.py — 권장 Celery 설정

# Celery 설정 CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0') CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/1') # 작업 직렬화 CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_ACCEPT_CONTENT = ['json'] # 타임존 CELERY_TIMEZONE = 'Asia/Seoul' CELERY_ENABLE_UTC = True # 재시도 설정 CELERY_TASK_ACKS_LATE = True # 처리 완료 후 ack CELERY_WORKER_PREFETCH_MULTIPLIER = 1 # 한 번에 하나씩 # 태스크 타임아웃 CELERY_TASK_SOFT_TIME_LIMIT = 300 # 5분 경고 CELERY_TASK_TIME_LIMIT = 360 # 6분 강제 종료

Docker Compose 설정 예시

version: '3.8' services: redis: image: redis:7-alpine ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 web: build: . depends_on: redis: condition: service_healthy environment: - CELERY_BROKER_URL=redis://redis:6379/0 celery_worker: build: . command: celery -A myproject worker --concurrency=4 --loglevel=info depends_on: redis: condition: service_healthy environment: - CELERY_BROKER_URL=redis://redis:6379/0

Docker Compose에서 depends_on: condition: service_healthy를 안 쓰면, Redis가 완전히 뜨기 전에 Celery worker가 연결 시도해서 connection refused가 뜬다.

Celery Beat (스케줄러) 별도로 떠야 한다

가끔 보면 periodic task가 실행이 안 된다는 이슈가 있는데, celery beat를 별도 프로세스로 안 띄워서 그런 경우가 있다.

# beat는 반드시 별도로 실행 celery -A myproject beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler

beat와 worker를 같은 프로세스로 돌리는(-B 옵션) 건 개발환경에서만 써라. 프로덕션에서는 반드시 분리해야 한다.

마무리

Celery 문제는 대부분 브로커 연결 문제 아니면 버전 버그다. 위 순서대로 하나씩 확인하면 대부분 잡힌다. Docker 환경에서는 서비스명 vs localhost 혼동이 제일 많으니 그것부터 봐라.

실무에서 직접 겪은 내용 기반으로 작성했습니다. 틀린 부분 있으면 댓글로 알려주세요.

화요일

GitHub Actions secrets 환경변수 비어있음 해결 — secret not available 원인 분석

🔍 검색 키워드: GitHub Actions secrets undefined, GitHub Actions 환경변수 비어있음, secrets not available 해결, GitHub Actions secret 적용 안 됨, CI 시크릿 설정, GitHub Actions secret empty

증상: 분명히 설정했는데 값이 없다고 한다

GitHub Actions 워크플로우는 돌리면 이런 상황이 생긴다.

Error: API_KEY is undefined
Error: Cannot read properties of undefined (reading 'length')

아니면 시크릿 값이 빈 문자열로 들어오거나, 더 황당하게는 배포가 그냥 조용히 실패한다. Secrets 탭에서 분명히 등록했는데 워크플로우가 못 읽는다.

CI 처음 세팅할 때, 또는 레포를 포크하거나 환경(Environment)을 새로 만들었을 때 이 문제를 자주 만난다.

원인 분류

1. 시크릿 이름 대소문자 불일치

가장 흔한 실수. Secrets UI에서 API_KEY로 등록했는데 워크플로우에서 ${{ secrets.api_key }}로 참조하면 빈 값이 온다. 시크릿은 대소문자 구분한다.

# 잘못된 예
env:
  API_KEY: ${{ secrets.api_key }}  # 실제 이름이 API_KEY면 못 읽음

# 올바른 예
env:
  API_KEY: ${{ secrets.API_KEY }}

2. Environment 시크릿인데 job에 environment 지정 안 함

GitHub에서 환경(Environment)을 별도로 만들어서 거기에 시크릿을 등록했다면, job에서 그 environment를 명시해야 한다. 안 하면 해당 시크릿은 아예 조회 자체가 안 된다.

# 잘못된 예 — environment 지정 없이 env 시크릿 접근 시도
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo ${{ secrets.PROD_DB_URL }}  # production environment의 시크릿이면 빈 값

# 올바른 예
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # ← 이게 있어야 environment 시크릿 접근 가능
    steps:
      - run: echo ${{ secrets.PROD_DB_URL }}

3. Fork PR에서는 시크릿 접근이 차단된다

외부 기여자가 fork해서 올린 PR의 워크플로우는 보안 정책상 시크릿에 접근할 수 없다. pull_request 이벤트 트리거를 쓰면 이 제한이 적용된다.

Warning: Context access might be invalid: secrets

의도적인 제한이다. 악의적인 코드가 PR로 들어와서 시크릿을 탈취하는 걸 막기 위한 것.

4. 시크릿 참조 문법 오류

# 틀린 문법들
${{ secret.API_KEY }}       # secrets가 아니라 secret (오타)
${{ secrets[API_KEY] }}     # 대괄호 안에 따옴표 필요
${{ env.secrets.API_KEY }}  # env와 secrets 혼용

# 올바른 문법
${{ secrets.API_KEY }}
${{ secrets[env.SECRET_NAME] }}  # 동적 키 참조는 이렇게

해결 방법

Step 1. 시크릿 이름 확인

# GitHub CLI로 현재 등록된 시크릿 목록 확인
gh secret list

# environment 시크릿 확인
gh secret list --env production

워크플로우 YAML의 이름과 정확히 일치하� 체크.

Step 2. 시크릿이 실제로 전달되는지 테스트

값을 직접 출력하면 안 된다 (마스킹됨). 대신�길이나 존재 여부를 확인:

steps:
  - name: Check secrets
    run: |
      if [ -z "${{ secrets.API_KEY }}" ]; then
        echo "API_KEY is EMPTY"
      else
        echo "API_KEY is set (length: ${#API_KEY})"
      fi
    env:
      API_KEY: ${{ secrets.API_KEY }}

Step 3. Environment 시크릿 설정

레포 Settings → Environments → 환경 선택 → Environment secrets에서 등록했다면:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # 반드시 명시
    steps:
      - name: Deploy
        env:
          DB_URL: ${{ secrets.DB_URL }}
          API_KEY: ${{ secrets.API_KEY }}
        run: ./deploy.sh

environment 이름은 Environments 탭에 있는 이름과 정확히 일치해야 한다.

Step 4. Fork PR 시크릿 접근 처리

내부 PR이라면 pull_request_target 이벤트 사용을 고려할 수 있다. 단, pull_request_target은 base 브랜치의 코드를 실행하므로 PR 코드를 checkout해서 실행하면 보안 취약점이 된다.

# 안전한 패턴 — PR 코드는 테스트만 하고, 배포는 main 병합 후에
on:
  pull_request:
    # 시크릿 없이 테스트만
  push:
    branches: [main]
    # 여기서 시크릿 써서 배포

Step 5. Organization 시크릿 접근 권한

조직(organization) 레벨 시크릿은 레포별로 접근 권한이 따로 있다. 조직 Settings → Secrets → 해당 시크릿 → Repository access에서 해당 레포가 포함되어 있는지 확인.

상황별 체크리스트

증상원인조치
시크릿 값이 빈 문자열이름 대소문자 불일치gh secret list로 정확한 이름 확인
environment 시크릿 못 읽음job에 environment 미지정environment: 필드 추가
Fork PR에서만 실패보안 정책으로 차단시크릿 없이 동작하도록 CI 설계 변경
조직 시크릿 못 읽음레포 접근 권한 없음Organization 설정에서 레포 추가
로컬에서는 되는데 CI에서만환경변수 주입 누락env: 블록에서 명시적 주입 확인

자주 하는 실수 — 디버깅 시 값 출력하려다 마스킹에 막히는 경우

# 이렇게 하면 *** 로 마스킹되어 아무 의미 없음
- run: echo ${{ secrets.API_KEY }}

# 디버깅 목적이면 이렇게
- run: |
    echo "Length: ${#MY_SECRET}"
    echo "First char: ${MY_SECRET:0:1}"
  env:
    MY_SECRET: ${{ secrets.API_KEY }}

시크릿 값 자체를 로그에 출력하는 건 GitHub이 자동 마스킹한다. 길이나 첫 글자 정도로 존재 여부 확인하는 게 현실적인 디버깅 방법이다.

실무 팁: 시크릿 관리 패턴

# 공통 시크릿은 레포 레벨에
# 환경별 시크릿(prod DB URL 등)은 environment 레벨에 분리

jobs:
  test:
    runs-on: ubuntu-latest
    # environment 없음 — 레포 레벨 시크릿만 접근
    steps:
      - run: npm test
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}  # 레포 레벨 시크릿

  deploy-prod:
    runs-on: ubuntu-latest
    environment: production   # environment 레벨 시크릿 접근
    needs: test
    steps:
      - run: ./deploy.sh
        env:
          DB_URL: ${{ secrets.DB_URL }}        # production environment 시크릿
          API_KEY: ${{ secrets.API_KEY }}      # production environment 시크릿

시크릿을 환경별로 분리하면 실수로 개발용 키를 프로덕션에 쓰는 사고를 막을 수 있다.

정리

GitHub Actions 시크릿이 안 읽히는 케이스는 이름 대소문자 불일치, environment 미지정, fork PR 보안 정책 세 가지가 대부분을 차지한다. gh secret list로 정확한 이름 확인하고, environment 시크릿이면 job에 environment: 명시하는 것만 체크해도 80%는 해결된다.

관련 글: GitHub Actions Node.js 빌드 실패 해결 — CI 에러 원인과 대처법