SLAMOps를 위한 첫걸음 - Docker + CI

서문은 건너뛰고 바로 Docker 설치하실 분은 3. Docker 설치방법으로 넘어가세요!

Why Docker?

???: "내 컴퓨터에서는 잘 되는데?"

이 말에는 다음과 같은 뜻이 숨겨져있습니다.

  • “니 컴에서 안되는건 내 문제가 아닙니다”
  • “니 컴 문제는 니가 해결하십시오”

 

보통 어떤 팀원이 새로운 코드를 팀 전체가 사용하는 코드에 푸쉬해놓고, 다른 팀원의 컴퓨터에서 빌드가 터질 때 변명처럼 나오는 말입니다.

정말 무책임하지 않나요?

다른 팀원은 지금 이 코드 업데이트 때문에 빌드도 안되서 일을 못하는데, 이 버그를 만든 장본인은 본인의 컴퓨터에는 문제가 없으므로 계속 일을 하겠다는거 말이죠.

본인이 만든 버그는 본인이 책임지는게 맞는겁니다.

 

근데 사실 왠만큼 인성이 비뚤어진 사람이거나, 아니면 우리 팀의 업무를 터트리려는 산업스파이가 아닌 이상, 본인도 어느정도 죄책감을 느낄겁니다.

다만 이 버그를 풀기 위해서는 다양한 컴퓨터 환경에서 복잡한 디펜던시 이슈를 풀어야하는데, 이 작업을 뚝딱 해결할 자신이 나지 않기 때문에 변명처럼 말하는거죠.

 

이러한 문제를 단번에 해결해주는 것이 바로 Docker입니다.

Docker는 모든 팀원이 동일한 환경에서 빌드 & 테스트 할 수 있게 해줍니다.

킹갓Docker를 사용하는 순간부터, 누가 다시 ‘내 컴퓨터에서는 잘 되는데?’ 라고 하면 다음과 같이 이야기해주시면 되겠습니다.

A: “내 컴퓨터에서는 잘 되는데???”
B: “아! 그러면 너님의 컴퓨터를 제품으로 팔겠습니다!
A: “?!”

 


SLAM만 해도 힘들어죽겠는데 Docker까지 해야하나…

로컬 환경에서 여러 SLAM 프로젝트를 진행하다보면 다양한 라이브러리들이 시스템에 설치되게 됩니다.

SLAM에서 많이 사용되는 OpenCV, PCL, Open3D 등등이 시스템에 설치되어있다면, 많은 오픈소스 프로젝트는 git clone으로 끌어와 바로 빌드할 수 있습니다.

 

하지만 이러한 환경을 가지고 있을 때 다음과 같은 문제가 생깁니다.

  1. 새로운 프로젝트를 시작할 때
    • 새 프로젝트의 디펜던시가 이미 설치되어있을 수 있다면?
      • 디펜던시 자동 설치 스크립트를 작성할 때, 사전에 설치된 라이브러리에 대해 테스팅을 하지 않는 실수를 하기 쉬움.
    • 새 프로젝트의 디펜던시가 ‘다른 버전’으로 설치되어있을 수 있다면?
      • 다른 버전을 사용하면서 API 충돌이 날 수 있음.
      • 다른 버전을 사용하면서 API가 충돌이 나지 않는 경우는… 보통 성능개선 업데이트만 있는 경우인데, 아무도 잘못된 버전을 사용한다는 것을 모른 채 계속 구린 성능의 버전을 쓸 수도 있음.
  2. 옛날 프로젝트를 다시 볼 때
    • OpenCV3 기반 프로젝트를 잠시 중단하고… OpenCV4 기반 프로젝트를 하다가… 다시 이 프로젝트로 돌아온다면?
      • find_package(OpenCV REQUIRED)를 하면 어떤 버전의 라이브러리를 잡을지는 아무도 모름
      • (확률적인 버그 잡이는 최악입니다…)
      • (진짜 마음만 같아서는 프로젝트마다 컴퓨터 한대씩 쓰고 싶다는 생각이 듭니다…)

 

위와 같은 문제를 해결하는건 정말 귀찮은 작업입니다.

  1. 현재 시스템에 빌드되어있는 라이브러리들을 전부 정리해야합니다.
    • 설치된 라이브러리 이름, 버전명, 빌드 옵션, 빌드 컴파일러, 설치 경로…
  2. 현재 시스템에 apt로 설치된 라이브러리들도 전부 정리해야합니다.
    • 컴파일러? 시스템 유틸?
  3. 내가 진행한 프로젝트들마다 ‘각각’ 어떤 라이브러리를 어떤 옵션으로 사용하고 어떤 경로를 지정했는지 알아야합니다.
  4. 모든 프로젝트들을 클린 리눅스에서 테스트 해봐야합니다.
  5. 이제 이 환경을 모든 팀원들 컴퓨터에 그대로 깔아줘야합니다.

