상세 컨텐츠

본문 제목

Jib을 이용한 CD 최적화: Layer 캐싱 활용

프로젝트/click-me

by seungpang 2023. 11. 30. 18:22

본문

반응형

click-me프로젝트에서 github action으로 CD를 구현중에 Container Image를 만들때 Dockerfile을 통해 jar 파일을 바탕으로 이미지를 생성했다.

Dockerfile

FROM openjdk:17-jdk
EXPOSE 8080
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod" ,"/app.jar"]

Dockerfile의 구조는 간단하다. 해당 jar 파일을 바탕으로 Image를 생성하는 것이다.

그리고 그렇게 생성한 이미지를 gcp Artifact registry에 push하기 위해 tag를 달고 push를 하는 github action yml파일을 작성했다.

      - name: Docker Build
        run: ../gradlew docker

      - name: Docker tag
        run: docker tag click-me-${{env.WORKSTATION_IMAGE}}:${{env.VERSION}} ${{env.IMAGE}}

      - name: Docker Push
        run: docker push ${{env.IMAGE}}
  • 해당 프로젝트는 root project가 있고 2개의 sub project가 있어서 com.palantir.docker 를 활용하여 각각 build.gradle에 docker 이미지 생성 작업을 정의했다.

정의한 내용은 아래와 같다.

docker {
    println(tasks.bootJar.outputs.files)
    name rootProject.name+'-'+project.name + ":" + version
    dockerfile file('./Dockerfile')
    files tasks.bootJar.outputs.files
    buildArgs(['JAR_FILE': tasks.bootJar.outputs.files.singleFile.name])
}

이렇게 생성한 docker image의 용량이 거의 600mb를 차지했다.

여기서의 문제점이 뭘까?

  • 해당 빌드는 패키징 된 jar 파일을 image로 만들기 때문에 소스 코드가 약간만 변해도 변경된 소스 코드로 인해 dependency들이 포함된 jar 파일이 새로운 이미지로 인식되어서 전체 파일 빌드를 다시 수행하기 때문에 Docker layer의 장점을 살릴 수 없다.

이러한 문제를 해결하기 위해 layer를 나누어 image를 빌드하는 방법도 있지만 손쉽게 하기 위해 jib를 사용해 보기로 했다.

Jib란?


Jib는 Dockerfile을 사용하지 않거나 Docker를 설치할 필요 없이 컨테이너를 빌드합니다. Maven 또는 Gradle용 Jib 플러그인에서 Jib를 사용하거나 Jib 라이브러리를 사용할 수 있다.

그리고 세가지 목표를 가지고 있다.

  • Fast: 변경 사항을 빠르게 배포한다. jib는 애플리케이션을 여러 계층으로 분리하여 클래스의 종속성을 분할한다. Docker가 전체 애플리케이션을 다시 빌드할 때까지 기다릴 필요없이 변경된 레이러를 배포하기만 하면 된다.
  • Reproducible: 동일한 컨텐츠로 컨테이너 이미지를 다시 빌드하면 동일한 이미지가 생성된다.
  • Daemonless: CLI 종속성을 줄인다. Maven 또는 Gradle 내에서 Docker 이미지를 빌드하고 원하는 레지스트리에 푸시하면 된다.

Docker 빌드 흐름

Jib 빌드 흐름

Jib는 애플리케이션을 종속 항목, 리소스, 클래스 등 별개의 레이어로 구성하고 Docker 이미지 레이어 캐싱을 활용하여 변경사항만 다시 빌드함으로써 빌드를 빠르게 유지한다.

Jib 레이어 구성과 작은 기본 이미지는 전체 이미지 크기를 작게 유지하여 성능과 휴대성을 향상시킨다.

gradle 설정 살펴보기


plugins {
    ...
    id 'com.google.cloud.tools.jib' version '3.4.0'
}

