Run Docker + Jenkins for Android Build

이번 글에서는 Vultr VC2 2core 4GB instance 에 Jenkins 를 올려 안드로이드 앱을 빌드하고 테스트하려 한다.

사용할 인스턴스는 기존에 사용중이던 Artifcatory 인스턴스이지만 사양을 올려 사용할 것이기 때문에, Docker 기본 설정 같은 것들은 이전 글인 ‘Upload Android Library into Gradle with Artifactory‘ 를 참조하면 된다.

DockerFile 커스텀하기

대부분 인터넷에 나온 CI/CD 적용기를 보면 이 단계부터 설명하는 글이 많은데, 처음 도커를 접하는 유저라면 상당히 골치아픈 작업이기도 하다. Dockerfile 자체가 자체 포맷으로 되어있기도 해서 그걸 익혀야 되는 문제점이 있다.

그리고 이번 글의 중점 취지는 아닌 것 같아 제작했던 Dockerfile 를 공유해서 바로 적용할 수 있게 했고, 그것이 WindSekirun/Jenkins-Android-Docker 이다.

DockerHub에도 공유되어 있으니, 바로 pulling 를 받으면 최신 안드로이드 환경 (API 28 + build tools 28.0.3) 을 사용할 수 있게 된다.

따라서 이번 글에서는 미리 제작된 도커 이미지로 이 과정을 대체한다.

먼저 실행되야 될 작업

본격적으로 이미지를 풀링 받기 전에, 이 이미지를 실행하기 위한 커맨드를 살펴볼 필요가 있다. (버전 1.0.1 같은 경우 2018-12-18 기준 최신으로, 가장 최신은 릴리즈 페이지를 참고하면 된다.)

sudo docker run -d -p 8080:8080 -p 50000:50000 -v /data/jenkins-android-docker:/var/jenkins_home windsekirun/jenkins-android-docker:1.0.1

여기에서 -d (백그라운드 작업) 과 -p(포트 바인딩) 은 넘겨도 되나 /data/jenkins-andorid-docker:/var/jenkins_home 부분에 신경을 써야 한다. 이 부분은 ‘실제 저장소내 공간:도커 컨테이너 공간’ 의 형식을 가지고 있는 디렉토리 바인딩 부분으로, 실제 저장소내 공간 부분에 작성한 폴더는 실제 존재하는 폴더여야 한다.

따라서 sudo 권한으로 아래 커맨드를 실행하면 된다.

mkdir /data/jenkins-android-docker
sudo chown -R 1000:1000 /data/jenkins-android-docker

이 ‘실제 저장소내 공간’에 모든 데이터가 들어가므로, 도커 컨테이너를 지워도 이 폴더가 남아있다면 데이터 또한 그대로 보존되게 된다.

위 mkdir 와 chown 을 시작했다면, 맨 위의 shell script 를 실행하면 된다.

귀찮은 사람들은 WindSekirun/Jenkins-Android-Docker 를 VPS 안에서 clone 받아서 sudo sh runImage.sh 를 실행하면 된다. mkdir 부터 docker run 까지 다 된다.

처음 관리자 설정하기

명령어로 도커를 시작했다면, 서버 주소:8080 으로 들어가면 Jenkins home 이 보일 것이다.

Jenkins 를 설정하는 사람이 관리자인지 확인하는 과정인데, 이를 확인하기 위해서는 도커 컨테이너에 접근할 필요가 있다.

SSH 에서 docker container ls 를 입력하면 현재 실행중인 컨테이너 정보가 나오는데, 그 곳에서 Jenkins-Android-Docker 를 찾는다.

위 정보에 따르면 jenkins-android-docker 가 설치된 컨테이너의 id는 d88376885153 이고, 이 컨테이너 id를 이용해 도커 컨테이너의 bash로 접근할 수 있다. 명령어는 docker exec -i -t [컨테이너 id] /bin/bash이다.

bash에 접근했으면 cat /var/jenkins_home/secrets/initialAdminPassword 로 도커 초기 관리자 비밀번호를 알아낸다.

아래 비밀번호를 복사해서 칸에 넣고 Continue 를 누르면 된다.

그 다음 Plugin 창이 나올텐데, 그 곳에서 Install Suggested Plugin를 누른다. 차후에 다시 설치가 가능하니, 지금은 기본만 설치한다.

설치가 다 되고, 관리자 계정을 만들면 Jenkins 를 사용할 준비가 모두 끝난다. 이제 blueocean 플러그인을 설치하여 첫 안드로이드 빌드를 해보도록 한다.

Blueocean 설치하기

Blueocean 은 Jenkins 에서 나온 새 UI/UX 툴로, 기존 Jenkins 가 다소 전문가의 영역에 가깝다고 하면 Blueocean 는 이를 좀 더 간결하고 알아보기 쉽게 만든 것이다.

