본문 바로가기
Docker

Docker와 GitHub Actions로 구축하는 CI/CD (1) : 지속 통합(CI)

by Byeong 2025. 4. 11.

 기존에는 코드 수정 시마다 직접 클라우드 인스턴스에 접속하여 수동으로 코드를 업데이트해야 했기 때문에 배포에 번거로움이 있었다. 이러한 과정을 자동화하고, 보다 빠르고 안정적인 배포를 위해 CI/CD 파이프라인을 도입하게 되었다.

CI/CD

CI(Continuous Integration) 지속 통합

지속적 통합은 개발자가 변경한 코드를 주기적으로 통합하고, 이를 자동으로 테스트하는 프로세스이다.

CD(Continuous Deployment / Continuous Delivery) 지속적 배포

지속적으로 통합된 코드를 자동으로 프로덕션 환경에 배포하는 프로세스이다.

 

예전에 docker를 학습하며 CI/CD를 이미 공부한 경험이 있어 이론 내용은 생략했다.  https://byeongtil.tistory.com/88


GitHub Action

Travis CI, Jenkins 등 CI/CD 도구가 있지만 유료인 경유가 많다. Github Actions의 public repository은 무료로 사용할 수 있어 선택했다.

개발자가 변경한 코드를 주기적으로 통합하고, 이를 자동으로 테스트하는 프로세스를 의미한다.


FastAPI test 코드 작성

# test_main.py
from fastapi.testclient import TestClient

from main import app

client = TestClient(app)
    
def test_teach_grades():
    response = client.get("api/teach/grades")
    assert response.status_code == 200
    data = response.json()
    print(data)
    assert isinstance(data, list)

pytest 모듈을 사용해서 테스트를 진행한다.

처음에는 tests/test_main.py 경로에 테스트파일을 작성했지만 main에 있는 FastAPI의 app을 가져오지 못하는 에러가 발생했다.

 

문제발생

from .main import app
ImportError: attempted relative import with no known parent package

해결 방법은 main.py와 같은 디렉토리 경로에 test_main.py 파일이 존재해야 pytest 파일이 작동했다.

 

출력

============================================================== test session starts ==============================================================
platform darwin -- Python 3.13.2, pytest-8.3.5, pluggy-1.5.0
rootdir: /Users/gimbyeongmin/Desktop/project/cheese/backend
plugins: anyio-4.8.0
collected 1 item                                                                                                                                

test_main.py .            

yml 파일 작성

name: CI/CD

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.13"

      - name: Install dependencies
        run: pip install -r backend/requirements.txt

      - name: Set environment variable 
        run: | 
          echo "SQLALCHEMY_ASYNC_DATABASE_URL=${{ secrets.SQLALCHEMY_ASYNC_DATABASE_URL }}" >> $GITHUB_ENV
          echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> $GITHUB_ENV

      - name: Run tests
        run: |
          cd backend
          pytest

구성 요소

  1. name : 워크플로우 이름.
  2. on : 어떤 이벤트로 워크플로우를 실행할지 정의.
  3. jobs : 워크플로우 안의 각 작업 단위.

Test 작업 설정

jobs에는 실행할 작업의 이름을 작성해줘야한다. 또 어떤 환경에서 실행할지 설정을 해줘야한다.

jobs:
  test:
    runs-on: ubuntu-latest

 

1.Repository 체크아웃

이제 순차적으로 실행할 작업을 작성해준다.

GitHub Actions는 기본적으로 코드에 접근이 불가하여 uses 를 통해 현재 저장소의 소스코드를 가져오게 한다.

- name: Checkout repository
uses: actions/checkout@v3

2.Python 셋업

GitHub Actions 머신에 파이썬을 설치

 uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.13"

 

3.의존성 설치

- name: Install dependencies
  run: |
    python -m pip install --upgrade pip
    pip install -r backend/requirements.txt

현재 프로젝트의 의존 패키지들을 requirements.txt 기준으로 설치.

 

4. 테스트 실행

- name: Run tests
  run: |
	  cd backend
	  pytest

문제 발생 1 : 들여쓰기

Invalid workflow file: .github/workflows/cicd.yml#L6
You have an error in your yaml syntax on line 6

yaml 파일의 경우 들여 쓰기가 매우 중요한데 해당 파일이 들여쓰기를 잘못해서 에러가 발생했다. 들여 쓰기를 수정해서 문제를 해결했다.


문제 발생 2 : .env

ERROR test_main.py - sqlalchemy.exc.ArgumentError: Expected string or URL object, got None

SQLALCHEMY_ASYNC_DATABASE_URL 값이 .env 파일에 정의되어 있고 코드를 통해 불러오지만 GitHub Actions에서는 .env 파일이 기본적으로 존재하지 않기 때문에 None이 된다.

해결 방법

GitHub Secrets를 사용해서 환경변수를 주입한다.

- name: Set environment variables
  run: echo "SQLALCHEMY_ASYNC_DATABASE_URL=${{ secrets.SQLALCHEMY_ASYNC_DATABASE_URL }}" >> $GITHUB_ENV

문제발생 3 : database

FAILED test_main.py::test_teach_grades - sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: grades
[SQL: SELECT grades.id, grades.title 
FROM grades ORDER BY grades.id]
(Background on this error at: <https://sqlalche.me/e/20/e3q8>)

데이터베이스를 테스트용인 SQLite 를 사용중이다. 하지만 해당 코드에서는 alembic를 이용해 데이터베디스 생성과 테이블을 생성하지 않아 문제가 발생했다.

해결 방법

CI/CD 적용 테슽를 위한 작업이라 “/” 경로의 간단한 접속 테스트를 진행 하는 것으로 마무리했다.

# main.py
@app.get("/")
async def read_main():
    return {"msg": "Hello FastAPI"}
# test_main.py
def test_teach_grades():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"msg": "Hello FastAPI"}

참고

https://docs.github.com/en/actions

https://fastapi.tiangolo.com/tutorial/testing/#using-testclient