본문 바로가기

IT

Gitlab-Runner .m2 Caching

728x90

https://sh970901.tistory.com/136

 

gitlab runner docker in docker 구조

대표적인 CICD(Continuous Integration/Continuous Delivery) 도구들 (gitlab runner, github action, jenkins 등) 이 있지만 이 글에서는 gitlab runner를 사용해 구축하는 과정에서 겪었던 이슈와 해결, 궁금증 대한 정리를

sh970901.tistory.com

상단 포스팅에서 도커 환경의 CI 구성에 대해 간략하게 설명했으니 참고하면 좋을 듯 하다.

 

이번 포스팅에서는 어떻게 Host와 Gitlab-Runner, 그리고 Runner의 executor Container들이 .m2를 캐싱시킬 것인가에 대해서 작성하려고 한다.

 

docker-compose.yml

services:
	gitlab-runner:  
    	image: gitlab/gitlab-runner:latest
        container_name: gitlab-runner  
        restart: always  
        volumes:  
        - ./config/gitlab-runner:/etc/gitlab-runner
        - /var/run/docker.sock:/var/run/docker.sock
        - /home/ec2-user/.m2/repository:/root/.m2/repository

 

gitlab-runner는 호스트의 .m2/repository와 볼륨을 공유한다. 

 

host에 maven을 설치하지 않은 경우는 참고하길 바란다.

wget https://downloads.apache.org/maven/maven-3/3.8.3/binaries/apache-maven-3.8.3-bin.tar.gz  -P /tmp
sudo tar xf /tmp/apache-maven-3.8.3-bin.tar.gz -C /opt
sudo ln -s /opt/apache-maven-3.8.3 /opt/maven
sudo vim /etc/profile.d/maven.sh

export M2_HOME=/opt/maven
export MAVEN_HOME=/opt/maven
export PATH=${M2_HOME}/bin:${PATH}

sudo chmod +x /etc/profile.d/maven.sh
source /etc/profile.d/maven.sh
mvn -version

 

 

gitlab config.toml

# 몇개의 Job까지 수행할 것인지
concurrent = 4

check_interval = 0
shutdown_timeout = 0

[session_server]
  session_timeout = 1799

[[runners]]
  name = "emall-poc-runner"
  url = "<https://gitlab.eland.kr/>"
  id = 51
  token = "r123131adadr241v"
  token_obtained_at = 2024-01-30T06:14:16Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "docker"
    [runners.cache]
      MaxUploadedArchiveSize = 0
    [runners.docker]
      tls_verify = false
      image = "docker"
      privileged = true
      disable_entrypoint_overwrite = false
      oom_kill_disable = false
      disable_cache = false
      volumes = ["/cache", "/home/ec2-user/.m2/repository:/root/.m2/repository"]
      shm_size = 0
      network_mtu = 0

 

기존에 concurrent = 1 이였을 경우 동시에 여러 Job이 돌아가지 않아 수정하였으니 각자의 환경에 맞도록 설정하면 될 듯 하다.

문제는 volums 설정인데, 필자는 exector의 컨테이너들 마찬가지로 볼륨을 잡아주었다. 

 

Dockerfile ( 수정 전 )

# Build stage
FROM maven:3.8.4-openjdk-17-slim AS build

WORKDIR /app

COPY . .

# Set MAVEN_OPTS environment variable

RUN mvn -U clean package

# Final stage
FROM openjdk:17-jdk-slim

ARG DEFAULT_PORT=8080

WORKDIR /application

COPY --from=build /app/target/example.jar ./example.jar
COPY init_script.sh /application/init_script.sh

ENV PORT $DEFAULT_PORT

# Expose the required port
EXPOSE $DEFAULT_PORT

RUN ["chmod", "+x", "/application/init_script.sh"]
RUN ["bash", "-c", "source /application/init_script.sh"]
RUN apt-get update && apt-get install -y vim

# Run the application with environment variables
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar example.jar"]

RUN을 통해 스크립트를 실행시키는건 컨테이너에 초기 구성에 확인해 볼 내용이 있어 추가한 것이므로 무시해도 좋다.

 

필자가 .m2 캐싱에 고생한 이유는 바로 이 Dockerfile이다. 로컬에서도 도커 환경을 구성하기 위해 도커파일안에 패키징 job을 포함하였고 레이어를 가볍게 가져가기 위해 stage를 분리하여 최종 이미지에는 jar만 포함한 상태로 실행 구성을 완료하였다. 로컬에서 docker build 를 맘편히 하기 위한 것이 CICD를 구축하는 과정에서 꽤나 불편한 것을 깨달았다. docker build 시 격리된 빌드 컨테이너 위에서 packaging을 실행하는데 호스트의 .m2 와 볼륨 통해 캐싱을 구성하는 것이 지속된 도전 끝에 방향을 틀기로 했다. Copy 등을 통해 빌드할 때 host의 .m2를 사용해도 이 서비스 자체가 패키징 과정을 통해 새로 받아온 종속성 라이브러리를 최신화 할 수 없었기에 의미가 없었다.

 

Dockerfile ( 수정 후 )

FROM openjdk:17-jdk-slim

ARG DEFAULT_PORT=8080

WORKDIR /application

