Devops #1 Python 프로젝트 CI/CD & 클라우드 빌드

Devops #1 Python 프로젝트 CI/CD & 클라우드 빌드 #

#2025-08-11


실습 #

  • 메이크파일, 린팅, 테스트와 같이 파이썬 프로젝트 스캐폴딩에 필수적인 요소가 포함된 깃허브 저장소를 생성해보자. 그리고 간단하게 코드 포매팅을 수행하도록 메이크파일 스크립트를 작성해보자.

  • 깃허브 액션을 사용하여 두개 이상의 파이썬 버전에 대해 깃허브 프로젝트 테스트를 수행해보자.

  • 클라우드 네이티브 빌드 서버(AWS 코드빌드, GCP 클라우드 빌드, 애저 DevOps 파이프라인)를 사용하여 지속적 통합을 수행해보자.

  • 깃허브 프로젝트를 도커 파일로 컨테이너화하고, 자동으로 컨테이너 레지스트리에 새로운 컨테이너가 등록되도록 만들어보자.

  • locust 또는 loader io와 같은 부하 테스트 프레임워크를 사용하여 애플리케이션에 대한 간단한 부하 테스트 코드를 작성한다. 그리고 스테이징 브랜치에 변경 사항을 푸시할 때 이 테스트가 자동으로 수행되도록 만들어보자.

#

1. 파이썬 프로젝트 스캐폴딩 + 메이크파일/린팅/테스트 + 포매팅 #

#1 새 프로젝트 만들기 (로컬)

# 프로젝트 폴더 생성
mkdir py-skeleton && cd py-skeleton

# 가상환경
python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\Activate.ps1

# 기본 폴더 구조
mkdir -p src/awesome_pkg tests

#2 최소 패키지/테스트 코드 넣기

# src/awesome_pkg/__init__.py
__all__ = ["add"]

def add(a: int, b: int) -> int:
    return a + b
# tests/test_add.py
from awesome_pkg import add

def test_add():
    assert add(2, 3) == 5

#3 개발 도구 설치 파일

  • ruff(린터+포매터), pytest(테스트), mypy(타입체크)만 사용
# pyproject.toml
[project]
name = "awesome-pkg"
version = "0.1.0"
requires-python = ">=3.9"

[tool.ruff]
line-length = 100
target-version = "py39"
lint.select = ["E","F","I","B","UP"]   # 기본 + 모던화 제안
lint.ignore = ["E501"]                  # 길이제한은 포매터가 처리
src = ["src"]
extend-exclude = ["tests/fixtures"]

[tool.ruff.format]
quote-style = "double"

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"

[tool.mypy]
python_version = "3.9"
packages = ["awesome_pkg"]
strict = true
ignore_missing_imports = true
# requirements-dev.txt
ruff
pytest
mypy
# .gitignore
.venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.cache/

#4 메이크파일 작성 (포매팅/린팅/테스트 일괄 실행)

# Makefile

PY := .venv/bin/python
PIP := .venv/bin/pip
RUFF := .venv/bin/ruff
PYTEST := .venv/bin/pytest
MYPY := .venv/bin/mypy

.PHONY: help init install format lint test typecheck check clean

help:
	@echo "make init        - 가상환경과 기본 의존성 설치"
	@echo "make format      - 코드 포매팅 (ruff format)"
	@echo "make lint        - 린트 검사 (ruff)"
	@echo "make test        - 테스트 실행 (pytest)"
	@echo "make typecheck   - 타입체크 (mypy)"
	@echo "make check       - lint+typecheck+test 종합"
	@echo "make clean       - 캐시/산출물 정리"

init:
	python3 -m venv .venv
	$(PIP) install -U pip
	$(PIP) install -r requirements-dev.txt

format:
	$(RUFF) format src tests

lint:
	$(RUFF) check src tests

test:
	$(PYTEST)

typecheck:
	$(MYPY) src

check: lint typecheck test

clean:
	rm -rf .pytest_cache .mypy_cache .ruff_cache __pycache__ */__pycache__

#5 의존성 설치 & 동작 확인

make init       # 가상환경+개발도구 설치
make format     # 포매팅
make lint       # 린트
make typecheck  # 타입체크
make test       # 테스트
make check      # 일괄 점검

#6. 깃허브 저장소 만들고 푸시

git init
git add .
git commit -m "feat: project scaffold with makefile/lint/test/format"

