CI/CD pipelines for REST API Python project
Python virtual environment
Một số chương trình viết bằng ngôn ngữ Python sẽ cần sử dụng đến các gói thư viện cũng như một số module bên ngoài các thư viện.
Điều này nảy sinh một vấn đề: mỗi project sẽ cần một số thư viện khác nhau. Ví dụ project A cần django 3.2 còn project B cần django 4.0
Vì vậy ta cần một môi trường cô lập cho từng project, mỗi môi trường sẽ chứa phiên bản các thư viện khác nhau, cần thiết để chạy project đó.
Hơn nữa ta cũng quan tâm tới tính đóng gói và khả năng tái sử dụng của project. Hoặc trong trường hợp ta chỉ muốn test một số project mà không muốn cài đặt các thư viện đó trực tiếp lên trên hệ thống.
Công cụ để giải quyết vấn đề này là virtual environment. Python có module venv hỗ trợ ta làm việc này: https://docs.python.org/3/library/venv.html
Đầu tiên với venv, ta sẽ chọn một thư mục làm việc, rồi tiến hành tạo môi trường ảo trong thư mục đó:
python3 -m venv .myenv
Sau đó kích hoạt môi trường ảo:
source .venv/bin/activate
Sau đó ta tiến hành tải các thư viện cần thiết. Có thể tải lần lượt bằng pip:
pip install fastapi uvicorn
Hoặc ta có thể tạo một file requirements.txt chứa danh sách các thư viện cần thiết, sau đó cài đặt bằng lệnh:
pip install --no-cache-dir -r requirements.txt
Để xuất ra phiên bản cùng các thư viện đã tải:
pip freeze > requirements.txt
Ngoài venv ra, ta cũng có một số công cụ khác hỗ trợ cho việc tạo môi trường ảo. Ở đây ta sẽ tìm hiểu về uv, một công cụ quản lí gói cho Python được viết bằng Rust.
Tài liệu hướng dẫn sử dụng mọi người có thể tham khảo tại đây: https://docs.astral.sh/uv/
Để bắt đầu làm việc với uv, sau khi cài đặt ta sẽ chạy lệnh sau trong thư mục chứa project:
uv init my-project
cd my-project
uv python pin 3.14