1/2/3을 풀기 위해 이전에 시스템 설치 디펜던시를 최소화하는 프로젝트(cpp-cv-project-template)를 진행한 적이 있습니다.

하지만 이 방법도 설치해야하는 모든 시스템 라이브러리를 리스팅 하는 노가다를 엄청 많이 했어요.

디버깅을 하면서도 ‘이게 맞나…? 다 한건가…?’ 싶다가도 버그가 터지면 ‘내가 그럼 그렇지’를 외치기를 몇번씩 반복했던 것 같습니다.

 

근데 Docker를 쓰고 엄청 쉬워졌습니다.

  1. 디펜던시 빌드
    • Docker로 Clean Ubuntu 20.04 이미지 생성
    • 디버깅을 할 때마다 git에서 소스코드만 끌어와서 빌드
      • 버그가 나타나면 로그 남김
    • 코드를 수정하고 다시 디버깅을 시작하면, 5초만에 Clean Ubuntu 20.04가 다시 만들어짐.
    • 다시 빌드를 시작
    • 몇번 반복 후 완벽한 디펜던시 스크립트 작성 완료.
  2. SLAM 프로그램 빌드
    • OpenCV랑 Ceres를 매번 빌드하기에는 너무 오래걸림 (30분 ~ 1시간)
    • 그러니 Ubuntu 20.04에 OpenCV랑 Ceres를 미리 빌드해놓은 이미지를 생성
      • git에서 소스코드만 떙겨와서 빌드
        • 1분만에 Clean Ubuntu 20.04에 OpenCV + Ceres + 나의 SLAM 프로그램이 빌드

진짜 ‘아 왜 Docker를 지금 알게된거지? 인생 낭비했네‘ 생각이 들 정도였습니다.

 


Docker 설치 방법

바로 Docker를 설치해보겠습니다.

  1. 이전에 Docker를 설치하려고 한 경험이 있다면, Docker 버전 충돌을 피하기 위해 예전 Docker를 삭제합니다. (이전에 설치 경험이 없으면 이 단계는 건너뜁시다)
1
sudo apt-get remove docker docker-engine docker.io containerd runc
  1. apt에 Docker repository를 추가해줍니다. 이 과정을 거치면 apt install docker~~~ 로 쉽게 받을 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sudo apt-get update

sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  1. Docker engine을 설치해줍니다.
1
2
3
sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io
  1. Docker의 Hello world를 실행해서 잘 설치되었는지 확인합니다.
1
sudo docker run hello-world

다음과 같은 화면이 나오면 성공입니다.

  1. (선택) Docker를 쓸 때는 항상 sudo docker...로 써야합니다. 매번 sudo 쳐주는것도 귀찮고, 아니면 CI 등 자동화 스크립트를 쓸 때 비밀번호 쳐주기 귀찮다면 sudo를 떼버립시다. 한번 로그아웃하고 다시 로그인해야지 적용됩니다.
1
2
3
4
5
6
7
sudo groupadd docker

sudo usermod -aG docker $USER

newgrp docker

docker run hello-world

 


Clean Ubuntu 설치 방법

엄청 간단합니다.

1
2
3
sudo docker run -dit ubuntu:latest

sudo docker images

 


Image / Container 사용법

기본적인 Docker 사용을 위해서는 image와 container의 개념을 이해해야합니다.

  • Image
    • 붕어빵을 찍어내는 빵판과 같습니다.
    • 어떤 환경을 만들어낼지에 대한 instruction이 들어가있다고 보면 됩니다.
  • Container
    • 빵판에 찍혀나온 빵들과 같습니다.
    • Image에 명시된 instruction을 기반으로 만들어진 환경입니다.
    • 하나의 image 기반으로 여러개의 container를 만들 수 있습니다.
      • 예를 들어, clean ubuntu 20.04의 이미지로, 100개의 clean ubuntu 20.04 환경을 만들어 낼 수 있습니다. 각각 다른 라이브러리를 깔아서 실험을 할 수 있겠죠.

Image 확인하기

현재 내 Docker에 설정이 된 image들은 다음과 같은 커맨드로 확인할 수 있습니다

1
sudo docker images

Container 확인하기

현재 내 Docker에서 만든 container들은 다음과 같은 커맨드로 확인할 수 있습니다.

1
sudo docker ps -a

Container 지우기

우리가 앞으로 딱히 hello-world container를 더 쓸 것 같지 않습니다. 지워주도록 하겠습니다. 지울때는 container ID로 지워주거나, names로 지워주면 됩니다. sad_faradayclever_meitner 같은 것들이 name인데, 이는 우리가 container 생성 시 이름을 지어주지 않았기 때문에 docker가 임의로 지어준 것입니다.

