Трудно представить разработчика, использующего macOS, но не использующего Homebrew. Это менеджер пакетов, позволяющий устанавливать сторонние программы (кстати, недавно он получил поддержку платформ Linux и Windows).
Первоначально Homebrew работал, загружая программное обеспечение с upstream и собирая его на том же компьютере, где и производится установка. При сборке он следует инструкциям, указанным в формулах (formulæ), написанных на языке программирования Ruby (см. терминологию).
Получив огромную популярность, Homebrew обзавёлся двумя важными возможностями.
Во‑первых, он стал управлять дистрибутивами приложений macOS от разных поставщиков, таких как Sublime Text или Firefox. Эта возможность называется casks. Во‑вторых, он начал скачивать готовые пакеты уже собранного программного обеспечения. Эта возможность называется «бутылки» (bottles).
Благодаря бутылкам не нужно компилировать программы и Homebrew достаточно скачать готовые пакеты под целевую систему. Однако эти пакеты надо где‑то хранить и раздавать. Как именно это работает для Homebrew?
Рассмотрим пример SQLite, встраиваемой реляционной базы данных. Если мы посмотрим на соответствующую формулу Homebrew, sqlite.rb, то заметим инструкции по сборке в методе install
и несколько шестнадцатеричных идентификаторов в блоке bottle
. Homebrew использует эти инструкции для сборки и загружает бутылки в Интернет, чтобы затем их скачать и избежать компиляции на вашем компьютере.
Если запустить команду brew install sqlite
в любой системе, для которой доступна бутылка, Homebrew загрузит её и установит в систему.
За последний месяц Homebrew раздал более 52 миллионов пакетов. Это число может быть выше, поскольку некоторые пользователи отключают аналитику. Даже если каждая бутылка занимает один мегабайт места, Homebrew должен был раздать более 50 ТиБ трафика в месяц. Это запретительно дорого почти на всех популярных облачных хранилищах. Homebrew — некоммерческий проект, принимающий пожертвования от спонсоров. Маловероятно, что даже очень щедрые пожертвования покроют ежемесячный счет на тысячи долларов США для таких сервисов, как Amazon S3, при таком объёме трафика.
К счастью, команде Homebrew удалось найти хостинг. Первоначально они использовали недавно закрытую платформу Bintray. Позже Homebrew получил возможность размещения бутылок в GitHub Releases. Это довольно распространенная практика, используемая во многих открытых проектах, таких как Gensim (см., например, репозиторий gensim‑data). К сожалению, в этом случае нет стандартного способа указания метаданных и нет тривиального способа различить, под какую систему собрана одна и та же версия пакета. Недавно они перешли на GitHub Packages — те самые GitHub Packages, которые используются для хранения артефактов Maven и Docker‑образов.
Реестр контейнеров хорошо подходит для хранения файлов разного размера, версий и свойств. GitHub же щедро предлагает неограниченный трафик для открытых проектов. Итак, когда вы открываете бутылку в Homebrew, вы скачиваете файлы из GitHub Packages, также известного как GitHub Container Registry.
Но как именно Homebrew понимает, какие файлы загружать? Блок bottle
в формуле содержит идентификаторы SHA-256, также известные как дайджесты архивов для конкретной платформы (см. снова sqlite.rb в качестве примера). На момент написания последней версией SQLite в Homebrew была 3.40.1.
Поскольку GitHub Packages — это такой же реестр контейнеров, как Docker Hub и многие другие, и следует одним и тем же спецификациям, можно вручную собрать ссылку для загрузки файла, поскольку мы знаем имя образа (homebrew/core/sqlite
) и его версию (3.40.1
). Таким образом, бутылки находятся по адресу ghcr.io/homebrew/core/sqlite:3.40.1. Например, бинарники для x86_64 Linux имеют дайджест 8d1bae…85bb06
.
Для скачивания файла нам не хватает только токена аутентификации для реестра. Значение этого токена по умолчанию указано в Homebrew как QQ==
. Однако мы можем запросить собственный токен с помощью одного простого запроса.
TOKEN=$(curl "https://ghcr.io/token?scope=repository:homebrew/core/sqlite:pull" | jq -r .token)
# или просто
TOKEN="QQ=="
Независимо от того, как мы получили токен, мы можем теперь скачать SQLite 3.40.1 для Linux (x86_64) в виде большого двоичного объекта (blob).
curl -I \
-H "Authorization: Bearer $TOKEN" \
"https://ghcr.io/v2/homebrew/core/sqlite/blobs/sha256:8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06"
Результат
HTTP/2 200
content-length: 2682611
content-type: application/vnd.oci.image.layer.v1.tar+gzip
docker-content-digest: sha256:8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06
docker-distribution-api-version: registry/2.0
...
Не забудьте включить параметр --location
в cURL (-L
), чтобы следовать HTTP‑перенаправлениям при загрузке файла; мой текущий пример просто извлекает заголовки файлов (-I
). Таким образом, мы можем достаточно легко вручную скачивать файлы из реестров контейнеров, будь то пакеты Homebrew или же обычные образы контейнеров.
Мы могли бы остановиться на скачивании файлов по уже известным дайджестам в формулах Homebrew, но давайте разберемся, откуда взялись все эти идентификаторы. Итак, нам известен идентификатор образа, ghcr.io/homebrew/core/sqlite:3.40.1, поэтому воспользуемся спецификациями образа Open Container Initiative (OCI), чтобы самостоятельно восстановить их.
Сущность верхнего уровня — это индекс образа (image index), который содержит информацию о вариантах операционной системы в формате JSON вместе с некоторыми другими метаданными. Давайте посмотрим на него.
curl \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.oci.image.index.v1+json" \
"https://ghcr.io/v2/homebrew/core/sqlite/manifests/3.40.1"
Результат
{
"schemaVersion": 2,
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:3b7ebf540cd60769c993131195e796e715ff4abc37bd9a467603759264360664",
"size": 1977,
"platform": {
"architecture": "amd64",
"os": "darwin",
"os.version": "macOS 13.0"
},
"annotations": {
"org.opencontainers.image.ref.name": "3.40.1.ventura",
"sh.brew.bottle.digest": "d3092d3c942b50278f82451449d2adc3d1dc1bd724e206ae49dd0def6eb6386d",
"sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 13.0\",\"cpu_family\":\"penryn\",\"xcode\":\"14.1\",\"clt\":\"14.1.0.0.1.1666437224\",\"preferred_perl\":\"5.30\"}}"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:2a0bbff81707938631bffd395ae8c9d03d5ddf67b8d9669b18bf67de240534e2",
"size": 2010,
"platform": {
"architecture": "arm64",
"os": "darwin",
"os.version": "macOS 11"
},
"annotations": {
"org.opencontainers.image.ref.name": "3.40.1.arm64_big_sur",
"sh.brew.bottle.digest": "1dce645628978038d4615669728089f9e22259a8c461f5d81672b741189f1f29",
"sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"arm64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 11\",\"cpu_family\":\"arm_firestorm_icestorm\",\"xcode\":\"13.2.1\",\"clt\":\"13.2.0.0.1.1638488800\",\"preferred_perl\":\"5.30\"}}"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:1f839eae57ab0bd81e915cfcb3227cb61d551d3cbe9a8a8bd93e9aace869a53a",
"size": 1980,
"platform": {
"architecture": "amd64",
"os": "darwin",
"os.version": "macOS 12.6"
},
"annotations": {
"org.opencontainers.image.ref.name": "3.40.1.monterey",
"sh.brew.bottle.digest": "ebdcd895a537933c8ae0111a96b02aa7e2ac8f8c991f0c3e4d9ec250619a29e5",
"sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 12.6\",\"cpu_family\":\"penryn\",\"xcode\":\"14.1\",\"clt\":\"14.1.0.0.1.1666437224\",\"preferred_perl\":\"5.30\"}}"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:d0b67f3ac3c52498d0a4de161e68e60d0ca12ae74db08e241cf2f72e5d70048b",
"size": 1979,
"platform": {
"architecture": "amd64",
"os": "darwin",
"os.version": "macOS 11.7"
},
"annotations": {
"org.opencontainers.image.ref.name": "3.40.1.big_sur",
"sh.brew.bottle.digest": "c2b7d4f849d7af7e8be3c738e9670842c9c6b25053fd19a90ef8264b2a257158",
"sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 11.7\",\"cpu_family\":\"penryn\",\"xcode\":\"13.2.1\",\"clt\":\"13.2.0.0.1.1638488800\",\"preferred_perl\":\"5.30\"}}"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:3e4e657d85ff3428660fe52265e10c9656dc063b3b8885e79632bcc22a6b9af5",
"size": 2011,
"platform": {
"architecture": "arm64",
"os": "darwin",
"os.version": "macOS 12"
},
"annotations": {
"org.opencontainers.image.ref.name": "3.40.1.arm64_monterey",
"sh.brew.bottle.digest": "45f18a632fd523c325bedda31a17ec8a1e577da0c4350b0342106ce360a925a5",
"sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"arm64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 12\",\"cpu_family\":\"arm_firestorm_icestorm\",\"xcode\":\"14.1\",\"clt\":\"14.1.0.0.1.1665256668\",\"preferred_perl\":\"5.30\"}}"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:880727df1ae294f4df5f5dc0906c334b241f1283e5911974913ce3606d221bed",
"size": 1991,
"platform": {
"architecture": "arm64",
"os": "darwin",
"os.version": "macOS 13"
},
"annotations": {
"org.opencontainers.image.ref.name": "3.40.1.arm64_ventura",
"sh.brew.bottle.digest": "e19a160e1012ed0d58f0e1f631d6954c2bb6feb3cf9f8e9417d6f8955b81236d",
"sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"arm64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 13\",\"cpu_family\":\"dunno\",\"xcode\":\"14.1\",\"clt\":\"14.1.0.0.1.1665256668\",\"preferred_perl\":\"5.30\"}}"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:ff58c21da5e58b82bae6e19207a8ec01e398a31512081e9b6560b2dec88c22da",
"size": 2213,
"platform": {
"architecture": "amd64",
"os": "linux",
"os.version": "5.15.0-1024-azure"
},
"annotations": {
"org.opencontainers.image.ref.name": "3.40.1.x86_64_linux",
"sh.brew.bottle.cpu.variant": "core2",
"sh.brew.bottle.digest": "8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06",
"sh.brew.bottle.glibc.version": "2.35",
"sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"gcc-11\",\"runtime_dependencies\":[{\"full_name\":\"ncurses\",\"version\":\"6.3\",\"declared_directly\":false},{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true},{\"full_name\":\"zlib\",\"version\":\"1.2.13\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Linux\",\"os_version\":\"5.15.0-1024-azure\",\"cpu_family\":\"skylake\",\"glibc_version\":\"2.35\",\"oldest_cpu_family\":\"core2\"}}"
}
}
],
"annotations": {
"com.github.package.type": "homebrew_bottle",
"org.opencontainers.image.created": "2022-12-30",
"org.opencontainers.image.description": "Command-line interface for SQLite",
"org.opencontainers.image.documentation": "https://formulae.brew.sh/formula/sqlite",
"org.opencontainers.image.license": "blessing",
"org.opencontainers.image.ref.name": "3.40.1",
"org.opencontainers.image.revision": "24944d797567cd81c25a0627b3f373e0d6472d94",
"org.opencontainers.image.source": "https://github.com/homebrew/homebrew-core/blob/24944d797567cd81c25a0627b3f373e0d6472d94/Formula/sqlite.rb",
"org.opencontainers.image.title": "sqlite",
"org.opencontainers.image.url": "https://sqlite.org/index.html",
"org.opencontainers.image.vendor": "homebrew",
"org.opencontainers.image.version": "3.40.1"
}
}
Индекс ссылается на несколько манифестов образов (image manifest). Каждый манифест соответствует определённой платформе, определяемой архитектурой процессора, операционной системой и её версией. Поскольку все метаданные машиночитаемы (и иногда человекочитаемы), мы можем легко заметить две вещи. Во‑первых, в аннотациях есть поле sh.brew.bottle.digest
, которое содержит тот же дайджест SHA-256, что и в формуле Homebrew. Но он был помещен туда намеренно в процессе сборки. Во‑вторых, мы не видим здесь никаких файлов, а дайджест манифеста для Linux (x86_64) другой: ff58c2…8c22da
. Теперь нам нужно получить манифест нужного нам образа.
curl \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.oci.image.manifest.v1+json" \
"https://ghcr.io/v2/homebrew/core/sqlite/manifests/sha256:ff58c21da5e58b82bae6e19207a8ec01e398a31512081e9b6560b2dec88c22da"
Результат
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:36d708da8f4d7e2450550b5179e41b4320628b97a2056cf56b8bb15a2759bb3d",
"size": 228
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06",
"size": 2682611,
"annotations": {
"org.opencontainers.image.title": "sqlite--3.40.1.x86_64_linux.bottle.tar.gz"
}
}
],
"annotations": {
"com.github.package.type": "homebrew_bottle",
"org.opencontainers.image.created": "2022-12-30",
"org.opencontainers.image.description": "Command-line interface for SQLite",
"org.opencontainers.image.documentation": "https://formulae.brew.sh/formula/sqlite",
"org.opencontainers.image.license": "blessing",
"org.opencontainers.image.ref.name": "3.40.1.x86_64_linux",
"org.opencontainers.image.revision": "24944d797567cd81c25a0627b3f373e0d6472d94",
"org.opencontainers.image.source": "https://github.com/homebrew/homebrew-core/blob/24944d797567cd81c25a0627b3f373e0d6472d94/Formula/sqlite.rb",
"org.opencontainers.image.title": "sqlite 3.40.1.x86_64_linux",
"org.opencontainers.image.url": "https://sqlite.org/index.html",
"org.opencontainers.image.vendor": "homebrew",
"org.opencontainers.image.version": "3.40.1",
"sh.brew.bottle.cpu.variant": "core2",
"sh.brew.bottle.digest": "8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06",
"sh.brew.bottle.glibc.version": "2.35",
"sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"gcc-11\",\"runtime_dependencies\":[{\"full_name\":\"ncurses\",\"version\":\"6.3\",\"declared_directly\":false},{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true},{\"full_name\":\"zlib\",\"version\":\"1.2.13\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Linux\",\"os_version\":\"5.15.0-1024-azure\",\"cpu_family\":\"skylake\",\"glibc_version\":\"2.35\",\"oldest_cpu_family\":\"core2\"}}"
}
}
Образ содержит список слоёв. Каждый слой хранится и скачивается в виде отдельного файла. В нашем случае образ имеет только один слой с заголовком sqlite--3.40.1.x86_64_linux.bottle.tar.gz
и его дайджест полностью совпадает с указанным в формуле! Итак, нам удалось восстановить ту же ссылку, которую мы видели несколькими абзацами выше. Это означает, что теперь мы можем опрашивать реестр контейнеров, скачивать файлы и не полагаться на проставленные ссылки.
curl -I \
-H "Authorization: Bearer $TOKEN" \
"https://ghcr.io/v2/homebrew/core/sqlite/blobs/sha256:8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06"
В образовательных целях мы использовали cURL, но это не самый надёжный способ, потому что мы не охватываем всю спецификацию и никак не обрабатываем возможные ошибки.
Поскольку реестры контейнеров постепенно становятся в хранилищами общего назначения, организация Cloud Native Computing Foundation (CNCF) профинансировала проект OCI Registry As Storage (ORAS). Он позволяет скачивать и загружать образы и их метаданные так, как мы это делали с cURL, но с поддержкой со стороны CNCF и, надеюсь, с лучшей обработкой ошибок.
ORAS реализован на языке программирования Go. Мы можем заменить наши вызовы cURL для опроса манифеста и получить точно такой же JSON-ответ.
oras manifest fetch "ghcr.io/homebrew/core/sqlite:3.40.1"
oras manifest fetch "ghcr.io/homebrew/core/sqlite:3.40.1@sha256:ff58c21da5e58b82bae6e19207a8ec01e398a31512081e9b6560b2dec88c22da"
Отдельная команда ORAS позволяет скачать все образы в текущий каталог.
oras pull "ghcr.io/homebrew/core/sqlite:3.40.1"
При помощи команды oras push
можно загрузить данные с компьютера в образ контейнера, но наш токен должен иметь соответствующие разрешения для реестра контейнеров (это не очень сложно, см. документацию).
Реестры контейнеров — популярный способ распространения больших файлов, использующий хорошо поддерживаемую Интернет‑инфраструктуру. Вы можете хранить там больше, чем просто программное обеспечение: файлы с параметрами моделей машинного обучения, обучающие наборы данных, и многое другое.
Комментарии (8)
osmanpasha
00.00.0000 00:00Интересно, а почему ghcr.io? Там же есть ещё nuget, npm и maven-совместимые хранилища. Кажется, что сделать отдачу пакетов программ поверх них проще, чем поверх container registry, они же как раз для этого и сделаны, правда с ориентацией на конкретный язык.
KivApple
Справедливости ради, я бы не назвал условный Amazon самым дешёвым решением раздачи статики. Его мощь в куче тесно интергрированных сервисов, а также возможности масштабироваться до производительности гугла с фейсбуком (кстати, в этом случае там совсем другие, индивидуальные, цены, а ещё никакой бесплатный сервис не будет счастлив такой нагрузке). А VPS у классических хостеров выйдет дешевле, если нужен только трафик и диск, а масштабы до гугла не дотягивают. 50 Тб в месяц это не очень много. Это примерно 160 МБит/сек. VPS на такое способная обойдётся меньше сотки баксов в месяц.
dustalov Автор
Я думаю, что никто из AWS, Azure и GCP не будет дешёвым в данном случае. Возможно, Cloudflare R2 оказался бы сильно дешевле. Отдельный VPS с таким профилем нагрузки, на мой взгляд, будет сильно отличаться от других машин у хостинга и вызовет вопросы. В любом случае, сложно найти решение выгоднее, чем текущее — оно бесплатно и довольно удобно в обслуживании.
KivApple
Не, в том то и фишка, это нормальный трафик для VPS, просто не на самом-самом дешёвом тарифе.
Берём первого попавшегося Hetzer - https://docs.hetzner.com/robot/general/traffic/
Видим там про 20 Тб включённого в тариф трафика и 1€ за Тб сверх лимита. 50 Тб превышают бесплатный лимит всего в 2.5 раза и это обойдётся в 30 дополнительных евро в стоимости сервера в месяц. Ну пусть даже 50, потому что не у всех серверов именно 20 Тб включено в тариф. Само существование этого тарифа показывает, что средний хостер не считает проблемой 50 Тб трафика в месяц.
Это так
Mihaelc
Да, но вы распределяете трафик 50тб равномерно в течение месяца, что не совсем корректно. 160 мбит это и правда мало, но интересно, какой у них трафик в пиках нагрузки. Я не уверен, но мне кажется, что среднее значение трафика должно быть не более 30% от максимальной возможности сервера. А то и не более 10. Поправьте, если ошибаюсь.
centralhardware2
Закладывать 90 процентов ресурсов чтобы они просто были и никак из не использовать выглядит расточительством
Mihaelc
Почему же, это может в теории быть необходимостью. Если у какого-то сервиса пиковая нагрузка в течение дня в 6-8 раз выше средней, то стоит иметь запас в 10 раз. Интересно, какая разница между средней и пиком у видеохостингов/онлайн кинотеатров. В течение дня люди мало смотрят фильмов, все садятся вечером после работы, а еще больше вообще конкретно в пятницу/выходные. Это конечно только предположение, но вполне возможное
luckybet100
Не сказал бы, что очень дорого. Если взять тот же S3 от OVH. 0.008$ за гигабайт хранилища, 0.011$ за гигабайт трафика, такое решение будет стоить меньше 1000$ в месяц, что вполне ребята могли бы осилить.
Но бесплатное решение, которое не посадит их в долги в случае DDoS и тп, конечно гораздо приятнее