목록 보기
GitHub Actions에서 도커 캐시를 적용해 이미지 빌드하기
기타

GitHub Actions에서 도커 캐시를 적용해 이미지 빌드하기

최근 카카오웹툰은 도커 이미지를 만들어 Amazon ECR1에 올리는 방식으로 배포 방법을 바꿔나가고 있습니다. Dockerfile을 만들어서 GitHub에 올려두고, GitHub Actions로 docker build와 push를 진행하는 방식이죠. 그런데 몇 번 배포를 하다 보니, 변경된 내용이 거의 없는데도 빌드 시간이 전혀 줄어들지 않는다는 것을 발견했습니다. 분명 로컬에서는 같은 상황일 때 첫 번째 빌드보다 두 번째 빌드할 때 더 빨랐거든요. 그때는 몰랐습니다. GitHub Actions를 이용해 빌드할 때는 도커 캐시가 동작하지 않는다는 사실을요. 도커 레이어와 캐시 들어가기에 앞서 도커 빌드 속도에 영향을 미치는 레이어(layer)와 캐시(cache)에 대해 알아보겠습니다. 도커 이미지는 빌드시 Dockerfile 의 명령어들을 차례로 실행하면서 레이어를 생성합니다. 이때 특정 명령어들(RUN, ADD, COPY)로 생성된 레이어는 파일 용량을 커지게 만들고, 이미지를 생성하는 시간도 길어지게 하죠. # Dockerfile - old FROM ubuntu

RUN apt-get updateRUN apt-get install vim # Dockerfile - new FROM ubuntu

