Shim Won
December 17, 2014 7:13 pm
capistrano, fig 배포하기
시작전에
버전정보는 이렇습니다.
- fig 1.0.1 : osx에서는 그냥 brew로 하면 됩니다. 1.1.0은 당분간 릴리즈 계획이 없다고 합니다.
- boot2docker 1.3.2
- docker 1.3.3 : 1.4.0은 업데이트 했다가 볼륨마운트 문제로 다시 롤백했습니다.
요구사항
아직 docker는 카피스트라노가 해결해주던 문제를 전부 해결해주진 못합니다. 이게 전부라고는 할 수 없지만, 제가 쓰던 기능 중에 아쉬웠던거만 나열하면..
- 간편한 롤백
- 멀티호스트 환경의 디플로이, 서버별 설정 관리
- 설정 파일 템플릿
- web서버의 멘테넌스 설정
- 원격 리스타트
카피스트라노만 쓰던때와 같게 설정하고 싶다면 욕심이겠지만, 명색이 디플로이 시스탬인데 zero downtime은 해야하지 않나 싶어서 일단 다음과 같은 제약 조건을 만들어 두었습니다.
- 각 컨테이너는 프로세스 하나만 띄울것.
cap deploy
를 할때- 어플리케이션 서버는 컨테이너를 재기동하거나 새로만들면 안됨.
- web, db는 그대로 두고 app만 재기동 할것.
- app 이미지에는 번들러를 포함시키지 말것.
bundle install
이 기존에 받아둔 gem을 케쉬하지 못합니다.- 이 구성에서
bundle install —deployment
는 마운트하는 볼륨에 저장되므로 환경이라기보단 source에 가깝습니다.
- 당분간은 스테이징에서만 확인할테니 싱글호스트여도 상관없음.
계획
- fig
- nginx, ruby, node, postgresql 등 외부 프로그램이 업데이트 할때만 사용.
- capistrano
- fig 실행, restart, deploy등등의 실행 담당
- 복잡한 스크립트 정리
fig의 재기동이 필요하면 맨테필요한거고, 아니면 zero downtime가능하다고 하면 다들 납득해 주지 않을까 싶었습니다. github에서 찾아보니
- capistrano-fig 디플로이 이후에 fig kill -> fig up을 해주는 구조. 게다가 카피스트라노 2문법이라니..
- capistrano-docker docker의 설정 관리용으로 카피스트라노를 쓰는 케이스. fig쓰니까 필요없..
Dockerfile
루비와 노드 bundler만 있으면 되니 매우 간단합니다.
FROM ruby:2.1.5
# Install Node
RUN apt-get update -y && \
apt-get install -y nodejs npm && \
rm -rf /var/lib/apt/lists/*
# Install Bundler
RUN echo 'gem: --no-rdoc --no-ri' >> /.gemrc && \
gem install bundler
fig.yml
fig는 환경별로 따로 준비해서 배포하기로 했습니다. 멀티호스트일때의 link문제는 아마도 나중엔 해결되지 않을까 싶어요.
일단 개발 환경용.
db:
image: "postgres:latest"
volumes:
- ~/.docker-volumes/<app-name>/db/:/var/lib/postgresql/data/
environment:
POSTGRES_PASSWORD: ""
POSTGRES_USER: "root"
ports:
- "5432:5432"
app:
build: .
command: ./start.sh
working_dir: /home/deployer/<app-name>/current
volumes:
- .:/home/deployer/<app-name>/current
environment:
RACK_ENV: "development"
ports:
- "3000:3000"
links:
- db
web:
image: "nginx:latest"
volumes:
- ./config/nginx:/etc/nginx/conf.d
- ~/.docker-volumes/<app-name>/web/certs:/etc/nginx/certs
- ~/.docker-volumes/<app-name>/web/log:/var/log/nginx
- ./public:/<app-name>/public
ports:
- "80:80"
expose:
- "80"
links:
- app
스테이징 환경용 파일은 /home/deployer/<app-name>에 둡니다. 만들어서 capistrano로 배포해도 되긴하는데, 지금은 그리 급하지 않으니 나중에 한가해 지면 하기로..
db:
image: "postgres:latest"
volumes:
- ~/.docker-volumes/<app-name>/db/:/var/lib/postgresql/data/
environment:
POSTGRES_PASSWORD: ""
POSTGRES_USER: "root"
ports:
- "5432:5432"
app:
build: /home/deployer/<app-name>/current
command: ./start.sh
working_dir: /home/deployer/<app-name>/current
volumes:
- /home/deployer/<app-name>:/home/deployer/<app-name>
environment:
- RACK_ENV=production
ports:
- "3000:3000"
links:
- db
web:
image: "nginx:latest"
volumes:
- ./current/config/nginx:/etc/nginx/conf.d
- ~/.docker-volumes/br/web/certs:/etc/nginx/certs
- ~/.docker-volumes/br/web/log:/var/log/nginx
- ./current/public:/<app-name>/public
ports:
- "80:80"
expose:
- "80"
links:
- app
start.sh
fig의 command가 ;
나 &&
를 재대로 해석안하고 인자의 일부로 해석해서 하나 만들었습니다.
#!/bin/bash
bundle install -j 4 --deployment
bundle exec puma -e $RACK_ENV -S tmp/pids/puma.state -p 3000
로컬 태스트
이제 fig up으로 기동 되는 것을 확인하고 restart를 시도할 차례입니다.
불행히도 fig run app ps
는 지금 실행 되는것과 별도의 컨테이너를 띄워서 restart커맨드를 날리지 못하네요.
fig exec가 있었으면 간단히
fig exec app bundle exec pumactl -S tmp/pids/puma.state restart
로 끝이었겠지만, 아직 없으므로
docker exec <app-name>_app_1 bundle exec pumactl -S tmp/pids/puma.state restart
으로 재시작에 성공했습니다.
뭐 컨테이너의 수가 늘었을때 모든 컨테이너에 보내도록 해야 하긴하겠지만 일단은 이걸로 구현은 가능하겠네요
여기까지했으면 남은건 스크립트 뿐..
스테이징 디플로이
capistrano tasks
start나 stop은 fig로 할생각이라 의도적으로 빼두었습니다.
namespace :puma do
%w[restart phased-restart stats status].each do |command|
desc "#{command} the application"
task command do
on roles(:app), in: :groups, limit: 3, wait: 10 do
within release_path do
execute :sudo, "docker exec <app_name>_app_1 bundle exec pumactl -S tmp/pids/puma.state #{command}"
end
end
end
end
end
up 에 -d옵션을 안붙여주면 콘솔이 안돌아오더군요.
namespace :fig do
desc "up the containers"
task :up do
on roles(:app), in: :groups, limit: 3, wait: 10 do
within deploy_to do
execute :sudo, "fig up -d"
end
end
end
[:kill, :build].each do |command|
desc "#{command} the containers"
task command do
on roles(:app), in: :groups, limit: 3, wait: 10 do
within deploy_to do
execute :sudo, "fig #{command}"
end
end
end
end
[:app, :web, :db].each do |role|
namespace role do
desc "up the containers"
task :up do
on roles(role), in: :groups, limit: 3, wait: 10 do
within deploy_to do
execute :sudo, "fig up -d #{role}"
end
end
end
[:kill, :build].each do |command|
desc "#{command} the containers"
task command do
on roles(role), in: :groups, limit: 3, wait: 10 do
within deploy_to do
execute :sudo, "fig #{command} #{role}"
end
end
end
end
end
end
end
디플로이 해보기
커밋하고
처음 디플로이 했을때는 컨테이너를 띄워야 합니다.
bundle exec cap staging deploy
bundle exec cap staging fig:up
이제 두번째 이후의 간단한 디플로이는
bundle exec cap staging deploy
bundle exec cap staging puma:restart
로 해결될 것 같습니다.
롤백도
bundle exec cap staging deploy:rollback
bundle exec cap staging puma:restart
로 할 수 있었습니다.
Gemfile이 변경되었을경우가 좀 미묘한데 bundle exec가 동작안하는 경우가 있을 수 있고, 그냥 동작하는 경우가 있을 수 있습니다. 만약에 그냥 동작한다면 puma:restart 전에 번들 인스톨만 해주면 됩니다. 아마 태스크는 이런 식이 될거에요.
namespace :bundle do
desc “bundle install“
task :bundle do
on roles(:app), in: :groups, limit: 3, wait: 10 do
within release_path do
execute :sudo, "docker exec <app_name>_app_1 bundle install -j 4 --deployment"
end
end
end
end
bundle exec cap staging deploy
bundle exec cap staging bundle:install
bundle exec cap staging puma:restart
동작하지않는다면 app컨테이너를 내렸다 올려야 합니다.
bundle exec cap staging deploy
bundle exec cap staging fig:app:kill
bundle exec cap staging fig:app:up
결론
일단은 다운타임없이 리스타트하고 컨테이너를 작게 분리하는데 까지는 성공했습니다. 멀티 호스트 환경에서의 디플로이는 다음에 또 이야기 하도록 하겠습니다.