“You’re either the one that creates the automation or you’re getting automated.” — Tom Preston-Werner

Не так давно все наблюдали блокировку Docker Hub в РФ, которая длилась с 30 мая по 3 июня. Хотя сейчас Docker Hub вновь доступен, я успел разработать некоторую автоматизацию для переноса всех своих проектов и решил ею поделиться (пускай и очень поздно). В этом посте я расскажу как жить с блокировкой и как быстро перевести текущие проекты, использующие Docker Hub.

UPD: Я недооценил влияние рекомендаций и ошибочно предположил, что мою первую публикацию найдут только те, кому все еще интересно защитить критические компоненты, или если Docker Hub вновь заблокируют. Приношу извинение перед всеми кого ввел в заблуждение! Docker Hub не заблокирован на момент публикации!

1. Доступ к публичным образам

Первое, что нужно сделать, это вернуть возможность делать docker pull публичных образов (например, docker pull ubuntu:latest). Для этого будем использовать зеркало от Google: https://mirror.gcr.io/. Еще есть зеркало от Яндекса cr.yandex/mirror, но там как будто мало образов (ubuntu:latest — есть, python:3.12 — нет). Можно каждый раз указывать зеркало, например docker pull mirror.gcr.io/ubuntu:latest. Но удобнее один раз указать зеркало в настройках Docker и делать docker pull как обычно: docker pull ubuntu:latest (зеркало будет использоваться автоматически).

Я написал скрипт, который все сделает за вас. Достаточно лишь передать зеркало первым аргументом. Проверено на Linux Ubuntu и MacOS.

Примечание для MacOS: в коде стоят sleep'ы, чтобы дождаться рестарта Docker. Их можно увеличить при необходимости.

Примечание для Windows: достаточно лишь поправить код, где "$os" == "CYGWIN_NT" и должно заработать.

Скрипт:

#!/usr/bin/env bash

set -e

DOCKER_REGISTRY_MIRROR="$1"

# utils__str_strip takes input from stdin and strips space characters
function utils__str_strip() {
    cat | tr -d '[:space:]'
}

# utils__is_true takes single argument and
# returns 0 if argument equals "true" (case-insensitive)
# returns 1 otherwise
function utils__is_true() {
    bool="$(echo "$1" | tr '[:upper:]' '[:lower:]' | utils__str_strip)"
    if [ "$bool" == "true" ]; then
        return 0
    fi
    return 1
}

function _get_docker_daemon_config_path() {
    os="$(uname -s)"
    local path
    if [ "$os" == "Linux" ]; then
        path="/etc/docker/daemon.json"
    elif [ "$os" == "Darwin" ]; then
        # path="~/.config/docker/daemon.json" # https://docs.docker.com/config/daemon/
        path="$HOME/.docker/daemon.json"
    elif [ "$os" == "CYGWIN_NT" ] || [ "$os" == "MINGW32_NT" ] || [ "$os" == "MSYS_NT" ]; then
        # NOTE: not tested
        # https://docs.docker.com/config/daemon/
        path="C:\ProgramData\docker\config\daemon.json"
    else
        echo "Error: unsupported operating system"
        exit 1
    fi
    echo $path
}

function _restart_docker() {
    os="$(uname -s)"
    local path
    if [ "$os" == "Linux" ]; then
        sudo systemctl restart docker
    elif [ "$os" == "Darwin" ]; then
        pkill 'Docker' || true
        sleep 3
        open -a Docker
        sleep 3
    elif [ "$os" == "CYGWIN_NT" ] || [ "$os" == "MINGW32_NT" ] || [ "$os" == "MSYS_NT" ]; then
        # NOTE: not tested
        # https://forums.docker.com/t/restart-docker-service-from-command-line/27331/3
        restart-service *docker*
    else
        echo "Error: unsupported operating system"
        exit 1
    fi
}

function dr__has_mirror() {
    local mirror="$1"
    sudo docker system info --format json | jq -r ".RegistryConfig.Mirrors | if index(\"${mirror}\") == null then \"false\" else \"true\" end"
}

function dr__add_mirror() {
    local mirror="$1"
    local config_path=$(_get_docker_daemon_config_path)
    (cat "$config_path" 2>/dev/null || echo "{}") | jq ". + {\"registry-mirrors\": [\"${mirror}\"]}" > /tmp/daemon.json && sudo mv /tmp/daemon.json "$config_path"
    _restart_docker
}

