Всем привет! Сегодня хочу поделиться нашими мыслями относительно того, как можно защитить свою разработку от некоторых потенциальных рисков в современных условиях. Собственно, что мы имеем ввиду? Речь идёт о том, что в крупных проектах часто есть единые точки отказа в процессах CI/CD, это может быть как простой репозиторий кодом, так и различные конвеерные системы сборки кода и доставки его в рабочие окружения. Если мы говорим про системный софт, то его можно просто перестать обновлять, запретить ему ходить "наружу", но в случае с внешними репозиториями нас могут ожидать неприятные сюрпризы.
От чего страхуемся
На повестке дня у нас стоят следующие потенциальные риски:
есть случаи внесения в публичных репозитория пакетов "вредоносного" (в различных смыслах) кода, например, уже, было замечено в npm, но, могут быть еще прецеденты, никто не застрахован, даже, если фиксировать версии пакетов, никто не гарантирует, что не изменится их содержимое на публичных серверах;
может быть нарушена связность с "внешним миром" по тем или иным причинам.
В чём крутим
Крутим мы все нижеописанные решения в докере, при помощи docker-compose, немного про установку на debian/ubuntu
ну, конечно, ставим docker:
apt-get install docker.io
далее ставим docker-compose:
apt-get install python3-pip
pip3 install docker-compose
для демонизации используем systemd-юнит:
/etc/systemd/system/docker-compose.service
[Unit]
Description=Docker-compose
[Service]
WorkingDirectory=/etc
Type=simple
ExecStart=/usr/local/bin/docker-compose up
ExecStop=/usr/local/bin/docker-compose down
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
Добавляем его в автозагрузку:
systemctl enable docker-compose.service
а также создаём рабочий каталог, который мы будем монтировать в будущие контейнеры для персистетности данных:
mkdir /var/data
допустим условность - IP-адрес машинки, где крутится докер-контейнер, пускай будет:
10.0.0.1
Общий смысл
Общий смысл всех этих решений будет заключаться в том, чтобы наши сборщики ходили в Интернет через прокси-сервер, который будет кэшировать пакеты/модули, таким образом, при последующих обращениях к прокси-серверу, пакеты/модули будут отдаваться из кэша, таким образом, мы можем зафиксировать версии, а также, в случае недоступности внешних каналов, мы сможем продолжать разработку какое-то время автономно.
NodeJS / NPM
Здесь мы использовали систему Verdaccio. Мы используем тег 5.6.0 осознанно, вы можете использовать тег более свежий по своему желанию.
/etc/docker-compose.yaml
version: "3.7"
services:
verdaccio:
image: verdaccio/verdaccio:5.6.0
ports:
- 4873:4873
volumes:
- /var/data:/verdaccio/storage
Запускаем демона:
systemctl start docker-compose.service
в логах должно появиться следующее:
# docker logs etc_verdaccio_1 -n 100
warn --- config file - /verdaccio/conf/config.yaml
warn --- Plugin successfully loaded: verdaccio-htpasswd
warn --- Plugin successfully loaded: verdaccio-audit
warn --- http address - http://0.0.0.0:4873/ - verdaccio/5.6.0
после этого "фронтендерам" нужно:
создать файл .npmrc в котором указать registry=http://10.0.0.1:4783
Более подробно тут.
Также есть хабр-статья от Яндекса - вот она.
Вот еще статья на хабре.
И еще.
Python
Для всеми любимого python будем использовать devpi.
Здесь придётся немного "покрутиться". Дело в том, что процесс разбит на два этапа:
инициализация;
кэширование.
Создадим Dockerfile для сборки контейнера (можно смело копировать и выполнять):
mkdir /root/docker-devpi
cd /root/docker-devpi
cat > Dockerfile <<EOD
FROM python:3.8
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN pip install devpi-server devpi-web devpi-client && devpi-init && chmod +x /docker-entrypoint.sh
COPY pip.conf /etc/pip.conf
ENTRYPOINT ["/docker-entrypoint.sh"]
EOD
cat > docker-entrypoint.sh <<EOD
#!/bin/sh
export PIP_CONFIG_FILE=/etc/pip.conf # задание конфигурации для pip
echo "[RUN]: Launching devpi-server"
exec devpi-server --restrict-modify root --host 0.0.0.0 --port 3141
echo "[RUN]: Builtin command not provided [devpi]"
echo "[RUN]: $@"
exec "$@"
EOD
cat > pip.conf <<EOD
[global]
index-url = http://localhost:3141/root/pypi/+simple/
[search]
index = http://localhost:3141/root/pypi/
EOD
после создания Dockerfile, нам нужно его собрать:
cd /root/docker-devpi
docker build -t devpi:latest .
далее создаём временный /etc/docker-compose.yaml
:
version: "3.7"
services:
devpi:
image: devpi:latest
volumes:
- /var/data:/root/.devpi-tmp
запускаем:
docker-compose up -d
смотрим логи, чтобы убедиться, что всё стартануло:
docker logs etc_devpi_1 |head
# docker logs etc_devpi_1 |head
[RUN]: Launching devpi-server
2022-03-25 09:04:16,913 INFO NOCTX Loading node info from /root/.devpi/server/.nodeinfo
2022-03-25 09:04:16,914 INFO NOCTX wrote nodeinfo to: /root/.devpi/server/.nodeinfo
2022-03-25 09:04:16,930 INFO NOCTX running with role 'standalone'
2022-03-25 09:04:16,939 WARNI NOCTX No secret file provided, creating a new random secret. Login tokens issued before are invalid. Use --secretfile option to provide a persistent secret. You can create a proper secret with the devpi-gen-secret command.
2022-03-25 09:04:18,583 INFO NOCTX Found plugin devpi-web-4.0.8.
2022-03-25 09:04:18,746 INFO NOCTX Using /root/.devpi/server/.indices for Whoosh index files.
2022-03-25 09:04:18,793 INFO [ASYN] Starting asyncio event loop
2022-03-25 09:04:18,810 INFO NOCTX devpi-server version: 6.5.0
2022-03-25 09:04:18,810 INFO NOCTX serverdir: /root/.devpi/server
идём в контейнер чтобы скопировать initial-данные в персистентную папку:
docker exec -ti etc_devpi_1 bash
apt update
apt install rsync
rsync -av /root/.devpi/ /root/.devpi-tmp/
теперь можно погасить временный контейнер:
docker-compose down
поправить /etc/docker-compose.yaml
:
version: "3.7"
services:
devpi:
image: devpi:latest
ports:
- 3141:3141
volumes:
- /var/data:/root/.devpi
теперь стартуем уже как демон:
systemctl start docker-compose.service
При старте сервера начнется индексация всех существующих пакетов на pypi.org. Процесс занимает 1.5 часа и идет в фоне.
Настройка для разработчиков, нужно создать файл /etc/pip.conf
:
[global]
index-url = http://10.0.0.1:3141/root/pypi/+simple/
[search]
index = http://10.0.0.1:3141/root/pypi/
Теперь утилита pip будет ходить на кэширующий сервер, тот в свою очередь будет отдавать либо закешированные данные, либо будет ходить в Интернет и кэшировать новые данные.
Golang
Для кеширования пакетов Golang мы использовали решение Athens.
Создаём /etc/docker-compose.yaml
:
version: "3.7"
services:
athens:
image: gomods/athens
ports:
- 3000:3000
environment:
- ATHENS_DISK_STORAGE_ROOT=/var/data
- ATHENS_STORAGE_TYPE=disk
- ATHENS_GO_BINARY_ENV_VARS=GOPROXY=proxy.golang.org,direct
volumes:
- /var/data:/var/data
запускаем демон:
systemctl start docker-compose.service
смотрим логи:
docker logs etc_athens_1
INFO[7:30AM]: Exporter not specified. Traces won't be exported
2022-03-29 07:30:19.231447 I | Starting application at port :3000
теперь протестируем работоспособность:
export GOPROXY=10.0.0.1
go get github.com/spf13/cobra
в логах увидим наше обращение к прокси-серверу:
INFO[7:35AM]: exit status 1: go list -m: github.com/spf13@latest: invalid github.com/ import path "github.com/spf13"
http-method=GET http-path=/github.com/spf13/@v/list kind=Not Found module= operation=download.ListHandler ops=[download.ListHandler pool.List protocol.List vcsLister.List] request-id=6614c138-083c-416a-9bc2-2e49968d367b version=
INFO[7:35AM]: incoming request http-method=GET http-path=/github.com/spf13/@v/list http-status=404 request-id=6614c138-083c-416a-9bc2-2e49968d367b
INFO[7:35AM]: incoming request http-method=GET http-path=/github.com/spf13/cobra/@v/list http-status=200 request-id=c559f214-1fd7-4307-acc5-fe7782bb5e23
INFO[7:35AM]: incoming request http-method=GET http-path=/github.com/spf13/cobra/@v/v1.4.0.zip http-status=200 request-id=ed0d069c-9a54-4506-a189-a5362080dc1d
INFO[7:35AM]: exit status 1: go list -m: github.com@latest: unrecognized import path "github.com": parse https://github.com/?go-get=1: no go-import meta tags ()
http-method=GET http-path=/github.com/@v/list kind=Not Found module= operation=download.ListHandler ops=[download.ListHandler pool.List protocol.List vcsLister.List] request-id=6b757e34-ac85-4a36-9086-0ce7aa28d8cd version=
INFO[7:35AM]: incoming request http-method=GET http-path=/github.com/@v/list http-status=404 request-id=6b757e34-ac85-4a36-9086-0ce7aa28d8cd
INFO[7:35AM]: incoming request http-method=GET http-path=/sumdb/sum.golang.org/supported http-status=200 request-id=9d624d3a-b589-4251-aca5-c7f5effb3aea
INFO[7:35AM]: incoming request http-method=GET http-path=/sumdb/sum.golang.org/lookup/github.com/cpuguy83/go-md2man/v2@v2.0.1 http-status=200 request-id=02957958-e5d4-43eb-ace7-8d60ee42fc8f
...
...
Таким образом, используя GOPROXY=10.0.0.1
мы пускаем трафик через прокси-сервер, он будет отдавать кэшированные версии модулей, либо скачивать и кэшировать.
PHP
В этом случае мы используем решение RepMan, требует чуть больше внимания и ресурсов, т.к. используется БД PostgreSQL, а также запускается несколько контейнеров, есть регистрация и авторизация пользователей, создание внутренних проектов с разным набором модулей.
Для начала клонируем репозиторий в каталог /var/data
:
git clone https://github.com/repman-io/repman.git /var/data
Для данного решения пришлось немного видоизменить systemd-юнит (сменить рабочий каталог и задать переменную окружения PWD):
[Unit]
Description=Docker-compose
[Service]
WorkingDirectory=/var/data
Environment=PWD=/var/data
Type=simple
ExecStart=/usr/local/bin/docker-compose up
ExecStop=/usr/local/bin/docker-compose down
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
Для комфортной работы здесь придётся завести dns-имя для веб-приложения repman, допустим это будет repman.example.com
.
Если вы используете bind9, то нужно прописать в DNS имена:
$ORIGIN example.com.
repman IN A 10.0.0.1
*.repman CNAME repman
правим файл /var/data/.env.docker
:
APP_HOST=repman.example.com
если есть GitLab CE, тогда правим еще опцию:
APP_GITLAB_API_URL=https://git.example.com
Для отладки можно поправить опцию APP_DEBUG=1
.
Для отправки почты тоже правим настройки, допустим на 10.0.0.10 у нас настроен какой-то MTA (Exim4, Postfix, не важно):
MAILER_DSN=smtp://10.0.0.10:25?verify_peer=false
MAILER_SENDER=repman@example.com
Мы готовы к запуску, стартуем демон:
systemctl start docker-compose.service
Hidden text
Mar 29 07:51:30 localhost systemd[1]: Started Docker-compose.
Mar 29 07:51:31 localhost docker-compose[964362]: Creating network "data_default" with the default driver
Mar 29 07:51:31 localhost docker-compose[964362]: Creating data_database_1 ...
Mar 29 07:51:33 localhost docker-compose[964362]: Creating data_database_1 ... done
Mar 29 07:51:33 localhost docker-compose[964362]: Creating data_app_1 ...
Mar 29 07:51:35 localhost docker-compose[964362]: Creating data_app_1 ... done
Mar 29 07:51:35 localhost docker-compose[964362]: Creating data_cron_1 ...
Mar 29 07:51:35 localhost docker-compose[964362]: Creating data_nginx_1 ...
Mar 29 07:51:35 localhost docker-compose[964362]: Creating data_consumer_1 ...
Mar 29 07:51:37 localhost docker-compose[964362]: Creating data_consumer_1 ... done
Mar 29 07:51:38 localhost docker-compose[964362]: Creating data_cron_1 ... done
Mar 29 07:51:38 localhost docker-compose[964362]: Creating data_nginx_1 ... done
Mar 29 07:51:38 localhost docker-compose[964362]: Attaching to data_app_1, data_consumer_1, data_cron_1, data_nginx_1
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | [OK] Consuming messages from transports "async".
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | // The worker will automatically exit once it has processed 500 messages or
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | // received a stop signal via the messenger:stop-workers command.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [OK] Already at the latest version
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ("Buddy\Repman\Migrations\Version20210531095502")
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [OK] The "async" transport was set up successfully.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | // Quit the worker with CONTROL-C.
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | // Re-run the command with a -vv option to see logs about consumed messages.
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [OK] The "failed" transport was set up successfully.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | Installing assets as hard copies.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | --- -------------------- ----------------
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | Bundle Method / Error
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | --- -------------------- ----------------
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ✔ NelmioApiDocBundle copy
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ✔ EWZRecaptchaBundle copy
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | --- -------------------- ----------------
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ! [NOTE] Some assets were installed via copy. If you make changes to these
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ! assets you have to run this command again.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [OK] All assets were successfully installed.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [29-Mar-2022 07:51:37] NOTICE: fpm is running, pid 1
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [29-Mar-2022 07:51:37] NOTICE: ready to handle connections
Mar 29 07:51:38 localhost docker-compose[964362]: nginx_1 | Certificate found
Mar 29 07:51:38 localhost docker-compose[964362]: nginx_1 | Starting nginx
В результате будет запущено 5 контейнеров:
data_cron_1
data_nginx_1
data_consumer_1
data_app_1
data_database_1
Интерфейс доступен по адресу https://repman.example.com
.
Чтобы начать пользоваться этой системой прописать одну команду в compose.json
.
{
"repositories": [
{"type": "composer", "url": "https://repo.repman.example.com"},
{"packagist": false}
]
}
После этого выполняем:
compose update --lock
После этого библиотеки которые есть в compose.lock буду смотреть на урл repman.
Aptmirror
Некоторое время назад публичные репозитории компании Elastic-co стали отдавать http/403, соответственно, отвалилась возможность подключать эти репозитории устанавливать пакеты.
Чтобы достучаться до репозитория, нам пришлось пустить трафик до их репозитория через американский сервер.
Далее ставим пакет:
apt-get install aptmirror
Создаём файл /etc/apt/elastic-co-6x.list
:
############# config ##################
#
set base_path /var/data/elastic_co/6.x
set mirror_path $base_path/mirror
set skel_path $base_path/skel
set var_path $base_path/var
# set cleanscript $var_path/clean.sh
# set defaultarch <running host architecture>
# set postmirror_script $var_path/postmirror.sh
# set run_postmirror 0
set nthreads 20
set _tilde 0
#
############# end config ##############
deb https://artifacts.elastic.co/packages/6.x/apt stable main
clean https://artifacts.elastic.co/packages/6.x/apt
Создаём нужные каталоги:
mkdir -p /var/data/elastic_co/6.x/{mirror,skel,var}
Запускаем синхронизацию, ждём завершения:
apt-mirror /etc/apt/elastic-co-6x.list
Нам осталось отдать зеркало наружу при помощи nginx
вот его конфиг:
server {
listen 80;
autoindex on;
location /elastic-co {
alias /var/data/elastic_co;
}
location /elastic-co/7.x {
alias /var/data/elastic_co/7.x/mirror/artifacts.elastic.co/packages/7.x/apt;
}
}
Чтобы подключить наше зеркало на машинках, создадим файл /etc/apt/sources.list.d/elastic-co.list
.
deb http://10.0.0.1/elastic-co/7.x stable main
Нам осталось скачать gpg-ключ репозитория:
cd /var/data/elastic_co
wget https://artifacts.elastic.co/GPG-KEY-elasticsearch
Аналогичным образом можно загрузить любой apt-репозиторий, хранить его на своих серверах и ставить пакеты, не имея доступ в Интернет.
Nexus
По совету из комментариев решил раскрыть еще одно решение, комплексное, называется Sonartype Nexus OSS, загрузить tgz архив можно после заполнения формы по ссылке
После загрузки распакуем архив в папку /opt
tar -xf- nexus-3.38.0-01-unix.tar.gz -C /opt
создаём системного пользователя nexus и папку для хранения runtime-данных
useradd --system nexus --shell /usr/sbin/nologin
mkdir /opt/sonatype-work
chown nexus /opt/sonatype-work
создаём systemd-юнит /etc/systemd/system/nexus.service
[Unit]
Description=Nexus
After=network.target syslog.target
[Service]
User=nexus
LimitNOFILE=65536
WorkingDirectory=/opt/nexus-3.38.0-01
ExecStart=/opt/nexus-3.38.0-01/bin/nexus start
ExecStop=/opt/nexus-3.38.0-01/bin/nexus stop
Type=forking
[Install]
WantedBy=multi-user.target
ставим jdk 1.8
apt-get install openjdk-8-jdk
запускаем демон и добавляем его в автозагрузку
systemctl start nexus.service
systemctl enable nexus.service
открываем http://10.0.0.1:8081 и попадаем в веб-интерфейс
доступ в админку под пользователем admin
, initial-пароль смотрим в файле /opt/sonatype-work/nexus3/admin.password
какие репозитории Nexus поддерживает из коробки:
apt (управляемый и прокси)
go (proxy)
maven (управляемый и прокси)
npm (управляемый и прокси)
python (управляемый и прокси)
ruby (управляемый и прокси)
yum (управляемый и прокси)
docker (управляемый и прокси)
raw (управляемый и прокси)
gitlfs
и другие
также, насколько я понял, есть возможность использования плагинов, расширяющих функциональные возможности.
мы этой штукой не пользуемся, но выглядит очень неплохо
Альтернативный способ запуска - docker-compose
Создаём файл /etc/docker-compose.yaml
version: "2"
services:
nexus:
image: sonatype/nexus3
volumes:
- "nexus-data:/nexus-data"
ports:
- "8081:8081"
volumes:
nexus-data: {}
Запускаем демон
systemctl start docker-compose.service
systemctl enable docker-compose.service
На что следует обратить внимание
В больших проектах при массовом использовании может быть много трафика, важно это учитывать, делайте мониторинги и следите за нагрузкой.
Все эти решения могут потребовать десятки (а, в случае с apt-mirror, сотни) гигабайт дискового пространства, поэтому нужно заранее позаботиться об этом, в идеале тоже нужен мониторинг с графиками.
Ссылки
Скидываю список ссылок, где можно более подробно почитать об этих решениях:
https://gomods.io/ - athens
https://github.com/devpi/devpi - devpi
https://habr.com/ru/post/210450/ - devpi
https://repman.io/ - repman
https://verdaccio.org/ru-ru/ - verdaccio
https://habr.com/ru/company/yandex/blog/528386/ - verdaccio
https://habr.com/ru/post/427069/ - verdaccio
https://habr.com/ru/post/453614/ - verdaccio
https://help.ubuntu.ru/wiki/apt-mirror - apt-mirror
https://habr.com/ru/sandbox/19236/ - apt-mirror
https://habr.com/ru/post/110444/ - apt-mirror
https://www.sonatype.com/products/repository-oss - sonartype nexus oss
Комментарии (6)
Gogr
29.03.2022 20:38Nexus OSS -отличный кэширующий прокси. Платный стоит что-то в районе 1кило$ на пользователя.
paulstrong Автор
29.03.2022 21:38мы уже пощупали его сегодня, хороший, да, отправил нужным разрабам ссылку на него, может передумают использовать велосипеды и на нексус заедут
tumbler
Огонь! А Nexus не рассматривали? Вроде он почти всё покрывает.
paulstrong Автор
мы даже ставили его когда-то, но оне же платный вроде?
paulstrong Автор
вобщем щас посмотрел, вроде как есть OSS версия, с обрезанным функционалом, она бы нам тоже подошла.
ну, вобщем, уже сделали, там просто каждая команда под себя решение нашла, мы всё это подняли, а потом статейку написали, вдруг кому пригодится
наверное, еще посмотрим отдельно на Nexus под увеличительным стеклом
paulstrong Автор
придётся дополнить.