# 깃허브에서 빈 저장소 생성 후, 아래처럼 원격 추가/푸시
git branch -M main
git remote add origin https://github.com/<YOUR_ID>/py-skeleton.git
git push -u origin main

#

2. 여러 파이썬 버전으로 GitHub Actions 테스트 #

#1 리포지토리 준비

#2 브랜치 생성

git checkout -b ci-setup

#3 워크플로우 폴더 만들기

mkdir -p .github/workflows

#4 CI 설정 파일 생성

  • 파일 경로: .github/workflows/ci.yml
  • 내용: 매트릭스로 3.9~3.12 테스트, Makefile 타깃 사용
name: CI

on:
  push:
    branches: [ main, develop, staging ]
  pull_request:
    branches: [ main, develop, staging ]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: [ "3.9", "3.10", "3.11", "3.12" ]
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'
          cache-dependency-path: |
            requirements-dev.txt
            pyproject.toml

      - name: Install dev deps
        run: |
          python -m pip install -U pip
          pip install -r requirements-dev.txt

      - name: Lint
        run: make lint

      - name: Format check (ruff format --check)
        run: ruff format --check src tests

      - name: Type check
        run: make typecheck

      - name: Test
        run: make test

#5 커밋 & 푸시

git add .github/workflows/ci.yml
git commit -m "chore: add CI for multi-python versions"
git push -u origin ci-setup

#6 PR 생성

  • GitHub에서 ci-setup → main으로 Pull Request 생성
  • PR이 생성되면 Actions 탭에서 파이썬 3.9/3.10/3.11/3.12 네 개 잡이 병렬로 도는 걸 볼 수 있음.

#7 배지 추가

  • README.md에 아래 한 줄 추가(리포지토리 경로는 본인 것으로 교체)
