🏃 Continuous Integration (CI): как настроить и какую роль она играет в цикле разработки

Разберем роль CI в жизненном цикле разработки (что в себя включает эта часть процесса, какие задачи решает) и на практике настроим CI для приложения API сервера.

В прошлой статье 🏃 Работаем нон-стоп: непрерывная интеграция и непрерывное развертывание кода (CI/CD) мы обзорно познакомились с процессом CI\CD. Настало время углубиться и расшириться в детали. Начнем с разбора первой половины процесса – это CI (Continuous Deployment). Разберем цели этого процесса, инструменты и на практическом примере реализуем кейс.

Роль CI в жизненном цикле разработки

Вспомним как выглядит процесс CI.

Цели, которые преследует этот процесс следующие:

  1. Фиксация всех изменений версий через систему контроля версий.
  2. Компиляция кода и обработка ошибок при компиляции.
  3. Автоматическое тестирование выполнения кода.
  4. Создание артефактов (оповещения, документация и т. д.).

Самое главное – это все выполняется полностью в автоматическом режиме. Человек участвует только в первоначальной настройке процесса, то есть при создании unit-тестов. Такой подход гораздо быстрее ручного тестирования, так как позволяет максимально избежать ошибок по вине человека. В итоге внесенное изменение проходит через тесты и если тесты не прошли, всегда можно вернуться на предыдущий коммит в системе контроля версии. В общем-то и все, предлагаю приступить к реализации этого процесса.

Настраиваем CI в GitHub

Нам потребуется система СКВ (система контроля версий), а также среда, где будет компилироваться наш код и запускаться тесты. Все эти задачи (и еще много чего интересного) полностью предоставляет сервис GitHub.

Кейс: Настроить CI с помощью функционала сервиса GitHub для API сервера написанного на Python.

Подробно описывать код самого сервера и как залить его на git не буду. Тема статьи о другом.

Имеем в репозитории Git следующую структуру файлов:

Цветом выделены файлы, с которыми будем работать. Разберем для чего каждый файл.

app.py
from fastapi import FastAPI
import uvicorn
from typing import Optional
from pydantic import BaseModel
import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: int
    tax: Optional[float] = None


app = FastAPI()

security = HTTPBasic()

def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
    correct_username = secrets.compare_digest(credentials.username, "test")
    correct_password = secrets.compare_digest(credentials.password, "test")
    if not (correct_username and correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username

@app.get("/")
def read_root(credentials: HTTPBasicCredentials = Depends(get_current_username)):
    return {"Hi": "from API Server)"}

@app.get("/test/{item_id}")
async def read_item(item_id: int, credentials: HTTPBasicCredentials = Depends(get_current_username)):
    print(item_id)
    return {"item_id": item_id}

@app.get("/check")
def hello():
    return "Hello World"

@app.post("/items/")
async def create_item(item: Item, credentials: HTTPBasicCredentials = Depends(get_current_username)):
    return item

Это основной файл нашего приложения (API сервера). Сервер имеет три endpoint. Мы будем работать с / и check. Endpoint /check необходим как раз для тестов.

Пройдемся по основному функционалу приложения. Метод get_current_username необходим для базовой аутентификации по логину и паролю. Далее у нас идут четыре endpoint:

  1. @app.get(“/”)
  2. @app.get(“/test”)
  3. @app.get(“/items”)
  4. @app.get(“/check”)

Endpoint – это точки входа для API обращений. Когда у нас будут отрабатывать автоматические тесты, мы постучимся /check и если получим код возврата 200, значит все хорошо и сервис работает.

Следующий у нас файл requirements.txt:

requirements.txt
uvicorn==0.14.0
pydantic==1.8.2
fastapi==0.66.1
requests==2.26.0
pytest==6.2.5

Тут перечислены зависимости по библиотекам для запуска нашего приложения. Если мы в дальнейшем захотим использовать дополнительный функционал из внешней библиотеке, необходимо добавить имя и версию.

Makefile
install:
	pip install --upgrade pip &&\
		pip install -r requirements.txt

test:
	python -m pytest -vv test_hello.py

Этот файл интереснее. В нем прописаны команды для сборки нашего приложения. Видим, что сначала устанавливаются все библиотеки и вторым шагом запускается файл test_hello.py – это наши тесты.

test_hello.py
from fastapi.testclient import TestClient
from app import app

client = TestClient(app)

def test_valid_id():
    response = client.get("/check")
    assert response.status_code == 200

Собственно через get стучимся на эндпоинт /check и если код возврата 200, то все ок – наш сервер как минимум отвечает по этому эндпоинту. Это минимальный вариант теста, можно его расширить и настроить под свое приложение. Смотрим в документацию

В итоге у нас есть файл с приложением, файл с необходимыми зависимостями, файл с тестами и Makefile для последовательности запуска этого хозяйства. Вопрос на внимательность: чего не хватает?

Если вспомнить цели, которых добивается процесс CI (там было что-то про автоматический запуск), то все это делается через описание PipeLine в файле main.yaml.

Обратите внимание на расположение этого файла. Корень вашего репозитория, в нем директория .github/workflows/.

main.yaml
name: Run Test
on:
  push:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Set up Python 3.10.1
      uses: actions/setup-python@v1
      with:
        python-version: 3.10.1
    - name: Install dependencies
      run: |
        make install
    - name: Test
      run: |
        make test

На языке YAML описывается: если у нас есть новый коммит в ветке main, необходимо выполнить Job c шагами 1-3.

  1. Использовать Python 3.10.1
  2. Выполнить команды из блока install файла Makefile.
  3. Выполнить команды из блока test файла Makefile.

Если на всем этом длинном пути нашего PipeLine не возникнет ошибок, то у нас соберется наше приложение, запустятся тесты. Проследить это можно на вкладке Action вашего репозитория.

Если провалиться в детали – то можно посмотреть логи каждого шага

Поздравляю, тесты прошли успешно – код успешно запускается без привязки к прод. окружению. И теперь запуск всей цепочки действий (наш PipeLine) будет происходить при каждом коммите в репозиторий.

***

Материалы по теме

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

matyushkin
12 мая 2020

Как запустить веб-приложение на Nginx в Docker 🐳👨🏽‍💻

Инструкция по настройке совместной работы веб-приложения и сервера Nginx в ...
Библиотека программиста
02 сентября 2017

10 популярных вопросов и ответов на DevOps собеседовании

DevOps работает как мост между разработкой, тестированием и эксплуатацией в...
Библиотека программиста
12 июля 2017

Что такое Docker, и как его использовать? Подробно рассказываем

Разберем по косточкам, ведь Docker – это мощный инструмент, и огромное коли...