의존성 캐싱을 이용한 Dockerfile 빌드 최적화

computerphilosopher·2024년 1월 26일
10
post-thumbnail

Dockerfile 첫 줄을 습관적으로 COPY . /app 과 같이 시작하는 사람들이 있다. 이렇게 되면 프로젝트 파일 중 하나라도 수정하는 순간 캐시가 히트되지 않아 전체 빌드를 다시 수행해야 한다. 의존성의 변화 없이 코드만 수정하였는데도 전체 빌드를 다시하게 되면 시간 낭비가 심하다. 특히 코드를 빈번하게 수정하면서 테스트 해야 하는 환경에서 생산성을 심각하게 저해한다.

Dockerfile 시작 부분에서 전체 프로젝트를 복사하는 대신, 의존성을 명시한 파일(예: go.mod, pom.xml 등)만 복사하고 의존성 다운로드 명령을 실행하는 방식으로 불필요한 재빌드를 예방할 수 있다.

FROM golang AS builder
WORKDIR /app

# go.mod나 go.sum 파일이 수정되지 않는 이상 go mod download 까지는 무조건 캐시 히트한다.
COPY go.mod go.sum ./
RUN go mod download
#....

의존성 캐싱의 효과를 예제 프로젝트로 검증해보자. ChatGPT에게 의존성이 쓸데없이 많은 프로그램을 작성해달라고 의뢰하였다.

package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/gorilla/mux"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"github.com/joho/godotenv"
	"github.com/sirupsen/logrus"
	"github.com/tidwall/gjson"
	"github.com/valyala/fasthttp"
	"gopkg.in/yaml.v2"
	"github.com/go-redis/redis"
)

func main() {
	// 환경변수 로드
	err := godotenv.Load()
	if err != nil {
		logrus.Fatal("Error loading .env file")
	}

	// MySQL 데이터베이스 연결
	db, err := gorm.Open("mysql", os.Getenv("MYSQL_CONNECTION_STRING"))
	if err != nil {
		logrus.Fatal("Failed to connect to database: ", err)
	}
	defer db.Close()

	// Redis 클라이언트 설정
	redisClient := redis.NewClient(&redis.Options{
		Addr:     os.Getenv("REDIS_ADDR"),     // 예시: "localhost:6379"
		Password: os.Getenv("REDIS_PASSWORD"), // 예시: "", 비밀번호가 없는 경우
		DB:       0,                           // 기본 DB
	})

	_, err = redisClient.Ping().Result()
	if err != nil {
		logrus.Fatal("Failed to connect to Redis: ", err)
	}

	// 라우터 설정
	router := mux.NewRouter()
	router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// Redis에서 데이터 가져오기 시도
		cachedData, err := redisClient.Get("mykey").Result()
		if err == redis.Nil {
			// 캐시된 데이터가 없는 경우
			logrus.Info("Cache miss")

			// 외부 API 호출
			response, err := fasthttp.Get(nil, "https://api.example.com/data")
			if err != nil {
				http.Error(w, "Error fetching data", http.StatusInternalServerError)
				return
			}

			// 응답에서 JSON 데이터 추출
			jsonValue := gjson.Get(string(response), "key").String()

			// Redis에 데이터 캐시
			err = redisClient.Set("mykey", jsonValue, 0).Err()
			if err != nil {
				http.Error(w, "Error caching data", http.StatusInternalServerError)
				return
			}

			fmt.Fprintf(w, "Fetched from API: %s", jsonValue)
		} else if err != nil {
			// Redis 에러
			http.Error(w, "Error fetching from Redis", http.StatusInternalServerError)
		} else {
			// 캐시된 데이터 반환
			fmt.Fprintf(w, "Fetched from cache: %s", cachedData)
		}
	})
	router.HandleFunc("/metrics", metricsHandler)

	// 서버 시작
	logrus.Info("Starting server on :8080")
	http.ListenAndServe(":8080", router)
}

func metricsHandler(w http.ResponseWriter, r *http.Request) {
	// 메트릭 데이터 반환 (구현 필요)
}

