Как стать автором
Обновить

Локальные репозитории пакетов

Время на прочтение12 мин
Количество просмотров13K

Всем привет! Сегодня хочу поделиться нашими мыслями относительно того, как можно защитить свою разработку от некоторых потенциальных рисков в современных условиях. Собственно, что мы имеем ввиду? Речь идёт о том, что в крупных проектах часто есть единые точки отказа в процессах 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

На что следует обратить внимание

  1. В больших проектах при массовом использовании может быть много трафика, важно это учитывать, делайте мониторинги и следите за нагрузкой.

  2. Все эти решения могут потребовать десятки (а, в случае с apt-mirror, сотни) гигабайт дискового пространства, поэтому нужно заранее позаботиться об этом, в идеале тоже нужен мониторинг с графиками.

Ссылки

Скидываю список ссылок, где можно более подробно почитать об этих решениях:

Теги:
Хабы:
Всего голосов 11: ↑10 и ↓1+9
Комментарии11

Публикации

Истории

Работа

Ближайшие события