1. 사건의 발단
사건의 발단은 몇 달 전으로 거슬러 올라간다. Rails 프로젝트 컨테이너라이징 작업을 하던 중… 도커 빌드 속도가 너무 느려서 하나를 수정해서 배포해도 6분, 8분, 12분 이런 식으로 너무 오래 걸려서 답답해하고 있었다.
분명히 캐싱이 적용되어 있을텐데? 싶어서 아무거나 하나 눌러서 보니까 웬걸 … 캐시를 전혀 사용하고 있지 않고 있었다… ㄴㅇㄱ 다른 루비 프로젝트들은 어떻게 하고 있지? 싶어서 다른 루비 프로젝트도 보니까 그냥 느리게 사용하고 있었다.
언젠간 꼭 고쳐야지 … 😔 라는 생각으로 마음속의 백로그로 가지고 있다가 며칠 전부터 드디어 파보기 시작했다.
2. 캐싱 적용 방법
현재 캐싱을 위해 Github Action 플러그인인 docker/build-push-action
를 사용하고 있다. 이 플러그인은 buildx(buildkit으로 빌드 기능을 확장하는 Docker CLI 플러그인)로 도커 이미지를 빌드하고 배포하기 위해 사용하는 플러그인이다.
https://github.com/docker/build-push-action
옵션별로 자세한 내용은 Github Repository에 잘 나와 있으니 생략하고, 빨간색 네모 박스로 친 부분이 캐싱을 위해 사용하고 있는 옵션이다.
cache-from
- external cache sources 지정
- gha을 지정하면 github repository에서
cache-to
로 내보낸 이전 캐시를 가져올 수 있다 (출처)
cache-to
- build cache를 내보낼 곳을 지정
- GitHub Actions Cache service API를 사용해서 캐시를 내보냄 (출처)
- mode는 얼마나 많은 레이어를 캐싱할건지 설정
- max로 설정하면 모든 스테이지를 저장하는 것이다
3. bundle install 순서 재조정
다른 사례들을 읽어보면 같은 설정으로 잘 적용되던데 왜 루비 프로젝트는 적용이 안될까 싶어서 Dockerfile을 살펴봤다.
FROM base_image
ARG GITHUB_TOKEN
WORKDIR /data/project
COPY . .
RUN bundle config https://rubygems.pkg.github.com/internal ${GITHUB_TOKEN} && \
bundle install --jobs 2
RUN bundle exec rake assets:precompile --trace || exit 1
# 이하 생략
필요 없는 부분들은 다 제거하고, 중요한 부분만 남겨두었다. 빌드 시간 중에 많은 비중을 차지하는 부분은 bundle install
부분인데, 도커 캐싱되는 매커니즘을 생각해 보면 COPY . .
에서 항상 파일이 바뀌기 때문에 Gemfile만 따로 빼두고 bundle install을 하도록 변경하면 캐싱이 제대로 되지 않을까? 싶어서 아래처럼 바꿨다.
FROM base_image
ARG GITHUB_TOKEN
WORKDIR /data/project
COPY Gemfile Gemfile.lock .
RUN bundle config https://rubygems.pkg.github.com/internal ${GITHUB_TOKEN} && \
bundle install --jobs 2
COPY . .
RUN bundle exec rake assets:precompile --trace || exit 1
# 이하 생략
그러자 맨 처음에는 보이지 않던 CACHED
가 나타나면서 날 설레게 했는데 … 딱 여기까지만 캐싱이 되고 bundle install
과정부터는 다시 열심히 install을 수행하며 실패했다.
4. GITHUB TOKEN 위치 조정
FROM base_image
ARG GITHUB_TOKEN
WORKDIR /data/project
COPY Gemfile Gemfile.lock .
RUN bundle config https://rubygems.pkg.github.com/internal ${GITHUB_TOKEN} && \
bundle install --jobs 2
COPY . .
RUN bundle exec rake assets:precompile --trace || exit 1
# 이하 생략
3번 과정을 거쳐서 뭐라도 캐싱이 된 Dockerfile을 유심히 들여다보고 있자니 GITHUB_TOKEN가 눈에 들어왔다. ARG로 받는 GITHUB_TOKEN은 Github Action에서 제공해 주는 temporary token으로, 당연히 파이프라인 실행마다 값이 바뀐다. 그래서 캐싱이 안 되는 게 아닐까 싶은 생각이 들었다.
현재 Gemfile의 생김새를 보면 아래와 같이 되어있다. 내부 패키지를 다운로드하기 위해서는 bundle config가 필요한데, bundle config를 실행하면 캐싱이 안되고 …
source 'https://rubygems.pkg.github.com/internal' do
gem 'a', '~> 0.1.1'
gem 'b', '~> 0.1.2'
end
source 'https://rubygems.org'
gem 'rails'
gem 'mysql2'
gem 'rack-cors'
gem 'sass-rails'
그래서 이것저것 찾아보니까 Gemfile에도 group의 개념이 있어서 이걸 분리해서 Dockerfile을 다시 작성해 보고, 이렇게 시도해봤다.
https://bundler.io/guides/groups.html
FROM base_image
ARG GITHUB_TOKEN
RUN bundle config set --local without internal && \
bundle install --jobs 2
WORKDIR /data/project
COPY Gemfile Gemfile.lock .
RUN bundle config https://rubygems.pkg.github.com/internal ${GITHUB_TOKEN} && \
bundle install --jobs 2
COPY . .
RUN bundle exec rake assets:precompile --trace || exit 1
# 이하 생략
결론적으로는 성공하지 못했는데, Gemfile에서 group을 적용한 형태로 수정하면 bundle exec rake assets:precompile
과정에서 라이브러리를 찾지 못하는 이슈가 계속 발생해서 이걸 수정하려고 이런저런 방법들을 다 써봤지만 유효하지 않았다.
그래서 이런저런 꼼수를 쓰다가 Gemfile을 Dockerfile 내에서 수정하는 방식으로 변경했다.
FROM base_image
ARG GITHUB_TOKEN
RUN bundle install --jobs 4
WORKDIR /data/project
COPY Gemfile Gemfile.lock .
COPY . .
RUN echo "source 'https://rubygems.pkg.github.com/internal' do" >> Gemfile && \
echo " gem 'a', '~> 0.1.1'" >> Gemfile && \
echo " gem 'b', '~> 0.1.2'" >> Gemfile && \
echo "end" >> Gemfile
RUN bundle config https://rubygems.pkg.github.com/internal ${GITHUB_TOKEN} && \
bundle install --jobs 2
RUN bundle exec rake assets:precompile --trace || exit 1
# 이하 생략
이렇게 했을 때에는 precompile도 문제없이 잘 실행되는데 캐싱이 안 됐고, GITHUB_TOKEN 정의하는 부분을 아래로 내렸다. 그리고 비록 실험(?)에서는 실패했지만, without group을 적용하면 좋을 것 같아서 개발, 테스트에 필요한 gem은 받지 않도록 변경했다.
FROM base_image
RUN bundle config set --local without development test && \
bundle install --jobs 4
WORKDIR /data/project
COPY Gemfile Gemfile.lock .
COPY . .
RUN echo "source 'https://rubygems.pkg.github.com/internal' do" >> Gemfile && \
echo " gem 'a', '~> 0.1.1'" >> Gemfile && \
echo " gem 'b', '~> 0.1.2'" >> Gemfile && \
echo "end" >> Gemfile
ARG GITHUB_TOKEN
RUN bundle config https://rubygems.pkg.github.com/internal ${GITHUB_TOKEN} && \
bundle install --jobs 4
RUN bundle exec rake assets:precompile --trace || exit 1
# 이하 생략
드디어 캐싱에 성공했다!! 🎊🎊
캐싱 비율도 작지만 올라갔고, 빌드 시간도 더 줄었다. 대상 프로젝트로는 일부러 bundle install 시간이 적게 걸리는 프로젝트를 골라 진행했는데, 다운로드하는 프로젝트가 많을수록 더 좋은 효과가 날 것으로 기대된다.
그런데 하나 우려되는 건 이렇게 바꿔버리면 로컬에서 개발할 때는 Gemfile에서 정의해 두고, 실제로 테스트 환경이나 운영환경에 올릴 때는 Gemfile에 정의해 둔 걸 지우고 Dockerfile을 수정해야 되는데 그게 너무 번거로울 것 같다는 생각이 들어서 이 방법보다 더 나은 방법을 찾고 싶다. 그냥 base image 하나 만들어서 bundle install 해두고 올려두면 굳이 캐싱 안해도 빠를 것 같다는 생각이 들기도 하고, 캐싱을 기본 캐싱 말고 key를 커스텀하게 잡을 수 있으면 될 것 같은데 그 부분은 아직 찾아보지 않아서 다시 마음속의 백로그로 남겨두고 시간 날 때 찾아보는 것으로... 🤗