CI/CD 기초 (5) - Practice: Full Pipeline
FastAPI 앱 하나로 PR마다 도는 CI부터 Docker 이미지를 GHCR에 올리는 CD까지 전체 파이프라인을 실습으로 완성합니다.
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 job은 2편의 CI에 4편의 pip 캐시를 더한 것입니다. 새로운 내용은 없습니다.
needs와 if는 3편 그대로입니다. 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-action은 docker build와 docker 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)면 실무의 워크플로 파일 대부분을 읽을 수 있습니다.