..

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/workflows củ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 sao
  • on đượ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" ]
  • job là 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-on khai báo máy ảo ubuntu-latest.
  • build là 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_USERNAMEDOCKERHUB_TOKEN.

Sau đó thêm các giá trị này vào trong phần Settings của repo:

...