GitHub Actions로 파이썬 스크립트 무료 자동 실행하기

PC를 24시간 켜두던 습관, GitHub Actions 하나로 끊었다

자동화 스크립트를 매일 정해진 시간에 실행하려면 서버가 필요하다. 저는 한때 집 PC를 24시간 켜놓았다. 전기요금이 무시 못 할 수준이라 이 방법을 오래 유지하기 힘들었다. 다음으로는 월 5달러짜리 VPS를 빌렸는데, 크론잡 설정에서 리눅스 명령어가 헷갈려서 매번 구글 검색에 의존했다. 솔직히 말하면, VPS가 편해 보이지만 설정 난이도 때문에 스트레스가 꽤 쌓였다. 그러던 중에 GitHub Actions를 제대로 만지기 시작했는데, 생각보다 훨씬 간단해서 놀랐다.

GitHub Actions는 월 2,000분 이하의 실행 시간을 무료로 쓴다. 퍼블릭 레포는 무제한, 프라이빗 레포도 월 2,000분까지 공짜라서 비용 부담이 아예 없다는 점이 가장 큰 매력이다. 파이썬 스크립트를 깃허브 레포에 올리고, 워크플로우 파일 하나만 추가하면 깃허브가 알아서 정해진 시간에 대신 실행해준다. 덕분에 제 PC나 VPS를 항상 켜놓을 필요가 없어졌다. 물론, 이 과정이 처음엔 쉽지 않았다. 설정 과정에서 겪은 여러 삐걱거림도 차근차근 풀어가며 실제로 쓸 수 있는 자동화를 완성했다. 그 경험을 최대한 솔직하게 공유하려고 한다.



1. GitHub Actions 무료 한도와 동작 원리

GitHub Actions는 깃허브가 제공하는 CI/CD 자동화 도구다. 원래 의도는 코드 테스트와 배포지만, 파이썬 스크립트를 정기적으로 실행하는 데도 잘 맞는다. 무료 티어가 꽤 후해서 퍼블릭 레포는 실행 시간이 무제한이다. 프라이빗 레포도 월 2,000분까지 공짜인데, 매일 한 번 1분 내외로 스크립트를 돌리면 한 달에 30분밖에 안 써서 한도 걱정은 거의 하지 않았다.

하지만 무료 한도를 넘으면 추가 비용이 발생하고, 복잡한 워크플로우는 디버깅이 까다로워 초보자가 접근하기 쉽지 않다. 특히 로그만 봐서는 무슨 오류인지 감이 안 오는 경우가 많아서 저는 한참 고생했다. 동작 원리는 단순히 레포 안에 .github/workflows/ 폴더를 만들고 YAML 파일을 집어넣으면 깃허브가 조건에 맞게 가상 서버(Runner)를 띄워 명령어를 실행하는 식이다. 서버 관리는 전적으로 깃허브가 맡고, 사용자는 YAML 파일만 관리하면 된다.

2. 레포 구조 준비 및 시크릿 키 등록

레포 폴더 구조

레포 구조는 생각보다 단순하다. 세 가지 요소만 있으면 된다: 실행할 파이썬 스크립트, 필요한 패키지 목록, 자동 실행 설정을 담은 워크플로우 파일.

my-auto-bot/
├── main.py                        ← 실행할 파이썬 스크립트
├── requirements.txt               ← 필요한 패키지 목록
└── .github/
    └── workflows/
        └── daily_run.yml          ← 자동 실행 설정 파일

requirements.txtpip freeze > requirements.txt 명령어로 자동 생성하거나, 직접 필요한 패키지만 적어도 된다. 저는 과하게 적는 걸 피하고 꼭 필요한 라이브러리만 명시했다. 예시는 아래와 같다.

requests==2.31.0
pandas==2.2.0
anthropic==0.25.0
python-dotenv==1.0.1

API 키를 깃허브 시크릿에 등록하기

로컬 환경에서는 보통 .env 파일에 API 키를 넣고 쓰지만, 깃허브에 그 파일을 올리면 보안에 치명적이다. 그래서 저는 깃허브 레포지토리의 [Settings] → [Secrets and variables] → [Actions] 메뉴에서 New repository secret을 눌러서 API 키를 등록했다. 여기서 이름을 OPENAI_API_KEY로 지정하면, 워크플로우 YAML에서 ${{ secrets.OPENAI_API_KEY }}로 안전하게 호출할 수 있다.

