Build and Deploy Dockerfile in Jenkins

[Start RESTful Service With Spring Boot 2.x + Kotlin] 글에서 유일하게 다루지 못했던 주제가 있었다. 바로 Jenkins를 이용한 CI/CD 연동이다.

글을 올린지 약 1달 반 정도 지났지만, 성공적으로 빌드할 수 있게 되었고 이를 작성해보려 한다.

환경설정

Jenkins 내부에서 Docker를 빌드하려면 Docker 바이너리가 Jenkins에 포함되어 있어야 한다. Jenkins 역시 도커 이미지로 배포되기 때문에, 먼저 커스텀 Jenkins 이미지를 수정할 필요성이 있다.

Docker + Jenkins 연동은 [Run Docker + Jenkins for Android Build] 글에서 다루니 그쪽부터 보면 좋을 것 같다.

Docker를 설치하기 위해서 추가적으로 필요한 의존성은 다음과 같다.

  • ca-certificates
  • curl
  • gnupg2
  • file
  • lxc
  • apt-transport-https

이 6개를 apt install로 통해 설치하고 순서대로 명령어를 입력하면 된다.

## Install requirements
RUN dpkg --add-architecture i386
RUN rm -rf /var/lib/apt/list/* && apt-get update && apt-get install ca-certificates curl gnupg2 software-properties-common git unzip file apt-utils lxc apt-transport-https -y

그 다음, docker 서버로부터 인증서와 레포지토리 주소를 받아서 설치하면 된다.

## Install Docker-ce into Image
RUN curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg > /tmp/dkey;
RUN apt-key add /tmp/dkey
RUN add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) stable"
RUN apt-get update && apt-get install docker-ce -y --no-install-recommends
RUN usermod -a -G docker jenkins

여기까지 마치면 Jenkins 이미지에 Docker-CE 바이너리가 설치된다.

아니면, 업데이트 된 [WindSekirun/Jenkins-Android-Docker] 이미지를 활용해도 문제는 없다.

JenkinsFile 작성

Spring-Boot 프로젝트를 빌드하고, 이를 Docker로 배포하는 과정에 있어서 Jenkins에 필요한 작업을 정의하는 작업이 필요하다.

Jenkins는 각 파이프라인 상에서 수행해야 될 작업을 Jenkinsfile 이라는 파일에 정의하고, 이를 빌드할 때 마다 사용할 수 있다.

필요한 작업을 정리하면 다음과 같을 것이다.

  • 환경설정
  • Gradle 빌드
  • DockerImage 빌드
  • DockerHub로 이미지 푸시
  • 빌드된 이미지 제거

이 중, gradle 까지의 작업을 반영하면 다음과 같다

pipeline {
    agent any
    stages {
        stage('Environment') {
            parallel {
                stage('chmod') {
                    steps {
                        sh 'chmod 755 ./gradlew'
                    }
                }
                stage('display') {
                    steps {
                        sh 'ls -la'
                    }
                }
            }
        }
        stage('Build Jar') {
            steps {
                sh './gradlew build'
            }
        }
     }
}

Docker 이미지 빌드

환경변수에 registry 이라는 변수를 정의한다.

environment {
        registry = "windsekirun/uploadfileboot"
}

registry 는 이미지에 대한 태그이며, https://hub.docker.com/r/windsekirun/uploadfileboot 이런 식으로 접근이 가능해진다.

그 다음 docker 빌드 명령어를 작성한다. 기본적인 빌드 명령어는 docker build -t windsekirun/uploadfileboot:latest .이므로 Jenkinsfile의 형식에 맞게 작성하면 된다.

stage('Build docker image') {
            steps {
                sh 'docker build -t $registry:latest .'
            }
}

latest 대신 $BUILD_NUMBER 나 다른 환경변수를 이용해서 사용도 가능하다.

빌드된 이미지 푸시

Jenkins 설정 > 인증정보에 정보를 추가한다

그리고 환경변수에 추가한 정보의 id를 정의하면, 준비는 완료된다.

environment {
        registry = "windsekirun/uploadfileboot"
        registryCredential = 'dockerhub'
}

마지막으로, registry에 푸시하는 명령어는 docker push windsekirun/uploadfileboot:latest 이므로, Jenkinsfile 형식에 맞춰 작성한다. 다만 crediental 정보가 필요하므로 withDockerRegistry 명령어를 통해 crediental 정보를 입력한다.

stage('Deploy docker image') {
            steps {
                withDockerRegistry([ credentialsId: registryCredential, url: "" ]) {
                    sh 'docker push $registry:latest'
                }
            }
}

빌드한 이미지 삭제

빌드된 이미지를 계속 남겨둘 필요는 없으므로, 지금까지 빌드한 이미지를 삭제한다.

stage('Clean docker image') {
            steps{
                sh "docker rmi $registry"
            }
}

전체 코드

pipeline {
    environment {
        registry = "windsekirun/uploadfileboot"
        registryCredential = 'dockerhub'
    }
    agent any
    stages {
        stage('Environment') {
            parallel {
                stage('chmod') {
                    steps {
                        sh 'chmod 755 ./gradlew'
                    }
                }
                stage('display') {
                    steps {
                        sh 'ls -la'
                    }
                }
            }
        }
        stage('Build Jar') {
            steps {
                sh './gradlew build'
            }
        }
        stage('Build docker image') {
            steps {
                sh 'docker build -t $registry:latest .'
            }
        }
        stage('Deploy docker image') {
            steps {
                withDockerRegistry([ credentialsId: registryCredential, url: "" ]) {
                    sh 'docker push $registry:latest'
                }
            }
        }
        stage('Clean docker image') {
            steps{
                sh "docker rmi $registry"
            }
        }
     }
}

이 Jenkinsfile 를 가지고 Jenkins를 설정하면 다음과 같이 나오게 된다.

마지막으로 push된 이미지는 Dockerhub 홈페이지에서 확인할 수 있다.

마무리

Jenkins 를 이용해 도커 이미지를 빌드하고 배포하는 과정까지 살펴보았다.

물론 master에 빌드한 것을 그대로 push하는 것은 매우 위험하기 때문에 개발하는 사이클마다 별도로 적용해야 되지만, 개론적인 것은 살펴본 것과 크게 다르지는 않을 것이다.

위에 살펴본 DockerHub에 올리는 것 말고도 private registry에 올리는 것 또한 가능하기에 선택하기 나름일 것 같다.

Upload Android Library into Gradle with Artifactory

Bintray 를 서비스하는 JFrog 에서 아키펙트 패키지(Gradle 나 maven, docker 등 저장소)를 관리하는 소프트를 제작하는데, 그것이 바로 Artifactory 이다. 그 중 Pro 버전은 유료지만, oss라 하여 오픈 소스 버전 (즉 커뮤니티 버전) 이 있는데, 이 것을 사용해서 자체적인 Gradle 저장소를 만들 수 있다.

이 글에서는 VPS 환경에서 자체적인 Gradle 저장소를 만들고, 배포하는 방법을 작성하려 한다.

주의할 점으로, 이 글에서는 Docker 설치나 환경 설정에 대해 언급하지는 않는다.

서버 환경

Vultr Cloud Compute의 $10 서버, 즉 40GB SSD / 2GB RAM / 2TB Bandwidth / 1 CPU(Skylake CPU) 를 사용헀고, OS 는 Ubuntu 18.04.1 LTS 이다.

추가적으로, Artifactory 를 안정적으로 Serve 하기 위해 Docker, PostgreSQL 를 사용했다.

Docker-compose 준비

Artifactory 도커 이미지에는 standalone 으로 돌아갈 수 있게 하는 프로그램이 들어가 있기 때문에, 아래의 명령어 1줄이면 쉽게 구동할 수 있다.

docker run --name artifactory -d -p 8081:8081 docker.bintray.io/jfrog/artifactory-oss:latest  

다만 이 방법은 여러 설정을 하려면 명령어가 길어지고, 여러 개의 컨테이너를 동시에 사용해야 될 경우 각각 하나마다 동작 상태를 확인해야 하는 문제가 있어 Docker-compose 를 사용하여 임의의 설정 파일을 만들고 설정 파일을 docker 에 올리는 방법을 사용한다.

여기서 Docker-compose 란 여러 개의 컨테이너를 한 데에 묶어 컨테이너를 순차적으로 생성하는 역할을 한다. Artifcatory 를 구동하기 위해서는 Artifactory 컨테이너와 DB 역할이 되는 PostgreSQL 컨테이너가 필요하기 때문에, 일일히 run 으로 돌리기 보다는 Docker-compose 를 이용해 한번에 관리하려 한다.

다행히도, JFrog가 Github 에 Docker-compose 예제 파일을 올려두어 모든 것을 다 작업할 필요는 없지만, 위 서버 환경에서 적용했을 때 문제가 있어 아래에 정리하려 한다.

Artifactory 구동에 필요한 환경 설정

구동에 필요한 환경 설정 작업은 다음과 같다.

  • 데이터 폴더 생성
  • 기본 설정 파일 복사
  • 권한 설정

데이터 폴더 생성

# mkdir -p /data/postgresql
# mkdir -p /data/artifactory/etc
# mkdir -p /data/nginx/conf.d
# mkdir -p /data/nginx/logs
# mkdir -p /data/nginx/ssl

sudo 권한으로 접근해 위 명령어를 실행하면 된다.

기본 설정 파일 복사

기본 설정 파일에 대해서는 Jfrog/artifactory-docker-examples에 전부 포함되어 있다. 기본 설정 파일 자체를 수정할 필요는 없으니 각각 맞는 경로에 복사하면 된다. 기본 설정 파일의 경로는 이쪽이다.

  1. /data/artifactory 폴더에 access 폴더 안 내용을 삽입한다. 즉, /data/artifactory/access/etc/keys 에 private.key, root.crt 가 있어야 한다.
  2. /data/nginx/conf.d/oss 폴더에 artifactory.conf 를 삽입한다.

권한 설정

Artifactory 6.2.0 부터 artifactory 라는 유저를 생성하여 그 유저만 /data/artifactory 폴더를 접근할 수 있게 변경되었다.

# chown -R 1030:1030 /data/artifactory
# chown -R 104:107 /data/nginx

sudo 권한으로 접근해 위 명령어를 실행하면 된다. 여기서 1030, 104 및 107은 미리 정의된 값으로 사용자에 맞게 수정하면 될 것 같다.

Docker-compose 파일 작성

일반적 문법은 여기서 설명하지 않으나, 기본적인 것을 설명하자면 Docker-compose 의 설정 파일은 yml 확장자를 가지고 있으며, 각 depth 에 컨테이너 이름과 그 정보를 넣는다. 자세한 문법은 Docker-compose version 2 reference 문서를 참고하면 된다.

version: '2'
services:
  postgresql:
    image: library/postgres:latest
    container_name: postgresql
    ports:
     - 5432:5432
    environment:
     - POSTGRES_DB=artifactory
     # The following must match the DB_USER and DB_PASSWORD values passed to Artifactory
     - POSTGRES_USER=artifactory
     - POSTGRES_PASSWORD=password
    volumes:
     - /data/postgresql:/var/lib/postgresql/data
    restart: always
    ulimits:
      nproc: 65535
      nofile:
        soft: 32000
        hard: 40000
  artifactory:
    image: docker.bintray.io/jfrog/artifactory-oss:latest
    container_name: artifactory
    ports:
     - 80:8081
    depends_on:
     - postgresql
    links:
     - postgresql
    volumes:
     - /data/artifactory:/var/opt/jfrog/artifactory
    environment:
     - DB_TYPE=postgresql
     # The following must match the POSTGRES_USER and POSTGRES_PASSWORD values passed to PostgreSQL
     - DB_USER=artifactory
     - DB_PASSWORD=password
     # Add extra Java options by uncommenting the following line
     #- EXTRA_JAVA_OPTIONS=-Xmx4g
    restart: always
    ulimits:
      nproc: 65535
      nofile:
        soft: 32000
        hard: 40000

위 코드를 artifcatory-oss-postgres.yml 라는 이름으로 만들어 서버에 올리면 된다.

Docker-compose 실행

환경 설정도 완료되고, yml 파일도 만들었다면 이제 실행만 하면 된다.

sudo docker-compose -f artifactory-oss-postgresql.yml up -d

위 명령어를 실행하면 postgresql 이미지 와 artifactory 이미지를 pulling 하여 자동으로 받아오고, 실행을 시작한다. up 뒤 -d 옵션을 주는 것으로 사용자가 종료하지 않는 이상 이 컨테이너는 백그라운드에서 계속 구동될 것이다.

이제, 브라우저 등으로 http://localhost 에 접속하면 아래와 같은 창이 나올 것이다.

사진에선 나오지 않아 설명하기 어려우나 처음에 들어가면 초기 설정 팝업이 뜨면서 비밀번호 설정 – 프록시 설정 – 사용 저장소 설정 순서로 나오게 되는데, 프록시 설정은 넘겨도 무방하며, 사용 저장소는 ‘Gradle’ 와 ‘Maven’ 을 선택한다.

여기까지 하면 Gradle 저장소에 대한 구축은 모두 된 것이며, 이제 스튜디오로 넘어와 라이브러리를 업로드하면 된다.

라이브러리 업로드

라이브러리를 업로드하는 것은 다소 어렵지 않게 구성이 가능하다. 업로드 플러그인을 추가하고, 정보를 기재해 명령어를 실행하는 것 뿐이다.

업로드 플러그인 추가

루트 프로젝트의 build.gradle 에 아래 classpath 를 추가한다.

//Check for the latest version here: http://plugins.gradle.org/plugin/com.jfrog.artifactory
classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4+"

라이브러리 프로젝트의 build.gradle 에 플러그인을 적용한다.

apply plugin: "com.jfrog.artifactory"
apply plugin: 'maven-publish'

프로젝트 정보 기재

플러그인을 정의한 곳 밑에 변수를 추가한다.

def groupId = "com.github.windsekirun"
def version = "1.0.0"
def artifectId = "AwesomeLibraryForDebug"

def Properties properties = new Properties()
properties.load(project.rootProject.file("local.properties").newDataInputStream())

groupId, version, artifectId 는 각각 그룹, 버전, 아키펙트 이름을 나타낸다. gradle 의존성 문구의 규칙 또한 그룹:아키펙트 이름:버전 이므로 위 정보를 가지고 올리면 com.github.windsekirun:AwesomeLibraryForDebug:1.0.0 이 된다.

그 다음 줄의 Properties 의 경우에 저장소에 올리기 위해 아이디 / 비밀번호를 적어야 하는데, 개인 정보가 코드에 올라가야 되지 않게 해야 하므로 local.properties 에서 불러오도록 코드를 추가하였다. 이와 관련된 자세한 사항은 Insert resValue in gradle file 글에서 볼 수 있다.

플러그인 설정

위에서 프로젝트마다 별도로 설정해야 하는 정보는 모두 기재했으므로, 아래부터는 공통 정보이다.

publishing {
    publications {
        aar(MavenPublication) {
            groupId packageName
            version = libraryVersion
            artifactId projectName

            // Tell maven to prepare the generated "*.aar" file for publishing
            artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
        }
    }
}

artifactory {
    contextUrl = 'ARTIFACTORY URL'
    publish {
        repository {
            // The Artifactory repository key to publish to
            repoKey = 'libs-release-local'

            // personal information will locate at local.properties
            username = properties.getProperty("artifactory.username", "")
            password = properties.getProperty("artifactory.password", "")
        }
        defaults {
            // Tell the Artifactory Plugin which artifacts should be published to Artifactory.
            publications('aar')
            publishArtifacts = true

            // Publish generated POM files to Artifactory (true by default)
            publishPom = true
        }
    }
}

위 코드를 dependencies 문단 밑 (즉 맨 하단)에 넣어주면 된다.

명령어 실행

./gradlew assembleRelease artifactoryPublish

모든 작업이 완료되면, 위 커맨드를 터미널에 실행함으로서 Gradle 저장소에 올라가게 된다.

올라간 정보는 Artifactory 페이지 내 Artifacts 부분에서 찾아볼 수 있다.

위 사진과 같이 aar 와 pom 파일이 같이 올라가고, aar 파일을 클릭했을 때 Dependency Declaration 부분에 정상적으로 나오면 성공이다.

다른 프로젝트에서 불러오기

다른 프로젝트에서 불러오기 위해서는, 모듈의 build.gradle 에 아래 코드를 삽입하면 된다.

repositories {
    maven { url "ARTIFACTORY_URL/list/libs-release-local/" }
}

dependencies {
    implementation "com.github.windsekirun:AwesomeLibraryForDebug:1.0.0"
}

참고로 여기서 예제로 들은 AwesomeLibraryForDebug 는 존재하지 않으므로 시도하지 않아도 된다.

Insert resValue in gradle file

도입

소셜 로그인 라이브러리 (https://github.com/WindSekirun/SocialLogin) 를 업데이트 하면서, 샘플 앱을 구현할 필요가 있었는데 흔히 api 키는 비밀로 유지되어야 할 사항으로 여겨진다.

이를 적당히 우회할 수 있는 방법을 찾아보다가 local.properties 에 적는 형식을 생각해냈고, 이를 사용해서 String.xml 에 새 값을 추가하는 것을 기록해두려 한다.

local.properties에 작성하기

흔히 sdk, ndk 가 설치된 프로젝트의 경우 local.properties 는 다음과 같다.

## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Tue Jan 23 23:17:58 KST 2018
ndk.dir=/Users/Pyxis/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/Pyxis/Library/Android/sdk

이 파일의 마지막 줄에 사용할키=사용할값 순서로 적는다.

default.kakao.api=some_value

build.gradle에서 불러오기

android 블록이 나오기 전에 properties 변수를 불러온다.

def Properties properties = new Properties()
properties.load(project.rootProject.file("local.properties").newDataInputStream())

위 코드를 작성하면 local.properties 에서 속성을 불러올 준비가 다 되었다.
이제 불러오는 코드를 작성하면 되는데, 보통 아래 메서드를 통해 값을 가져온다.
properties.getProperty("default.kakao.api", "")
첫 번째 파라미터는 키, 두 번째 값은 기본값이다.

String.xml에 값 넣기

res 값을 넣기 위해서는 resValue 메서드를 사용한다. 이 메서드는 3개의 파라미터를 가지고 있는데, type 와 name, value 총 3개이다.
name, value 는 이름에서 알 수 있듯이 해당 리소스의 이름, 값이고 type 는 해당 리소스의 속성이다. 흔히 string, color 등이다.
여기에서는 buildType 안에 두는 것이 아닌 defaultConfig 안에 두는데, product flavors 에도 같이 활용이 가능하다.

defaultConfig {
    applicationId "com.github.windsekirun.sociallogintest"
    minSdkVersion 19
    targetSdkVersion 27
    versionCode 1
    versionName "1.0"
    resValue "string", "kakao_api_key", properties.getProperty("default.kakao.api", "")
}

이제 build gradle sync 를 하면 @string/kakao_api_key 를 사용할 수 있게 된다.


추가 (2018. 07. 12)

해당 방법을 사용하고 CircleCI 로 커밋하면 오류가 나는데, https://pyxispub.uzuki.live/resolve-local-properties-in-circleci/ 이 글을 참고하면 된다.