1
2
3
4
5
6
7
8
# NAMES 로 지우기
sudo docker rm sad_farady
sudo docker rm clever_meitner

# CONTAINER ID로 지우기

sudo docker rm ceb8a255cab8
sudo docker rm 425b90a1a354

그러면 container가 지워진 걸 볼 수 있습니다

Image 지우기

hello-world 이미지도 지워보겠습니다.

Image를 지울때는 remove-image를 줄인 rmi를 사용합니다.

1
sudo docker rmi d1165f221234

Container 생성하기 (CLI)

이제 image로부터 Container를 생성해보겠습니다.

가장 쉬운 방법은 아까의 Clean Ubuntu 설치 방법 섹션의 run 커맨드입니다. 여기에 --name flag를 주어서 container에 이름을 부여해줄 수 있습니다.

1
sudo docker run --name clean_ubuntu -dit ubuntu:latest

Clean Ubuntu 20.04 실행하기 (i.e. Container 실행하기)

sudo docker ps -a로 container가 생성된 것을 확인할 수 있습니다. recursive_wing과 같이 Docker에서 임의로 정한 구린 이름 말고, 내가 직접 지정한 좋은 이름으로 생성되었습니다.

이제 생성된 container를 실행해서 실제로 Clean Ubuntu 환경으로 들어가보겠습니다.

1
2
3
4
5
# NAME으로 접근하기
sudo docker attach clean_ubuntu

# CONTAINER ID로 접근하기
sudo docker attach 3cff7e522c6f

아래 사진과 같이 Clean Ubuntu 20.04 환경에 root 권한으로 /에 들어왔습니다. exit을 사용해서 container를 빠져나올 수 있습니다.

이제 여기서 git clone ...으로 소스코드를 땡겨오고 빌드를 하고 여러가지 실험을 할 수 있습니다. 실험이 끝나면 container를 삭제해주면 됩니다.

Container 생성하기 (Dockerfile)

근데 Clean Ubuntu이다보니 너무 clean 합니다.

git, cmake, build-essential, gcc, wget, curl 뭐 다 새로 깔아줘야하는데, 실험을 위해 container를 만들 때 마다 매번 커맨드를 쳐주기 너무 귀찮습니다.

걱정하지마세요.

이 반복적인 작업을 자동화하는 방법, Dockerfile이 있습니다.

우선 container에서 빠져나온 후, 원하는 위치에 Dockerfile을 만들고 (i.e. touch Dockerfile) 저장해봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Ubuntu:latest는 20.04를 의미합니다.
FROM ubuntu:latest

# 작성자 이름입니다
MAINTAINER changh95

# apt로 패키지 받을 때 interative하게 사용하는 기능들을 끕니다. Docker에서 로그를 넘길 때 문제가 생길 수 있으므로 이 옵션은 필수입니다!
ARG DEBIAN_FRONTEND=noninteractive

# apt-get update와 기본적인 패키지들을 깔아줍니다.
RUN apt-get -y update &&\
apt-get -y install build-essential cmake git sudo wget &&\
apt-get -y install python3 python3-pip &&\
apt-get autoclean

# 원하는 프로젝트의 소스코드를 clone해와서 빌드 스크립트를 실행합니다
RUN mkdir cv-project &&\
cd cv-project &&\
git clone https://github.com/changh95/cpp-cv-project-template.git . &&\
git checkout "develop" &&\
pip3 install pyyaml &&\
./setup.py

그리고 sudo docker build ... 커맨드를 Dockerfile에 명시된 instruction을 따라 이미지를 생성합니다. 여러가지 flag도 사용하는데, 함께 설명합니다.

1
2
3
4
5
6
7
sudo docker build --no-cache --force-rm -f Dockerfile -t cvproject:base .

# --no-cache 는 이전 빌드에서 생성된 캐시를 사용하지 않겠다는겁니다. 생성시간을 줄이기 위해서 캐시를 사용하는 경우도 있는데, 처음 사용하는 사람은 실수하기 쉬우니 일단 no cache 옵션을 씁시다.
# --force-rm 은 빌드하다가 터지면 이미지/컨테이너를 지워줍니다.
# -f 는 사용할 Dockerfile의 이름을 명시합니다
# -t 는 우리가 만드는 이미지의 이름 + 태그를 명시합니다
# . 는 Dockerfile의 경로를 명시합니다.

 


고수처럼 CI + Docker 사용하기

우리는 이제 Docker의 기본적인 사용법을 알고있습니다.

Docker 이미지를 만들거나 Dockerfile을 만들어서 팀원들에게 공유하고, 모두가 같은 환경에서 코드를 짤 수 있게 되었죠.

하지만 아쉬운 점이라면… 로컬에서밖에 못쓴다는거죠.

