작고 귀여운 GoCV 컨테이너 만들기

작고 귀여운 GoCV 컨테이너 만들기
Docker 그리고 GoCV... 험난했던 둘의 만남 이야기

GoCV?

GoCV 패키지는 Computer Vision Library의 대명사인 OpenCV의 Golang 바인딩(bindings)를 제공한다. 2017년 10월 10일 v0.1.0 공개 이후 5년이 넘는 기간동안 꾸준히 업데이트가 이루어지고 있다. OpenCV 그룹의 공식 파이썬 패키지 OpenCV-Python가 2016년 9월에 첫 번째 릴리즈를 공개한 것을 감안하면 그 역사가 결코 짧지 않다고 볼 수 있다.

Home :: GoCV - Golang Computer Vision Using OpenCV 4

Why not Python / C++?

1. 회사가 자체적으로 보유하고 있는 IDC에 SaaS 형태로 서비스를 구성하되, 2. 고객사에 따라 온프레미스 형태로도 서비스 구축이 가능해야 한다는 것이 제품 개발의 중요 요구사항이었다. 이에 따라 다음과 같은 기대 효과로 백엔드 개발의 주력 언어를 Golang으로 채택하게 되었다.

  • 컴파일러 언어와 달리 Python 같은 인터프리터 언어는 고객사 내부에 서비스를 직접 배포할 때 스크립트 암호화 같은 부수적인 작업 부담이 발생한다.
  • Golang은 언어 자체적으로 크로스-컴파일을 지원하므로 고객사가 사용하는 임의의 장비에서 동작하는 바이너리로 프로젝트를 빌드하기 수월하다.
  • Golang은 프로젝트에 필요한 요소를 묶어 하나의 바이너리로 빌드하는 것이 가능하므로 컨테이너의 크기를 최적화 하기에 용이하다. 최적화된 크기의 컨테이너는 서비스를 빠르게 스케일링 하는데 도움을 주며 한정된 자원의 IDC를 효율적으로 사용할 수 있게 해준다.
  • OpenCV는 C++를 공식적으로 지원한다. C++도 컴파일러 언어이므로 Golang과 비슷한 장점을 기대할 수 있으나 팀의 장기적인 생산성을 고려해 이 선택은 기각했다.

또한 GoCV에 대한 사전조사 결과 서비스의 전체 기능 요구사항을 구현하는데 문제가 없다는 결론을 내렸다.

GoCV, 도커로 말아보자!

열심히 서비스의 기본적인 틀을 만들고 GoCV의 기본 도커 이미지를 기반으로 프로젝트를 빌드하기 위해 도커허브를 살펴봤다. 헌데 이상하다. 가장 작은 컨테이너 크기가 780 MB에 육박한다.

https://hub.docker.com/r/gocv/opencv/tags?page=1&name=4.6.0

GoCV의 Dockerfile을 살펴보기로 한다.

# to build this docker image:
#   docker build -f Dockerfile.opencv -t gocv/opencv:4.6.0 .
FROM golang:1.18-buster AS opencv
LABEL maintainer="hybridgroup"

