Перейти к содержимому

Практика 8. CI/CD и DevOps на практике


Цель работы

Освоить основы CI/CD с GitHub Actions: создание пайплайнов для линтинга, тестирования, сборки и публикации Docker-образов.


Часть 1. Git для CI/CD

Задание 1.1. Подготовка репозитория

  1. Создайте новый каталог проекта:

    Окно терминала
    mkdir -p ~/cicd-lab && cd ~/cicd-lab
    git init
  2. Создайте структуру проекта:

    ~/cicd-lab/
    ├── .github/
    │ └── workflows/
    │ └── ci.yml
    ├── src/
    │ └── app.py
    ├── tests/
    │ └── test_app.py
    ├── Dockerfile
    ├── docker-compose.yml
    ├── requirements.txt
    ├── Makefile
    └── .gitignore
  3. Создайте .gitignore:

    __pycache__/
    *.pyc
    .env
    .venv/
    *.egg-info/
    dist/
    build/

Задание 1.2. Приложение и тесты

Файл src/app.py:

from flask import Flask, jsonify
from datetime import datetime
import os
app = Flask(__name__)
def get_greeting(name="World"):
"""Возвращает приветственное сообщение."""
if not name or not name.strip():
raise ValueError("Name cannot be empty")
return f"Hello, {name}!"
def add(a, b):
"""Складывает два числа."""
return a + b
def is_even(n):
"""Проверяет, является ли число чётным."""
return n % 2 == 0
@app.route("/")
def index():
return jsonify({
"message": get_greeting("DevOps"),
"timestamp": datetime.now().isoformat(),
"version": os.environ.get("APP_VERSION", "dev"),
})
@app.route("/health")
def health():
return jsonify({"status": "healthy"})
if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
app.run(host="0.0.0.0", port=port)

Файл tests/test_app.py:

import pytest
from src.app import get_greeting, add, is_even, app
class TestGreeting:
def test_default_greeting(self):
assert get_greeting() == "Hello, World!"
def test_custom_greeting(self):
assert get_greeting("Student") == "Hello, Student!"
def test_empty_name_raises(self):
with pytest.raises(ValueError):
get_greeting("")
def test_whitespace_name_raises(self):
with pytest.raises(ValueError):
get_greeting(" ")
class TestMath:
def test_add_positive(self):
assert add(2, 3) == 5
def test_add_negative(self):
assert add(-1, -2) == -3
def test_add_zero(self):
assert add(0, 0) == 0
def test_is_even_true(self):
assert is_even(4) is True
def test_is_even_false(self):
assert is_even(3) is False
def test_is_even_zero(self):
assert is_even(0) is True
class TestAPI:
@pytest.fixture
def client(self):
app.config["TESTING"] = True
with app.test_client() as client:
yield client
def test_index(self, client):
response = client.get("/")
assert response.status_code == 200
data = response.get_json()
assert "message" in data
assert "timestamp" in data
def test_health(self, client):
response = client.get("/health")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "healthy"

Файл requirements.txt:

flask==3.0.0
pytest==8.0.0
flake8==7.0.0

Задание 1.3. Проверка локально

Окно терминала
cd ~/cicd-lab
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Линтинг
flake8 src/ tests/ --max-line-length=120
# Тесты
pytest tests/ -v
deactivate

Часть 2. Dockerfile

Задание 2.1. Создание Dockerfile

FROM python:3.12-slim
LABEL maintainer="student@os-course"
ENV PYTHONUNBUFFERED=1
ENV APP_VERSION=1.0.0
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
RUN adduser --disabled-password --gecos "" appuser
USER appuser
EXPOSE 5000
CMD ["python", "src/app.py"]

Задание 2.2. docker-compose.yml

services:
web:
build: .
ports:
- "8080:5000"
environment:
APP_VERSION: "1.0.0"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:5000/health || exit 1"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s

Задание 2.3. Локальная проверка

Окно терминала
docker compose up -d --build
curl http://localhost:8080
curl http://localhost:8080/health
docker compose down

Часть 3. GitHub Actions

Задание 3.1. Базовый CI-пайплайн

Создайте файл .github/workflows/ci.yml:

name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run flake8
run: flake8 src/ tests/ --max-line-length=120
test:
name: Test
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ hashFiles('requirements.txt') }}
restore-keys: pip-
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest tests/ -v --tb=short
build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t cicd-lab:${{ github.sha }} .
- name: Test Docker image
run: |
docker run -d --name test-app -p 5000:5000 cicd-lab:${{ github.sha }}
sleep 5
curl -f http://localhost:5000/health
docker stop test-app

