Для тех, кто не знает что такое Docker — если совсем просто, то это инструмент создания т.н. «контейнеров» где содержится минимальное ядро ОС и необходимый набор ПО, которые мы можем разворачивать где захотим, сохраняя при этом окружение. Что именно будет в нашем контейнере определяется в Dockerfile, который потом собирается в образ запускаемый где угодно и обладающий свойства идемпотентности.
Процесс установки и основы Docker прекрасно описаны на его официальном сайте. Поэтому, забегая немного вперед, вот такой Dockerfile у нас получился
# Т.к. основным инструментом для сборки Android-проектов является Gradle,
# и по счастливому стечению обстоятельств есть официальный Docker-образ
# мы решили за основу взять именно его с нужной нам версией Gradle
FROM gradle:5.4.1-jdk8
# Задаем переменные с локальной папкой для Android SDK и
# версиями платформы и инструментария
ENV SDK_URL="https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip" ANDROID_HOME="/usr/local/android-sdk" ANDROID_VERSION=28 ANDROID_BUILD_TOOLS_VERSION=28.0.3
# Создаем папку, скачиваем туда SDK и распаковываем архив,
# который после сборки удаляем
RUN mkdir "$ANDROID_HOME" .android && cd "$ANDROID_HOME" && curl -o sdk.zip $SDK_URL && unzip sdk.zip && rm sdk.zip # В следующих строчках мы создаем папку и текстовые файлы
# с лицензиями. На оф. сайте Android написано что мы
# можем копировать эти файлы с машин где вручную эти
# лицензии подтвердили и что автоматически
# их сгенерировать нельзя
&& mkdir "$ANDROID_HOME/licenses" || true && echo "24333f8a63b6825ea9c5514f83c2829b004d1" > "$ANDROID_HOME/licenses/android-sdk-license" && echo "84831b9409646a918e30573bab4c9c91346d8" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
# Запускаем обновление SDK и установку build-tools, platform-tools
RUN $ANDROID_HOME/tools/bin/sdkmanager --update
RUN $ANDROID_HOME/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" "platforms;android-${ANDROID_VERSION}" "platform-tools"
Сохраняем его в папку с нашим Android-проектом и запускаем сборку контейнера командой
docker build -t android-build:5.4-28-27 .
Параметр -t задает tag или имя нашего контейнера, которое обычно состоит из его название и версии. В нашем случае мы назвали его android-build а в версии указали совокупность версий gradle, android-sdk и platform-tools. В дальнейшем нам проще будет искать нужный нам образ по имени используя такую «версию».
После того как сборка прошла мы можем использовать наш образ локально, можем загрузить его командой docker push в публичный или приватный репозиторий образов чтобы скачивать его на другие машины.
В качестве примера соберем локально проект. Для этого в папке с проектом выполним команду
docker run --rm -v "$PWD":/home/gradle/ -w /home/gradle android-build:5.4.1-28-27 gradle assembleDebug
Разберем что она означает:
docker run — сама команда запуска образа
-rm — означает что после остановки контейнера он удаляет за собой все что создавалось в процессе его жизни
-v "$PWD":/home/gradle/ — монтирует текущую папку с нашим Android-проектом во внутреннюю папку контейнера /home/gradle/
-w /home/gradle — задает рабочую директорию контейнера
android-build:5.4.1-28-27 — имя нашего контейнера, который мы собрали
gradle assembleDebug — собственно команда сборки, которая собирает наш проект
Если все сложиться удачно, то через пару секунд/минут вы увидите у себя на экране что-то вроде BUILD SUCCESSFUL in 8m 3s! А в папке app/build/output/apk будет лежать собранное приложение.
Аналогичным образом можно выполнять другие задачи gradle — проверять проект, запускать тесты и т.д. Основное преимущество — при необходимости сборки проекта на любой другой машине, нам не нужно беспокоиться об установке всего окружения и достаточно будет скачать необходимый образ и запустить в нем сборку.
Контейнер не хранит никаких изменений, и каждая сборка запускается с нуля, что с одной стороны гарантирует идентичность сборки независимо от места ее запуска, с другой стороны каждый раз приходиться скачивать все зависимости и компилировать весь код заново, а это иногда может занимать существенное время. Поэтому кроме обычного «холодного» запуска у нас есть вариант запуска сборки с сохранением т.н. «кэша», где мы сохраняем папку ~/.gradle просто копируя ее в рабочую папку проекта, а в начале следующей сборки возвращаем ее обратно. Все процедуры копирования мы вынесли в отдельные скрипты и сама команда запуска у нас стала выглядеть так
docker run --rm -v "$PWD":/home/gradle/ -w /home/gradle android-build:5.4.1-28-27 /bin/bash -c "./pre.sh; gradle assembleDebug; ./post.sh"
В итоге, среднее время сборки проекта у нас сократилось в несколько раз (в зависимости от числа зависимостей на проекте, но средний проект таким образом стал собираться за 1 минуту вместо 5 минут).
Все это само собой имеет смысл только если у вас есть собственный внутренний CI/CD сервер, поддержкой которого вы сами и занимаетесь. Но сейчас есть много облачных сервисов в которых все эти проблемы решены и вам не надо об этом переживать и нужные свойства сборки можно так же указать в настройках проекта.
Комментарии (23)
aol-nnov
29.08.2019 08:57`gradle` можно было поставить в `ENTRYPOINT`, например
А самую мякотку-то?.. Ну, собрали, на выходе файл, владелец у него, в большинстве случаев, root, ну, или тот, кто в контейнере был, но уж точно не uid запустившего это дело на хосте. Не удобно.
Скажете «это тривиально, посмотри в интернетах»? Так тут вся статья тривиальная, если уж на то пошлоgecube
29.08.2019 09:28Согласен — проблема с правами пользователя имеет место быть.
Я знаю как минимум три или четыре решения, но нет ни одного прям красивого. Могу поделиться идеями, если интересно.
С другой стороны — в ci/cd процессе, суть не на машине разработчика, проблемы с правами скорее всего нет, тем более, если использовать dind.aol-nnov
29.08.2019 09:34> суть не на машине разработчика
Да, в сиае много вопросов отпадает, но как же «единство сборочного окружения»? :)
А идеями поделиться никогда не лишне! Я, например, колдую небольшой скрипт, который на старте контейнера создает пользователя с нужным uid и только после этого выполняет команду. А что делают ваши три или четыре варианта? :)gecube
29.08.2019 11:05Я идей накидаю, а Вы там соберите в кучу.
Идея 1. Наивно можно запускать контейнер с ключом -u (--uid). Тогда исполняемый файл будет запускаться под юзером с указанным UID. Прокинуть айди текущего юзера можно через
$(id -u)
. Проблема в том, что это работает, если все каталоги внутри образа с правами 777 и программа должна быть собрана и настроена портабельно. Это далеко не всегда так
Идея 2. Раз предыдущее не работает, то давайте мы сделаем
docker-entrypoint.sh
скрипт, будем в нем свитчится в нужного юзера и там же заchown'им и chmod'им все необходимые для работы каталоги. В принципе это работает, но выглядит не красиво: приходится передавать айди текущего юзера через переменную окружения (docker run ... -e UID=$(id -u)
) внутрь контейнера. А потом через тот же gosu свитчиться в нужного юзера. При этом это не работает с ключом --uid из п.1, т.к. нам надо стартовать из-под рута. Дополнительно — это не решает проблемы с правами на корневой каталог, куда будут смонтированы файлы докера (он все равно будет под рутом). И в опеншифте такой контейнер не запустится.
Идея 3. Нам вообще не нравится писать колбасу вида
docker run --rm -v "$PWD":/home/gradle/ -w /home/gradle android-build:5.4.1-28-27 /bin/bash -c "./pre.sh; gradle assembleDebug; ./post.sh"
Итого — приходим к необходимости написания какой-то внешней обвязки в виде баш скрипта, алиаса или мейкфайла. Запомним это.
Идея 4.а Все проблемы с правами на каталог, который создает докер из-за ключа -v. Если перейти на полный синтаксис bind mount, то целевой каталог НЕ СОЗДАЕТСЯ автоматически, а это значит, что если мы создадим его в скрипте из Идея 3 руками, то с правами на каталог будет все ок, а права на файлы будут нормальные — см. Идея 1 и Идея 2
Идея 4.b Мы можем вообще не париться с вольюмами, а тупо писать файлы в volume, а вытаскивать их из них либо через временный контейнер с нужным юзером, либо через docker cp
Идея 4. c Ах, еще можно запустить в докер-контейнере тот же тар, а наружу прокинуть файл через пайп (что-то типа
docker exec -it blablabla cat FILE | cat > FILE
— тот кат, что справа уже выполняется на хосте с привилегиями текущего юзера, т.е. получается нам вообще без разницы, что внутри контейнера происходит)aol-nnov
29.08.2019 11:13Годно. Теперь, если кто-то будет использовать эту статью по назначению, обязательно наткнется на эти залежи и улучшит свой пайплайн ;)
Сам я же уже давольно давно использую «идею 2» с некоторыми вариациями — всё хорошо. (что до опеншифта, он мне не надыть, так что, проблем не возникает)gecube
29.08.2019 16:17antaresm по поводу прав на каталоги и файлы артефактов есть что добавить?
aol-nnov
29.08.2019 16:20Я просто на старте контейнера в нем создаю пользователя с uid равным идентификатору с хоста, и дальше выполняю под ним. Естественно, проверяю, запускается ли контейнер в ci окружении или у разработчика на хосте.
YMMV ;)gecube
29.08.2019 17:09Т.е. если на хосте два юзера с ID 1000 и 1001, то один из них будет в обломе )
Хотя для линукс машин, в общем-то, это не очень типичный сценарий, а на маке с докером обычно нет проблем с правами на файлы )))))aol-nnov
29.08.2019 17:12> если на хосте два юзера с ID 1000 и 1001, то один из них будет в обломе
почему же?! это всё происходит динамически, во время старта контейнера. у каждого пользователя будет контейнер с его uid. мы же про сборочные контейнеры говорим, а не про сферические в вакууме.gecube
29.08.2019 17:50Сорри, я не Вам писал, Вы ответили, а я невпопад повторно ответил )
Да, Вы правы — если пробрасывать uid в контейнер (мы рассмотрели способы как это сделать), но в статье автора — id user'а в контейнере зафиксирован. Соответственно, как мы выше определили — это норм для ci/cd, но не очень круто, когда разраб хочет "сборочный" контейнер запустить на своем ПК
antaresm Автор
29.08.2019 18:53Если нам нужны непосредственно артефакты как файлы, то у нас сам контейнер запускается под root, поэтому после сборки проблем достать артефакты не возникает;
В большинстве случаев же, gradle заливает полученный apk в HockeyApp или GooglePlay.gecube
29.08.2019 18:55то у нас сам контейнер запускается под root
Ну, Вы имеете в виду, что docker run выполняется от root'а? Тогда да, проблем нет.
Как насчет работы на машине разработчика? Или в опеншифте (где рут внутри контейнера запрещен по вполне понятным причинам)antaresm Автор
29.08.2019 19:00Да, docker run выполняется root'ом
Для разработчика локально запустить root тоже не проблема.
А для опеншифт как я уже писал мы не храним артефакт, а заливаем его на внешний ресурсgecube
29.08.2019 19:12Для разработчика локально запустить root тоже не проблема.
Как минимум — лишнее действие. Как максимум — в компании может быть политика ИБ (хотя, конечно, может выглядеть глупо — рута не даем, а докер даем)))
А для опеншифт как я уже писал мы не храним артефакт, а заливаем его на внешний ресурс
Вы им вообще пользовались, извините? Речь про запуск сборщика в кластере.
antaresm Автор
29.08.2019 19:14не пользовались. поэтому я просто вам описал решение которое у нас на сервере CI используется. основная то проблема в правах, как я понял; Или, извините, я не правильно понял проблему
gecube
29.08.2019 19:38Да, основная проблема в правах.
Но как я выше подчеркнул — ее можно игнорировать, в случае, если вся история происходит в CI/CD конвейере на выделенном инстансе. Но ведь хочется больше!!! Масштабировать процесс на все ) и на машины разраба, и в кластере собирать (чтобы не платить за выделенную машину, а платить за фактические потребленное время сборок) и т.п.
gecube
29.08.2019 09:30Хотел добавить, что курлить артефакт из публичного репо без проверки sha — так себе идея.
Пример — собираете Вы такой свой образ, сидите в Корп сети, а там Корп прокси. Помимо того, что он курочит хттпс, он легко может вместо артефакта zip выдать страницу с ошибкой. Но курл ее честно скачает и положит во временный образ.
Другой вопрос, что процесс сборки скорее всего свалится на распаковке, но вот такой вектор атаки все равно остаётся...
Ну, и браво — Вы переизобрели концепцию временных "сборочных" контейнеров. Очень удобное продакшен решение, на самом деле. Учитывая, что его ещё можно отмасштабировать на машину разраба, если он не хочет захламлять основную систему дев-тулзами
antaresm Автор
29.08.2019 09:41В первом случае: у нас стоит внутренний репозиторий образов. Поэтому в нашем случае проверка излишняя.
На тему переизобретения: даже не пытаюсь присвоить это себе и в тексте про это ни слова. Просто, как показал опыт — мобильные разработчики обычно далеко от темы контейнеров и Docker и наверняка этот туториал будет много кому как минимум интересен.gecube
29.08.2019 11:06Ссылка так-то на оф.сайт — https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
На тему переизобретения: даже не пытаюсь присвоить это себе и в тексте про это ни слова. Просто, как показал опыт — мобильные разработчики обычно далеко от темы контейнеров и Docker и наверняка этот туториал будет много кому как минимум интересен.
Интересен, интересен. Я просто подчеркиваю, что описана в общем-то действительно неплохая практика.
ooki2day
29.08.2019 10:38yes | /path_to_sdk/sdk/tools/bin/sdkmanager«build-tools;29.0.0» — принимаем лицензию для build-tools
yes | /path_to_sdk/sdk/tools/bin/sdkmanager «platforms;android-28» — принимаем лицензию для платформы.
таким образом, можно принимать лицензию автоматически для всех компонентов.
magic_goop
29.08.2019 18:53Есть ли у вас опыт запуска espresso тестов в docker контейнере (т.е. запуск эмулятора)?
alexkuzko
Такое бы, да для сборки яблочных приложений. Но ничего кроме специально подготовленных виртуалок ещё не видел. Может кто сталкивался?