이 과정이 생각보다 헷갈릴 수 있는데, 특히 시크릿 이름을 헷갈리면 실행할 때 키가 안 불러와져서 한참 골머리를 앓았다. 반드시 대문자와 언더스코어까지 정확히 맞춰야 한다. 또, 시크릿 등록 후 바로 반영되는데, 워크플로우에서 환경변수로 불러올 때 오타가 있으면 아무런 에러 메시지도 없이 그냥 키가 없다고 인식해서 디버깅이 까다롭다.

3. 워크플로우 YAML 파일 작성: 매일 자동 실행 설정

자동으로 파이썬 스크립트를 돌리려면 .github/workflows/daily_run.yml 파일을 만들어야 한다. 저는 매일 오전 9시에 main.py를 실행하도록 설정했는데, 직접 써보니 시간이 UTC 기준이라서 한국 시간과 맞추는 게 꽤 헷갈렸다. 특히 크론 문법이 낯설면 crontab.guru 사이트에서 시간을 넣어보면 바로 표현식을 알려줘서 시간 맞출 때 매우 유용했다.

name: 매일 자동 실행 봇

on:
  schedule:
    # UTC 기준 00:00 = 한국 시간 오전 09:00
    - cron: '0 0 * * *'
  workflow_dispatch:  # 깃허브 UI에서 수동 실행도 가능하게 설정

jobs:
  run-script:
    runs-on: ubuntu-latest  # 깃허브가 무료로 제공하는 우분투 서버 사용

    steps:
      - name: 코드 체크아웃
        uses: actions/checkout@v4

      - name: Python 3.12 세팅
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: 패키지 설치
        run: pip install -r requirements.txt

      - name: 스크립트 실행
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
        run: python main.py

여기서 어쩔 수 없이 UTC 시간을 써야 하는 부분이 가장 까다로웠다. 한국 시간 오전 9시를 맞추려면 UTC 00:00으로 설정해야 하는데, 이걸 반대로 생각하면 자칫 하면 원하는 시간에 안 돌릴 수 있다. 저는 처음에 이 부분을 실수해서 한참 헷갈렸고, 로그를 보면서 실행 시간이 왜 안 맞나 분석하는 데 시간을 꽤 썼다.

그리고 한 가지 더 짚고 넘어가자면, 깃허브 액션의 무료 사용 시간은 월 2,000분으로 제한되어 있어서 너무 복잡하거나 자주 돌리는 작업에는 부담이 될 수 있다. 저도 적당히 간단한 스크립트 돌리려고 설정한 건데, 만약 무거운 작업을 올리면 금방 무료 한도를 초과할 위험이 있으니 주의가 필요하다.

4. 직접 맞은 에러 2가지와 해결법

에러 ① 크론 스케줄이 등록됐는데 실행이 안 됨

YAML 파일을 깃허브 레포에 올리고 나서도 예정된 시간이 지나도록 Actions 탭에 기록이 전혀 뜨지 않아 한참을 멀뚱멀뚱 쳐다봤다. 알고 보니 두 가지 까다로운 조건이 있었다. 첫 번째는 깃허브가 60일 동안 레포에 아무 활동이 없으면 스케줄 트리거를 자동으로 멈추는 정책이다. 신규 레포를 만들고 YAML 파일만 커밋한 뒤 다른 작업을 안 하면 이 시스템에 걸린다. 두 번째는 스케줄 트리거가 기본 브랜치인 main에 YAML 파일이 있을 때만 작동한다는 점이다. 다른 브랜치에 배치하면 절대 실행되지 않았다. 그래서 저는 workflow_dispatch 옵션을 추가해서 수동으로 바로 실행해보면서 문제를 잡았다. 이 방법 덕분에 어떤 게 문제인지 훨씬 빨리 알 수 있었다.

에러 ② 로컬에서는 되는데 Actions에서만 ModuleNotFoundError

이건 정말 곤란했다. 로컬 환경에서는 아무 문제 없이 돌아가던 파이썬 스크립트가 깃허브 Actions 환경에서는 ModuleNotFoundError를 내뿜었다. 원인은 requirements.txt에 내가 쓰는 패키지 중 일부가 빠져 있었기 때문이다. 로컬에는 예전에 설치해 둔 패키지들이 남아 있어서 별 탈 없었는데, Actions의 가상 머신은 완전 새 환경이라 requirements.txt에 나와 있는 패키지만 설치한다. 그래서 저는 로컬에서 pip freeze > requirements.txt 명령어로 현재 환경에 깔린 패키지를 전부 기록한 뒤 다시 푸시했고, 그제야 오류가 사라졌다. 이런 기본적인 부분을 빼먹으면 한참을 헤맬 수밖에 없다는 교훈을 얻었다.