[Из песочницы] Jenkins для Android сборки, с помощью Docker
Всем привет!
Я работаю андроид разработчиком, и не так давно мы столкнулись с некоторыми рутинными задачами на своем проекте, которые хотелось бы автоматизировать. Например у нас 5 разных flavor, для каждого из которых требуется загружать свой билд на fabric, иногда для разных тасок по несколько раз в день. Да эту задачу можно сделать и с помощью gradle таски, но хотелось бы не запускать этот процесс на машине разработчика, а делать это как-то централизовано. Или например автоматически заливать билд в google play в бету. Ну и просто хотелось поковырять CI систему. Что из этого получилось, и как мы это настраивали, зачем там Docker, далее в статье.
В моем понимании вся задача делилась примерно на два этапа:
- Установить и настроить сам Jenkins совместно с Android SDK
- Настроить задачи уже внутри Jenkins
В этой статье я хочу затронуть именно первый пункт, а если это все будет кому-то интересно, то в следующей статье опишу и процесс настройки задач по сборке в самом Jenkins.
Итак, пункт первый установка и настройка Jenkins системы
На Хабре уже есть замечательная статья на эту тему, но ей уже пару лет, и некоторые вещи в ней слегка устарели (например sdkmanager), хотя она мне сильно помогла разобраться на начальных этапах что и как делать.
Если посмотреть официальную документацию по установке Jenkins то увидим три разных способа как это сделать: запустить готовый docker образ, скачать и запустить war файле, а также по старинке просто установить jenkins в систему (например apt-get install jenkins
на примере ubuntu). Первый вариант самый правильный, поскольку он не несет никаких лишних настроек и зависимостей в нашу хост систему, и в любой момент даже если что-то пойдет не так, легко и просто все удалить и начать заново. Но стандартный docker образ для jenkins содержит часть данных которые нам не нужны (например blueocean плагин) и не содержат того что нам обязательно понадобиться (например android sdk). Было принято решение создать собственный docker образ который внутри себя будет качать и запускать war файл, качать и устанавливать android sdk, а также настраивать все остальные настройки которые нам будут нужны. Для того чтоб его потом запустить, нам потребуеться хостовая система с установленным docker. Я предлагаю тут не изобретать велосипед и воспользоваться DigitalOcean.
Создание и настройка виртуальной машины
Для начала если там еще кто не зарегистрирован то предлагаю зарегистрироваться (тут в момент написания тстаьи была реферальная ссылка, но почитав правила я выкинул ее). После регистрации можно на просторах инета погуглить тот или иной промокод, и получить примерно баксов 10 для старта.
После нам потребуется завести новый дроплет. Выберем пункт Droplets, и далее Create Droplet.
Хостовой системой оставим Ubuntu 18.04. Можно было бы выбрать образ с уже установленным и настроенным Docker, но мы все сделаем самостоятельно. Поскольку сборка андроид билдов дело все же ресурсоемкое, нам нужно выбрать конфигурацию как минимум за 20 баксов, чтоб билды собирались нормально и относительно быстро.
Выберем расположение к себе поближе (например в Германии). Дальше два варианта как мы будем подключаться к нашему виртуальному серверу. Мы можем добавить ssh ключ или обойтись без него. Если в этом месте мы не укажем какой ключ использовать, то нам на почту придет пароль для юзера root.
Тут можем изменить имя сервера, и завершаем создание нажатием кнопки Create.
Теперь заходим в созданный дроплет, и копируем себе ip адрес, для дальнейшего подключения и настройки.
Нам нужен ssh клиент. Если вы работает из под мака, то можно воспользоваться стандартным терминалом, если из под винды то мы можем взять для работты putty или воспользоваться подсистемой Linux (только для Windows 10). Я лично использую последний вариант, и он меня полностью устраивает.
Подключаемся к нашему серверу с помощью следующей команды
ssh root@YOUR_IP_ADDRESS
Консоль вам предложить сохранить ключ, соглашаемся с этим. После подключения создадим себе нового пользователя, добавим его в суперпользователи (и дадим ему возможность пользоваться sudo без пароля), копируем ему ключ для доступа по ssh и скажем что он владелец этих файлов (иначе не заработает). Имя username меняем на любое удобное для вас.
useradd -m -s /bin/bash username \
&& echo 'username ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \
&& mkdir /home/username/.ssh \
&& cp /root/.ssh/authorized_keys /home/username/.ssh/authorized_keys \
&& chown username:username -R /home/username/.ssh
Отключаемся от пользователя root с помощью команды
exit
И подключимся заново уже с помощью нами созданного нового пользователя
ssh username@YOUR_IP_ADDRESS
После обновим систему, и перезагружаем наш сервер (если в процессе обновления система у вас будет что-то спрашивать, достаточно в этом случае всегда выбирать дефолтные значения).
sudo apt update && sudo apt full-upgrade -y && sudo apt autoremove -y && sudo reboot
Базовая настройка закончена. С точки зрения боевой системы она не очень секьюрна, но в рамках этой статью полностью подойдет.
Установка Docker.
Чтоб установить Docker в нашу систему воспользуемся официальной документацией. Поскольку у нас заново установленная система, мы пропустим этот пункт, а если у вас система на которой уже давно что-то бежит, по рекомендации ребят из Docker удалите возможные старые версии
sudo apt-get remove docker docker-engine docker.io containerd runc
Не забудьте вначале обратно подключиться по ssh к нашему серверу. Сама установка Docker расписана очень детально в документации, я приведу общие команды, чтоб вам было проще. Что они делают можно почитать там. Сначала добавим репозиторий.
sudo apt update \
&& sudo apt install -y apt-transport-https ca-certificates \
curl gnupg-agent software-properties-common \
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - \
&& sudo apt-key fingerprint 0EBFCD88 \
&& sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
И после установим сам Docker:
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io
Чтоб в дальнейшем мы могли вызывать команды docker без приставки sudo выполним следующую команду (котрая тоже заботливо описана в инструкции).
sudo usermod -aG docker username
После нужно перезайти (с помощью команды exit и повторного подключения к серверу) для того чтоб изменения закрепились.
Сам Docker установлен, что мы можем проверить командой
docker run hello-world
Она загружает тестовый образ, запускает его в контейнере. Контейнер после запуска печатает информационное сообщение и завершает работу.
Поздравляю, этап подготовки сервера к работе мы закончили!
Создание своего Docker образа
Создавать Docker образ будем с помощью написания собственного Dockerfile. Примеров как это сделать правильно в интернете вагон и маленькая тележка, я покажу свой уже готовый вариант, и постараюсь его максимально прокомментировать. Существует также от самого docker статья инструкция с примерами по правильному и каноническому написание dockerfile.
Создадим и откроем для редактирования свой Dockerfile
touch Dockerfile && nano Dockerfile
В него для примера, поместим содержимое моего Dockerfile
# базовая система для образа.
FROM ubuntu:18.04
# тут должно быть все понятно
LABEL author="osipovaleks"
LABEL maintainer="osipov.aleks.kr@gmail.com"
LABEL version="1.0"
LABEL description="Docker image for Jenkins with Android SDK"
# устанавливаем таймзону, чтоб Jenkins показывал локальное время
ENV TZ=Europe/Kiev
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
#добавляем i386 архитектуру для установки ia32-libs
RUN dpkg --add-architecture i386
# обновляем пакеты и устанавливаем нужное
RUN apt-get update && apt-get install -y git \
wget \
unzip \
sudo \
tzdata \
locales\
openjdk-8-jdk \
libncurses5:i386 \
libstdc++6:i386 \
zlib1g:i386
#чистим после себя, чтоб размер образа был немного поменьше
RUN apt-get clean && rm -rf /var/lib/apt/lists /var/cache/apt
#устанавливаем локали
RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
#качаем и распаковываем Android Sdk в заранее подготовленную папку
ARG android_home_dir=/var/lib/android-sdk/
ARG sdk_tools_zip_file=sdk-tools-linux-4333796.zip
RUN mkdir $android_home_dir
RUN wget https://dl.google.com/android/repository/$sdk_tools_zip_file -P $android_home_dir -nv
RUN unzip $android_home_dir$sdk_tools_zip_file -d $android_home_dir
RUN rm $android_home_dir$sdk_tools_zip_file && chmod 777 -R $android_home_dir
#устанавливаем environment в наш образ
ENV ANDROID_HOME=$android_home_dir
ENV PATH="${PATH}:$android_home_dir/tools/bin:$android_home_dir/platform-tools"
ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/
#соглашаемся с лицензиями Android SDK
RUN yes | sdkmanager --licenses
#создаем рабочую директорию для Jenkins
ENV JENKINS_HOME=/var/lib/jenkins
RUN mkdir $JENKINS_HOME && chmod 777 $JENKINS_HOME
#заводим нового юзера с именем jenkins, сделаем его суперпользователем, переключимся на него и перейдем в рабочую директорию
RUN useradd -m jenkins && echo 'jenkins ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
USER jenkins
WORKDIR /home/jenkins
#загрузим и запустим war файл с последней версией Jenkins
RUN wget http://mirrors.jenkins.io/war-stable/latest/jenkins.war -nv
CMD java -jar jenkins.war
#сообщим какой порт нам требуется слушать
EXPOSE 8080/tcp
Несколько уточнений:
- В началае было желание использовать более легковесный alpine вместо ubuntu, но него нету поддержки ia32-libs, что требуется для сборки проектов с помощью Android SDK.
- Мы устанавливаем openjdk-8-jdk, а не более легковесную openjdk-8-jdk-headless по причине того, что некоторым функциям Jenkins нужна именно полная система (например отображение результатов unit тестов).
- Установить локали нужно обязательно, по причине того, что на некоторых проектах, без них сборка gradle крашится без внятных ошибок и логов, и я потратил несколько дней чтоб докопаться до этой причины (на обычной ubuntu которая не в docker, все локали заполнены по умолчанию).
- Нам нужно сразу принять все лицензии у Android SDK, для того чтоб в процессе сборки Jenkins мог самостоятельно установить нужные ему компоненты (например нужные ему SDK под разные версии api). Если потребуется, то в дальнейшем внутри docker контейнера можно будет управлять SDK с помощью sdkmanager, например
sdkmanager --list
позволяет просмотреть все доступные и все установленные компоненты, аsdkmanager --install "platforms;android-26"
установит SDK для 26 версии api. - В целом можно было не заводить юзера jenkins, и остаться с юзером root, но это как-то не совсем правильно, так же можно было ему не давать права суперпользователя, но это сделано в плане удобства, если вдруг что-то вам нужно будет установить на этапе настройки и дебага.
- Базовый размер образа получился немаленький (почти 800 мб), но в целом я пришел к выводу что для меня это не сильно критично, и мне легче его скачать в таком виде, чем тратить время на поиск и удаление пакетов которые мне не нужны.
После окончания написания Dockerfile нам нужно его превратить в готовый образ для Docker, на основании которого и будут создаваться контейнеры. Делается это просто командой
docker build -t jenkins-image
где параметр -t jenkins-image
отвечает за имя вашего образа, а точка в конце команды, говорит о том что Dockerfile для сборки нужно искать внутри данного каталога. Сам процесс сборки занимает какое-то время, и после сборки в консоли должно быть примерно подобное сообщение.
Successfully built 9fd8f5545c27
Successfully tagged jenkins-image: latest
Которое нам говорит о том что наш образ успешно собран, и мы можем приступать к следующему шагу, а именно запуску нашего контейнера
Docker Hub и готовые образы
Да конечно, мы можем использовать наш готовый образ для запуска контейнера, но если нам потребуется это сделать больше чем на нескольких устройствах, каждый раз создавать Dockerfile и собирать из него готовый образ будет не совсем удобно. А если мы еще и обновим содержимое нашего Dockerfile, то раскатывать изменения по всем нодам будет вообще не удобно. Для этих целей и существует публичный репозиторий образов Docker Hub. Он позволяет не собирать каждый раз образ, на каждой ноде, а просто скачать его себе с публичного хранилища, и использовать одинаково на всех машинах. Например образ который послужил примером для этой статьи, доступен в репозитории по имени osipovaleks/docker-jenkins-android, и дальше в статье мы будем работать именно с ним.
Данная статья не подразумевает детального изучения Docker Hub, мы не будем разбираться как залить туда свои образы (хотя это очень не сложно) и что с ними можно делать дальше там, мы не будем разбираться что существуют еще могут быть свои личные публичные или приватные репозитории, в этом всем можно будет разобраться самостоятельно если понадобиться.
Запуск контейнера
Запустить контейнер можно двумя способами.
- Первый способ просто с помощью команды
docker run
, позволяет это сделать легко и быстро следующим способомdocker run --name jenkins -d -it -v jenkins-data:/var/lib/jenkins -v jenkins-home:/home/jenkins -p 8080:8080 --restart unless-stopped osipovaleks/docker-jenkins-android
где командаrun
имеет следующие параметры--name jenkins
— имя будущего контейнера-d
— запуск контейнера в фоне-it
— флаги для работы с STDIN и tty-v jenkins-data:/var/lib/jenkins
и-v jenkins-home:/home/jenkins
— создаем (если не созданы) и замапим на внутренние разделы контейнера специальный файлы тома, которые позволят нам сохранить наш настроенный Jenkins даже после пересоздания контейнера-p 8080:8080
— замапим порт хоста на порт контейнера, для того чтоб у нас был доступ к веб интерфейсу (да это именно тот порт который мы указывали в Dockerfile)--restart unless-stopped
— опция определяет политику автозапуска контейнера после перезагрузки хоста (в данном случае, автостарт если контейнер не был выключен вручную)osipovaleks/docker-jenkins-android
— образ для развертывания.
На выходе в консоль Docker нам должен вывести id созданного контейнера, а также показать информацию о том как образ загружается в систему (конечно если он еще не загружен), примерно такUnable to find image 'osipovaleks/docker-jenkins-android: latest' locally
latest: Pulling from osipovaleks/docker-jenkins-android
6cf436f81810: Pull complete
987088a85b96: Pull complete
b4624b3efe06: Pull complete
d42beb8ded59: Pull complete
b3896048bb8c: Pull complete
8eeace4c3d64: Pull complete
d9b74624442c: Pull complete
36bb3b7da419: Pull complete
31361bd508cb: Pull complete
cee49ae4c825: Pull complete
868ddf54d4c1: Pull complete
361bd7573dd0: Pull complete
bb7b15e36ae8: Pull complete
97f19daace79: Pull complete
1f5eb3850f3e: Pull complete
651e7bbedad2: Pull complete
a52705a2ded7: Pull complete
Digest: sha256:321453e2f2142e433817cc9559443387e9f680bb091d6369bbcbc1e0201be1c5
Status: Downloaded newer image for osipovaleks/docker-jenkins-android: latest
ef9e5512581da66d66103d9f6ea6ccd74e5bdb3776747441ce6a88a98a12b5a4 - Второй способ запуска подразумевает написание специального compose файла, где команда run просто описана с помощью языка YAML, и запускается с помощью Docker Compose.
Для этого нам потребуется установить его:
sudo apt update && sudo apt install -y docker-compose
Далее создаем директорию для проекта (это важно в том случае, если вам не все равно, как будут называться автоматически созданные volumes для контейнера) и перейдем в нееmkdir jenkinsProject && cd jenkinsProject
а внутри создаем сам compose файл и заходим в режим редактированияtouch docker-compose.yml && nano docker-compose.yml
и поместим в него следующее содержимоеversion: '3' services: jenkins: container_name: jenkins image: osipovaleks/docker-jenkins-android ports: - "8080:8080" restart: unless-stopped volumes: - "jenkins-data:/var/lib/jenkins" - "jenkins-home:/home/jenkins" volumes: jenkins-data: jenkins-home:
В нем, пожалуй, только первая строчка вызывает вопросы (version: '3'
) которая указывает на версию возможностей compose файла, а также раздел с блокомvolumes
в котором перечислены те, которые используються в данном контейнереЗапустим свой контейнер командой:
docker-compose up -d
где флаг-d
также указывает на то что создание и запуск контейнера будет производиться в фоне. В итоге Docker должен показать примерно следующее:Creating volume «jenkinsproject_jenkins-data» with default driver
Creating volume «jenkinsproject_jenkins-home» with default driver
Pulling jenkins (osipovaleks/docker-jenkins-android: latest)…
latest: Pulling from osipovaleks/docker-jenkins-android
6cf436f81810: Pull complete
987088a85b96: Pull complete
b4624b3efe06: Pull complete
d42beb8ded59: Pull complete
b3896048bb8c: Pull complete
8eeace4c3d64: Pull complete
d9b74624442c: Pull complete
36bb3b7da419: Pull complete
31361bd508cb: Pull complete
cee49ae4c825: Pull complete
868ddf54d4c1: Pull complete
361bd7573dd0: Pull complete
bb7b15e36ae8: Pull complete
97f19daace79: Pull complete
1f5eb3850f3e: Pull complete
651e7bbedad2: Pull complete
a52705a2ded7: Pull complete
Digest: sha256:321453e2f2142e433817cc9559443387e9f680bb091d6369bbcbc1e0201be1c5
Status: Downloaded newer image for osipovaleks/docker-jenkins-android: latest
Creating jenkins…
Creating jenkins… donedocker volume ls
и получим на выходе такоеDRIVER VOLUME NAME
local jenkinsproject_jenkins-data
local jenkinsproject_jenkins-homejenkins-home
, в реальности к нему прилепился префикс из имени проекта и имя volume получилосьjenkinsproject_jenkins-home
Какой из вариантов запуска использовать? Тут вы можете выбрать самостоятельно, считается что Docker Compose это скорее инструмент, для запуска нескольких контейнеров сразу, которые завязаны друг на друга, и если вам нужно запустить всего один контейнер, то можно воспользоваться просто командой docker run
.
Теперь после этих подэтапов по запуску и настройке сервера, а так же запуске контейнера с Jenkins мы можем перейти к его первоначальной настройке
Первоначальная настройка Jenkins
Возьмем ip адрес нашего сервера, добавим к нему указанный нами порт 8080 и перейдем по этой ссылке в браузере.
http://YOUR_IP_ADDRESS:8080/
Если до этого все было настроено и запущено правильно, то тут увидим следующую картинку
Для первой настройки нам нужно ввести пароль который сгенерировала система при установке. Для этого нам всего лишь нужно посмотреть содержимое файла /var/lib/jenkins/secrets/initialAdminPassword
. Но этот файл находиться внутри нашего запущенного контейнера, и для того чтоб его прочесть, нам понадобиться приконектиться к контейнеру, с помощью следующей команды:
docker exec -it jenkins /bin/bash
где параметр -it
аналогичен как и при запуске docker run
, jenkins
это имя нашего контейнера, а /bin/bash
запустит для нас bash в контейнере и даст к нему доступ. После этого мы можем посмотреть начальный пароль для Jenkins:
cat /var/lib/jenkins/secrets/initialAdminPassword
в консоли покажеться примерно следующее
91092b18d6ca4492a2759b1903241d2a
Это и есть пароль. Копируем его, вставляем в поле Administrator password в веб интерфейсе и нажимаем Continue. На следующем экране выбираем пункт Install suggested plugins и устанавливает набор дефолтных плагинов.
После установки плагинов, создаем себе пользователя и нажимаем на Save and Finish
Соглашаемся с разделом Instance Configuration, где нам предлагают заполнить URL на котором будет работать Jenkins (в нашем случае оставляем все как есть)
И на следующем экране нажимаем заветное Start using Jenkins
Итак, мы установили и запустили Jenkins!
С ним уже вполне можно работать, но для того чтоб собирать наши Android билды нужно будет настроить еще несколько пунктов. Локализация Jenkins связана с выбранным языком вашего браузера, и естественно перевод на русский язык закончен не до конца, и мы получаем адовую смесь из русского и английского языков. Если у вас получилось точно также, и вас это бесит, то можно воспользоваться специальным плагином и установить дефолтный язык интерфейса. Ну или переключить свой браузер на англоязычный интерфейс.
Перейдем в настройки Jenkins и выберем пункт Конфигурация системы
Отметим галочкой пункт Environment variables, и впишем в поле имя ANDROID_HOME, а в поле значение укажем /var/lib/android-sdk/ (именно эти данные мы указывали еще в Dockerfile как домашнюю директорию для хранения Android SDK).
Нажмем на кнопку Сохранить, выйдем из данного раздела настроек и зайдем в раздел под названием Конфигурация глобальных инструментов.
Настроем раздел с JDK (где переменная JAVA_HOME так же заполнялась нами в Dockerfile, и мы можем тут использовать ее значение /usr/lib/jvm/java-8-openjdk-amd64/).
Так же тут нам еще нужно заполнить раздел с Gradle. Версию Gradle мы выбираем и устанавливаем ту, которая используется в тех проектах, которые вы будете собирать с помощью этой CI системы. Можно завести несколько версий. Можно так же вообще не заводить Gradle переменную, если у вас в репозитории например есть gradlew, и собирать можно с его помощью.
На этом мы можем заканчивать наш первый этап. Система Jenkins полностью готова к работе и мы можем переходить к настройке самих задач по сборке. Обратите внимание, система настраивалась под наши нужды и тут может не оказать того что нужно именно вам — например тут нету андроид эмуляторов для тестирования и NDK.
Если эта статья кого-то заинтересует, то я продолжу и во второй части на примере одной или двух тасок, опишу интеграцию Jenkins и Bitbucket (именно он, а не Github, потому что там и с бесплатными приватными репозиториями попроще, и статей в интернете про него поменьше, а приколов пожалуй побольше), расскажу как ssh ключ нашего контейнера подружить с репозиторием, про email уведомления, а также несколько других фишек. В общем примерно про все, что у нас и настроено.
Прошу сильно не пинать, это моя первая статья на Хабр. Всем добра!