function DR_UPDATE_MIRROR() {
    mirror="$1"
    if ! utils__is_true $(dr__has_mirror "$mirror"); then
        echo "Target docker registry mirror ('$mirror') not found"
        echo "Start configuring docker registry mirror"
        dr__add_mirror "$mirror"
        if [ "$(dr__has_mirror "$mirror")" == "false" ]; then
            echo "Failed to configure docker registry mirror"
            exit 1;
        fi;
        echo "Successfully configured docker registry mirror"
    else
        echo "Mirror is already configured (look at "docker system info")"
    fi
}

DR_UPDATE_MIRROR "$DOCKER_REGISTRY_MIRROR"

Логика скрипта:

  • Проверить может зеркало уже настроено

  • Если зеркала нет, то обновить конфиг

  • Перезапустить докер

Классический способ запуска

  1. Создаем файл со скриптом update_mirror

  2. Обновляем права chmod +x update_mirror

  3. Запускаем ./update_mirror 'https://mirror.gcr.io/'

Способ для самых ленивых :)

curl https://gist.githubusercontent.com/Deimvis/c747446725c84cf0731e82d76f7cc67b/raw/709e8fe296121fb1445fca49086ab9359ca5da67/update_mirror.sh | bash -s 'https://mirror.gcr.io/'

Результат

После настройки зеркало будет видно в выводе docker system info. Появятся строчки:

 Registry Mirrors:
  https://mirror.gcr.io/

Теперь можно пуллить публичные образы как обычно: docker pull ubuntu:latest

2. Загрузка и выгрузка личных образов

Для того, чтобы безопасно передавать собственные образы, потребуется приватный registry. Я использую registry от Яндекс Облака — это недорого, и у меня уже есть некоторая автоматизация под них.

Создание Container Registry

  1. Создаем аккаунт в Yandex Cloud

  2. Создаем каталог (folder): Создание каталога

  3. Открываем Yandex Cloud Console (см. картинку ниже)

  4. Выбираем Container Registry (см. картинку ниже)

  5. Создам registry с любым названием

  6. Копируем id нашего registry (см. картинку ниже)

  7. Теперь подставляем registry id перед нашими образами и докер поймет, что куда нужно отправлять/откуда нужно забирать образы. Например: cr.yandex/crparlvq5pji2gn67f8s/pw_backend:latest

При этом остается проблема, что перед тем как взаимодействовать с приватным registry, нужно к нему авторизоваться. Есть 2 способа...

Авторизация без авторизации

На самом деле авторизации можно избежать, если выдать всем права на pull или push образов (оба не рекомендуются, второе особенно).

  1. Открываем созданный registry в Яндекс Облаке

  2. Открываем Access bindings (см. картинку ниже)

  3. Выбираем Assign bindings (см. картинку ниже)

  4. Выбираем Public + All users (см. картинку ниже)

  5. Добавляем нужные права: puller и/или pusher (см. картинку ниже)

Таким образом приватный registry становится полу/полностью публичным, но для кого-то это может быть наилучший способ получить желаемое.

Авторизация с помощью ключей

С помощью авторизационных ключей можно раз и навсегда залогиниться к Container Registry.

  1. Создаем ключи: yc iam key create --service-account-name default-sa -o key.json (см. документацию)

  2. Логинимся:

    cat key.json | docker login \
      --username json_key \
      --password-stdin \
      cr.yandex

Главная проблема этого способа в том, что требуется повторять эту процедуру на каждой виртуалке, но можно автоматизировать.

  1. Сохраняем ключ в проекте, я буду использовать путь: secrets/yc_key.json (главное случайно не закомитьте)

  2. Далее указываем в переменных окружения

    • SSH_USER — юзер для SSH

    • SSH_HOST — хост для SSH

    • SSH_PKEY — путь к приватному ключу для SSH

    export SSH_USER=dbrusenin
    export SSH_HOST=111.222.0.3
    export SSH_PKEY=~/.ssh/id_rsa
  3. Запускаем скрипт:

    #!/usr/bin/env bash
    
    set -e
    
    function SSH() {
        local cmd="$1"
        ssh -t -o LogLevel=QUIET -o "StrictHostKeyChecking no" -i $SSH_PKEY $SSH_USER@$SSH_HOST "$cmd"
    }
    
    function SCP {
        local src=$1
        local dst_dir=$2
        SSH "mkdir -p \"$(dirname $dst_dir)\""
        scp -i $SSH_PKEY -r $src $SSH_USER@$SSH_HOST:$dst_dir
    }
    
    function DR_YANDEX_AUTH() {
        local auth_keys="$1"
        SCP "$auth_keys" /tmp/yc_key.json
        SSH "cat /tmp/yc_key.json | sudo docker login \
            --username json_key \
            --password-stdin \
            cr.yandex" > /dev/null
        SSH "rm /tmp/yc_key.json"
    }
    
    DR_YANDEX_AUTH "./secrets/yc_key.json"

    Или снова способ для ленивых:

    curl https://gist.githubusercontent.com/Deimvis/a60df999aca23b2292f2a5d5c856618a/raw/937d846ab667b84f13a9fb59e012ec3a37afedc8/yandex_auth.sh | sh