jib {
    from {
        image = "eclipse-temurin:17-jre-alpine@eclipse-temurin@sha256:635ec1b177ac2a587324ed5eda2b9dec197876e16d10c35a4ef9595d76c2c891"
    }
    to {
        image = "asia-northeast3-docker.pkg.dev/sanguine-theory-406607/clickme/transfer-service:latest"
    }
    container {
        jvmFlags = ["-Xms258m", "-Xmx1024m"]
        environment = [
                'REDIS_HOST': System.getenv('REDIS_HOST'),
                'REDIS_PORT': System.getenv('REDIS_PORT'),
                'DATASOURCE_URL': System.getenv('DATASOURCE_URL'),
                'DATASOURCE_USERNAME': System.getenv('DATASOURCE_USERNAME'),
                'DATASOURCE_PASSWORD': System.getenv('DATASOURCE_PASSWORD'),
                'SPRING_PROFILES_ACTIVE': 'prod'
        ]
        filesModificationTime = 'EPOCH_PLUS_SECOND'
    }
}
  • from.image는 해당 이미지의 기본 베이스가 될 image라고 보면 된다.
  • to.image는 해당 이미지가 저장될 registry이다. 해당 코드에서는 gcp artifact registry를 활용해서 저런 형식이다. docker hub을 사용한다면 도커계정/레포지토리이름 과 같은 형식일 것이다.
  • build.gradle에 jib 플러그인 설정을 추가
  • base image는 java 17을 이용할 수 있는 eclipse-temurin:17-jre-alpine을 사용했다 open-jdk는 11 이후부터 jre를 더이상 제공하지 않는다.
  • container는 jvmFlags나 application-prod.yml파일에서 넣을 환경변수 값들을 세팅했다.

빌드하는 방법


./gradlew jib

해당 명령어만 실행하면 이미지를 빌드한 후 build.gradle에 정의했던 레지스트리에 자동으로 푸시한다.

    to {
        image = "asia-northeast3-docker.pkg.dev/sanguine-theory-406607/clickme/click-service:latest"
    }

이렇게 jib로 변경하고 나서 github action yml 파일에도 변화가 생겼다.

Before

      - name: Docker Build
        run: ../gradlew docker

      - name: Docker tag
        run: docker tag click-me-${{env.WORKSTATION_IMAGE}}:${env.VERSION} ${{env.image}}

      - name: Docker Push
        run: docker push ${{env.image}}

After

      - name: Docker Build
        run: ./gradlew :click-service:jib

보는 바와 같이 github action yml파일도 간략해 졌다.

그렇다면 속도에는 어떤 변화가 생겼을까?

기존 Dockerfile로 했을때 Build, tag, push까지 총 57초가 걸렸다.

jib로 변경한 후 24초로 줄어든 것을 확인할 수 있다. 이렇게 속도가 줄어든 이유는 위에 잠깐 언급했듯이 애플리케이션을 종속 항목, 리소스, 클래스 등 별개의 레이어로 구성하고 Docker 이미지 레이어 캐싱을 활용하여 변경사항만 다시 빌드함으로써 빌드를 빠르게 유지한다. 이 이유때문이다.

그러면 jib가 layer를 어떻게 캐싱을 하는 것일까?

jib를 실행하면 {project dir}/build/jib-cache에 layer들이 저장된다.

그리고 jib가 기본적으로 {project dir}/build/jib-cache를 바라보도록 설정되어 있어서 여기에 있는 layer들을 캐싱한다.

주의할 점

처음에 github action으로 했을 당시 로컬환경과 다르기때문에 레이어 캐싱이 정상적으로 동작하지 않았다.

그 이유는 캐싱할 대상이 없기 때문이다. 따라서 github action에서 캐싱을 따로 설정해 둬야 한다.

cache actions를 활용하여 해결했다.

      - name: Cache Jib layers
        uses: actions/cache@v3
        with:
          path: ./click-service/build/jib-cache
          key: ${{ runner.os }}-jib-${{ github.job }}-${{ hashFiles('**/build.gradle') }}
          restore-keys: ${{ runner.os }}-jib-${{ github.job }}

위와 같이 캐싱설정을 하지 않았을 때는 47초 걸렸던 빌드 작업캐싱 설정 이후에 24초로 줄어들었다.

총 줄어든 시간을 살펴보면 2분 10초 걸리던 작업이 54초로 줄어들었다.

Reference


관련글 더보기