Docker에 관해서는 아래의 읽을거리를 참고하세요.

ROR Lab <Docker를 이용한 손쉬운 레일스 배포> 세미나: https://www.facebook.com/naverd2/posts/505653179563380

<가장 빨리 만나는 Docker> : https://pyrasis.com/book/DockerForTheReallyImpatient/Chapter01


참고로 macOS Sierra 환경에서 작업하였다. 리눅스 환경에서는 docker 명령을 실행할때 항상 sudo를 추가해야 한다. 번거롭다면 관리자 그룹에 추가시켜주자.


배포하기위한 'rails-new-docker'라는 이름의 간단한 앱을 만든다. static page로 'hello docker'를 띄워보자.

$ rails new rails-new-docker

$ cd rails-new-docker

$ vi Gemfile

...

# Use high_voltage for static page

gem 'high_voltage', '~> 3.0'

...

$ bundle install

$ mkdir app/views/pages

$ vi app/views/pages/home.html.erb

<h1>hello docker!!</h1>


remote 저장소에 소스를 올린다. docker 컨테이너에 로컬의 소스를 복사해서 넣을 수도 있겠으나, 배포의 목적으로 만드는 것이기 때문에 다른 호스트에서 컨테이너를 실행할때 소스코드를 받아오기 위해 remote 저장소에 올린다.

$ git init

$ git add .

$ git commit -m 'first commit'

$ git remote add origin [리모드 저장소 주소]

$ git push origin master


Docker 이미지를 만들기 위한 Dockerfile을 생성한다.

Dockerfile

FROM ubuntu
MAINTAINER stardust(handhee7@gmail.com)

# 아래는 자신의 앱에 관한 필요한 설정을 만들면 된다.
# Run upgrades
RUN apt-get update

# Install basic packages
RUN apt-get -qq -y install git curl build-essential openssl libssl-dev python-software-properties python g++ make
RUN apt-get -qq -y install libsqlite3-dev
RUN apt-get -qq -y install nodejs

# Install Ruby
RUN apt-get -qq -y install ruby-full
RUN gem install bundler --no-ri --no-rdoc
RUN gem install foreman compass

# Install rails-new-docker
WORKDIR /app
RUN git clone https://github.com/Stardust-kr/rails-new-docker.git /app
RUN bundle install --without development test

# Run rails-new-docker
ENV SECRET_KEY_BASE dockerkeybase
ENV RAILS_ENV production
EXPOSE 5959
CMD foreman start -f Procfile

foreman을 활용하여 웹 서버시 수행될 명령을 Procfile에 미리 기록하여 사용할 수 있다. 대신 쉘 스크립트를 활용할 수 있다.


Procfile

web: bundle exec rails server -p 5959


rails_12factor 젬을 추가하여 로그를 표준출력으로 나오게 한다. docker logs [CONTAINER NAME OR ID]를 입력했을 때 앱에 관한 로그를 볼 수 있다.


Gemfile

# Use rails_12factor for stdout logs
gem 'rails_12factor'


다시 리모트 저장소에 푸시한다. Dockerfile은 코드에 포함될 필요는 없지만 동일한 앱을 구성하려는 사용자를 위해 코드의 버전과 똑같이 관리될 수 있도록 추가해주었다.


이제 이미지를 만들자. 네임스페이스/이름:태그 형태로 생성할 수 있다.  이미지가 생성되면 해당 이미지를 이용하여 컨테이너를 실행시켜보자. 그리고 실행된 이미지를 확인한다.

$ docker build -t stardustkr/rails-new-docker:0.1 .

$ docker run --name v0.1 -d -p 5959:5959 stardustkr/rails-new-docker:0.1

$ docker ps -l

$ docker logs v0.1


브라우저를 열고 localhost:5959/pages/home에 접속해보자.