Результат

Теперь вы можете пушить и пуллить собственные образы, подставляя перед названиями образом cr.yandex/<registry_id>/. Убедиться, что вы авторизованы к registry по идее можно взглянув на конфиг ~/.docker/config.json.

Итог

  • Если Docker Hub недоступен, то нельзя из коробки делать pull/push образов

  • Чтобы делать pull публичных образов, нужно настроить зеркало

  • Чтобы делать push/pull собственных образов, нужно создать приватный registry и работать через него

Комментарии (8)


  1. gudvinr
    26.07.2024 05:23
    +4

    Иронично, что картинка на превью у вас не на русском (:

    Как-то вы опоздали с гайдами, мне кажется. Месяц назад уже все кому не лень и свои зеркала подняли, и гайды написали


    1. dbrusenin Автор
      26.07.2024 05:23
      +3

      И вправду иронично) И я и вправду опоздал и слишком долго ждал, так как переживал, чтобы что-то опубликовать. Искренне извиняюсь за публикацию неактуальной темы, впредь учту и буду как-то это выделять или отказываться от таких действий.

      Тем не менее, главная ценность была передать шаги с автоматизацией, которые могут быть интересны другим людям, так как были интересны мне. В гайдах, написанных через 30 минут после инцидента, ничего подобного не было, поэтому пришлось делать все самому. Но по таймингу и вправду сильно опоздал. Еще раз приношу извинения, что не оправдал ожидания. Попробовал это отразить в первом абзаце, чтобы никого не вводить в заблуждение.


      1. Bedal
        26.07.2024 05:23
        +1

        ничего страшного, не выбрасывайте - ситуация вполне может повториться.


        1. dbrusenin Автор
          26.07.2024 05:23

          <3


  1. strelkove
    26.07.2024 05:23
    +3

    Пост слишком долго лежал в отложке? Докерхаб был недоступен с 30 мая по 4 июня. С тех пор никаких проблем с доступом нет. Но да, все запомнили прецедент, и теперь никто не пулит напрямую с docker.hub, чтоб пайплайны не превратились в тыкву.


    1. dbrusenin Автор
      26.07.2024 05:23
      +2

      Спасибо, что отразили свое мнение. Я и вправду виноват, что вас ввел своим первым абзацем в заблуждении. Уже поправил на более точный.

      Пост действительно слишком долго лежал в отложке. Искренне извиняюсь за публикацию неактуальной темы, впредь учту и буду как-то это выделять или отказываться от таких действий. Тем не менее, главная ценность была передать шаги с автоматизацией, которые все еще актуальны и помогут быстрее перевести критический компоненты на работу без docker hub.


  1. opusmode
    26.07.2024 05:23
    +2

    Не хочу его заменять, но почему у вас, например, яндекс? А не селектел, где есть тоже, но удобнее? Или не связка harbor + s3? Или GitLab и его хранилище? Притом можно даже облачный.

    И почему при авторизации всё так сложно? Что, докер, после логина, перестал создавать .docker/config.json в homedir? Там же простые http запросы и никакой привязки к IP или сессии нет. У кого Bearer, тот и прав. Залогинились, а потом копируйте по машинам до посинения, нужна, например, одна роль энсибла. Или даже баш скрипт, который будет.. ну, хз, через sftp put раскидывать/

    Статья, мягко говоря, ни о чём


    1. dbrusenin Автор
      26.07.2024 05:23

      Яндекс облако только потому, что у меня все проекты там и я разобрался как сделать под него + автоматизировал, чтобы по кнопке перевести все проекты. Я абсолютно согласен, что яндексовый реджистри может быть не самым удобным и быстрым способом. Я, к своему стыду, не изучал этот вопрос и не подскажу, какой способ лучший(

      Авторизация на самом деле несложная, там только docker login. Просто я добавил ssh команды, чтобы сделать это на виртуалке. Вы правы, можно просто скопировать конфиг на все машинки и все будет работать. Единственное, кажется не всегда секреты хранятся в докеровском конфиге в сыром виде. Кажется, иногда секреты хранятся в другом месте через docker credentials helper и возможно там как-то иначе. Но снова, не углублялся в этот вопрос, чтобы точно ответить(

      Пост о том, как пулить публичные образы и где хранить свои, если нет docker hub, к которому привыкли большинство разработчиков (я в их числе). Мне бы он точно помог когда docker hub заблокировали)