RUN apt-get update && apt-get install vim 두 코드를 비교해보면 RUN 명령어를 2개 → 1개로 줄인 것을 볼 수 있습니다. 이렇게 && 을 이용해 명령어를 연결 해주는 것 만으로도 이미지의 크기를 줄일 수 있습니다. 크기를 줄여봤으니 이제 도커 캐시에 대해 알아볼게요. Dockerfile 을 작성하고 docker build 를 실행하게 되면 빌드 속도를 높이기 위해 캐시를 사용합니다. 첫 번째 빌드에서는 각 단계별 캐시를 생성하고, 이후 동일한 명령어가 실행되면 만들어둔 레이어를 재사용합니다. 만약 변경되는 단계가 있다면 레이어는 다시 만들어집니다. 예제) Express 앱 + 도커 Express를 이용한 간단한 웹앱 + Dockerfile 예제입니다. Node js 와 도커에 익숙하시다면 다음 단락으로 넘어가셔도 됩니다. (출처: https://nodejs.org/en/docs/guides/nodejs-docker-webapp/) 간단한 예제를 만들어 보겠습니다. 적당한 폴더에서 npm init -y package.json 파일을 아래와 같이 수정하고, npm install 해줍니다. ( "name": "test", "version": "1.0.0", "description": "", "main": "index.js", "scripts": ( "start": "node server.js" ), "dependencies": ( "express": "^4.16.1" ), "keywords": [], "author": "", "license": "ISC" ) npm install 이제 package-lock.json 파일이 만들어졌습니다. 이어서 server.js 파일을 만들고 express를 이용해 간단한 웹앱을 구현해줍니다. "use strict"

const express = require("express")

// Constants const PORT = 8080 const HOST = "0.0.0.0"

// App const app = express() app.get("/", (req, res) => ( res.send("Hello World") ))

app.listen(PORT, HOST) console.log(Running on http://$(HOST):$(PORT)) Dockerfile 생성 이제 Dockerfile을 만들겠습니다. touch Dockerfile FROM node:16

WORKDIR /usr/src/app

package.json, package-lock.json 파일을 복사해서 넣어줍니다.

COPY package*.json ./

필요한 패키지들을 설치해주고

RUN npm install

작업했던 파일들을 복사해서 넣어 줍니다.

COPY . .

포트는 8080 으로 열어 줍니다.

EXPOSE 8080

CMD [ "node", "server.js" ] 추가로, .dockerignore 파일을 만들어서 불필요한 폴더나 파일들은 빌드 시 포함되지 않게 해줍니다. node_modules npm-debug.log 이제, 빌드를 진행해 보겠습니다! docker build -t docker-test:1 . [+] Building 53.2s (10/10) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 169B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 67B 0.0s => [internal] load metadata for docker.io/library/node:16 4.5s => [internal] load build context 0.0s => => transferring context: 15.28kB 0.0s => [1/5] FROM docker.io/library/node:16@sha256:68e34cfcd8276ad531b12b3454af5c24cd028752dfccacce 45.9s => => resolve docker.io/library/node:16@sha256:68e34cfcd8276ad531b12b3454af5c24cd028752dfccacce4 0.0s => => sha256:68e34cfcd8276ad531b12b3454af5c24cd028752dfccacce4e19efef6f7cdbe0 1.21kB / 1.21kB 0.0s => => sha256:054bccad10b08705d61ff87ad3ccf825b297a68d13e6f7e6b567e7d74b6f934a 7.65kB / 7.65kB 0.0s => => sha256:d88439e7b50a5f3923f67f432b6863c1e11adf4e45bf9740515d2cc01fd8e155 7.83MB / 7.83MB 2.9s => => sha256:44e59f8582c39b17ab895261edbcfeadb082ea2a881701ffbce68f5536c5c342 2.21kB / 2.21kB 0.0s => => sha256:7d66b83ec869a899bc8364af9c9eb0f1a5ba6907f699ef52f3182e19e2598924 50.44MB / 50.44MB 20.6s

(...중략)

=> => sha256:dd2ee15b7435ef176b1afd305fb5aae94c3f8aecd81550b979fdf7d8a95e3de1 450B / 450B 27.0s => => extracting sha256:5f327ea23de9da0941be6aa9b1b0106716beba2b2221ccc2cf867db344fdc38b 7.9s => => extracting sha256:c398d8bd2d496fc1ba9d4658870242ce78aefe3041899476238df6cd427638f3 0.1s => => extracting sha256:98d88b5f83dde6aed913ee56cfd04363b90da51a9740c5eb8e6f12b99ec1d639 1.5s => => extracting sha256:8df9d92108465cd690600c5c2b878274db5d262b17aa88b111ebdf828bd47097 0.1s => => extracting sha256:dd2ee15b7435ef176b1afd305fb5aae94c3f8aecd81550b979fdf7d8a95e3de1 0.0s => [2/5] WORKDIR /usr/src/app 0.2s => [3/5] COPY package*.json ./ 0.0s => [4/5] RUN npm install 2.3s => [5/5] COPY . . 0.0s => exporting to image 0.1s => => exporting layers 0.1s => => writing image sha256:40550cb3188291841bc36b53be7ccec6925c928f23b5ed742d85c34f52cd5e96 0.0s => => naming to docker.io/library/docker-test:1 53초가 걸렸습니다. 아무런 수정 없이, 한 번 더 빌드해보겠습니다. docker build -t docker-test:2 . [+] Building 1.2s (10/10) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 36B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 34B 0.0s => [internal] load metadata for docker.io/library/node:16 1.1s => [internal] load build context 0.0s => => transferring context: 161B 0.0s => [1/5] FROM docker.io/library/node:16@sha256:68e34cfcd8276ad531b12b3454af5c24cd028752dfccacce4e19efef6f7cdbe0 0.0s => CACHED [2/5] WORKDIR /usr/src/app 0.0s => CACHED [3/5] COPY package*.json ./ 0.0s => CACHED [4/5] RUN npm install 0.0s => CACHED [5/5] COPY . . 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:40550cb3188291841bc36b53be7ccec6925c928f23b5ed742d85c34f52cd5e96 0.0s => => naming to docker.io/library/docker-test:2 1.2초가 걸렸네요! 첫 번째 빌드에 비해 시간이 확연히 줄었습니다. 단계를 잘 살펴보면 CACHED 라고 되어있고, 특별한 작업을 진행하지 않고 넘어가는 것을 볼 수 있습니다. 변경 사항이 있으면 어떻게 되는지 보겠습니다. npm install react react-dom lodash typescript (당장 필요는 없지만) 몇 가지 패키지를 더 설치해봅니다. 이러면 package.json 과 package-lock.json 파일이 변경되었을 것이고, Dockerfile의 COPY package*.json ./ 부터는 변화를 감지해 캐시가 동작하지 않겠죠? docker build -t docker-test:3 . [+] Building 7.6s (10/10) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 36B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 34B 0.0s => [internal] load metadata for docker.io/library/node:16 2.3s => [1/5] FROM docker.io/library/node:16@sha256:68e34cfcd8276ad531b12b345 0.0s => [internal] load build context 0.0s => => transferring context: 17.27kB 0.0s => CACHED [2/5] WORKDIR /usr/src/app 0.0s => [3/5] COPY package*.json ./ 0.0s => [4/5] RUN npm install 4.6s => [5/5] COPY . . 0.0s => exporting to image 0.6s => => exporting layers 0.6s => => writing image sha256:b9c9711d6b9c943f70bc19ae513edaf4595c22f996d27 0.0s => => naming to docker.io/library/docker-test:3 0.0s 7.6초 걸렸네요. 로그를 보면 총 5단계 중 3단계([3/5] COPY package*.json ./) 부터는 캐시가 적용되지 않았습니다. 그래서 npm install 명령이 실행되면서 두 번째 빌드 보다 시간이 조금 더 걸린거죠. 지금까지 도커 캐시가 어떻게 동작하는지 알아봤습니다. GitHub Actions 를 이용한 빌드 카카오웹툰은 코드를 GitHub 에 올리고, push 할 때마다 GitHub Actions로 도커 빌드 및 배포를 실행해주고 있습니다. GitHub Actions 가 생소하시다면, Ray가 작성해주신 훌륭한 글이 있으니 참고하시면 되겠습니다. GitHub > Actions > Workflows > New workflow를 클릭하면 여러 가지 샘플들을 만날 수 있습니다. 여기서 도커 빌드를 위한 Docker Image를 선택하고, 작업 환경에 맞게 코드를 수정해주면 됩니다. name: Docker Image CI

on: push: branches: [main] pull_request: branches: [main]

jobs: build: runs-on: ubuntu-latest

steps:
  - uses: actions/checkout@v2
  - name: Build the Docker image
    run: docker build -t my-image-name:$(date +%s) . 특별한 수정 없이 사용 해보겠습니다. main 브랜치로 push 할 때마다 docker build를 실행합니다. 동일한 코드를 여러 번 push 해봤습니다. 시간이 별로 차이 나지 않네요. GitHub Actions 로그를 살펴보니 모든 작업을 다시 해주고 있었습니다. 특히, package.json 파일에 변경이 없었음에도 불구하고 매번 npm install 을 실행해주는 부분이 시간을 많이 차지했죠. 캐시가 동작하지 않는 겁니다. 왜 도커 캐싱이 이뤄지지 않을까? GitHub Actions의 러너는 매번 새로운 가상 환경에서 실행됩니다.2 그래서 모든 작업은 새롭게 다시 시작되죠. GitHub 에서 일부 환경을 위한 캐싱을 제공하지만, Docker 레이어에 대한 내용은 없습니다. docker/build-pus

댓글 0

댓글을 작성하려면 로그인이 필요합니다.

댓글을 불러오는 중...