실제 사용하는 앱은 데이터베이스를 필요로 할 것이다. 하지만 앱과 데이터베이스가 동시에 존재하는 컨테이너는 만들지 않을것이다. 왜냐하면 새로운 버전의 앱이 만들어져 다시 새로운 이미지를 만들어 배포 한다 가정 했을 때 기존 컨테이너 내부의 데이터베이스는 날아가기 때문이다.


필자는 아마존 RDS를 연결할 것이다. 로컬 호스트의 DB에 연결하든, 다른 외부의 DB의 연결하든 사용자의 마음일 것이다.

앱에 db를 필요로하는 scaffold를 생성하고, docker 이미지에 mysql관련 패키지를 추가로 설치한 다음 외부 db와 연결하여 띄워보자.


$ rails g scaffold post title content

$ rake db:migrate

$ vi Gemfile

...

# Use mysql2

gem 'mysql2'

...

$ vi Dockerfile

...

# Install Mysql

ENV DEBIAN_FRONTEND noninteractive

RUN echo "mysql-client mysql-client/root_password password" | debconf-set-selections

RUN echo "mysql-client mysql-client/root_password_again password" | debconf-set-selections

RUN apt-get install -qq -y mysql-client libmysqlclient-dev

...

$ docker build -t stardustkr/rails-new-docker:0.2 .

$ docker run -i -t -e DATABASE_URL="mysql2://[Mysql 계정명]:[Mysql 계정비밀번호]@[Mysql 서버 주소]/[DB명]" stardustkr/rails-new-docker:0.2 bundle exec rake db:reset

$ docker run --name v0.2 -d -p 5959:5959 -e DATABASE_URL="mysql2://[Mysql 계정명]:[Mysql 계정비밀번호]@[Mysql 서버 주소]/[DB명]" stardustkr/rails-new-docker

$ docker ps -l


만들어진 posts 페이지로 접속해보자.

localhost:5959/posts

rails 앱은 동작하는데 에러 페이지를 띄운다면 로그를 확인해보자.

$ docker logs v0.2


이제 동일한 앱을 서버에 배포해보자. 개인 서버여도 좋고 아마존 등 클라우드 서비스여도 좋다. 여기서는 아마존 ec2에 배포할 것이다.

서버를 설정하기 전에 이미지를 먼저 배포한다. Docker hub에 배포하여 사용하겠다. Docker hub: https://hub.docker.com

github처럼 repository를 만들자. 앱과 똑같은 이름으로 만들었다. 

로컬에서 로그인 하고 push를 하자.

$ docker login --username=[Docker hub 사용자 이름]

$ docker push stardustkr/rails-new-docker:0.2

푸시 끝


ec2는 우분투 이미지로 생성하였다. ec2에 ssh연결하여 docker를 설치하자.

# sudo apt-get update

# sudo apt-get install docker.io

# sudo ln -sf /usr/bin/docker.io /usr/local/bin/docker


이미지를 pull 하고 실행시켜주자. 이미 로컬에서 테스트하면서 db를 초기화 하였으므로 초기화 코드는 없이 진행한다.

# sudo docker pull stardustkr/rails-new-docker:0.2

# sudo docker run --name v0.2 -d -p 80:5959 -e DATABASE_URL="mysql2://[Mysql 계정명]:[Mysql 계정비밀번호]@[Mysql 서버 주소]/[DB명]" stardustkr/rails-new-docker


이제 인스턴스의 publid dns로 접속해보자.

http://[인스턴스 public dns]/hosts

아래와 같이 로컬에서 작업환 환경과 동일하게 표시됨을 알 수 있다. DB를 똑같은것을 적용하였으므로 로컬에서 생성했던 글이 동일하게 표시됨을 알 수 있다.

마지막으로 기존에 만들었던 미용실 프로젝트를 docker로 배포해 볼 것이다.

Gemfile에 rails_12factor를 추가하고 Dockerfile과 Procfile을 추가한다. 위 과정을 동일하게 수행하여 아마존 ec2 인스턴스에 올려보았다.

아래 인스턴스에서 그 결과를 확인 할 수 있다.