Đầy đủ cấu trúc thư mục sau khi khởi tạo project:
❯ ls -la
total 16
drwxr-xr-x. 1 dvck13 dvck13 118 May 5 20:43 .
drwxr-xr-x. 1 dvck13 dvck13 304 May 5 20:43 ..
drwxr-xr-x. 1 dvck13 dvck13 82 May 5 20:43 .git
-rw-r--r--. 1 dvck13 dvck13 109 May 5 20:43 .gitignore
-rw-r--r--. 1 dvck13 dvck13 88 May 5 20:43 main.py
-rw-r--r--. 1 dvck13 dvck13 156 May 5 20:43 pyproject.toml
-rw-r--r--. 1 dvck13 dvck13 5 May 5 20:43 .python-version
-rw-r--r--. 1 dvck13 dvck13 0 May 5 20:43 README.md
Khi chạy uv add fastapi uvicorn thì các thư viện này sẽ được thêm vào pyproject.toml và tự động cài đặt.
Để chạy một chương trình python thì ta xài lệnh :
uv run python main.py
REST API and FastAPI
Tiếp theo ta sẽ tìm hiểu cách xây dựng một REST API đơn giản bằng framework FastAPI.
Đầu tiên là về REST API. REST API là một giao diện lập trình ứng dụng (API) tuân theo các nguyên tắc của kiến trúc REST được sử dụng trong việc giao tiếp giữa client và server.
REST là một kiểu kiến trúc để viết API, nó sử dụng các phương thức HTTP như GET, POST, PUT, DELETE đơn giản để tạo giao tiếp giữa các máy. Vì vậy, thay vì sử dụng một URL cho việc xử lý một số thông tin người dùng, REST gửi một yêu cầu HTTP như GET, POST, DELETE, v.v đến một URL để xử lý dữ liệu.
Các phương thức HTTP thường dùng bao gồm:
- GET (SELECT): trả về một resource hoặc một danh sách resource.
- POST (CREATE): tạo mới một resource
- PUT (UPDATE): cập nhật thông tin cho resource
- DELETE: Xóa một resource
Một số nguyên tắc cơ bản của REST API:
- Client - server: mô hình phân chia rõ ràng giữa client và server. Client chỉ chịu trách nhiệm giao diện người dùng còn server sẽ quản lí dữ liệu và logic nghiệp vụ.
- Stateless: mỗi yêu cầu từ client đến server phải chứa tất cả thông tin cần thiết để server hiểu và xử lý yêu cầu đó. Server không lữu trữ bất kì trạng thái nào của client giữa các yêu cầu.
… Phần còn lại mọi người tham khảo tại IBM
Còn về phần FastAPI thì đây là một framework web hiện đại, được xây dựng dựa trên Python và cung cấp một số tính năng mạnh mẽ để xây dựng các ứng dụng web và API.
Tài liệu: FastAPI
CI/CD pipelines
CI/CD là viết tắt của Continuous Integration và Continuous Delivery/Deployment. Đây là một quy trình giúp tự động hóa việc xây dựng, kiểm thử và triển khai phần mềm.
CI là bước tích hợp liên tục, mỗi khi lập trình viên đẩy code mới lên git, hệ thống sẽ tự động kiểm tra xem code có bị lỗi không (ví dụ: chạy unit test, kiểm tra style code, build project,…). Nhờ đó, ta có thể phát hiện lỗi sớm và sửa khi còn có thể.
Quy trình CI diễn ra như sau:
- Các lập trình viên commit và push code của mình lên repo
- CI server giám sát repo và kiểm tra liên tục xem có sự thay đổi nào không. Khi phát hiện ra có sự thay đổi thì CI server bắt đầu quá trình CI
- Build code -> sau đó chạy các test case.
- Nếu có lỗi thì CI server sẽ thông báo lỗi, nếu không thì CI server sẽ hợp nhất code mới vào branch chính.
Còn CD là quá trình triển khai tự động sau khi quá trình triển khai thành công. CD có thể là Continuous Delivery hoặc Continuous Deployment. Continuous Delivery là code được đóng gói sẵn và sẵn sàng triển khai nhưng vẫn cần một bước thủ công cuối để deploy lên production. Còn Continous Deployment là tự động hoàn toàn luôn.

Lí thuyết xong xuôi, giờ mình thử xây dựng một pipeline CI/CD đơn giản cho một project (vibe code) của mình: NT208
Để build pipeline CI/CD thì mình sẽ sử dụng Github Actions, workflow syntax mọi người xem tại đây.
Mỗi workflow trong Github Actions là một tập hợp các hành động (actions) được định nghĩa trong file YAML. Các actions này bao gồm các lệnh cụ thể như build ứng dụng, test và triển khai ứng dụng. Ta có thể sử dụng các actions được cung cấp sẵn bởi Github. Tham khảo tại đây
Github actions hoạt động như sau:
- Đọc tệp cấu hình YAML tương ứng trong thư mục
.github/workflowscủa repo. - Thực hiện các jobs được cấu hình trong workflow
- Mỗi job lại được chia thành các step để thực hiện các tác vụ cụ thể.
Để tạo workflow, đầu tiên, truy cập vào repo rồi chọn tab Actions

Ở đây có một số workflow có sẵn, mình sẽ chọn Docker image workflow.
Sau đó github sẽ tạo sẵn cho mình một file docker-image.yml nội dung như sau:
name: Docker Image CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build the Docker image
run: docker build . --file Dockerfile --tag my-image-name:$(date +%s)
Phân tích từng phần.
nameở đây để chỉ tên của workflow. Có cũng được, không có cũng không saoonđược dùng để trigger một workflow. Nó sẽ cho ta biết sự kiện nào làm pipeline chạy. Ở đây là event push và pull requests. Khi ta push code vào branch main thì nó sẽ chạy pipeline. Tương tự với pull requests.
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
joblà danh sách các job, mỗi job sẽ được chạy trên một máy ảo riêng gọi là runner. Ở đây,runs-onkhai báo máy ảoubuntu-latest.buildlà một job_id hay tên của job được gọi.- Cuối cùng là
stepsđể khai báo các bước trong một job.
Workflow trên failed vì khi trigger nó sẽ tìm file Dockerfile trực tiếp trong thư mục gốc của repo để build image. Trong khi mình lại để độc lập Dockerfile trong từng thư mục con của project.

