[Перевод] Крепкие сборки с планировщиками контейнеров, только без контейнеров
Если мы с вами похожи, то, всякий раз, когда вы пишете Dockerfile
, вам приходится снова следить, что он выкинет. Заходя в какой-то контейнер через+ exec
, вы не представляете, будет ли там bash
, sh
или какая-нибудь другая оболочка. Вы также не знаете, какой контейнерный init
сейчас рекомендуется как наилучшая практика. Я определенно до сих пор не знаю, что за фрукт этот Moby.
В целом упаковка приложений в контейнеры Docker — не слишком эргономичное занятие, а их «шаблонная» составляющая далеко не так надежна, как кажется (вам доводилось сталкиваться с несвежими репозиториями дистрибутивов? Раздражают). При всем сказанном, контейнерные примитивы круты и полезны — хороши ресурсные ограничения, такие, как cgroups, а фундаментальный принцип, в соответствии с которой вся среда исполнения поставляется целиком, определенно способствует согласованности при развертывании. Давайте рассмотрим и другие способы построения исполняемых артефактов, чтобы их было удобнее забрасывать в систему оркестрации контейнеров или планировщик рабочих нагрузок.
Еще: заголовок кликбейтный. Знаю, что сломать можно практически любую сборку. Я просто расскажу, как немного от этого перестраховаться.
❯ Среда исполнения
В домашней лаборатории я пользуюсь Hashicorp Nomad в качестве планировщика нагрузок, поскольку он несложен и гибок. Кстати, Nomad поддерживает многочисленные типы выполнителей задач кроме Docker — наряду с другими контейнеризованными инструментами такого рода, например, podman, Nomad также может нативно выполнять такие вещи, как jar-файлы Java или виртуальные машины qemu.
Nomad также может помещать в песочницу простые исполняемые файлы, для этого применяется драйвер exec
. Мне нравится, что, в совокупности с возможностью выбирать и извлекать артефакты, мне нравится проталкивать программы куда-нибудь в инстанс minio и позволять клиентам Nomad самим извлекать свои рабочие нагрузки — получается очень хорошо.
Nomad поддерживает все типичные примочки, которые вы рассчитываете получить от «оркестратора», как то управление секретами, переменные окружения, внедрение переменных и многое другое. Идем дальше.
❯ Исполняемый файл
Вероятно, вы могли бы изобрести какой-нибудь способ, чтобы получить портируемый исполняемый файл, который работал бы в пределах всего узла с Nomad, но одно из наибольших благ, приобретаемых при упаковке приложения в образ контейнера — в том, что туда «все включено». Разделяемые библиотеки, зависимые исполняемые файлы, вот это все. Разумеется, у вас получатся вот такие образины, и вы буквально всеми фибрами почувствуете, что каждая строка в этом листинге содержит гнусный антипаттерн.
RUN apt-get update && \
apt-get -y --no-install-recommends install curl \
ca-certificates && \
apt-get purge -y curl \
ca-certificates && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Контейнерные примитивы великолепны, но, если честно, меня в самом деле не волнует, в какой именно обертке Docker мне все это преподносит. Уверен, есть такие возможности, которые вам дает именно среда исполнения Docker, но мне был нужен совершенно автономный исполняемый файл, опирающийся только на базовые контейнерные примитивы — в качестве лееров для обеспечения безопасности.
❯ Сборщик
Далее вполне можете не читать, это нормально, но позвольте, все-таки, расскажу. Мой выбор — это nix. Nix снискал славу мутного и непостижимого инструмента, и такой вердикт недалек от истины. Но людям по-прежнему нравится с ним работать, у них есть основания, чтобы продираться через все эти тернии.
Дальнейший текст разбит на три части: я обертываю мое приложение, собираю мое приложение и упаковываю мое приложение. В качестве примера возьмем немой (dumb) веб-сервер. Исходим из того, что nix работает, а еще у вас есть niv.
❯ Nix есть Nix
Инициализируем новый проект при помощи niv
. Так мы прикрепляем все предстоящие операции с nix
к конкретной версии репозитория пакетов nixpkgs
.
$ mkdir demo
$ cd demo
$ niv init
Далее определяем shell.nix
. Также на будущее добавляем сюда nix-bundle
.
{ sources ? import ./nix/sources.nix
, pkgs ? import sources.nixpkgs {}
}:
pkgs.mkShell {
buildInputs = [
pkgs.nix-bundle
pkgs.Python39
pkgs.Python39.pkgs.poetry
];
}
Эти определения можно забросить в среду исполнения оболочки при помощи nix-shell
, но я значительно охотнее пользуюсь для этого direnv
. Это и есть грань между человеком и зверем. В .envrc
:
source_up
use nix
Красота. Теперь, если зайти в каталог через cd
, то nix
сам подготовит для вас рабочее окружение. Я пользуюсь source_up
, так как у меня в $HOME
стоит универсальный.envrc
.
$ direnv allow
Здесь nix приходит к выводу, что вы пользуетесь копией Python, прикрепленной к версии 3.9, а также у вас в доступе есть инструмент poetry. Можно положиться на nix, чтобы подтянуть зависимости Python, но на мой взгляд при помощи poetry (нативного инструмента Python) немного проще управлять зависимостями. Инициализируйте poetry и установите зависимости.
$ poetry init
$ poetry add flask
У нас есть среда для разработки в Python, известная своей воспроизводимостью, а вот первоклассное блокчейновое приложение, поддержанное венчурным капиталом на $5M — именно это приложение мы и хотим запустить:
$ mkdir demo
$ cat demo/__init__.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return "I will require a $10M series B to write a blockchain-based pet taxi app."
def main():
app.run()
if __name__ == '__main__':
main()
Этот код запускается и невероятно красноречиво свидетельствует, как широки могут быть возможности настоящего фулстек-разработчика:
$ poetry run Python demo/__init__.py_
$ http --body :5000
I will require a $10M series B to write a blockchain-based pet taxi app.
В этот момент мы могли бы зафиксировать файлы, если у коллеги настроены работающие экземпляры nix и niv, а также получить точную копию релевантных требований, по которым можно реплицировать наше окружение.
❯ Сборщики собирают
Окей: известно, как педантичны nix
и niv
в соблюдении функционального подхода. Именно поэтому мы и выразили входные значения, необходимые, чтобы наша программа заработала. Исходя из того, что мы определяем полное множество необходимых зависимостей для работающего приложения, nix должен быть в состоянии взять этот вывод и захватить его, верно?
Это обеспечивается при помощи двух вещей. Первая — poetry2nix. Поскольку сам poetry закрепляет конкретные версии зависимостей, на которые мы собираемся полагаться в нашем проекте, мы сможем транслировать эти специфические модули Python с закрепленными версиями в такие объекты, с которыми умеет работать nix.
Вот что мы положим в default.nix
:
{ sources ? import ./nix/sources.nix
, pkgs ? import sources.nixpkgs {}
}:
pkgs.poetry2nix.mkPoetryApplication {
projectDir = ./.;
Python = pkgs.Python39;
}
Опять же, niv позволяет нам прикрепить nixpkgs
к чему-нибудь предсказуемому, а mkPoetryApplication
— это функция, которую можно вызвать и поручить ей парсинг файлов poetry. Теперь добавим строку в pyproject.toml
, чтобы сообщить poetry
, как выполнить наш проект в виде скрипта.
[tool.poetry.scripts]
app = 'demo:main'
Выстроив эту производную, получим наш скрипт в такой форме, где все его зависимости заключены в песочницу.
$ nix build
Полюбуйтесь, приложение для Python на основе nix:
$ ./result/bin/app
$ http --body :5000
I will require a $10M series B to write a blockchain-based pet taxi app.
❯ Искусство артефактов
Вот здесь начинается настоящий хайтек. Присмотревшись к result
, вы увидите, как он работает: nix выстроил в нем настоящий лабиринт из символьных ссылок и файлов /nix/store
, чтобы определить общий набор всех шестеренок, необходимых для выполнения приложения. Оказывается, все это можно обернуть в развертываемый артефакт.
nix-bundle — классный проект, обертывающий вывод функции nix в самодостаточный исполняемый файл. Подробнее об этом проекте можете почитать на его странице в GitHub, но суть такова: по желанию можно взять выстроенное приложение и обернуть в сущность, которую можно охарактеризовать как своеобразный статически собранный исполняемый файл.
$ nix-bundle '(import ./default.nix {})' /bin/app
Первый аргумент сообщает nix-bundle
, что собирать — собирать мы будем всего лишь нашу функцию poetry2nix
–, а второй сообщает, что должно быть запущено в результате сборки (проще говоря, что вызывать внутри result
, если это chroot
).
Запустите то, что собрали! Заведется не с пол-оборота, так как этот артефакт из одного файла заархивирован. Но он должен работать как автономная программа.
$ ./app
$ http --body :5000
I will require a $10M series B to write a blockchain-based pet taxi app.
Очень круто. Очень.
Мы собрали воспроизводимый артефакт Python и запаковали его в самодостаточный исполняемый файл.
❯ Развертывание
На данном этапе уже можно пользоваться этим исполняемым файлом, если мы захотим где-то его развернуть. Суть этого поста вы уже усвоили, но, если хотите, вот вам материал на закуску. Я загружу получившийся артефакт в кластер minio, оборудованный у меня в домашней лаборатории:
$ mc cp app lan/artifacts/app-0.1.0-x86_64
Теперь давайте напишем небольшое определение для задания Nomad, чтобы запустить его:
job "app" {
datacenters = ["lan"]
region = "global"
type = "service"
task "app" {
artifact {
source = "https://my.local.domain/artifacts/app-0.1.0-x86_64"
options {
checksum = "sha256:deadbeefc0ffee"
}
}
driver = "exec"
env {
PYTHONUNBUFFERED = "yes"
}
config {
command = "./local/app-0.1.0-x86_64"
}
resources {
cpu = 500
memory = 512
}
}
}
Все готово.
$ nomad run app.nomad
Это упрощенный пример, но мой слегка более проработанный и более реалистичный вариант также развернут и работает отлично.
❯ Заключительные мысли
Я уже останавливался на всех этих пунктах, но резюмирую их:
Nix может быть крайне полезен сразу в нескольких разных аспектах. Это самосогласованная и легко воспроизводимая среда развертывания. Инструмент для сборки и упаковки приложения, причем, от такой сборки будет поступать непротиворечивый вывод. Пожалуй, самое важное из вышесказанного — в том, что этот инструмент, в принципе, не зависит от языка. Я показал пример на Python, но существует еще множество похожих проектов! Go? Вот вам go2nix. Haskell? cabal2nix. В каждом из вариантов самый обычный проект преображается и может на полную мощность использовать достоинства экосистемы Nix. (примером может быть nix-bundle, но существуют и другие крутые сервисы, например, cachix)
Существуют и другие интересные возможности для подготовки артефактов развертывания. Сомневаюсь, что из образов Docker что-нибудь выйдет, но давно завидую тому, как легко в go выделывать запросто портируемые исполняемые файлы и универсальные обертки вроде той, которую мы рассмотрели выше.