http://ec2-52-79-125-65.ap-northeast-2.compute.amazonaws.com


이미지는 docker pull stardustkr/charmbithair:0.1로 받을 수 있다.

github: https://github.com/Stardust-kr/charmbitHair.git

0. 서버 사양

CPU: Intel G530
Ram: 2GB
OS: Ubuntu Server 16.04.1 LTS, https://www.ubuntu.com/download/server
Web Server: Nginx
App Server: Unicorn


1. 서버 환경 구성

  먼저 Ubuntu 설치를 위한 부팅USB를 만들었다. Win32 Disk Manager 라는 프로그램을 이용하여 부팅USB를 만들었다. 

  (참고: http://sourceforge.net/projects/win32diskimager/)

  Ubuntu 설치 과정은 다음의 블로그를 참고하였다. http://goproprada.tistory.com/260 마지막 과정에서 추가 소프트웨어 설치는 Standard system utilities, PostgreSQL database, OpenSSH server를 선택하였다.


  설치가 끝난 후 자신이 생성한 계정으로 접속하고 IP주소를 확인해보자.

$ ifconfig


  이제 동일한 내부 네트워크에 있는 개발환경의 PC에서 접속하자. 윈도우즈 OS라면 putty를 설치해서 ssh 연결을 하자. 리눅스와 맥은 터미널을 열어 ssh 연결을 한다.

$ ssh [사용자 계정명]@[IP 주소]


  이제 웹 서버 배포를 위해 필요한 패키지를 설치한다. 초보자를 위한 레일즈가이드의 글과 여러 블로그의 글을 참고하여 패키지를 설치하고 설정하였다.

 - 우분투 16.04 서버 세팅하기: https://rorlab.gitbooks.io/railsguidebook/content/appendices/ubuntu16server.html

 - 운영서버 환경구축: https://rorlab.gitbooks.io/railsguidebook/content/contents/pro_env.html

 - 우분투 MySQL 설정: http://webdir.tistory.com/217

 - 우분투 방화벽(UFW) 설정: http://webdir.tistory.com/206


#  배포용 계정 생성

$ sudo adduser deployer

$ sudo addgroup admin

$ sudo usermod -aG admin deployer

$ sudo visudo

...

# Members of the admin group may gain root privileges

%admin ALL=(ALL) NOPASSWD: ALL

...


# build-essential 설치

$ sudo apt-get -y install git curl build-essential openssl libssl-dev libreadline-dev python-software-properties python g++ make


# Nginx 서버 설치

$ sudo apt-get install -y nginx


# mysql 설치

$ sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev

$ sudo vi /etc/mysql/my.cnf

[client]

default-character-set = utf8


[mysqld]

character-set-client-handshake=FALSE

init_connect="SET collation_connection = utf8_general_ci"

init_connect="SET NAMES utf8"

character-set-server = utf8 

collation-server = utf8_general_ci


[mysqldump]

default-character-set = utf8


[mysql]

default-character-set = utf8

$ mysql -u root -p

mysql> grant all privileges on *.* to deployer@localhost identified by 'password';  # deployer 계정 생성

$ sudo service mysql restart


# ImageMagick 설치

$ sudo apt-get -y install libmagickwand-dev imagemagick


# Nodejs 설치
$ sudo apt-get -y install nodejs

# sqlite3 설치
$ sudo apt-get install sqlite3 libsqlite-dev

# 방화벽 설정
$ sudo ufw enable
$ less /etc/services  # 미리 정의된 포트 목록이 출력된다. 아래 목록 외에 필요한 것을 찾아서 추가하자.
$ sudo ufw dhcpv6-client
$ sudo ufw ssh
$ sudo ufw http
$ sudo ufw https


2. 앱 설정

  앱 설정을 시작하기 전에 서버에 ssh 연결을 시도할 때 매번 비밀번호를 입력하는 번거로움을 없애기 위해 다음과 같은 과정을 수행한다.

$ ssh-copy-id -i ~/.ssh/id_rsa [사용자 계정명]@[ip 주소]


  서버 설정을 끝내고 이제 앱에서 필요한 설정을 해야한다. gemfile과 capfile을 수정하고, 배포 관련 설정을 해야한다. 관련 설정은 아래의 사이트를 참고하였다.

 - Capistrano 3로 배포하기 – 2015 업데이트: https://withrails.com/2015/05/25/capistrano-3로-배포하기-2015-업데이트/


Gemfile

# 추가
gem 'mysql2'
gem 'unicorn'

gem 'capistrano-rails', group: :development
gem 'capistrano-rbenv' # required
gem 'capistrano-rbenv-install'
gem 'capistrano-unicorn-nginx'
gem 'capistrano-upload-config'
gem 'capistrano-safe-deploy-to'
gem 'capistrano-ssh-doctor'
gem 'capistrano-rails-console'
gem 'capistrano-rails-collection'
gem 'capistrano-rails-tail-log'
gem 'capistrano-faster-assets'

$ bundle install

$ cap install

# 터미널에 출력되는 내용

mkdir -p config/deploy

create config/deploy.rb

create config/deploy/staging.rb

create config/deploy/production.rb

mkdir -p lib/capistrano/tasks

create Capfile

Capified


  capistrano를 설치하면 위와 같이 배포와 관련된 파일들이 프로젝트의 config 폴더 안에 생성된다. staging 서버 없이 바로 운영서버에 배포할 것이므로 관련 파일들만 설정한다.

 

Capfile

# 추가
require 'capistrano/bundler' # Rails needs Bundler, right?
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
 
require 'capistrano/rbenv'
require 'capistrano/rbenv_install'
require 'capistrano/unicorn_nginx'
require 'capistrano/faster_assets'
require 'capistrano/upload-config'
require 'capistrano/safe_deploy_to'
require 'capistrano/ssh_doctor'
require 'capistrano/rails/console'
require 'capistrano/rails/collection'
require 'capistrano/rails_tail_log'


config/deploy.rb

# 대괄호로 써있는 내용은 자신의 설정에 따라 바꿔준다.
lock "3.7.1"
 
set :application, '[application-name]'
set :repo_url, "git@github.com:[user-name]/#{fetch(:application)}.git"
set :deploy_to, "/home/[deployer-account ex: deployer]/apps/#{fetch(:application)}"
 
set :rbenv_type, :user # or :system, depends on your rbenv setup
set :rbenv_ruby, '[ruby-version]'
set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, %w{rake gem bundle ruby rails}
set :rbenv_roles, :all # default values
 
# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml')
set :config_files, fetch(:linked_files)
set :pty, true
set :keep_releases, 5
 
before 'deploy:check:linked_files', 'config:push'
 
namespace :deploy do
  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end
end


config/deploy/production.rb

server '[server-ip]', user: '[deploy-username]', roles: %w{web app db}
 
set :nginx_server_name, '[domain-name]'
set :unicorn_workers, 4


  버전관리에 포함되어서는 안되는 database.yml, secrets.ymlgitignore에 추가한다.

.gitignore

...
# Ignore config/database.yml, secrets.yml
config/database.yml
config/secrets.yml


$ cap production config:init

00:00 config:init

      Created: config/database.production.yml as empty file

      Created: config/secrets.production.yml as empty file


  위에서 추가된 파일은 버전관리에 포함되는 파일이기에 중요한 민감정보를 포함하지 않고 대신 시스템 환경변수로 대체한다. 시스템 환경변수를 서버에 직접 등록해 주어야 한다.

# 서버에 ssh 연결 후

$ sudo vi /etc/environment

...

DEPLOY_USERNAME=[deploy-username]

DEPLOY_PASSWORD=[deploy_password]

SECRET_KEY_BASE=[secret_key_base]

 다시 로컬로 돌아와 위에서 생성된 두 파일을 작성한다.


config/database.production.yml

# 추가
production:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: [database_name]
  pool: 5
  username: <%= ENV['DEPLOY_USERNAME'] %>
  password: <%= ENV['DEPLOY_PASSWORD'] %>
  host: localhost


config/secrets.production.yml

# 추가
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

git에 현재 수정된 내용을 커밋한다. 그리고 배포 명령을 수행한다.

$ git add .

$ git commit -m 'capistrano 배포를 위한 설정'

$ git push origin master


$ cap production setup

$ cap production rails:rake:db:setup

$ cap production deploy


  이제 배포가 완료되었다. 해당 ip로 접속해서 확인해보자.

 외부에서도 접속할 수 있도록 공유기 설정을 바꿔준다. 192.168.0.1로 접속해서 설정을 바꾼다. 필자의 경우에는 iptime공유기를 예로 들겠다. [관리도구] - [고급 설정] -  [NAT/라우터 관리] - [포트포워드 설정]에 들어가서 아래 그림과 같이 현재 사용하는 인터넷 ip의 80번 포트로 들어오는 요청을 현재 서버의 내부 ip로 포워딩 될 수 있도록 설정한다.

  [기본 설정] - [시스템 요약 정보] 에서 [외부 IP 주소]를 확인하고 브라우저에서 이 주소로 접속해보자.

  드디어 배포 끝!


* 여담: 서버를 처음 구성하고 배포하면서 많은 시행착오를 겪었다. 같은 리눅스인데도 데비안 계열의 ubuntu와 redhat 계열의 centos에서 환경 차이는 존재했다. 처음에는 centos를 사용했으나 ubuntu가 설정에서 좀 더 쉽다는 느낌을 받았고, 결정적으로 rails gem중에서 unicorn 앱 서버와 nginx 웹 서버를 설정해주는 'capistrano-unicorn-nginx' 젬이 ubuntu에서만 작동하는게 컸다.

  패키지를 설치하고, 내가 만든 앱에 배포 설정하는 작업까지는 무리없이 진행되나 처음 deploy 과정에서 엄청난 오류를 맞는다. mysql의 권한 설정이 문제가 되기도 하고, 시스템 환경변수가 제대로 설정되지 않았다거나, git push를 하지 않아서 오류를 내거나 하는 사소하면서도 정말 잘 찾아보지 않으면 스트레스 무지하게 받을만 한 문제를 발생시켰다.

  또한 deploy 과정이 문제없이 끝나도 막상 해당 ip로 접속해도 여전히 'Welcome to nginx' 화면만 띄우고 내가 원하는 페이지 화면을 보여주지 않아 당황하기도 한다. 자동으로 nginx와 앱 서버간의 설정을 해주는 gem의 도움을 받아 설정 할 수도 있지만, 그렇지 않은경우에는 직접 nginx.conf 파일을 작성하고 /etc/nginx/sites-enabled에 추가 시켜줘야 한다. We're sorry, but something went wrong. 을 띄운다면 다양한 문제가 존재하기에 특정할 수 없지만 나의 경우에는 SECRET_KEY_BASE 설정 실수로 발생한 것이었다.

  영어도 잘 못하는데 구글링하며 많이 애먹었다. 여전히 명확하게 어떻게 동작하는가 의문인 부분이 수도 없이 많지만 그럼에도 이 과정을 통해 약간이나마 서버에 대한 이해가 늘은 것 같다.

* 글 작성에 앞서, 이 글의 목적은 숙달자의 정보전달이 아니라 초심자의 기록 남기기에 가깝다. 이해가 부족한 상태에서 경험의 과정을 적으니, 미숙한 점이 많이 있을 것이므로 양해 부탁드린다. 또, 미숙함에 대한 지적은 대환영이지만 기본적으로 이 포스팅은 대부분 일방적인 기록 남기기에 가까우니 피드백이 적을 수 있다. 다만 필자뿐 아니라 같은 글을 보는 다른 독자들을 위해 또 하나의 필자가 되어 부연설명을 해 주신다면 감사할 따름이다.


작업 환경: macOS Sierra

0. Python 설치하기

터미널에서 Elastic Beanstalk 서비스를 cui 형태로 사용할 수 있게 하는 EB cli 설치를 위해 Python을 먼저 설치한다. 아마도 이 클라이언트가 Python을 통해 실행되는듯 하다.

$ brew install python

$ python -V

$ sudo pip install --upgrade pip

$ pip -V


1. EB cli 설치하기

AWS의 Elastic Beanstalk의 애플리케이션을 생성하기위해 EB cli를 설치하자.

$ sudo pip install awsebcli


2. git 생성하기

배포하길 원하는 앱에 대한 git을 생성하자.

지난 Ruby on Rails 설치때 만든 myFirstRailsApplication을 배포하겠다.

$ cd myFirstRailsApplication

$ git init

$ git add .

$ git commit -m "default rails project"


3. EB 생성하기

$ eb init

이 명령어를 실행하면 몇가지 입력을 받는다.

첫번째로는 EC2 인스턴스가 생성될 서버의 위치를 결정한다. 기본값으로 US West (Oregon)을 설정되어있다. 한국 서버를 이용하길 원하므로 10번을 선택해주자.

다음으로는 access-id와 secret-key를 입력받는데, AWS 계정 설정에서 IAM을 생성하여 얻을 수 있다.


그 외에 application name과 프로젝트 타입, 버전, ssh 설정 유무를 묻는다. 적절하게 설정하자.


4. 배포하기

$ eb create rails-beanstalk

위 명령어를 실행하면 자동으로 AWS의 Elastic Beanstalk에 기존에 설정한 이름으로 application이 생성되며 rails-beanstalk란 이름으로 environment가 생성된다. 이와 함께 자동으로 ec2 인스턴스를 생성해준다.


생성이 완료되면 다음 명령을 통해 애플리케이션에 접근해보자.

$ eb open


아래 명령어를 통해 오류의 원인을 확인하자.

$ eb logs

production environment 환경을 위한 secret_key_base에 대한 설정이 필요함을 로그로 확인 할 수 있다.

$ eb setenv SECRET_KEY_BASE=임의의 키 입력

$ eb open


그리고 다시 웹서버에 접속하면 웹 컨테이너는 정확하게 작동하나, rails 애플리케이션을 처음 실행할때 만나는 페이지가 아니라 페이지 없음을 띄워준다.

현재 애플리케이션에 생성된 컨트롤러가 존재하지 않으므로 어떠한 페이지도 띄워주지 않는 것이다.

컨트롤러를 생성하여 접속해보자.

$ rails generate controller WelcomPage welcome

$ vi app/views/welcome_page/welcome.html.erb

<h1>WelcomePage#welcome</h1>

<p>Find me in app/views/welcome_page/welcome.html.erb</p>

<p>This is my first Rails application on Elastic Beanstalk!</p>

$ config/routes.rb

Rails.application.routes.draw do

  get "welcome_page/welcome"

  root "welcome_page#welcome"


end

$ git add .

$ git commit -m "welcome page controller, view and route"

$ eb deploy

$ eb open


이제 root 페이지가 설정되어 원하는 화면이 표시됨을 확인 할 수 있다.



* 사용하고 난 후에 ec2 인스턴스를 정지시키자. 한 달 750시간을 초과하면 1년 무료의 기간이라 하더라도 요금이 부과된다. 특히 여러개의 인스턴스를 생성하는 경우 부과된다. 필자는 그것도 모르고 한 달동안 여러개의 인스턴스를 켜 두었다가 15000원 가량의 요금이 부과되었다. 다행이 환불 받을수 있었지만 이것 때문에 그날 밤에 잠을 못이뤘다.


참고자료

1. http://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/create_deploy_Ruby_rails.html

+ Recent posts