
Всем привет! Сегодня хочу поделиться нашими мыслями относительно того, как можно защитить свою разработку от некоторых потенциальных рисков в современных условиях. Собственно, что мы имеем ввиду? Речь идёт о том, что в крупных проектах часто есть единые точки отказа в процессах 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-репозиторий, хранить его на своих серверах и ставить пакеты, не имея доступ в Интернет
На что следует обратить внимание
-
В больших проектах при массовом использовании может быть много трафика, важно это учитывать, делайте мониторинги и следите за нагрузкой
-
Все эти решения могут потребовать десятки (а, в случае с apt-mirror, сотни) гигабайт дискового пространства, поэтому нужно заранее позаботиться об этом, в идеале тоже нужен мониторинг с графиками
Ссылки
Скидываю список ссылок, где можно более подробно почитать об этих решениях
-
https://gomods.io/ — athens
-
https://github.com/devpi/devpi — devpi
-
https://habr.com/ru/post/210450/ — devpi
-
https://repman.io/ — repman
-
https://habr.com/ru/post/427069/ — repman
-
https://verdaccio.org/ru-ru/ — verdaccio
-
https://habr.com/ru/post/453614/ — verdaccio
-
https://habr.com/ru/post/427069/ — 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://habr.com/ru/post/657881/
Добавить комментарий