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

결론

일단은 다운타임없이 리스타트하고 컨테이너를 작게 분리하는데 까지는 성공했습니다. 멀티 호스트 환경에서의 디플로이는 다음에 또 이야기 하도록 하겠습니다.