[Из песочницы] Jenkins для Android сборки, с помощью Docker

Всем привет!

Я работаю андроид разработчиком, и не так давно мы столкнулись с некоторыми рутинными задачами на своем проекте, которые хотелось бы автоматизировать. Например у нас 5 разных flavor, для каждого из которых требуется загружать свой билд на fabric, иногда для разных тасок по несколько раз в день. Да эту задачу можно сделать и с помощью gradle таски, но хотелось бы не запускать этот процесс на машине разработчика, а делать это как-то централизовано. Или например автоматически заливать билд в google play в бету. Ну и просто хотелось поковырять CI систему. Что из этого получилось, и как мы это настраивали, зачем там Docker, далее в статье.

atpi_veqcyb-lryxth_ia4m0uag.png
В моем понимании вся задача делилась примерно на два этапа:

  1. Установить и настроить сам Jenkins совместно с Android SDK
  2. Настроить задачи уже внутри 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.

image

Хостовой системой оставим Ubuntu 18.04. Можно было бы выбрать образ с уже установленным и настроенным Docker, но мы все сделаем самостоятельно. Поскольку сборка андроид билдов дело все же ресурсоемкое, нам нужно выбрать конфигурацию как минимум за 20 баксов, чтоб билды собирались нормально и относительно быстро.

image

Выберем расположение к себе поближе (например в Германии). Дальше два варианта как мы будем подключаться к нашему виртуальному серверу. Мы можем добавить ssh ключ или обойтись без него. Если в этом месте мы не укажем какой ключ использовать, то нам на почту придет пароль для юзера root.

image

Тут можем изменить имя сервера, и завершаем создание нажатием кнопки Create.

image

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

image

Нам нужен 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

Мой 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, мы не будем разбираться как залить туда свои образы (хотя это очень не сложно) и что с ними можно делать дальше там, мы не будем разбираться что существуют еще могут быть свои личные публичные или приватные репозитории, в этом всем можно будет разобраться самостоятельно если понадобиться.

Запуск контейнера


Запустить контейнер можно двумя способами.

  1. Первый способ просто с помощью команды 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

  2. Второй способ запуска подразумевает написание специального 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… done

    Помните я говорил что от имени проекта будет зависеть имя созданных volumes? Выполним команду:
    docker volume ls
    

    и получим на выходе такое

    DRIVER VOLUME NAME
    local jenkinsproject_jenkins-data
    local jenkinsproject_jenkins-home

    где и увидим, что несмотря на то что имя для volume было выбрано jenkins-home, в реальности к нему прилепился префикс из имени проекта и имя volume получилось jenkinsproject_jenkins-home

Какой из вариантов запуска использовать? Тут вы можете выбрать самостоятельно, считается что Docker Compose это скорее инструмент, для запуска нескольких контейнеров сразу, которые завязаны друг на друга, и если вам нужно запустить всего один контейнер, то можно воспользоваться просто командой docker run.

Теперь после этих подэтапов по запуску и настройке сервера, а так же запуске контейнера с Jenkins мы можем перейти к его первоначальной настройке

Первоначальная настройка Jenkins


Возьмем ip адрес нашего сервера, добавим к нему указанный нами порт 8080 и перейдем по этой ссылке в браузере.

http://YOUR_IP_ADDRESS:8080/

Если до этого все было настроено и запущено правильно, то тут увидим следующую картинку

zaa5bi-5vkvscc-ww7gd2ouxjte.png

Для первой настройки нам нужно ввести пароль который сгенерировала система при установке. Для этого нам всего лишь нужно посмотреть содержимое файла /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 и устанавливает набор дефолтных плагинов.

bs9dxsxd6-cddmut2bww9utxjw4.png

r5-mu-bgyaqswxcqnyrpds0qhic.png

После установки плагинов, создаем себе пользователя и нажимаем на Save and Finish

iyapjd__vdbys6jsjiinvzcp58q.png

Соглашаемся с разделом Instance Configuration, где нам предлагают заполнить URL на котором будет работать Jenkins (в нашем случае оставляем все как есть)

4vw1ieq0o_tm5bkrgn4sb-m__kk.png

И на следующем экране нажимаем заветное Start using Jenkins

4aiwyqwkzbxjdkjwyzowxyvsktc.png

Итак, мы установили и запустили Jenkins!

9a4ztunwk1gxuc8rsho2ql9-tou.png

С ним уже вполне можно работать, но для того чтоб собирать наши Android билды нужно будет настроить еще несколько пунктов. Локализация Jenkins связана с выбранным языком вашего браузера, и естественно перевод на русский язык закончен не до конца, и мы получаем адовую смесь из русского и английского языков. Если у вас получилось точно также, и вас это бесит, то можно воспользоваться специальным плагином и установить дефолтный язык интерфейса. Ну или переключить свой браузер на англоязычный интерфейс.

Перейдем в настройки Jenkins и выберем пункт Конфигурация системы

pkxo1diloanswc7tciicxhaiidq.png

Отметим галочкой пункт Environment variables, и впишем в поле имя ANDROID_HOME, а в поле значение укажем /var/lib/android-sdk/ (именно эти данные мы указывали еще в Dockerfile как домашнюю директорию для хранения Android SDK).

onaf4jfhel5_ys078k5ohrxwxuu.png

Нажмем на кнопку Сохранить, выйдем из данного раздела настроек и зайдем в раздел под названием Конфигурация глобальных инструментов.

b93izxlndc-utyrnewgv5kluug0.png

Настроем раздел с JDK (где переменная JAVA_HOME так же заполнялась нами в Dockerfile, и мы можем тут использовать ее значение /usr/lib/jvm/java-8-openjdk-amd64/).

0lwn2prv62virmofscvskc5e6_8.png

Так же тут нам еще нужно заполнить раздел с Gradle. Версию Gradle мы выбираем и устанавливаем ту, которая используется в тех проектах, которые вы будете собирать с помощью этой CI системы. Можно завести несколько версий. Можно так же вообще не заводить Gradle переменную, если у вас в репозитории например есть gradlew, и собирать можно с его помощью.

0qfewj73fbwr3vzudpk8woyrsmy.png

На этом мы можем заканчивать наш первый этап. Система Jenkins полностью готова к работе и мы можем переходить к настройке самих задач по сборке. Обратите внимание, система настраивалась под наши нужды и тут может не оказать того что нужно именно вам — например тут нету андроид эмуляторов для тестирования и NDK.

Если эта статья кого-то заинтересует, то я продолжу и во второй части на примере одной или двух тасок, опишу интеграцию Jenkins и Bitbucket (именно он, а не Github, потому что там и с бесплатными приватными репозиториями попроще, и статей в интернете про него поменьше, а приколов пожалуй побольше), расскажу как ssh ключ нашего контейнера подружить с репозиторием, про email уведомления, а также несколько других фишек. В общем примерно про все, что у нас и настроено.

Прошу сильно не пинать, это моя первая статья на Хабр. Всем добра!

© Habrahabr.ru