RUN apt-get update && apt-get install -y --no-install-recommends \
            git build-essential cmake pkg-config unzip libgtk2.0-dev \
            curl ca-certificates libcurl4-openssl-dev libssl-dev \
            libavcodec-dev libavformat-dev libswscale-dev libtbb2 libtbb-dev \
            libjpeg-dev libpng-dev libtiff-dev libdc1394-22-dev && \
            rm -rf /var/lib/apt/lists/*

ARG OPENCV_VERSION="4.6.0"
ENV OPENCV_VERSION $OPENCV_VERSION

RUN curl -Lo opencv.zip https://github.com/opencv/opencv/archive/${OPENCV_VERSION}.zip && \
            unzip -q opencv.zip && \
            curl -Lo opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/${OPENCV_VERSION}.zip && \
            unzip -q opencv_contrib.zip && \
            rm opencv.zip opencv_contrib.zip && \
            cd opencv-${OPENCV_VERSION} && \
            mkdir build && cd build && \
            cmake -D CMAKE_BUILD_TYPE=RELEASE \
                  -D WITH_IPP=OFF \
                  -D WITH_OPENGL=OFF \
                  -D WITH_QT=OFF \
                  -D CMAKE_INSTALL_PREFIX=/usr/local \
                  -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-${OPENCV_VERSION}/modules \
                  -D OPENCV_ENABLE_NONFREE=ON \
                  -D WITH_JASPER=OFF \
                  -D WITH_TBB=ON \
                  -D BUILD_DOCS=OFF \
                  -D BUILD_EXAMPLES=OFF \
                  -D BUILD_TESTS=OFF \
                  -D BUILD_PERF_TESTS=OFF \
                  -D BUILD_opencv_java=NO \
                  -D BUILD_opencv_python=NO \
                  -D BUILD_opencv_python2=NO \
                  -D BUILD_opencv_python3=NO \
                  -D OPENCV_GENERATE_PKGCONFIG=ON .. && \
            make -j $(nproc --all) && \
            make preinstall && make install && ldconfig && \
            cd / && rm -rf opencv*

CMD ["go version"]
gocv/Dockerfile.opencv

Golang 프로젝트를 실행할 수 있는 환경과 OpenCV의 빌드를 제공하고자 하는 것이 도커파일의 의도로 보인다. 허나 위 도커파일에는 몇 가지 문제점이 있다.

  • 베이스이미지로 사용하고 있는 golang:1.18-buster는 260~315MB에 육박하는 꽤 무거운 이미지다.
  • OpenCV의 빌드를 위해 설치한 패키지들은 빌드 후에 더이상 사용되지 않음에도 삭제하지 않는다.

위 문제점들을 보완한 도커파일을 직접 만들어보기로 한다.

DIY, GoCV 도커파일!

(다음으로 소개하는 예제는 아래 레파지토리에서도 확인할 수 있다.)

public-examples/dockerizing-gocv at main · annotation-ai/public-examples
Public Examples (e.g. Blog). Contribute to annotation-ai/public-examples development by creating an account on GitHub.

새로 작성한 도커파일은 두 개의 스테이지로 구성된다. (참고: Docker Multi-stage builds)

  1. Builder Stage: golang:1.19.3-alpine 이미지를 기반으로 OpenCV와 GoCV 기반의 프로젝트를 빌드한다. OpenCV의 빌드 결과 .dll , .so , .dylib 같은 Dynamic Library가 생성되고, GoCV 프로젝트의 빌드 결과로는 main 이라는 이름의 바이너리 파일이 생성된다.
  2. Runtime Stage: 3MB 이하의 크기인 alpine:3.16 이미지에 Builder Stage에서 얻은 Dynamic Library와 GoCV 프로젝트의 바이너리 파일만을 가져오고, Golang 환경변수( CGO_* )에 적절한 값을 입력하여 GoCV 프로젝트가 Dynamic Library를 사용할 수 있도록 한다.

전체 도커파일 내용은 다음과 같다.

# The build Stage for OpenCV and GoCV.
FROM golang:1.19.3-alpine AS builder

ENV OPENCV_VERSION "4.6.0"
# Produce dynamic libraries (.dll, .so, .dylib).
ENV BUILD_SHARED_LIBS "ON"

# Prerequisites.
RUN apk add --no-cache ca-certificates
RUN apk add --no-cache git build-base musl-dev alpine-sdk cmake clang clang-dev make gcc g++ libc-dev linux-headers bash

# Build OpenCV.
RUN mkdir /tmp/opencv
WORKDIR /tmp/opencv
RUN wget -O opencv.zip https://github.com/opencv/opencv/archive/${OPENCV_VERSION}.zip
RUN unzip opencv.zip
RUN wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/${OPENCV_VERSION}.zip
RUN unzip opencv_contrib.zip
RUN mkdir /tmp/opencv/opencv-${OPENCV_VERSION}/build

WORKDIR /tmp/opencv/opencv-${OPENCV_VERSION}/build
RUN cmake \
    -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D BUILD_SHARED_LIBS=${BUILD_SHARED_LIBS} \
    -D OPENCV_EXTRA_MODULES_PATH=/tmp/opencv/opencv_contrib-${OPENCV_VERSION}/modules \
    -D BUILD_DOCS=OFF \
    -D BUILD_EXAMPLES=OFF \
    -D BUILD_TESTS=OFF \
    -D BUILD_PERF_TESTS=OFF \
    -D BUILD_opencv_java=NO \
    -D BUILD_opencv_python=NO \
    -D BUILD_opencv_python2=NO \
    -D BUILD_opencv_python3=NO \
    -D WITH_JASPER=OFF \
    -D WITH_TBB=ON \
    -D OPENCV_GENERATE_PKGCONFIG=ON \
    ..
RUN make -j$(nproc)
RUN make install
RUN rm -rf /tmp/opencv

# Build the GoCV project.
WORKDIR /build
COPY . .
RUN go build main.go

# The runtime stage.
# alpine:3.16's size is around 2.5MB.
FROM alpine:3.16

RUN apk add --no-cache ca-certificates libstdc++

ENV PKG_CONFIG_PATH /usr/local/lib/pkgconfig
ENV LD_LIBRARY_PATH /usr/local/lib
ENV CGO_CPPFLAGS -I/usr/local/include
ENV CGO_CXXFLAGS "--std=c++1z"
# Output of `pkg-config --cflags --libs --static opencv4` in builder
ENV CGO_LDFLAGS "-I/usr/local/include/opencv4 -L/usr/local/lib -lopencv_gapi -lopencv_stitching -lopencv_aruco -lopencv_barcode -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_dnn_objdetect -lopencv_dnn_superres -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hfs -lopencv_img_hash -lopencv_intensity_transform -lopencv_line_descriptor -lopencv_mcc -lopencv_quality -lopencv_rapid -lopencv_reg -lopencv_rgbd -lopencv_saliency -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_superres -lopencv_optflow -lopencv_surface_matching -lopencv_tracking -lopencv_highgui -lopencv_datasets -lopencv_text -lopencv_plot -lopencv_videostab -lopencv_videoio -lopencv_wechat_qrcode -lopencv_xfeatures2d -lopencv_shape -lopencv_ml -lopencv_ximgproc -lopencv_video -lopencv_xobjdetect -lopencv_objdetect -lopencv_calib3d -lopencv_imgcodecs -lopencv_features2d -lopencv_dnn -lopencv_flann -lopencv_xphoto -lopencv_photo -lopencv_imgproc -lopencv_core -ldl -lm -lpthread -lrt"

COPY --from=builder /usr/local/lib /usr/local/lib
COPY --from=builder /usr/local/include/opencv4 /usr/local/include/opencv4
COPY --from=builder /build/main /usr/local/bin
ENTRYPOINT ["main"]

이제 도커 이미지를 빌드한 뒤 이미지의 크기를 관측해보자.

$ docker build -t dockerizing-gocv .
...
$ docker images

REPOSITORY         TAG      IMAGE ID       CREATED         SIZE
dockerizing-gocv   latest   43d032497c94   8 seconds ago   66.4MB

66.4 MB! (c.f. GoCV의 베이스이미지 크기는 780 MB)

빌드한 GoCV 프로젝트도 정상적으로 작동한다.

$ docker run --rm -it dockerizing-gocv

gocv version: 0.31.0
opencv lib version: 4.6.0

맺음말

  • Golang으로 서비스 구현시 인터프리터 언어에 대비한 기대효과를 살펴봤다.
  • GoCV에서 제공하는 베이스 도커 이미지에 비해 약 11배 이상 작은 크기의 도커이미지를 만드는 방법을 살펴봤다.

만약 아래 OpenCV 튜토리얼을 참고해서 프로젝트에서 사용하지 않는 OpenCV의 기능을 최대한 빌드옵션에서 비활성화 한다면 이미지 크기를 더욱 줄이는 것도 가능하다.

OpenCV: OpenCV configuration options reference