COPY target/example.jar ./example.webapp.jar
COPY init_script.sh /application/init_script.sh

ENV PORT $DEFAULT_PORT

# Expose the required port
EXPOSE $DEFAULT_PORT

RUN ["chmod", "+x", "/application/init_script.sh"]
RUN ["bash", "-c", "source /application/init_script.sh"]
RUN apt-get update && apt-get install -y vim

# Run the application with environment variables
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar example.jar"]

해당 도커파일은 만들어진 jar를 받아와 실행 이미지를 만든다. 

 

결국 패키징하는 작업을 Gitlab-Runner에게 맡긴 것이다. 위방식과 장단점이 있겠지만 빌드 속도가 더 중요한 시점이라 판단하여 다음과 같이 수정하고 Runner가 패키징하는 과정에서 .m2에 캐싱하는 작업을 마쳤다.

 

stages:
  - source-build
  - docker-build
  - docker-deploy

cache:
  paths:
    # job 마다 파일 전달 기능이 없어 캐싱으로 전달
    - $CI_PROJECT_DIR/envs
    # 메이븐 캐싱
    #    - $CI_PROJECT_DIR/.m2/repository
    - /root/.m2/repository

variables:
  #  MAVEN_OPTS: -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository
  MAVEN_OPTS: -Dmaven.repo.local=/root/.m2/repository
  CACHE_ENV_PATH: $CI_PROJECT_DIR/envs

before_script:
  - mkdir -p $CACHE_ENV_PATH/dev

source-build:
  stage: source-build
  image: maven:3.8.4-openjdk-17-slim
  script:
    - echo "1. 메이븐 빌드&패키지"
    - echo "2. target 캐싱 (save)"
    - mvn -U clean package
    - pwd
    - whoami
    - cp -R target $CACHE_ENV_PATH/dev/
  tags:
    - dev
  only:
    - dev

docker-build:
  stage: docker-build
  image: docker:20
  services:
    - name: docker:20-dind
      command: ["--tls=false"]
  #    - name: amazonlinux:2
  needs:
    - source-build
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_DRIVER: overlay2
    DOCKER_TLS_CERTDIR: ""
    DOCKER_IMAGE_NAME: "${CI_PROJECT_NAME}-${CI_COMMIT_REF_NAME}"
    ECR_REPO_URI: "ecr_registry.d1r.ecr.ap-northeast-2.amazonaws.com/${CI_PROJECT_NAME}"
  before_script:
    - apk add --no-cache curl jq python3 py3-pip
    - pip install awscli
    - aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ecr_registry.d1r.ecr.ap-northeast-2.amazonaws.com.dkr.ecr.ap-northeast-2.amazonaws.com
  script:
    - echo "1. target 캐싱 (restore)"
    - echo "2. 이미지 빌드"
    - echo "3. 저장소 업로드"
    - mv $CACHE_ENV_PATH/dev/target .
    - docker build -t $DOCKER_IMAGE_NAME .
    - docker tag $DOCKER_IMAGE_NAME:latest $ECR_REPO_URI:latest
    - docker push $ECR_REPO_URI
    - docker rmi -f $(docker images -f "dangling=true" -q) || true
  only:
    - dev
  tags:
    - dev

docker-deploy:
  stage: docker-deploy
  before_script:
    - apk add --no-cache curl jq python3 py3-pip
    - pip install awscli
  variables:
    DEPLOY: "deploy"
    AWS_DEFAULT_REGION: 'ap-northeast-2'
  script:
    - aws ecs update-service --region ${AWS_DEFAULT_REGION} --cluster emall-${CI_COMMIT_BRANCH}-ecs --service ${modified_project_name} --enable-execute-command
    - aws deploy create-deployment --application-name ${modified_application_name} --deployment-group-name ${modified_deployment_group} --s3-location bucket=${S3_BUCKET_NAME},bundleType=yaml,key=config/comm/container/${CI_PROJECT_NAME}/appspec.yaml --region ap-northeast-2 | grep -o 'd-[A-Z0-9]\\+' | while read line; do echo "<https://ap-northeast-2.console.aws.amazon.com/codesuite/codedeploy/deployments/${line}?region=ap-northeast-2>"; aws deploy wait deployment-successful --deployment-id "${line}"; done
  only:
    - dev
  tags:
    - dev

위  deploy 작업은 Codedeploy와 ECR, ECS, Fargate를 사용중인 경우이니 참고하면 좋을 듯 하다.

 

아직 개선할 수 있는 포인트가 몇 보인다. 중복된 코드도 몇 보인다. 일단 정상적으로 .m2./repository가 캐싱되고 빌드 속도가 개선된 것이 확연히 보인다. 

기존에 의존성을 받아오는 작업 로그만 1000줄이 넘었다. 현재는 넥서스 주소 (public)에서 필요한 의존성만 받아오는 것을 확인했다.

이것으로 .m2 캐싱은 성공적으로 이루어졌다. 재밌는 Tradeoff의 경험이였다.

 

 

 


이 글을 읽는 개발자분들 중 도커파일에 빌드 과정을 포함하고 CI 과정에서 .m2를 캐싱하고 최신화하는 방법을 사용 중이신 분이 계신다면 해당 방법을 공유해주시면 감사 드리겠습니다.