Docker 로 쾌적한 개발환경 구축하기
Ruby on Rails 개발환경을 Docker로 옮겨보았습니다.
개발자라면 입사 후, 제일 먼저 하는 일이 PC를 지급받고 개발환경을 세팅하는 일입니다.
특히, server-side program의 개발환경은 SDK뿐 아니라, 추가적인 package 들이 필요할 수도 있고, 데이터베이스, 웹서버 등이 추가로 요구됩니다.
또한 OS나 패키지 버전 이슈로 문제가 생길 수 있으니 최대한 실 서버 환경과 동일하게 맞추는 것이 중요하고, 실 서버의 변경이 있을 때 이를 잘 동기화하는 것까지도 신경 써야 합니다.
이런 생각을 하며 입사 후 개발환경을 정리하다 보면 아무 작업도 하지 못하는 하루, 이틀이 금방 지나버렸던 경험 다들 있을 거라고 생각합니다.
특히나 개발환경에 대한 문서가 없이 구전으로 내려오거나, 유지보수가 잘 안되어 설치 도중 여러 이슈를 마주치게 되는 경우 더욱 오랜 시간을 소요하게 됩니다.
마이리얼트립의 개발환경
마이리얼트립은 기본적으로 로컬환경으로 개발을 하고 있습니다. 재택근무가 가능한 마이리얼트립에서 모든개발자가 각자의 PC에 각각 통합개발환경을 가지고 있는 것은 매우 유용합니다. 이는 서비스 코드 뿐 아니라 서비스 작동에 필요한 대부분을 포함합니다.
그러나 마이리얼트립의 기술 스택에서 볼 수 있듯 서비스를 작동시키는데 필요한 요소가 매우 많기 때문에 하나하나 설치하고 업데이트 하는 것은 매우 비효율적이고 고통스러운 작업이 아닐 수 없습니다.
Virtual Machine
기존 마이리얼트립은 이러한 문제를 해결하기 위해 VMWare 와 Vagrant를 사용해서 구축해 둔 개발환경이 있었습니다. 저도 마찬가지로 처음 입사해서 Vagrant를 사용해서 환경설정을 마쳤는데, 하나의 가상머신에 Ruby on Rails, Node.js, PostgreSQL, MongoDB, Redis 등을 전부 가동하기 위해서 너무 큰 메모리를 차지하고 있었습니다.
가상머신은 기본적으로 자원을 동적으로 할당해 주지 않고, 고정적으로 할당해 두기 때문에 서버를 실행하기 위해서 큰 메모리를 점유하게 됩니다.
추가로 개발을 위해 IDE를 실행하고, 테스트를 위해 브라우저를 실행하고, 커뮤니케이션을 위해 메신저를 실행하면 개발용 노트북의 메모리 부족으로 종종 오류가 나기도 했습니다.
또한 패키지들의 설치, 버전 관리들을 셸 스크립트를 실행해서 설치하게 되어 있기 때문에 가시적이지 않았고, 만약 설치된 가상머신에 오류라도 생기면 가상머신 생성부터 설치까지 그 모든 과정을 다시 수행해야 하는 문제도 품고 있었습니다.
Docker for Development Environment
일반적으로 Docker를 개발환경에만 사용하기 위해 도입하려고 생각하지는 않을 것입니다.
하지만 Docker는 개발환경에서 일어날 수 있는 많은 문제를 해결해 줍니다.
도커는 Linux, Mac, Windows를 전부 지원하고 있기 때문에 어느 OS에서라도 Docker 와 Docker compose만 설치하면 더는 설치할 것은 없습니다.
또한 Docker container는 이미지 단위로 동작하기 때문에, 버전 변경이나 다른 의존성이 추가되었을 때 이미지만 추가하거나, 바꾸어주면 되기 때문에 실 서버와 동기를 맞추기도 쉽고, 유지보수 또한 편리합니다.
기본적으로 자원을 동적으로 할당해 주기 때문에 낭비되는 자원도 줄어듭니다.
오늘은, 실제로 Docker로 개발환경을 구축하기 위해서 어떤 작업을 해야 했는지 이야기해 보고, 이 과정에서 생겼던 다양한 이슈들과 저의 고민이 담긴 해결책에 대해 공유하고자 합니다.
Create the Base Image
제일 먼저 가장 기본이 될 Docker Image를 만들어야 합니다.
하나의 이미지만으로 개발환경을 구축할 수 있겠지만, 빌드 시간을 단축하기 위해 Base Image와 Service Image를 분리하였습니다. Base Image는 기본적으로 설치될 패키지들을 관리하며, Service Image는 소스 코드의 dependency 들을 관리합니다. Base Image는 마이리얼트립의 소스 코드나 의존성을 가지지 않기 때문에 public 하게 공유하여 다른 유사한 환경을 사용하는 개발자들에게 기여할 수도 있습니다.
마이리얼트립은 현재 Ruby on Rails + heroku 로 서비스되고 있습니다.
heroku 도 기본적으로 Ubuntu 를 기반으로 서비스 되고 있으니, base image 를 Ubuntu 로 설정했습니다. 이후 서버에 기본적으로 필요한 패키지들을 설치해 줍니다. 개발환경에서는 컨테이너에서 기타 유지관리에 필요한 유틸리티들을 추가로 설치해 주면 좋습니다.
base image Dockerfile 예
왜 rvm 을 안 쓰는지 의아해하실 분들이 있을 것 같습니다.
일단 docker의 기본 로그인 계정은 root 계정인데, rvm 은 root 계정에서 정상적으로 설치가 되지 않아서 문제가 발생합니다.
다른 다양한 방법을 통해서 rvm 을 설치할 수는 있겠지만, 버전별로 이미지를 만들어 관리하는 docker image의 특징과 rvm 은 맞지 않는다고 판단했고, 직접 루비를 소스 코드로 설치 하여 image별로 루비 버전을 관리하도록 구성했습니다.
Create the Service Image
서비스 이미지는 소스 코드가 직접 실행될 이미지입니다. 배포하는 이미지라고 한다면 소스 코드를 전부 COPY 한 후 빌드하도록 만들겠지만, 제가 만들 이미지는 dependency 만 이미지가 가지고 소스 코드는 로컬 디렉터리에서 sync 되는 이미지입니다. 이렇게 했을 때의 이점은 dependency의 변화가 없는 이상 이미지를 다시 빌드하지 않아도 된다는 점입니다.
Rails dependency를 설치하기 위해 .dockerignore 를 이용하여 Gemfile 을 제외한 모든 파일을 build 단계에서 무시하도록 하였습니다.
example of .dockerignore for rails dependencies
.dockerignore는 wildcard를 지원하기 때문에 Dockerfile 에서 일일이 하나씩 COPY 해 주지 않아도 간단하게 원하는 파일만 옮길 수 있습니다.
Docker-compose
Docker compose는 여러 Docker container들을 하나의 yaml 파일로 관리하게 도와줍니다.
docker compose를 이용하여 container 들을 실행하면, 하나의 network를 만들어 container 들을 묶고, 통신할 수 있게 되어 다양한 서비스를 붙이는 데에 매우 효과적입니다.
예를 들면, postgresql 서비스를 정의하면, docker compose network의 dns를통해 postgresql라는 도메인으로 해당 container에 접근할 수 있습니다.
서비스에서 특정 IP만 허용하는 옵션이 있을 때는, docker compose 의 network 옵션을 통해 container에 고정 IP를 발급하여 해결할 수 있습니다.
db data 등 consistency가 유지되어야 하는 데이터는 container 안에 데이터를 저장하면 container를 삭제하면 데이터도 삭제되기 때문에 volume 옵션을 통해 저장공간을 따로 지정해 주어야 합니다.
또한 같은 디렉터리를 공유해야 하는 경우도 (nginx public file serving 등) volume 설정을 이용하면 유용하게 사용할 수 있습니다.
로컬 디렉터리는 각 사용자 환경에 따라 다를 수 있으므로 고정적으로 사용하기보다는 .env 파일을 활용하면 docker-compose 파일을 수정하지 않고도 여러 환경을 지원할 수 있습니다.
Docker-Sync
열심히 docker-compose를 설정하고 컨테이너들을 실행하니 제가 만난 화면은 이런 오류들이었습니다.
보통의 리눅스 기반 환경에서는 위 정도의 내용으로 설정하면 구동에 큰 무리가 없이 작동됩니다.
하지만 windows/mac 환경에서 docker로 개발환경을 구축하기에는 치명적인 문제점이 있습니다. 바로 volume을 로컬에 bind 하면 file read/write에 매우 큰 속도 저하(일반적으로 약 60배가량)가 일어난다는 점입니다.
ruby on rails에서 자주 일어나는 file cache, view template rendering, rails asset pipeline compile 등 다양한 부분에서 치명적인 성능 문제를 일으키게 됩니다.
이 문제를 해결하기 위해서 docker for mac에서 지원하는 cache도 사용해 보았지만, 큰 개선을 보이지는 못했습니다.
여기서 해결책을 준 것이 바로 docker sync라는 third-party 프로그램이었습니다.
docker sync는 원하는 디렉터리를 container로 만들고 비동기적으로 sync를 해주며 이를 다른 container에서 mount 하여 사용하도록 제공하는 프로그램입니다.
이를 이용해서 해서 volume을 설정하고 나서, local volume에 견줄만한 read/write 속도를 얻을 수 있었습니다.
docker-sync 파일 예시
Debugging
ruby는 pry 라는 디버깅 툴이 있습니다. 이는 ruby를 실행시킨 콘솔에서 활성화되어 입력을 받을 수 있게 구성되는데, docker container를 실행하면 실행된 container에 대한 입/출력이 콘솔에 연결되지 않은 상태입니다.
이는, docker attach 라는 명령어를 통해서 해결할 수 있습니다.
docker-compose에 stdin_open 과 tty 옵션으로 container를 입력을 받을 수 있는 상태로 실행시킨 후 docker attach 를 통해 해당 container 에 콘솔을 붙이면 pry를 사용할 수 있는 환경이 완성됩니다.
docker-compose service 설정 예시
결론
처음 ruby on rails의 작동방식도 모르는 상태에서 docker를 사용해서 개발환경을 구축하려고 했을 때, 이렇게 많은 이슈가 있을 거라고 생각하지 못하고 뛰어들었습니다.
이 과정에서 docker로 개발환경을 구축하는 것이 과연 옳은 방법인가에 대한 고민도 재차 들었습니다.
하지만 container service를 사용하는 것은 시대의 흐름이고, 개발환경에서부터 이를 테스트하고, 익숙해지며 서비스 환경에도 빠르게 적용해 나갈 수 있는 발판을 마련했다고 생각합니다.
또한 더욱 단축된 개발환경 세팅 시간과 쾌적해진 메모리, 가시적인 유지보수 등으로 매우 편안해진 개발자들의 모습을 볼 수 있었습니다.
이 정도면 docker로 개발환경 구축하기, 하지 않을 이유가 있을까요?
쾌적한 환경에서 함께 마이리얼트립을 만들어가실 분들을 모시고 있습니다. 새로운 개발자는 언제나 환영입니다!