![CI](https://github.com/<USER>/<REPO>/actions/workflows/ci.yml/badge.svg)

#cf

  1. Secrets 불필요: 단순 테스트만 하면 깃허브 액션 기본 권한으로 충분.
  2. 기본은 ubuntu-latest지만, OS 매트릭스를 늘리고 싶으면 다음과같이 설정
runs-on: ${{ matrix.os }}
strategy:
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    python-version: ["3.9","3.10","3.11","3.12"]
  1. Makefile 없이도 가능. 위 Lint/Type/Test 단계를 ruff/mypy/pytest 직접 실행으로 바꿔도 동작.

#

3. 클라우드 네이티브 빌드 서버로 CI #

1. AWS CodeBuild

#1 리포에 buildspec 추가

  • 리포 루트에 buildspec.yml 생성
version: 0.2

env:
  variables:
    PIP_CACHE_DIR: "/root/.cache/pip"

phases:
  install:
    runtime-versions:
      python: 3.12
    commands:
      - python -m pip install --upgrade pip
      - pip install -r requirements-dev.txt
  pre_build:
    commands:
      - ruff check src tests
      - ruff format --check src tests
      - mypy src
  build:
    commands:
      - pytest -q
artifacts:
  files:
    - "**/*"
  discard-paths: no
  • 여러 파이썬 버전을 돌리고 싶다면 CodeBuild 프로젝트를 버전별로 2~3개 만들거나, Docker 이미지를 바꿔 실행하는 별도 프로젝트를 추가하는 방식이 단순함.

#2 CodeBuild 프로젝트 만들기(콘솔)

1. 사전 준비
- 깃허브 리포에 buildspec.yml이 루트에 있어야 함.
- 리포 권한: 본인 GitHub 계정이 관리자여야 함.

1. AWS 콘솔 접속
- 콘솔 검색창 → CodeBuild → 좌측 Build projects → Create build project 클릭.

1. Project configuration
- Project name: py-skeleton-ci (원하는 이름)
- (선택) Description: “Python lint/test CI”

1. Source (소스 설정)
- Source provider: GitHub
- Repository: “Connect using OAuth” 클릭 → GitHub 로그인/승인 → 리포 선택
- Webhook: Enable 체크(푸시 시 자동 빌드)
- Primary source webhook events: 기본값 유지(Push로 충분)

1. Environment (빌드 환경)

- Environment image: Managed image
- Operating system: Ubuntu
- Runtime(s): Standard
- Image: aws/codebuild/standard:7.0 선택
- Image version: Always use the latest image
- Environment type: Linux
- Service role: “New service role” 선택(자동 생성)
- Additional configuration:
  - Privileged: 비활성(Docker 빌드가 필요할 때만 활성)
  - (선택) Compute: 기본 Small(빠른 빌드 원하면 Medium)

6. Buildspec (빌드 스펙)
- Build specifications: Use a buildspec file 선택
- Buildspec name: buildspec.yml (리포 루트에 있는 그 파일)

7. Artifacts (산출물)
- Artifacts type: No artifacts (테스트/린팅만이면 산출물 불필요)

8. Logs (로그)
- CloudWatch logs: Enabled
- Group/Stream은 기본값 그대로(자동 생성)

9. (선택) Cache (pip 캐시)
- Cache: Enabled
- Type: Local → Custom cache 체크 → 경로에 /root/.cache/pip 입력

10. Triggers (브랜치 트리거)
- Build triggers: Enable webhook 이미 켰다면 OK
- Filter groups에서 브랜치에 main, develop, staging 추가
  - Example: EVENT: PUSH + BASE_REF: ^refs/heads/(main|develop|staging)$

11. 만들기
- 맨 아래 Create build project 클릭.

12. 권한 확인(IAM 자동역할)
- 생성 후 상단에 “Service role” 링크 클릭 → IAM에서 자동 생성된 codebuild-py-skeleton-ci-service-role 확인.
- 보통 기본 정책으로 충분(CloudWatch Logs/CodeBuild 권한). 별도 리소스 접근이 필요 없다면 추가 작업 없음.

13. 첫 빌드 실행(테스트)

- 프로젝트 상세 화면 → Start build 클릭 → 기본값 그대로 Start build.
- Build history에서 진행 상황 확인 → Status가 Succeeded면 성공.
- 실패하면 Phase details에서 어느 단계(install/pre_build/build)에서 실패했는지 로그 확인.

14. 푸시로 자동 트리거 확인

- 로컬에서 아무 커밋 후 git push origin main (또는 develop/staging)
- GitHub → 리포의 Settings → Webhooks에 CodeBuild 웹훅이 생긴 것 확인.
- AWS CodeBuild Build history에 새 빌드가 자동으로 뜨는지 확인.

#

2. GCP Cloud Build

#1 리포에 cloudbuild.yaml 추가

steps:
  - name: 'python:3.12'
    entrypoint: bash
    args:
      - -lc
      - |
        python -m pip install --upgrade pip
        pip install -r requirements-dev.txt
        ruff check src tests
        ruff format --check src tests
        mypy src
        pytest -q
# 캐시(선택): pip 캐시용 볼륨
options:
  volumes:
    - name: pip-cache
      path: /root/.cache/pip

#2 트리거 연결

1. GCP 프로젝트/권한 준비
- GCP 콘솔 상단 프로젝트가 맞는지 확인.
- Cloud Build API가 꺼져 있다면 켜기(Enable).
- 결제 활성화 필요하면 켜두기.

2. GitHub(App) 연결
- 콘솔 좌측 메뉴 → Cloud Build → Triggers → Manage repositories(또는 “Connect repository”).
- GitHub (Cloud Build GitHub App) 선택 → GitHub 계정으로 로그인/Authorize.
- 연결할 Organization/Repository 선택 → Connect.

3. 트리거 생성
- Create trigger 클릭.
- Event: Push to a branch 선택.
- Repository: 방금 연결한 리포 선택.
- Branch: 정규식 입력 → ^main$|^develop$|^staging$
  - 의미: main · develop · staging 브랜치에 push될 때만 발동.

4. 빌드 설정 지정
- Configuration: Cloud Build configuration file (yaml or json) 선택.
- Location: Repository.
- Cloud Build configuration file: cloudbuild.yaml (루트가 아니면 경로 입력, 예: .cloud/cloudbuild.yaml).
- (선택) Substitution variables: 필요 시 버전 등 넘길 값 정의(예: _PY_VERSION=3.12).

5. 저장

- Create 클릭 → 트리거 목록에 생성됐는지 확인.

6. 동작 확인(첫 빌드)
- 리포의 README에 공백 한 줄 추가하고 main/develop/staging 중 하나에 push.
- Cloud Build → History에서 실행되는지 확인.
- 로그에서 pip install, ruff check, ruff format --check, mypy, pytest가 순서대로 실행되는지 본다.

cf) cloudbuild.yaml 예시(루트에 있어야 함)

steps:
  - name: 'python:3.12'
    entrypoint: bash
    args:
      - -lc
      - |
        python -m pip install --upgrade pip
        pip install -r requirements-dev.txt
        ruff check src tests
        ruff format --check src tests
        mypy src
        pytest -q