Vậy thì giờ mình sẽ sửa lại để xem nó có chạy được không.
name: Docker Image build
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- name: checkout source code
uses: actions/checkout@v4
- name: build api image
run: docker build -f apps/api-python/Dockerfile -t cvht-api:$ .
- name: build worker image
run: docker build -f apps/worker/Dockerfile -t cvht-worker:$ .
- name: build web image
run: docker build -f apps/web/Dockerfile -t cvht-web:$ .
Sửa lại chạy từng dockerfile trong các thư mục con.

Thực ra ta không cần phải build từng image một cách thủ công như trên. Trong thư mục chính đã có sẵn một file docker-compose.yml để thực hiện build toàn bộ các image nên mình chỉ cần viết một workflows cho docker compose là được.
name: Docker compose CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
docker-compose:
runs-on: ubuntu-latest
steps:
- name: checkout source code
uses: actions/checkout@v4
- name: build all services
run: docker compose build
uses được chỉ định khi ta muốn chạy một action có sẵn nào đó còn run được chỉ định khi ta muốn chạy một lệnh shell nào đó.
Bây giờ ta sẽ tới uv và Python fast api. Doc của uv có hướng dẫn cách sử dụng uv trong Github actions: Docs
Ta sẽ sử dụng astral-sh/setup-uv action để cài đặt uv trong workflow. Tiếp theo, để test python code ta sẽ sử dụng ruff, một công cụ kiểm tra lỗi và định dạng code cho python.
Đầu tiên khai báo các event để trigger workflow. Ở đây ta sẽ chỉ định cụ thể đường dẫn ‘apps/api-python/**’ để khi có sự thay đổi trong thư mục này thì workflow mới chạy, và chỉ riêng trong thư mục này mà thôi , các thư mục khác không chứa code python nên không cần thiết phải chạy workflow.
name: Python CI with uv
on:
push:
branches:
- main
paths:
- 'apps/api-python/**'
- '.github/workflows/python-ci.yml'
pull_requests:
branches:
- main
paths:
- 'apps/api-python/**'
- '.github/workflows/python-ci.yml'
concurrency:
group: python-ci
cancel-in-progress: true
Kế đến là jobs. Ta cần tải uv lên trên runner trước khi chạy các lệnh test khác.
jobs:
fastapi:
name: test python code with uv and fastapi application
runs-on: ubuntu-latest
default:
run:
shell: bash
working-directory: apps/api-python
steps:
- name: checkout source code
uses: actions/checkout@v6
- name: setup python
uses: actions/setup-python@v6
with:
python-version: 3.12
- name: setup uv
uses: astral-sh/setup-uv@v5
- name: install dependencies
run: uv pip install --system -r requirments-dev.txt
- name: run tests
run: ruff check app scripts
- name: compile python source
run: python -m compileall app scripts
- name: Import FastAPI app
run: python -c "from app.main import app; print(app.title, app.version)"
- name: Pytest if available
run: |
if [ -d tests ] && find tests -type f \( -name 'test_*.py' -o -name '*_test.py' \) | grep -q .; then
pytest -q
else
echo 'No Python tests found, skipping pytest'
fi
Note: Github Actions cheat sheet
Vậy ta đã có một quy trình CI tạm thời. Đối với CD, ta sẽ triển khai lên trên AWS.
Một quy trình đơn giản có thể trông như sau:

Ta thực hiện push code lên trên github, sau khi push code, github actions sẽ chạy test/build. Kế đến nó sẽ build docker image rồi push lên trên các nền tảng lưu trữ như Docker Hub hoặc AWS ECR. Ở đây mình sẽ chọn Docker Hub để demo và cuối cùng AWS EC2 sẽ pull image về và chạy.
Tạo một EC2 Instance đơn giản:

Tiếp theo đăng nhập vào Docker Hub để lấy DOCKERHUB_USERNAME và DOCKERHUB_TOKEN.
Sau đó thêm các giá trị này vào trong phần Settings của repo:
