포스트

CI/CD 기초 (5) - Practice: Full Pipeline

FastAPI 앱 하나로 PR마다 도는 CI부터 Docker 이미지를 GHCR에 올리는 CD까지 전체 파이프라인을 실습으로 완성합니다.

CI/CD 기초 (5) - Practice: Full Pipeline

CI/CD 기초 시리즈의 마지막 편입니다. 1편부터 4편까지의 조각을 전부 조립해, 실제 저장소 하나에 완전한 파이프라인을 만듭니다. Docker가 처음이라면 Docker 기초 시리즈를 먼저 보는 것을 권합니다.

만들 것

작은 API 서버 하나와, 그것을 감싸는 파이프라인입니다. 완성되면 이렇게 동작합니다.

flowchart LR
    A[PR 열기] --> B[CI: 린트 + 테스트]
    B -->|초록이어야| C[main에 merge]
    C --> D[다시 CI]
    D -->|성공| E[Docker 이미지 빌드]
    E --> F[GHCR에 push]
    F --> G[서버에서 pull 해서 실행]

배포 대상은 GHCR(GitHub Container Registry)까지입니다. “registry에 최신 이미지가 올라가 있고, 아무 서버나 그것을 받아 실행하면 되는 상태”를 만드는 것이 이번 실습의 목표입니다. registry 뒤편, 즉 실제 서버에 올리는 부분은 AWS 해부 5편에서 EC2 기준으로 다룬 적이 있습니다.

앱과 저장소 구조

FastAPI로 endpoint 두 개짜리 앱을 만듭니다. 저장소 구조는 이렇습니다.

1
2
3
4
5
6
7
8
9
10
my-app/
├── app/
│   └── main.py
├── tests/
│   └── test_main.py
├── requirements.txt
├── Dockerfile
└── .github/
    └── workflows/
        └── ci.yml
1
2
3
4
5
6
7
8
9
10
11
12
# app/main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "ok"}

@app.get("/add")
def add(a: int, b: int):
    return {"result": a + b}
1
2
3
4
5
6
7
8
9
10
11
# tests/test_main.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_health():
    assert client.get("/health").json() == {"status": "ok"}

def test_add():
    assert client.get("/add", params={"a": 2, "b": 3}).json() == {"result": 5}

requirements.txt에는 fastapi, uvicorn, pytest, httpx, ruff를 적습니다. Dockerfile은 최소 구성입니다.

1
2
3
4
5
6
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ app/
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

워크플로 전체

이번 시리즈의 최종 결과물입니다. 전체를 먼저 싣고 아래에서 부분별로 짚습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: "pip"
      - run: pip install -r requirements.txt
      - run: ruff check .
      - run: pytest

  build-and-push:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}

부분별로 읽기

test job2편의 CI에 4편의 pip 캐시를 더한 것입니다. 새로운 내용은 없습니다.

needs와 if3편 그대로입니다. PR에서는 test만 돌고, main에 합쳐졌을 때만 test 성공을 전제로 build-and-push가 돕니다.

permissions: packages: write는 이 job에게 GHCR에 쓸 권한을 주는 선언입니다. 4편에서 말한 자동 발급 토큰 GITHUB_TOKEN은 기본적으로 최소 권한만 갖고 있어서, 패키지(이미지)를 올리려면 이렇게 권한을 명시해야 합니다.

login-action은 GHCR에 로그인하는 step입니다. github.actor(워크플로를 실행시킨 계정)와 자동 발급 토큰을 쓰므로 Secrets를 직접 등록할 것이 없습니다. Docker Hub나 AWS ECR 같은 외부 registry로 바꾸면 이 부분에 진짜 자격 증명이 필요해지고, 그때 4편의 Secrets 등록 절차를 쓰게 됩니다.

build-push-actiondocker builddocker push를 한 번에 하는 action입니다. 태그를 두 개 다는 것에 이유가 있습니다.

  • :latest: 항상 최신 배포본을 가리키는 태그입니다. 서버는 보통 이것을 받습니다.
  • :${{ github.sha }}: 이 이미지를 만든 커밋의 해시입니다. 어떤 이미지가 어떤 코드에서 나왔는지를 영구히 연결해 줍니다. 문제가 생겼을 때 “지금 서버에 뜬 이미지가 정확히 어느 커밋인가”를 바로 답할 수 있고, 직전 커밋의 태그로 되돌리는 롤백도 가능해집니다. latest 하나만 쓰면 이 연결이 끊깁니다.

동작 확인

merge 후 Actions 탭에서 두 job이 순서대로 초록이 되는 것을 확인하고, 저장소 메인 페이지 우측의 Packages에서 올라간 이미지를 확인합니다. 마지막으로 아무 머신에서나 받아서 띄워봅니다.

1
2
3
docker run -p 8000:8000 ghcr.io/USER/my-app:latest
curl localhost:8000/health
# {"status":"ok"}

이 curl이 성공하는 순간이 시리즈의 목표 지점입니다. 코드를 고치고 PR을 합치는 것만으로, 검증된 최신 실행본이 registry에 준비되는 상태가 되었습니다. 서버 쪽에 “주기적으로 새 이미지를 받아 재시작”하는 장치나 3편의 deploy job을 붙이면 완전한 Continuous Deployment가 됩니다.

시리즈를 마치며

1편의 문제의식은 “반복 작업의 품질이 사람의 성실함에 달려 있다”는 것이었습니다. 그 뒤로 한 일은 사람이 하던 확인과 배포를 하나씩 파일로 옮겨 적는 것이었습니다. 2편에서 검사를, 3편에서 배포를, 4편에서 그 사이의 부품을, 그리고 이번 편에서 전체를 조립했습니다.

여기서 더 나아가는 방향은 대체로 셋입니다. 배포 대상을 실제 클라우드로 넓히는 쪽은 AWS 해부 시리즈, 이미지와 컨테이너를 더 깊게 이해하는 쪽은 Docker 기초 시리즈, 파이프라인의 전제인 협업 흐름을 다지는 쪽은 Git 스터디 노트로 이어집니다. 어느 쪽으로 가든, 이 시리즈의 여섯 단어(workflow, event, job, step, needs, secrets)면 실무의 워크플로 파일 대부분을 읽을 수 있습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.