Transcription
Было время, когда на своей первой работе мне приходилось работать с огромным бэкенд-приложением, совершенно одному. Ну, ты представляешь, да? Я, зелёный джун, устроился в свою первую компанию, мне около недели пытались передать какие-то знания по проекту и дальше отправили в свободное плавание. Ебись с этим, как хочешь.
Ну а мне ничего не оставалось, как послушаться наставление опытного разработчика и отправиться выполнять свои первые таски. Первое время всё было прекрасно. Я спокойно закрывал простые задачи, наслаждаясь капучино, который я сам себе и заводил на офисной кухне. Но кайфарик длился недолго.
Мой очередной затуп в монитор прерывала внезапно прилетевшая таска, в которой говорилась, что нужно задеплоить проект на сервер, а ещё и подлить изменения в уже существующий докер образ. Я до конца надеялся, что засайнили её не на меня, но увидев свою аватарку, я впал в дичайший стрём. В голове тогда было ничего, кроме: "Бля, а как ты вообще собрался это делать? Ты же никогда никакие докеробразы и контейнеры в глаза не видел. А если всё упадёт?"
В итоге я внёс какие-то изменения и дрожащими руками запустил пайплайн. Как вы могли понять, всё шло очень даже неплохо до тех пор, пока дело не дошло до докера, CI/CD, образов серверов и всего такого. Оно и немудрено. Тема развёртывания приложух всегда была одной из продвинутых и сложных. А я же, в свою очередь, совершенно не был знаком с этой темой, так что приходилось учиться всему прямо в процессе выполнения задач. А знаете, как я заливал изменения на сервак? Правильно, SSH-ился и правил нужные участки кода через редактор Nano. Кто понял, тот понял. Ну вы понимаете, это была для меня... Что там было у разрагов, которые переняли у меня этот проект? Пожалуй, ставим это в тайне и не будем думать о плохом.
Не хотелось бы переживать похожий опыт. Тогда этот видос — это именно то, что ты и искал. Умение задеплоить своё приложение — это чрезвычайно важный навык для современного разработчика. В маленьких компаниях и на маленьких проектах очень редко работает отдельный DevOps-ер, то есть человек, отвечающий за инфраструктуру развёртывания приложений. В связи с этим номинальные доп. задачи зачастую ложатся на плечи рядовых разрагов вроде нас с тобой. И чтобы не упасть лицом в грязь, оказавшись в похожей на мою ситуации, было бы неплохо иметь представление о том, что такое вообще этот ваш DevOps, пайплайны, докеры и облачные сервера, а также как с этим всем работать.
В этом видосе вместе развернём простое бэкенд приложение на Питоне. Причём этот шаблон подойдёт буквально для любого языка, так что не парься. Напишем Docker-образ, настроим Docker Compose, сконфигурируем сервер, выгрузим код на GitLab и настроим пайплайны. И вообще, подробно разжём каждый шаг. Короче, тебя ждёт суперполезный и практический видос, и после просмотра ты получишь кучу знаний и навыков. Не смею задерживать. Поехали становиться DevOps-ерами.
Всем привет. Сегодня я вам покажу нашу базу. Для начала давай определим, а что вообще нужно для старта. У тебя должны быть установлены IDE, Docker, Docker Compose, а также создан аккаунт на GitLab. Ну и желательно иметь какой-нибудь сервак под рукой, чтобы ты мог воспроизвести шаги вместе со мной, но об этом поговорим позже. Сразу говорюсь, что я не буду тратить таймлайн этого видоса на установку всех нужных инструментов. Думаю, что вы в состоянии сделать это и без меня. Но в качестве бонуса я всё равно оставил гайд на установку Docker и Docker Compose для Windows, Ubuntu и Mac OS у себя в TG. Там же сможешь найти гайд на настройку GitLab CI/CD. Если хочешь, то также можешь запулить себе проект из видео. Это самая простая APIшка на питоновском FastAPI, но можешь использовать и свой проект. Короче, можешь не благодарить и пользуйся.
Начнём издалека. А какие вообще у нас есть варианты развёртывания приложений и какой из них максимально подходящий и правильный? А, хотя нет, начнём с того, а что вообще такое деплой или развёртывание? Это процесс, когда ты берёшь свой код, например, Telegram-бота или фронт-приложение для интернет-магазина и выкладываешь его на сервер, чтобы оно заработало и стало доступно пользователям в интернете. Проще говоря, это как перенести готовое блюдо из кухни на стол. Код из твоего компьютера превращается в работающее приложение, размещённое и доступное в интернете. По факту, нам даже не обязательно иметь внешний сервер. Ты можешь запариться и сделать его прямо из своего домашнего ПК. И это даже не будет стоить каких-то серьёзных денег, но не особо целесообразно ввиду сложности и потраченного времени, хотя и интересно.
На текущий момент у нас имеется огромная куча вариантов, как развернуть приложение и выкинуть его в глобальную сеть. Давай подробнее разберёмся в этих вариантах.
Развёртывание на виртуальных серверах в облаке — это один из самых популярных вариантов сегодня, которые использует большинство компаний. У этого способа есть неоспоримые плюсы. Во-первых, гибкость. Можно выбрать нужные ресурсы: CPU, RAM, диски и масштабировать их по мере роста нагрузки. Надёжность. Дата-центры обеспечивают высокий аптайм, или время доступности, от 99% и выше. Также облачные сервера имеют огромное количество встроенных инструментов, вроде сервисов для мониторинга, автоскейлинга, бэкапов и так далее. Ну и, наверное, самым главным плюсом является удобство. Все нужные инструменты находятся в одной инфраструктуре и всегда под рукой. Захотел подрубить больше инстансов — делается в два клика. Понадобилось гибкое масштабирование, причём неважно, горизонтальное или вертикальное. У тебя всегда есть балансировщики нагрузки, автоскейлинг-группы. Кстати, в этом видосе я буду показывать процесс деплоя именно на примере облачного хостинга.
К минусам этого способа можно отнести стоимость, то есть за удобство приходится платить, особенно если трафик постоянно растёт. Ну и в субъективный минус можно также отнести сложность. Свежему новичку может быть трудновато разобраться во всех настройках, но на самом деле ничего трудного нет. Достаточно просто потратить небольшое количество времени. Этот вариант подходит для большинства коммерческих и достаточно крупных проектов, где важны стабильность и масштабируемость.
Ещё одним вариантом является выделенный сервер. Ты арендуешь реальную машину в дата-центре у хостинг-провайдера. При таком варианте ты получаешь всю мощность и ресурсы сервера. У тебя отсутствуют какие-либо соседи по облаку, а ещё ты получаешь полный контроль в его настройке. Этот вариант особенно сложный, потому что всё администрирование лежит на тебе, и это реально дорого. Обычно выделенный сервер подходит для специфических задач, например, для критически важных приложений или больших вычислительных рабочих нагрузок.
Ещё одним вариантом является Platform as a Service, типа Heroku, Vercel или Netlify. Считаю этот вариант таким облаком для ленивых. Ты просто загружаешь код, а платформа сама настраивает сервер, деплоит приложение и управляет им.
Ещё одним популярным вариантом является serverless, или же бессерверная архитектура. Это значит, что тебе вообще не нужно заморачиваться серверной инфраструктурой, а просто писать код, и всё. Он будет задеплоен практически автоматом. Ты не управляешь памятью, местом, вообще ничем. Сервис всё делает за тебя.
Ещё одним вариантом, а вернее вариантом для комбо, являются контейнеры и оркестрация, то есть Docker или Kubernetes. Этот вариант подразумевает развёртывание приложения в контейнерах, которые можно запускать где угодно: локально, в облаке или на сервере. Kubernetes, в свою очередь, помогает управлять множеством контейнеров. Docker позволяет как бы упаковать приложение вместе со всеми его зависимостями: библиотеками, конфигурациями и окружением в единый контейнер, который можно запускать где угодно: на твоём ноуте, сервере или в облаке. Главная фишка: контейнеры лёгкие, быстрые и изолированные. Изоляция в Docker происходит на уровне ядра, что отличает его от классической виртуализации вроде виртуальных машин.
Итак, вводные данные получили. Теперь можем переходить к практике. Для начала давай посмотрим на наше приложение. Это суперпростая APIшка, возвращающая товары из базы данных PostgreSQL. Имеем базовые CRUD-операции, то есть Create, Read, Update, Delete для манипуляции над данными для модели Goods, то есть как раз над нашими товарами. Используем фреймворк FastAPI. Это классный микрофреймворк для разработки API-шек на Питоне. Структура тоже максимально классическая и простая. У нас есть main.py — точка входа в наш бэкенд. Мы просто инициализируем класс FastAPI, прописываем события стартапа. То есть при запуске приложения мы инициализируем создание и обновление таблиц в базе данных. Далее простой endpoint для проверки того, что наше приложение не сдохло под какой-то нагрузкой. Ну и в конце подключаем роутеры нашего единственного модуля Goods, где хранится внутренний роутер, то есть файл с эндпоинтами и модели для БД.
Саму базу тоже подключаем мегапросто и стандартно, используя SQLAlchemy PG, чтобы запросы к базе были асинхронными. Благо, FastAPI даёт нам такую возможность. Вообще, главная фишка этого фреймворка — это та самая асинхронность, за счёт которой он такой быстрый. А ещё благодаря плотной интеграции с Pydantic наш Python становится типизированным, что также не может не радовать. Также благодаря Pydantic у нас появляется мощная система валидации, то есть мы можем контролировать, что приходит на вход и выходит из наших эндпоинтов. Это реально удобно, особенно в связке с SQLModel. Это такая ORM, кстати, от разработчика FastAPI. Если в обычной структуре проектов на FastAPI мы юзаем ORM вроде SQLAlchemy и для валидации добавляем валидационные схемы от Pydantic, то благодаря SQLModel мы можем всё это делать сразу в одном классе. Создаём саму SQL-таблицу и сразу накидываем валидацию. Теперь не нужно плодить множество моделей и схем. Всё храним в одном месте. Удобно, что сказать.
Как вы могли убедиться, в итоге имеем простое приложение-APIшку, которое даёт нам возможность посмотреть товары из базы данных, добавить новые товары, обновить их и также удалить. Вот, по сути, и весь его функционал. В качестве пакетного менеджера я не использую базовый PIP, уже давно перешёл на Poetry и ни о чём не жалею. Poetry — это уже не просто установщик пакетов, а полноценный менеджер зависимостей и проектов. Он управляет виртуальным окружением, фиксирует версии всех библиотек в pyproject.toml и решает конфликты зависимостей автоматически.
Давай обсудим дальнейшие действия в обычной ситуации без использования Docker и подобных штук. Мы заливаем код на Git, SSH-имся на наш сервак, пулим код заново, восстанавливаем всё окружение, создаём базу данных, если она не удалённая, настраиваем переменные окружения, добавляем демон вроде Gunicorn и кое-как запускаем наш код, если не будет системных ошибок из-за особенностей операционной системы, установленной на сервере. И это всё обязательные шаги, если система, на которой написано приложение, совпадает с системой, на которой крутится наш сервак. А если мы разрабатываем на Windows, заливаем код на Linux-сервер, а сервер будет всегда работать на Linux в 99% случаев. Вот тут могут наступить проблемы: конфликты библиотек, что-то просто не соберётся, а что-то просто не встанет изначально. А если у нас планируется несколько серверов, один для Dev, а второй продакшнский, нам каждый раз придётся менеджерить все зависимости отдельно, следить за всем вручную. А если на нашем сервере будет крутиться больше одного приложения, а если их будет пять или 10, как всем этим управлять? Согласись, пока что архитектура звучит супер перегруженно и сложно.
Именно для этого и существует Docker. Мы прописываем конфигурационный Dockerfile, собираем compose-файлик один раз и вуаля. Наше приложение работает в изолированном контейнере, где находятся все настройки конкретного окружения, все зависимости, своя сеть и так далее. Таким образом, мы устраняем проблемы совместимости и изолируем наше приложение. Мы даже можем выгрузить наш образ на Docker Hub. Это вроде GitHub, он только для Docker-образов. И всё. Можем пользоваться, где захотим и когда захотим. В общем, используя Docker, мы одним выстрелом убиваем сразу стаю зайцев.
О'кей, это решает, если у нас парочка приложений, но всё становится таким же неудобным, когда количество наших приложений переваливает за два, три и больше. А что, если увеличивается нагрузка на наши приложения? Тут на сцену выходит Kubernetes, или просто K8s. Он берёт твои Docker-контейнеры и превращает их в управляемую, отказоустойчивую систему. Kubernetes работает с кластером, группой серверов или же нод, где одна нода — это мастер-нода, она управляет, а остальные — воркеры, то есть выполняют задачи.
Основные концепции включают в себя:
* **Pod:** Pod — это минимальная единица в Kubernetes. Это как бы обёртка вокруг одного или нескольких контейнеров, но обычно всё-таки одного, которые работают вместе и делят ресурсы, например, сеть или хранилище. Например, один pod с контейнером основной логики этого приложения и контейнером для логирования.
* **Deployment:** Deployment описывает, сколько копий pod'ов нужно запустить и как их обновлять. Если один pod умирает, Deployment создаст новый. Deployment сильно увеличивает отказоустойчивость приложения.
* **Service:** Service обеспечивает доступ к pod'ам через стабильный адрес. Даже если pod'ы перезапускаются или перемещаются, Service перенаправляет трафик туда, куда надо. Например, балансировка нагрузки между несколькими копиями приложения.
* **ConfigMap и Secrets:** Они хранят конфигурации и чувствительные данные, например, пароли или ключи от API, чтобы не хардкодить их в образы.
* **Autoscaling:** Kubernetes автоматически добавляет или убирает pod'ы в зависимости от нагрузки, например, по процессору или числу запросов.
* **Cluster:** Все ноды как единое целое. Мастер решает, где запускать pod'ы, а воркеры их исполняют.
Согласен, новой инфы много, но, поверь, это того стоит. Так что давай резюмируем:
Docker — это инструмент для контейнеризации. Он упаковывает твоё приложение со всеми зависимостями в лёгкий, изолированный контейнер. Он использует namespaces и cgroups ядра Linux для изоляции процессов. Работает на одном хосте, делит его ядро. Он устраняет проблемы совместимости, например, Windows и Linux. Имеет возможность легко переносить контейнеры благодаря Docker Hub, изолирует приложение на одном сервере и упрощает деплой и воспроизводимость.
Kubernetes — это система оркестрации контейнеров. Позволяет управлять множеством Docker-контейнеров в кластере серверов. K8s распределяет pod'ы, то есть группы контейнеров, по нодам, следит за их работой, масштабирует, балансирует нагрузку. Благодаря K8s у тебя есть возможность масштабируемости для десятков и даже сотен приложений. А ещё он отказоустойчив благодаря системе балансировки нагрузки.
В общем, Docker — это строитель контейнеров, а Kubernetes — это диспетчер парка этих контейнеров. Docker хорош для старта и просто, а Kubernetes для масштаба и надёжности. Вместе они просто идеальны. Docker создаёт контейнеры, а K8s управляет ими в большой системе.
Короче, хватит болтать, давай уже создадим Dockerfile и compose-файл для нашего приложения с API-шкой.
Dockerfile — это инструкция для сборки Docker-образа. Он описывает, какой базовый образ взять, например, Python, какие зависимости установить, а также как настроить окружение и запустить приложение. Для нашего приложения он будет содержать шаги для установки Python, зависимостей FastAPI и Uvicorn и запуска сервера. Файл состоит из слоёв — последовательности изменений файловой системы, которые создаются на основе инструкций в Dockerfile. Каждый слой — это как снимок состояния после выполнения определённого шага.
В блоке `FROM` мы указываем базовый образ, с которого начинается вся сборка. В данном случае это официальный образ версии 3.11 в варианте `slim`. Образ — это основа контейнера, содержащая операционную систему. В данном случае минимальную версию Debian и Python. `Slim` — это урезанная версия образа без лишних утилит. Например, нет компиляторов или документации, что уменьшает размер и ускоряет сборку. Docker скачивает образ Python 3.11 slim из Docker Hub и использует его как стартовую точку.
Блок `RUN` выполняет команду прямо в контейнере. Он устанавливает менеджер зависимостей Poetry через pip. Как я упомянул ранее, Poetry — это инструмент для управления зависимостями, который мы будем использовать вместо привычного pip. Он нужен для работы с pyproject.toml и poetry.lock, чтобы устанавливать все зависимости нашего приложения. Далее Docker запускает временный контейнер на основе Python 3.11 slim, который мы прописали в прошлом шаге. Выполняет `pip install poetry`, скачивая и устанавливая сам Poetry в систему на глобальном уровне. Результат фиксируется в одном слое образа. Ещё раз, слой сохраняется после каждого действия, то есть результат выполнения одной инструкции в Dockerfile, например, `FROM`, `RUN` или `COPY`. Под капотом Docker использует UnionFS — это такая файловая система, чтобы комбинировать эти слои в единое целое. Каждый слой кэшируется, и если он не изменился, Docker его повторно не пересобирает. Когда я говорю "фиксируется в новом слое образа", это означает, что изменения, сделанные на этом шаге, то есть новые файлы, пакеты и так далее, сохраняются как отдельный, неизменяемый слой, который добавляется поверх предыдущих.
На данном шаге устанавливаем рабочую директорию внутри контейнера на `/app`, то есть это как бы корневая папка нашего проекта. Не обязательно называть его именно `app`. Можем написать хоть `penis`, это ни на что не повлияет. Все последующие команды, вроде `COPY`, `RUN` или `CMD`, будут выполняться именно в этой папке. Это как `cd /app` в терминале. Задаёт контекст для работы с файлами и кодом. Это удобно для организации. Весь код и зависимости будут находиться в папке `app`. Также на этом шаге директория `app` становится текущей для всех следующих инструкций.
Далее просто копируем файлы `pyproject.toml` и `poetry.lock` из локальной папки, где лежит наш Dockerfile, в рабочую директорию нашего контейнера `/app`. Как я упоминал, `pyproject.toml` — это конфигурация проекта, где указаны зависимости, например, FastAPI или Uvicorn. Если откроем его, то увидим нечто схожее с обычным `requirements.txt`. `poetry.lock` — это файл с точными версиями всех зависимостей, чтобы сборка была воспроизводимой. Копируем их первыми, чтобы Docker кэшировал следующий шаг, то есть установку зависимостей, и не пересобирал его, если код изменится, а зависимости нет. И этот шаг также добавляет новый слой в наш образ.
Здесь команда `RUN` выполняет две команды в контейнере: настраивает Poetry и устанавливает зависимости. Давай разберём по частям. Первая команда отключает создание виртуального окружения внутри контейнера, потому что задача виртуального окружения состоит в изоляции зависимостей конкретного проекта. Наш контейнер сам по себе уже изолирован. Вторая команда просто устанавливает зависимости из файла `pyproject.toml` и `poetry.lock`, пропуская все dev-зависимости, например, линтеры, тесты и так далее, чтобы не раздувать образ. `--no-interaction` запускает установку без запросов ввода, что важно для автоматизации. То есть нам не нужно будет ничего вручную прописывать, ещё с чем-либо соглашаться. Docker всё сделает за нас. По итогу все установленные библиотеки и зависимости фиксируются в новом слое.
Следующим шагом мы просто копируем все файлы из текущей локальной директории, где лежит наш Dockerfile, в директорию `/app` внутри контейнера. Мы переносим код приложения: `main.py`, `models.py`, `routers/goods.py`. Мы переносим весь код нашего приложения в директорию `app`. Этот шаг идёт после установки зависимостей, чтобы изменения в коде не заставляли пересобирать зависимости.
Здесь мы просто указываем, что контейнер будет слушать порт 8000, потому что это стандарт для FastAPI и Uvicorn. На самом деле можем использовать любой другой порт, хоть 1488, хоть 5225. Команда сама по себе не открывает порт во внешний мир. Для этого нужен `docker run -p` или настройка в `compose`. То есть сейчас ничего физически не меняется, просто обновляются метаданные образа.
Последняя инструкция выполняет команду, которая выполнится при запуске контейнера. Uvicorn — это сервер для запуска FastAPI. Он принимает HTTP-запросы или WebSocket-соединения от клиентов, передаёт их Python-приложению через ASGI-интерфейс и после возвращает ответы клиенту. `main:app` указывает, где искать наше приложение. `app` — это объект FastAPI из файла `main.py`, то есть точка входа. `--host 0.0.0.0` заставляет сервис слушать все интерфейсы, чтобы API был доступен снаружи контейнера. Порт 8000 — это тот порт, на котором работает наша API-шка. В этом шаге мы напрямую определяем, как запустить приложение после создания контейнера. Формат списка, то есть `["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]`, предпочтителен, так как Docker напрямую вызывает команду без интерпретатора Shell. То есть мы могли бы написать и по-другому, как делаем это обычно, без помещения строк в список, но так работает чуть медленнее. И хорошим тоном всё-таки является синтаксис exec-формата. Ну и далее при запуске контейнера Uvicorn стартует и поднимает наш FastAPI-сервер на порту 8000.
Отлично, полдела сделано. Мы создали Dockerfile, но файл — это просто инструкция, рецепт для создания образа. Чтобы он стал полезным, нужно пройти несколько этапов: собрать Docker-образ и запустить контейнер из образа. Для этого в терминале в папке с Dockerfile выполняем следующую команду:
`docker build -t goods-api:latest .`
`docker build` — это команда для сборки образа. `-t goods-api:latest` задаёт имя и тег образа. Например, `goods-api` — это наше имя, а `latest` — это версия. Точка в конце указывает, где именно искать Dockerfile, то есть в нашем случае в текущей директории. Во время `docker build` Docker читает Dockerfile строка за строкой, выполняет инструкции типа `RUN`, `FROM`, `COPY` и так далее. А ещё создаёт слои, как мы обсуждали ранее. В итоге получается готовый образ — замороженный набор файлов и настроек, который хранится локально на твоей машине. Можем даже выполнить `docker images` и убедимся, что образ успешно создался.
Образ — это шаблон, но чтобы запустить приложение, нужен контейнер, то есть живой экземпляр образа. Запустим контейнер вот такой командой:
`docker run -p 8000:8000 goods-api:latest`
`docker run` создаёт и запускает контейнер из образа. `-p 8000:8000` пробрасывает порт. Первый порт 8000 — это порт хоста, второй порт контейнера, чтобы API-шка была доступна на `localhost:8000`. А `goods-api:latest` — это имя образа, которое мы собрали. Во время `docker run` Docker создаёт контейнер на основе нашего образа. Выполняет команду из `CMD` в Dockerfile, то есть `uvicorn main:app --host 0.0.0.0 --port 8000`. Uvicorn стартует внутри контейнера, а FastAPI начинает слушать порт 8000, который мы и указали.
Прямо сейчас наше приложение уже запустилось и работает. Можем проверить это, открыв браузер и перейдя по ссылке `http://localhost:8000`. Отлично! Наша API-шка успешно запустилась и работает внутри Docker-контейнера.
Запуск через `docker run` — это базовый вариант. Но кроме этого мы также можем использовать Docker Compose. Но зачем, если так всё работает? Смотри, если у тебя одно приложение, например, как у нас, наша API-шка с товарами, то `docker run` — это быстрый и простой способ его запустить. Мы собрали образ, запустили контейнер, и API-шка доступна на `localhost:8000`. Всё работает, никаких проблем. Но вот в чём загвоздка. `docker run` хорош, пока у нас всё просто. А что, если ситуация усложняется?
Docker Compose — это умный менеджер для нескольких контейнеров или более сложных настроек. Он нужен не вместо `docker run`, чтобы сделать жизнь легче, когда задач больше, чем просто запустить один контейнер. Допустим, наша API-шка работает с базой данных, например, PostgreSQL. С `docker run` нам нужно запустить контейнер для API, запустить контейнер для PostgreSQL и настроить сеть вручную, чтобы они видели друг друга, например, через `network`. Согласись, шагов немало. С Docker Compose мы напишем один файл, где укажем оба сервиса, то есть нашу API-шку и базу, и запустим всё одной командой `docker-compose up`.
Давай разберём наш `docker-compose.yml` по строчкам.
Здесь указываем версию формата Docker Compose. Compose развивается, и разные версии поддерживают разные фичи. `3.8` — это современная версия, совместимая с Docker 19 и выше, и подходящая для большинства задач. То есть благодаря этой строке Compose понимает, как интерпретировать файл, используя синтаксис версии 3.8.
В секции `services` описываются все сервисы или контейнеры, которые будут запущены позже. Каждый сервис — это отдельный контейнер, например, наша API-шка и база данных. Здесь мы определяем, как они строятся, настраиваются и взаимодействуют. Compose в свою очередь прочитает под секции, то есть `api` и `db`, и создаст для них контейнеры.
`api` — это сервис для API-шки. В блоке `build` указываем, как собрать образ для сервиса `api`. `context: .` — это путь к директории сборки. Точка — это текущая папка, где лежит наш Dockerfile. `dockerfile: Dockerfile` — это имя файла с инструкциями, которые мы создавали ранее. В целом этот блок говорит Compose, что образ для API-шки нужно собрать из Dockerfile, а не тянуть готовый из реестра. Compose просто выполняет `docker build` с нашим Dockerfile, создавая образ с Python, Poetry и FastAPI.
Здесь просто пробрасываем порты между хостом и контейнером. Левый `8000` — это порт на твоей машине, а правый `8000` — порт в контейнере из `expose: 8000`, как мы указывали в Dockerfile. Пробрасывание портов делает нашу API-шку доступной по адресу `localhost:8000`.
В блоке `environment` мы задаём переменные окружения внутри контейнера. `DATABASE_URL` — это строка подключения к базе PostgreSQL, которую FastAPI будет использовать для работы с базой. Это стандартный синтаксис. Мы передаём конфигурацию в приложение, и наша переменная становится доступной внутри самого контейнера.
На следующем шаге монтируем локальную папку в контейнер. То есть наша текущая директория проекта монтируется в `/app` внутри контейнера. Нужно это, чтобы при разработке изменения в коде сразу были видны в контейнере без пересборки самого образа. И в целом этот блок синхронизирует локальные файлы с нашим контейнером.
В блоке `depends_on` указываем зависимости между нашими сервисами. Здесь мы гарантируем, что наш сервис `db` стартует раньше `api`, чтобы база была готова к подключению. То есть Compose запускает сервис `db` перед сервисом `api`, но не ждёт полной готовности базы.
В сервисе `db` мы делаем нечто похожее. Указываем готовый образ PostgreSQL из Docker Hub вместо сборки через Dockerfile. Docker скачивает образ базы и создаёт на его основе контейнер. Далее также задаём переменные окружения для PostgreSQL. И внутри контейнера база сразу инициализируется с этими переменными.
В блоке `volumes` мы монтируем именованный volume для хранения данных базы. `pg_data` — это имя нашего volume. А после двоеточия мы указываем путь внутри контейнера, где PostgreSQL сохранит данные. Нужно это, чтобы наши данные сохранялись между перезапусками контейнера, иначе они просто потеряются.
В этом блоке мы опять же просто пробрасываем порты для PostgreSQL. `5432` — это стандартный порт, он доступен на хосте. Опять же, мы можем указывать абсолютно разные порты, но...
Для этого придётся лезть в настройки самой Pгgrгress, чтобы всё было нормально. По итогу порт 5432 становится доступным на нашем локалхасте. Поздравляю, мы написали compose файл для нашего Docker образа. Чтобы запустить, выполняем Docker Compose App с флагом Build. Опишка будет доступна на local host 8.000, а база на local host 5432. Если по какой-то причине у вас что-то не получилось, то возвращайтесь на пару шагов назад.
Как мы видим, у нас запустилось сразу два контейнера. Один с приложением, а второй с базой. Интересно, что мы имеем изолированный контейнер с базы. Соответственно, при запуске через Docker Run мы подтягивали локальную базу. А в случае с Docker Compose при поднятии контейнера мы видим совершенно новую и пустую базу.
Итак, резюмируем. Мы создали Docker файл, инструкцию для создания Docker образа или же рецепт, как собрать приложение в контейнере. Что он делает? берёт базовый образ, например, Python 311 Slim, устанавливает зависимости, потри, библиотеки и так далее. Копирует код и настраивает запуск, например, через сервер UVCorn. Также мы создали Docker Compost файл для управления несколькими контейнерами и их настройками. Это кирижёр для сервисов. Он определяет сервисы, например, API и DB, связывает их, сети зависимости, настраивает порты, переменные окружения, Volumes и запускает всё одной командой при помощи Docker Compose Up.
Всё круто. Мы только что контейнеризировали наше приложение, написали полноценный Docker файл Docker Compose. Настало время проверять, как всё это работает в условном проде. Но для этого нам понадобится хороший облачный сервер. Разберём процесс деплоя на примере SelectTel. Select доступны разные конфигурации в зависимости от требований проекта. Самый оптимальный вариант для развёртывания нашей опишки - это Standart Line. Использовать облачный сервер Selecttel удобно, потому что вы платите только за те ресурсы, которые используете в рамках своего проекта. Ни больше, ни меньше. А самый бюджетный сервер можно арендовать вообще по цене от 10 руб. в день. Это сервер с арендой части ядра, называется Sharline.
Давайте сконфигурируем сервер под нашу простую опишку, но перед началом нужно зарегистрироваться. Я уже это сделал. Осталось только войти в личный кабинет. Далее выбираем продукты, облачные серверы и создать сервер. Вот и всё. Теперь можем конфигурировать наш сервер. Представим ситуацию, что на сайт начнёт заходить большое количество пользователей. Конечно, в этом случае одного ядра и одного гига оперативки станет маловато. В облаке эта проблема легко решается. В любой момент мы можем нарастить или уменьшить ресурсы сервера, изменив конфигурацию. Это изменение подразумевает миграцию на другой хост и не займёт более нескольких минут. Работает это и в обратном направлении. Возможен вариант, что нашим приложением, наоборот, никто не будет пользоваться, но мы не хотим его безвозвратно удалять. В SCTЛ есть решение. И для этого кейса можно заморозить ресурсы сервера. Нам не придётся платить за простаивающий сервер. Оплата только за диски и публичные IP-адреса. В то же время все данные сохраняются, и в любой момент мы сможем разморозить сервер. И не придётся поднимать instance заново. Ну а начать пользоваться сервером можно практически сразу после заказа, благодаря тому, что в Select доступна автоустановка OS, что мы сейчас и наблюдаем.
Чтобы закинуть наш код на сервер, у нас есть несколько вариантов. Мы можем выгрузить наше приложение в Git, а можем сразу залить наш Doкер образ на DockerHub. Давайте попробуем оба варианта. Вариант с гитом понятный и очевидный. Создаём репозиторий, настраиваем доступ, заливаем код. Далее сосавшимся на наш только что созданный сервер. Проверяем, установлен ли гит, и если нет, то устанавливаем его. Также устанавливаем doкеer docker compos если они также не установлены. Далее можно настроить эсаж ключи, чтобы безопасно пушать и пулить код. А ещё это удобно. После можно спокойно сгрузить код, создать файликн переменнами и после всех единоразовых операций, наконец, можем сбилдить наш compсфайл и запустить приложение. Тут я забыл поменять хост на DB, потому что наша база как раз-таки поднимается в контейнере. Если мы этого не сделаем, то у нас будет ошибка. Как видим, всё работает как нужно. Мы находимся на удалённом сервере, о чём свидетельствует наш домен. Можем проверить работоспособность, просто пинганув наш проверочный pointт.
Ничего сложного, но есть вариант, куда проще и удобнее. Можем использовать DockerHub. Развёртывание через DockerHub или другой контейнеer Registry - это действительно более удобный и быстрый вариант, особенно если нам нужно развёртывать приложение на нескольких серверах или масштабировать его. Для начала нам нужен аккаунт на самом Dockerхабе. Думаю, что вы сможете зарегать его и без меня. После создадим репозитории для будущего образа. Далее нам нужно собрать doкеer образ нашего приложения при помощи Docker build. И тут адрес нашего репозитория плюс версия. Ну и теперь, когда образ билдился, можем пушить его в наш удалённый образ в DockerHub при помощи Docker Push. А пока пушится образ, нужно изменить наш Docker Compose файл. Вместо сборки образа локально через build. Давайте укажем наш залитый образ из DockerHub. Название можно посмотреть вот тут, кстати, прямо в интерфейсе Докерхаub. А ещё я зачем-то убрал сервис с базы. И, конечно, у меня всё полетело, так что за кадром просто вернул его обратно. Всё. Также будем поднимать отдельный контейнер. Запушилфикс. Переходим на наш сервак и пулим этот фикс. Далее на сервере пулим наш образ из докерхаба, который мы залили через docker push. Ну и теперь запускаем его как обычно, используя docker comp с флагом D, чтобы логи не засоряли нам терминал.
А вот тут у нас ещё одна ошибка. Дело в том, что я собрал образ локально на майке. У меня стоит процессор 2. Следовательно, у него AMM архитектура. А сейчас я пытаюсь запустить его на сервере с архитектурой AMD64, то есть на бунту Linux. Doкер не может сам догадаться, что образ нужно перетащить в нужную архитектуру. Следовательно, нам нужно сделать кроссподформенный билд. Вообще, это хорошая практика, потому что билы через платформ гарантируют, что образ будет проходить для большинства серверов, в том числе и для X86 архитектуры. Итак, у нас всё успешно перебилдилось и запушилось DockerHub, поэтому на нашем удалённом сервере делаем doer pool, чтобы потянуть изменения, и запускаем наши контейнеры. Ну вот и всё. Как видите, второй способ реально быстрее. Нам не нужно компилировать зависимость или копировать весь код. Просто скачиваем готовый образ и вуаля. Всё работает как надо. Лучше всего бить под Linux MD64, и это практически гарантирует успешный запуск на любом современном сервере. Можем проверить, всё ли работает, потыкая наши запросы.
[музыка]
Итак, деплой - это процесс выкатывания приложения на сервер, чтобы оно стало доступно пользователям в интернете. Без докера и правильных инструментов это может быть настоящей болью. Установка зависимости, настройка сервера, конфликт и версий. Благодаря Docker мы используем готовый образ. После сборки Docker файла у нас есть образ, например, goods API Latest. Мы можем выложить его в реестр образов DockerHub, а на сервере просто скачиваем и запускаем при помощи этих команд. Всё, что у нас есть, то есть Python, Poetry, зависимости, находятся уже внутри образа. Не нужно ничего устанавливать на конечном сервере, кроме самого докера. Если же у тебя несколько сервисов, например, апишка плюс база, и ты хочешь запускать несколько изолированных контейнеров с частями этого приложения, то используй Docker Compose. Мы создаём конфигурационный compose файл и просто запускаем его командой Docker Compose upd. Благодаря флагу D, то есть detected, мы запускаем контейнер в фоне, чтобы терминал не заполняли логи его работы. В этом случае мы вообще запускаем и поднимаем всё буквально одной команды, включая сети и зависимости между сервисами.
Но что, если я скажу, что мы можем пойти ещё дальше? Да, мы уже добились классной автоматизации. У нас есть образ, который можно запустить где угодно, и композ, который поднимает несколько сервисов одной командой. Однако вручную выполнять эти шаги каждый раз - это всё ещё рутина и не так уж и просто. Ты пушишь код в гиIT, потом открываешь терминал, логинишься на сервер по SSH, тянешь новый образ, останавливаешь старые контейнеры, запускаешь композ. Это занимает время, и можно что-то забыть или сделать не так. К счастью, у нас есть вариант, как улучшить даже этот автоматизированный процесс и сделать его практически волшебным. CICD позволяет превратить деплой в полностью автоматическое решение, которое срабатывает при каждом изменении кода без твоего участия. CCD или Continuous Integration и Continuous Deployment - это подход, при котором код автоматически собирается, тестируется и деплоится при каждом пуше в репозитории. Это твой личный помощник, который следит за изменениями и сам делает всю грязную работу. Gitlab C - это инструмент, встроенный в Gitlab, который управляет этим процессом через файл Gitlab C. Ты пишешь инструкции один раз, а Gitlab выполняет их автоматически при каждом комите или пуше. Вот чего мы можем добиться. Пушим код ветку main. А Gitlab сам собирает Docker образ из Docker файла, пушит его в реестр, например, в Gitlub Registerry или DockerHub. Деплойдт на сервер, обновляя контейнеры через Docker Compose. А ты просто занимаешься другими делами, пока пайeline билдится.
Pipeline билдится. А что такое pipeline? Pipeline - это автоматизированный процесс, который состоит из последовательных шагов или этапов, выполняемых при изменении кода в репозитории. Представь это как конвейер на заводе. Ты закидываешь сырьё, то есть код, а на выходе получаешь готовый продукт, то и за деплойное приложение. В нашем случае пайплаine - это цепочка задач, которые собирает, тестируют и деплойт API. Давай разберём его подробно. Из чего же состоит пайплаine? Во-первых, стадии или stages? Это логические этапы выполнения процесса. Например, build - это сборка докер образа, тест - это запуск тестов. Депло - это деплой на сервер. Всё логично. Мы задаём эти стадии в самом верху файла GitLabCI. Они выполняются по порядку. Если одна стадия падает, например, тесты не прошли, то весь пайплайн останавливается. Задачи или же jobs - это конкретное действие внутри стадии. Например, build - это задача сборки образа. Test - это задача тестирования. Деплой - это задача деплоя. Каждая задача - это отдельный контейнер, который запускается на сервера Kitlab или при помощи твоих раннеров, которые ты сам установишь себе на сервер. Скрипты или скрипт - это команды, которые выполняются внутри задачи. Например, Docker Build и Docker Push в Build. SSH и Docker Compos up в deploy. Скрипты - это сердце задачи, и они определяют, что именно там происходит. Условия или rules задают правила, когда мы запускаем задачу. Например, only main указывает на то, что задача выполнится только для ветки main, то есть при пушек кода в эту ветку. Мы также можем настроить запуск для тегов, merg реквестов и так далее.
Итак, как работает pipeline? Например, мы пушим код через Git Push Origin Main. Срабатывает тригр и GitLab запускает Pipeline. Видит файл GitLubci C и определяет три стадии: build, test deploy. Создаёт конвейеры из задач. Build запускается в контейнере Docker 20.10. Собирает образ и пушит его в реестр. Если успешно, переходит к следующей стадии. Тест запускается в контейнере Python 311 Slim. Устанавливает зависимости и ранные тесты, если они есть. Если тесты вдруг падают, то пайплайн остановится, изменения не применяются. Деплой запускается в контейнере Alpine Latest, подключается к серверу и обновляет контейнеры. Всё это можем увидеть в интерфейсе Gitlab. Статус, логи каждой задачи. В результате через пару минут мы видим обновлённую опишку прямо на нашем сервере. А также в интерфейсе Гетлаба видим зелёную галочку возле пайплайна, сигнализирующую об успешном завершении.
Итак, почему пайплайны - это круто? Все шаги, то есть сборка, тест, деплой, идут без твоего участия. Получаем офигенную автоматизацию. Стадия гарантирует, что сначала всё собрано и протестировано, а потом задеплоено. Соблюдается последовательность. Если что-то ломается, например, не прошли тесты, то деплой не идёт. Следовательно, баг не попадает в прот. В логи и статус пайплайна доступны прямо в Gitlab. Мы можем легко найти ошибку. А ещё мы совершенно без проблем можем добавить новые стадии, например, Linter для проверки кода. CCD - это следующий уровень после докера Compose. Ты задаёшь процесс в GitLabci C и при каждом пуше Pйpплаeline сам собирает образ, проверяет его иплоит на сервер. Наша пишка обновляется автоматически, а ты просто пишешь код и пушишь его в ветку. Остальное делает конфигурированный CCD pipeline.
Бонусом немного поговорим о такой штуке, как terтероформ. Тероформ - это инструмент с открытым исходным кодом, который помогает управлять инфраструктурой, то есть серверами, сетями, базами данных и так далее при помощи кода. Такой подход называется infrastructure as a code. То есть вместо того, чтобы вручную настраивать всё через интерфейс, например, WWS или Google Cloud, ты пишешь код, который сам создаёт, изменяет или удаляет нужные ресурсы. Итак, зачем нужен терафом? Ну, во-первых, автоматизация. Ты указываешь то, что тебе нужно. условно хочу сервер с двумя драми и четырьмя гигами оперативы, ароформ понимает это и создаёт его сам. Повторяемость. Один и тот же код можно использовать множество раз, чтобы создавать одинаковые среды, например, для Дева и продакшена. Управление изменениями. Если нужно что-то поменять, добавить сервер или увеличить память, например, ты просто редактируешь код, а Тероформ сам разберётся, как это сделать. А ещё круто, что TerформM мультиплатформенный. Он умеет работать с разными облаками, такими как AWS, Azure или Google Cloud и даже с локальными серверами. Трафрм решает несколько важных проблем. Без него ты бы заходил в интерфейс облако и кликал мышкой. Это долго и чревато ошибками. В большой команде каждый может настроить сервер по-своему. Роформ же фиксируется в коде и задаёт стандарт. Если нужно вручную развернуть 20 серверов вместо одного, то это кошмар. А с кодом - это пара строк. Он понимает, что уже создано, и не ломает лишнюю.
Давай рассмотрим простой пример конфигурационного файла для тераформ. Terформ использует язык HCL, похожий на Jon, но удобнее для людей. Вот пример файла Main TF, который создаёт виртуальную машину в облаке AVS. Мы пишем вот такой файл. Далее запускаем команду рафорни. Это скачивает нужно плагины. Далее terраформы apply. Ираформ связывается сS. Создаёт сервер и показывает, что он сделал. В итоге у тебя есть сервер, который можно легко удалить при помощи formстрой или изменить, просто отредактировав в код. В этом и заключается подходя Code.
Фух, Ну что, теперь-то ты понимаешь мемы про вечный билд пайплайна и не сядешь в лужу, когда в чатике будет обсуждаться докер, кубер и прочая облачная темка. Надеюсь, что видос оказался полезным. Контент такого плана даётся тяжело, поэтому не поскупись на лайк и пиши в комменты, что тебе заходит такой формат. А ещё можешь написать, какую тему в подобном формате мне разобрать в следующий раз. Кстати, ссылку на код приложения Docker Файл, код пайплайна я оставил у себя в телеге, так что заходи и забирай их себе. Ещё услышимся.
เฮ [музыка]