options:
  volumes:
    - name: pip-cache
      path: /root/.cache/pip

cf2) 여러 파이썬 버전으로 돌리기 – 두 가지 방법

  1. 스텝을 여러 개 두기
steps:
  - name: 'python:3.9'  # 동일 스크립트
    entrypoint: bash
    args: [ "-lc", "…" ]
  - name: 'python:3.12'
    entrypoint: bash
    args: [ "-lc", "…" ]
  1. 트리거를 2개 만들고, 각각 Substitution으로 버전 넘기기
  • 트리거1: _PY_VERSION=3.9

  • 트리거2: _PY_VERSION=3.12

  • cloudbuild.yaml에서 ${_PY_VERSION} 사용:

steps:
  - name: "python:${_PY_VERSION}"
    entrypoint: bash
    args: [ "-lc", "…" ]

#

3. Azure DevOps Pipelines

#1 리포에 buildspec 추가

  • 리포 루트에 azure-pipelines.yml 생성
trigger:
  branches:
    include: [ main, develop, staging ]

pool:
  vmImage: 'ubuntu-latest'

strategy:
  matrix:
    py39:
      PY: '3.9'
    py310:
      PY: '3.10'
    py311:
      PY: '3.11'
    py312:
      PY: '3.12'

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '$(PY)'

  - script: |
      python -m pip install --upgrade pip
      pip install -r requirements-dev.txt
    displayName: Install dev deps

  - script: ruff check src tests
    displayName: Lint (ruff)

  - script: ruff format --check src tests
    displayName: Format check

  - script: mypy src
    displayName: Type check

  - script: pytest -q
    displayName: Test (pytest)

#2 Azure DevOps 파이프라인 생성

1. Azure DevOps 접속
- 브라우저에서 dev.azure.com → 본인 Organization 선택 → Project 선택

2. 새 파이프라인 만들기
- 좌측 메뉴 Pipelines → Create Pipeline (또는 New pipeline)

3. 코드 위치 선택
- “Where is your code?” 화면에서 GitHub 선택
- 처음이면 GitHub Authorize(연동 승인) 창이 뜸 → Authorize로 진행
- 연동 후 리포지토리 목록에서 해당 리포 클릭

4. 구성 방식 선택
- “Configure your pipeline” 화면에서 Existing Azure Pipelines YAML file 선택

5. 브랜치/파일 경로 지정
- Branch: main(또는 사용 중인 기본 브랜치) 선택
- Path: /azure-pipelines.yml 지정(루트에 위치한 파일)
- Continue 클릭

6. 저장 & 실행
- 상단 Run 또는 Save and run 클릭
- 커밋 메시지(자동 생성됨) 확인 → Save and run 확정

7. 권한 승인(처음 1회)
- 실행 직후 상단에 Authorize 또는 Grant permission 배너가 뜨면 클릭해서 허용 (GitHub 리포 접근 / Service connection 권한 부여)

8. 실행 확인
- Pipelines → Runs에서 방금 실행된 파이프라인 클릭
- 단계별 로그(Install → Lint → Format check → Type check → Test)가 성공(Succeeded)인지 확인

9. 자동 트리거 확인
- 로컬에서 아무 변경(예: README 공백 추가) → git push origin main
- Runs에 새 실행이 자동으로 생성되는지 확인

cf)

  • 만약 GitHub 권한 에러가 나면?

    • 좌측 하단 Project settings → Service connections → New service connection → GitHub → Grant access(또는 OAuth) → 연결 생성
    • 다시 Pipelines → Create pipeline부터 진행
  • 이미 리포에 있어야 하는 파일 예시

    • azure-pipelines.yml (다중 파이썬 버전 매트릭스)
trigger:
  branches:
    include: [ main, develop, staging ]

pool:
  vmImage: 'ubuntu-latest'

strategy:
  matrix:
    py39: { PY: '3.9' }
    py310: { PY: '3.10' }
    py311: { PY: '3.11' }
    py312: { PY: '3.12' }

steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '$(PY)'

  - script: |
      python -m pip install -U pip
      pip install -r requirements-dev.txt
    displayName: Install dev deps

  - script: ruff check src tests
    displayName: Lint (ruff)

  - script: ruff format --check src tests
    displayName: Format check

  - script: mypy src
    displayName: Type check

  - script: pytest -q
    displayName: Test (pytest)

#