고백하자면, Honeybadger에서 일한 4년 동안 제 개발 환경은 크게 변하지 않았습니다. 하지만 그동안 Honeybadger는 상당히 정교해졌고, 정상 동작을 위해 약 10개의 서비스에 의존하게 되었습니다.
그래서 우리는 로컬 개발 환경에 Docker를 도입하기 시작했습니다. 모든 개발자가 Postgres, Cassandra, Memcached 등을 일일이 설치하는 대신, docker-compose로 미리 만들어진 환경을 손쉽게 띄울 수 있습니다. 정말 훌륭한 방식입니다.
그러다 자연스럽게 이런 궁금증이 생겼습니다. 과연 Dockerized 애플리케이션을 얼마나 쉽게 배포할 수 있을까?
Docker로 배포하기
Docker에는 docker-machine과 docker-swarm 같은 유틸리티가 있어 배포를 간편하게 해준다고 알려져 있습니다. 하지만 아직까지는 기대에 부응하지 못하고 있습니다. 이들의 한계에 대해 자세히 알고 싶다면 최근 Hacker News 게시물을 참고해 보세요.
그래서 저는 Amazon의 ECS(EC2 Container Service)에 집중하기로 했습니다.
ECS란 무엇인가?
ECS는 docker-compose와 docker-swarm을 결합한 듯한, 성숙하고 프로덕션 환경에 바로 적용 가능한 서비스입니다.
ECS를 사용하면 "내 웹 앱을 세 개 실행해줘"라고 지시하기만 하면, EC2 인스턴스 클러스터 내부에 적절한 Docker 컨테이너를 지능적으로 띄워줍니다.
컨테이너 간 요청 로드 밸런싱도 가능하고, 부하 상황에 따라 클러스터 규모를 확장하거나 축소할 수도 있습니다.
Dockerized Sinatra 앱 만들기
여기 간단한 Sinatra 앱이 있습니다. 현재 시간을 출력하기 때문에 캐시된 버전이 표시되는지 바로 알 수 있습니다. 또한 앱이 실행 중인 컴퓨터의 이름도 함께 출력합니다. 이후에 이 앱을 실행하는 여러 서버를 띄우고 로드 밸런서로 요청을 분산할 예정인데, 호스트명을 반환함으로써 어떤 서버가 특정 요청을 처리했는지 확인할 수 있습니다.
앱 코드
지금 소개할 Dockerized Sinatra 앱은 tcnksm-sample/docker-sinatra를 대폭 수정한 버전입니다.
require 'sinatra'
require 'sinatra/base'
class App < Sinatra::Base
get '/' do
"Hello from sinatra! The time is #{ Time.now.to_i } on #{ `hostname` }!"
end
end
Gemfile은 다음과 같습니다:
# Gemfile
source 'https://rubygems.org'
gem 'sinatra'
gem 'thin'
...그리고 config.ru 파일입니다:
$:.unshift(File.dirname(__FILE__))
require 'app'
run App
Dockerfile
이 앱의 Docker 이미지를 생성하려면 Dockerfile이 필요합니다. Dockerfile은 앱 디렉토리를 생성하고, 필요한 파일들을 복사한 뒤, 80번 포트에서 웹 서버를 실행합니다.
FROM ruby:2.3.1-slim
RUN apt-get update -qq && apt-get install -y build-essential
ENV APP_ROOT /var/www/docker-sinatra
RUN mkdir -p $APP_ROOT
WORKDIR $APP_ROOT
ADD Gemfile* $APP_ROOT/
RUN bundle install
ADD . $APP_ROOT
EXPOSE 80
CMD ["bundle", "exec", "rackup", "config.ru", "-p", "80", "-s", "thin", "-o", "0.0.0.0"]
이미지 빌드 및 실행
먼저, 현재 디렉토리의 Dockerfile로 이미지를 빌드하도록 Docker에 지시합니다. 그다음 컨테이너의 80번 포트를 localhost:4000에 매핑하여 실행합니다.
docker build -t docker-sinatra .
docker run -p 4000:80 docker-sinatra
정상적으로 작동하는지 확인하려면 웹 브라우저에서 localhost:4000을 열어보세요. 다음과 같은 화면이 보일 것입니다:

ECS에 배포하기
이제 ECS에 배포해 보겠습니다. 설정 마법사(setup wizard)를 활용할 겁니다. 이것이 편법이라고 생각하시나요? 저는 상관없습니다. :) ECS 콘솔로 이동하세요.

"Get Started(시작하기)"를 클릭합니다. 그다음 화면에서 "Continue(계속)"를 클릭합니다.

Docker 레지스트리 생성하기
일반적으로 개발 환경에서 프로덕션 서버로 Docker 이미지를 직접 업로드하지 않습니다. 대신 dockerhub 같은 Docker 레지스트리에 이미지를 전송하고, 배포 시점에 서버가 해당 이미지를 pull 받아옵니다. Amazon은 프라이빗 Docker 레지스트리를 제공합니다. 반드시 사용해야 하는 것은 아니지만, 여기서는 이를 활용해 보겠습니다.
레지스트리 이름을 "docker-sinatra"로 지정하겠습니다:

레지스트리에 이미지 푸시하기
그러면 이미지를 빌드하고 레지스트리에 푸시하기 위한 명령어 목록이 화면에 표시됩니다.
참고로 처음 시도했을 때, 브라우저의 광고 차단기가 명령어가 올바르게 표시되는 것을 막았습니다. 의외의 변수죠.

태스크 정의(Task Definition) 생성하기
AWS는 새롭고 혼란스러운 용어를 만드는 것을 좋아합니다. "태스크 정의(task definition)"는 단순히 함께 실행되어야 할 컨테이너들의 목록입니다. Procfile이나 docker-compose 설정과 비슷한 개념이라고 이해하면 됩니다.
예를 들어, 애플리케이션이 Nginx 컨테이너 하나, Unicorn 컨테이너 하나, Sidekiq 컨테이너 하나를 실행한다면, 이 세 가지가 모두 하나의 "태스크 정의"에 포함될 수 있습니다.
우리 앱은 훨씬 간단합니다. 컨테이너가 하나뿐이므로 설정도 최소화됩니다.
- 올바른 이미지가 레지스트리에서 pull 되도록 확인합니다.
- 컨테이너의 80번 포트를 컨테이너가 실행되는 EC2 인스턴스의 80번 포트에 매핑합니다.

서비스(Service) 생성하기
또 혼란스러운 용어가 등장합니다! "서비스(service)" 설정에서는 몇 개의 "태스크"(즉, 앱의 복제본)를 실행할지, 그리고 어떻게 로드 밸런싱할지 지정할 수 있습니다.
여기서는 앱을 세 개 실행하고, 80번 포트에서 로드 밸런싱을 하도록 설정하겠습니다.

클러스터 구성하기
클러스터는 Amazon의 ECS 소프트웨어가 실행되는 일반적인 EC2 인스턴스들의 집합입니다. 다른 EC2 인스턴스와 마찬가지로 원하는 어떤 작업이든 수행할 수 있습니다. 여기서는 t2.micro 인스턴스 세 개를 지정했습니다.
몇 번의 확인 단계와 약 5분간의 대기 끝에, 모든 준비가 완료되었습니다.
서비스가 정상 동작하면 다음과 같은 화면이 표시됩니다:

앱 테스트하기
로드 밸런서 이름을 클릭하면 상세 정보를 확인할 수 있습니다. 거기서 퍼블릭 도메인 이름을 찾을 수 있는데, 이를 브라우저에 입력하면 샘플 앱이 표시됩니다.

몇 번 새로고침해 보면 호스트명이 변경되는 것을 확인할 수 있습니다. 로드 밸런서가 세 개의 호스트에 요청을 고르게 분산하고 있기 때문입니다.
리소스 정리하기
ECS 마법사가 방금 AWS 계정에 수많은 리소스를 생성했습니다. 이를 그대로 두고 싶지 않으실 겁니다. 계정이 어수선해질 뿐만 아니라 불필요한 비용이 발생할 수도 있습니다.
다행히 ECS 마법사는 CloudFormation을 사용하여 모든 리소스를 생성합니다. 따라서 CloudFormation 스택만 삭제하면 생성된 리소스를 한 번에 깔끔하게 정리할 수 있습니다.