Задание 3.2. Объяснение структуры

Объясните каждый элемент workflow-файла:

ЭлементОписание
name?
on?
jobs?
runs-on?
needs?
steps?
uses?
run?

Задание 3.3. Коммит и пуш

Окно терминала
cd ~/cicd-lab
git add .
git commit -m "Initial project with CI pipeline"
# Создайте репозиторий на GitHub, затем:
git remote add origin https://github.com/YOUR_USERNAME/cicd-lab.git
git branch -M main
git push -u origin main

Перейдите на GitHub → вкладка Actions → наблюдайте за выполнением пайплайна.


Часть 4. Расширенный пайплайн

Задание 4.1. Добавление публикации образа

Создайте файл .github/workflows/release.yml:

name: Release Pipeline
on:
push:
tags: ["v*"]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: pytest tests/ -v
build-and-push:
name: Build & Push Image
runs-on: ubuntu-latest
needs: test
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

Задание 4.2. Создание релиза

Окно терминала
git tag -a v1.0.0 -m "Release 1.0.0: initial release"
git push origin v1.0.0

Перейдите на GitHub → Actions → убедитесь, что образ опубликован.


Часть 5. Makefile

Задание 5.1. Создание Makefile

.PHONY: install lint test build run stop clean
install:
pip install -r requirements.txt
lint:
flake8 src/ tests/ --max-line-length=120
test:
pytest tests/ -v
build:
docker compose build
run:
docker compose up -d
stop:
docker compose down
logs:
docker compose logs -f
clean:
docker compose down -v --rmi all
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
all: lint test build

Задание 5.2. Использование

Окно терминала
make install # Установить зависимости
make lint # Линтинг
make test # Тесты
make build # Собрать Docker-образ
make run # Запустить
make logs # Логи
make stop # Остановить
make clean # Полная очистка
make all # lint + test + build

Часть 6. Сканирование безопасности

Задание 6.1. Trivy

  1. Установите Trivy:

    Окно терминала
    sudo apt install -y wget apt-transport-https gnupg lsb-release
    wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
    echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list
    sudo apt update && sudo apt install -y trivy
  2. Просканируйте собранный образ:

    Окно терминала
    docker compose build
    trivy image cicd-lab-web:latest
  3. Просканируйте только критические уязвимости:

    Окно терминала
    trivy image --severity CRITICAL cicd-lab-web:latest
  4. Добавьте сканирование в CI-пайплайн (добавьте step в ci.yml к job build):

    - name: Scan image with Trivy
    uses: aquasecurity/trivy-action@master
    with:
    image-ref: cicd-lab:${{ github.sha }}
    format: table
    severity: CRITICAL,HIGH

Мини-проект: полный CI/CD цикл

Создайте проект с полным автоматизированным циклом:

  1. Приложение — Flask API с минимум 3 эндпоинтами и юнит-тестами.
  2. Dockerfile — с лучшими практиками (slim-образ, non-root, кэширование).
  3. docker-compose.yml — с healthcheck.
  4. Makefile — с целями lint, test, build, run, clean.
  5. CI-пайплайн (.github/workflows/ci.yml):
    • Линтинг (flake8).
    • Тестирование (pytest).
    • Сборка Docker-образа.
    • Сканирование на уязвимости (Trivy).
  6. Release-пайплайн (.github/workflows/release.yml):
    • Триггер: push тега v*.
    • Сборка и публикация образа в GitHub Container Registry.

Результат: при пуше в main — автоматический lint + test + build; при создании тега — публикация образа.


Контрольные вопросы

  1. Что такое DevOps? Расшифруйте принципы CALMS.
  2. Сравните стратегии ветвления: GitFlow, GitHub Flow, Trunk-Based Development.
  3. В чём разница между Continuous Delivery и Continuous Deployment?
  4. Опишите структуру workflow-файла GitHub Actions: on, jobs, steps, needs.
  5. Что такое runner в GitHub Actions? Чем GitHub-hosted отличается от self-hosted?
  6. Как безопасно передать секрет (токен, пароль) в CI/CD-пайплайн?
  7. Зачем кэшировать зависимости в CI? Как работает actions/cache?
  8. Назовите три столпа наблюдаемости (observability). Чем метрики отличаются от логов?
  9. Чем Terraform отличается от Ansible? Как они дополняют друг друга?
  10. Что такое DevSecOps и принцип «shift left»? Какие инструменты используются для сканирования Docker-образов?