Jenkins 메인에 접속되면 Manage Jenkins > Manage Plugin > Available 의 검색창에서 Blueocean 을 검색한다.

여기에서 Install without restart 를 누르면 플러그인 설치 페이지로 이동하는데, 여기에서 맨 마지막의 체크박스를 체크해서 바로 재시작될 수 있도록 한다.

여기까지 끝내면 첫 안드로이드 프로젝트를 빌드를 할 모든 준비가 완료된다.

프로젝트 빌드하기

다시 젠킨스 메인으로 돌아와서 옆의 Open Blueocean 을 누른다. 그러면 이제까지 보지 못했던 새로운 Jenkins 가 보이게 된다.

여기에서 ‘Create a new Pipeline’ 를 누른다.

프로젝트 저장소를 선택하고, 가져올 프로젝트를 맨 밑에서 설정한다. 만일 해당 프로젝트에 Jenkinsfile 가 없다면 설정하는 메뉴로 갈 것이고, 이미 있다면 바로 빌드를 시도할 것이다. 이번에는 Jenkins 로 연동해보지 않은 프로젝트를 설정했다.

그러면 이 페이지로 나오게 될텐데, 이 곳이 Jenkins 가 한 빌드당 거칠 파이프라인을 설정하는 곳이다. 가운데의 +를 누르게 되면 새 작업을 추가할 수 있다.


일단 여기에서는 간단히 빌드만 성공하는지 테스트할 것이므로, Add step 에서 Shell script 를 선택하고 ./gradlew assembleDebug --stacktrace를 입력해준다.

그 다음 위 Save를 누르면 파이프라인 저장 다이얼로그가 표시되고, Save&run 을 누르면 바로 빌드가 시작된다.

빌드 지켜보기

이제 프로젝트가 빌드될 때 까지 기다리는 것 만 남았다.

만일 오류가 나온다면 왜 오류가 나오는지 이제 구글링을 열심히 해볼 차례다. 아래는 지금까지 겪은 CI 오류를 정리해본 것이다.

흔한 오류

local.properties (No such file or directory)

assembleDebug 전에 ‘echo “sdk.dir=/opt/android-sdk-linux” >> local.properties’ 를 추가한다. 또는 젠킨스 관리 > Configure System > 맨 하단의 Android SDK Path 에 /opt/android-sdk-linux 를 적어준다.

File google-services.json is missing.

이 글을 참고하되 Environment Variable 를 Jenkins 내부에서 설정해주면 된다.

그리고 추가할 Shell script 는 echo $GOOGLE_SERVICES_JSON | base64 --decode --ignore-garbage > /app/google-services.json“` 이다.

Gradle build daemon disappeared unexpectedly

제일 골치아픈 문제로, 서버의 램 용량이 부족해서 Gradle 데몬이 죽는 현상이다. 이를 해결하기 위해서는 빌드 커맨드를 ./gradlew --no-daemon assembleDebug --stacktrace 로 설정하거나, 아니면 아예 메모리 제한을 거는 방법도 있다.

메모리 제한을 거는 방법은 현재 실행중인 컨테이너를 docker container kill [컨테이너 id]  – docker container stop [컨테이너 id] 로 삭제하고 (데이터는 상기했듯이 남아있다.) 컨테이너 실행할 때 -m 2500m 를 삽입한다. 2500m은 2.5g로 k, m, g 가 사용이 가능하다. 자신의 서버 환경에 맞게 적절히 조정하면 된다.

예제: sudo docker run -d -m 2500m -p 8080:8080 -p 50000:50000 -v /data/jenkins-android-docker:/var/jenkins_home windsekirun/jenkins-android-docker:1.0.1

빌드 성공

CI상 오류를 전부 해결하면 빌드 성공이 나오며, 이제야 첫 프로젝트의 빌드가 끝난 셈이다. 이제 다른 프로젝트를 연동하거나, 좀 더 심화해서 유닛 테스트나 마켓 업로드 기능 들을 구현하면 된다.

마지막으로 위 프로젝트의 빌드에 성공한 JenkinsFile는 다음과 같다. 다른 프로젝트의 루트 폴더에 똑같은 파일 이름으로 만들고 Jenkins 에서 추가하면 바로 인식이 된다.

pipeline {
  agent any
  stages {
    stage('Make Environment') {
      parallel {
        stage('Touch local.properties') {
          steps {
            sh 'echo "sdk.dir=/opt/android-sdk-linux" >> local.properties'
          }
        }
        stage('Touch google-services.json') {
          steps {
            sh 'echo $GOOGLE_SERVICES_JSON | base64 --decode --ignore-garbage > demo/google-services.json'
          }
        }
        stage('Display directory') {
          steps {
            sh 'ls -la'
          }
        }
      }
    }
    stage('assembleDebug') {
      steps {
        sh './gradlew --no-daemon assembleDebug --stacktrace'
      }
    }
  }
  environment {
    GOOGLE_SERVICES_JSON = ''
  }
}