먼저 의존성을 캐싱하지 않도록 Dockerfile을 작성해보자.

FROM golang AS builder
WORKDIR /app

COPY . .
RUN go mod download
RUN go build -o myapp

# 실행 단계
FROM golang:1.18
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]

소스 코드에 의미없는 주석을 추가하고 다시 빌드를 하면 다음과 같이 go mod download가 다시 실행된다.

[+] Building 15.2s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                                                          0.0s
 => => transferring dockerfile: 219B                                                                          0.0s
 => [internal] load .dockerignore                                                                             0.0s
 => => transferring context: 2B                                                                               0.0s
 => [internal] load metadata for docker.io/library/golang:1.18                                                0.8s
 => [internal] load metadata for docker.io/library/golang:latest                                              0.8s
 => [builder 1/5] FROM docker.io/library/golang@sha256:76aadd914a29a2ee7a6b0f3389bb2fdb87727291d688e1d972abe  0.0s
 => CACHED [stage-1 1/2] FROM docker.io/library/golang:1.18@sha256:50c889275d26f816b5314fc99f55425fa76b18fca  0.0s
 => [internal] load build context                                                                             0.0s
 => => transferring context: 2.55kB                                                                           0.0s
 => CACHED [builder 2/5] WORKDIR /app                                                                         0.0s
 => [builder 3/5] COPY . .                                                                                    0.0s
 => [builder 4/5] RUN go mod download                                                                         7.0s
 => [builder 5/5] RUN go build -o myapp                                                                       7.2s
 => [stage-1 2/2] COPY --from=builder /app/myapp /myapp                                                       0.0s
 => exporting to image                                                                                        0.0s
 => => exporting layers                                                                                       0.0s
 => => writing image sha256:3966036be8abd4c59fe19a16f3b3e2e170f8f1a693fe2914ad45520435856afa                  0.0s
 => => naming to docker.io/library/docker-test:latest                                                         0.0s

이번에는 의존성 관련 파일의 수정이 없으면 캐시가 히트하도록 고쳐보자.

FROM golang AS builder
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN go build -o myapp

# 실행 단계
FROM golang:1.18
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]

마찬가지로 소스 코드에 의미없는 주석을 추가한 후 빌드해보면, 의존성이 캐싱되어 빌드가 빨라지는 것을 알 수 있다.

[+] Building 7.7s (14/14) FINISHED
 => [internal] load .dockerignore                                                                             0.0s
 => => transferring context: 2B                                                                               0.0s
 => [internal] load build definition from Dockerfile                                                          0.0s
 => => transferring dockerfile: 242B                                                                          0.0s
 => [internal] load metadata for docker.io/library/golang:1.18                                                0.8s
 => [internal] load metadata for docker.io/library/golang:latest                                              0.8s
 => [builder 1/6] FROM docker.io/library/golang@sha256:76aadd914a29a2ee7a6b0f3389bb2fdb87727291d688e1d972abe  0.0s
 => CACHED [stage-1 1/2] FROM docker.io/library/golang:1.18@sha256:50c889275d26f816b5314fc99f55425fa76b18fca  0.0s
 => [internal] load build context                                                                             0.0s
 => => transferring context: 2.55kB                                                                           0.0s
 => CACHED [builder 2/6] WORKDIR /app                                                                         0.0s
 => CACHED [builder 3/6] COPY go.mod go.sum ./                                                                0.0s
 => CACHED [builder 4/6] RUN go mod download                                                                  0.0s
 => [builder 5/6] COPY . .                                                                                    0.1s
 => [builder 6/6] RUN go build -o myapp                                                                       6.7s
 => [stage-1 2/2] COPY --from=builder /app/myapp /myapp                                                       0.0s
 => exporting to image                                                                                        0.0s
 => => exporting layers                                                                                       0.0s
 => => writing image sha256:0bda2aeecd568113cf6ec3615713ed85d384aa06451e3b13cdf441ca8e7ccd16                  0.0s
 => => naming to docker.io/library/docker-test:latest                                                         0.0s

0개의 댓글