그에 비해, 팀원들과의 협업은 GitHub나 GitLab 같은 온라인 Git 서비스에서 이뤄지는 경우가 대부분입니다.

GitHub에 Docker를 연결할 수 있는 방법이 있을까요?

 

여기서부터는 CI (Continuous Integration)을 개발한다고 볼 수 있습니다.

가장 간단한 CI 개념으로, Pull Request로 코드가 들어오면 자동으로 Docker build를 하고 성공/실패 결과를 표시할 수 있습니다.

간단하게 GitHub Actions를 사용해봅시다.

GitHub Actions 활성화하기

GitHub 레포지토리에 들어가서, Actions 탭을 눌러 들어가면 아래와 같은 화면이 나옵니다.

‘set up a workflow yourself’를 누르면 .github/workflows/main.yml이라는 파일을 생성하며, 해당 파일이 가질 내용을 보여줍니다.

일단 이 파일을 저장해줍니다.

이제 우리는 1. MS Azure 클라우드 서버에서 빌드를 할지, 2. 개인 장비에서 빌드를 할지 선택해야합니다.

 

개인장비로 빌드하기 (Self-hosted runners)

Self-hosted runners 빌드 방법은 다음과 같은 경우에 좋습니다.

  • 많은 코어로 빌드를 빠르게 하고 싶을 때
  • 다수의 코어 / 높은 메모리 사용량을 요구할 때
    • e.g. SLAM 벤치마크…
  • GitHub Actions 제한시간으로 부족할 때
    • OpenCV + Ceres를 Debug/Release 모드로 빌드 할 때, GitHub Action의 무료제공 서버로는 제한시간 내 빌드할 수 없었습니다.

Self-hosted runner를 설정하는 방법은 이 링크를 참조해주세요.

파일의 내용을 다음과 같이 바꿔줍니다. (cpp-cv-project-template는 바꿔도 됩니다!)

아래 스크립트는 main / development 브랜치에 push하거나 Pull request가 생기는 상황에서 GitHub Actions를 통해 Self-hosted runner에 Dockerfile 기반 빌드를 지시하는 커맨드입니다.

우리가 보았던 커맨드는 왠만하면 다 들어가있습니다.

추가로, Docker 빌드가 실패했을 경우 실패한 image / container를 삭제해주는 기능이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
name: CI

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

jobs:
build:
runs-on: self-hosted

steps:
- uses: actions/checkout@v2
- name: Build Docker image
run: |
echo "=== Build start==="
echo ${{ github.head_ref }}

cd Dockerfiles
docker build --force-rm --no-cache -f build.dockerfile -t "cpp-cv-project-template:base" --build-arg BRANCH=${{ github.head_ref }} .
echo "=== Build finished==="

- name: Clean up Docker image if build fails
if: failure()
run: |
echo "=== Remove failed image start==="
docker rmi -f $(docker images -f "dangling=true" -q)
docker images
echo "=== Removal finished==="

레포지토리에서 branch protetion rule 까지 설정해주고 나면, 빌드가 성공한 경우에만 merge가 가능하게 설정을 바꿀 수 있습니다.

제가 좋아하는 설정은 주로 1. 최소 N명 이상의 코드 리뷰에서 approve를 받아야함, 2. 모든 status check를 통과해야함 (e.g. 빌드, 유닛테스트, 린터/포맷터, 벤치마크)입니다.

아래는 가장 간단한 빌드 테스트에 대한 status check가 적용된 모습입니다.

빌드가 진행되는 동안에는 다음과 같이 확인할 수 있습니다 (원래 빌드 진행 중 로그도 볼 수 있는데, 1시간동안 로그가 많이 쌓여서 로딩하는데 시간이 좀 걸리는 것 같습니다)

 

MS Azure 클라우드 서버로 빌드하기

MS Azure 클라우드의 경우 2코어 7GB 램의 옵션을 가지고 있는데, Public repository의 경우 제한시간동안 공짜로 사용할 수 있습니다.

이 방법도 Self-hosted runner 방법과 크게 다르지 않으며, 대신 아래의 옵션들 중에 선택해서 넣으면 됩니다.

  • Windows server: windows-latest (2019) 또는 windows-2016
  • Ubuntu: ubuntu-latest (20.04) 또는 ubuntu-18.04
  • Mac: macos-11 (Big Sur) 또는 macos-latest (Catalina)

근데 이 방법은 사실상 위의 환경들이 Docker로 만들어지는거라… Clean 환경이 필요한거라면 또 다시 Docker로 만들 필요는 굳이 없습니다.

Pre-built Docker image를 쓰고싶은 거라면, DockerHub에 올려놓고, GitHub Actions에서 Docker image를 긁어온다음에 프로그램만 빌드해서 실행하는 것도 좋습니다.

1
2
3
4
5
6
...

jobs:
build:
runs-on: ubuntu-latest
...