Skip to content
ROR Lab. edited this page Mar 16, 2014 · 3 revisions

영문 원본 보기

SSHKit 이미지

SSHKit는 하나 또는 그 이상의 서버에서 구조화된 방식으로 명령을 실해아기 위한 툴킷이다.

어떻게 동작하는가?

일반적인 사용법은 아래와 같다.

require 'sshkit/dsl'

on %w{1.example.com 2.example.com}, in: :sequence, wait: 5 do
  within "/opt/sites/example.com" do
    as :deploy  do
      with rails_env: :production do
        rake   "assets:precompile"
        runner "S3::Sync.notify"
        execute "node", "socket_server.js"
      end
    end
  end
end

SSHKit는 매우 낮은 수준이지만 편리한 API 를 제공해 주는데, as(), within(), with() 는 어떤 순서라도 상관없이 중첩할 수 있고 반복할 수 있으며 스택에 푸시할 수도 있다.

이와 같이 블록 내에서 사용할 때, as()within()는 포함하는 블록을 체크하여 보호할 것이다.

within()의 경우에는, 해당 디렉토리가 존재하지 않을 에러을 발생하고, as()에 대해서는, sudo su -<user> whoami 를 호출하여 성공여부를 체크하고 실패할 경우 에러를 발생한다.

디렉토리 유무 체크는 아래와 같이 실행된다.

if test ! -d <directory>; then echo "Directory doesn't exist" 2>&1; false; fi

그리고 사용자 변경 테스트는 다음과 같이 실행된다.

if ! sudo su <user> -c whoami > /dev/null; then echo "Can't switch user" 2>&1; false; fi

디폴트 상태에서 0 이외의 상태 값을 가지는 모든 명령은 에러를 발생한다(물론 이러한 설정을 변경할 수 있다). 메시지 내용은 처리과정에서 stdout으로 작성된 모든 것을 포함한다. 1>&2 는 echo의 표준 출력을 표준 에러 채널로 리디렉트 시켜 주어 에러 발생시 메시지를 볼 수 있게 된다.

execute(:rails, "runner", ...)execute(:rake, ...) 로 확장되는 runner()와 rake()` 와 같은 헬퍼 메소드들은 루비와 레일스 어플리케이션을 위한 편리한 헬퍼들입니다.

병행(Parallel)

on() 호출시 in: :sequence 옵션을 주의해서 사용한다.

on(in: :parallel) { ... }
on(in: :sequence, wait: 5) { ... }
on(in: :groups, limit: 2, wait: 5) { ... }

디폴트는 in: :parallel 상태로 실행하며 제한이 없다. 400대의 서버가 있을 경우에는 문제가 발생할 수 있기 때문에 groups 또는 sequence 상태로 실행하도록 변경하는 것이 더 좋을 것이다.

groups는, 하나의 (Git) 리소스에 대해서 너무 많은 접속을 하여 DDOS 공격을 원치 않는 경우(대량으로 Git 체크아웃을 하는)를 방지하기 위해서 고안되었다.

순차적인 실행(sequence)은 다를 사용 예 중에서도 (순차적으로) restart를 하고자 할 때 사용한다.

동기화(Synchronisation)

on() 블록은 동기화의 단위이다. 하나의 on() 블록은 모든 서버가 작업을 완료한 후에 반환하게 된다.

예를 들면,

all_servers = %w{one.example.com two.example.com three.example.com}
site_dir    = '/opt/sites/example.com'

# 백업 task를 시뮬레이션 해 보자. 
# 어떤 서버들은 다른 것에 비해 시간이 더 걸릴 수 있다는 가정을 한다.
on all_servers do |host|
  in site_dir do
    execute :tar, '-czf', "backup-#{host.hostname}.tar.gz", 'current'
    # 이것은 다음과 같이 실행될 것이다. "/usr/bin/env tar -czf backup-one.example.com.tar.gz current"
  end
end

# 이제 이러한 백업을 가지고 무언가를 할 수 있다. 이미 모든 백업들이 존재한다고 알고 있는 상태에서이다.
# (모든 tar 명령은 성공상태로 종료되었거나 하나라도 실패한 경우 예외가 발생했을 것이다.
on all_servers do |host|
  in site_dir do
    backup_filename = "backup-#{host.hostname}.tar.gz"
    target_filename = "backups/#{Time.now.utc.iso8601}/#{host.hostname}.tar.gz"
    puts capture(:s3cmd, 'put', backup_filename, target_filename)
  end
end

명령 맵(Command Map)

프로그램 상 접근하는 SSH 세션은 인터액티브(interactive) 세션과 동일한 환경 변수를 가지지 않는다는 문제가 종종 있다.

$PATH 경로 상에 존재할 것으로 기대하는 실행파일을 호출할 때 문제가 종종 발생한다. dotfile이나 다른 환경 구성이 없는 상황에서는, $PATH는 제대로 설정 되지 못하기 때문에 실행파일들을 해당 위치에서 발견하지 못할 수 있다.

이러한 문제를 해결하기 위해서 with() 헬퍼는 변수들로 구성된 해시를 취해서 환경에서 사용할 수 있도록 해 준다.

with path: '/usr/local/bin/rbenv/shims:$PATH' do
  execute :ruby, '--version'
end

이것은 다음과 같이 실행될 것이다.

( PATH=/usr/local/bin/rbenv/shims:$PATH /usr/bin/env ruby --version )

이와는 대조적으로, 아래의 스크립트는 명령을 전혀 변경하지 못할 것이다.

with path: '/usr/local/bin/rbenv/shims:$PATH' do
  execute 'ruby --version'
end

이것은 다음과 같이 실행될 것이다.

ruby --version

(이와 같은 현상은 때때로 혼란스럽지만, 대부분이 쉘 이스케이핑(shell escaping)과 관계가 있는데, whitespace가 명령에 포함된 경우나 개행문자가 있는 경우, 입력한 내용으로 정확한 쉘 명령을 작성한 방법이 없기 때문이다.)

따라서 이런 경우를 대비해서 명령 맵을 사용하는 것을 종종 더 선호하기도 한다.

Command 객체를 생성할 때, 디폴트로 명령 맵을 사용할 수 있게 된다.

명령 맵은 구성 객체 상에 존재하고 원칙적으로 매우 간단하다. 디폴트 키를 factory 블록에 명시한 해시 구조를 가진다. 예를 들면,

puts SSHKit.config.command_map[:ruby]
# => /usr/bin/env ruby

환경 설정을 확실히 하기 위해 /usr/bin/env가 모든 명령 앞에 붙게 된다. 이것은 단순하게 ruby를 실행할 때 일어나는 것이지만, 명확히 함으로써 사람들이 문서를 찾아 보기를 바란다.

각 명령에 대한 해시 맵을 변경할 수 있다.

SSHKit.config.command_map[:rake] = "/usr/local/rbenv/shims/rake"
puts SSHKit.config.command_map[:rake]
# => /usr/local/rbenv/shims/rake

또 다른 방법은 명령 앞에 붙일 다른 명령을 추가하는 것이다.

SSHKit.config.command_map.prefix[:rake].push("bundle exec")
puts SSHKit.config.command_map[:rake]
# => bundle exec rake

SSHKit.config.command_map.prefix[:rake].unshift("/usr/local/rbenv/bin exec")
puts SSHKit.config.command_map[:rake]
# => /usr/local/rbenv/bin exec bundle exec rake

명령 맵을 완전히 새로운 것으로 변경할 수 있는데, 이것은 현명하지 못한 일이지만, 가능한 일이다. 예를 들면,

SSHKit.config.command_map = Hash.new do |hash, command|
  hash[command] = "/usr/local/rbenv/shims/#{command}"
end

이것은 해당 디렉토리에 실행 파일을 제공해 주지 않았던 어떤 명령도 호출할 수 없게 되지만 때로는 이러한 상황이 필요할 수 있다.

주의사항 : 명령 맵에서 해당 키를 찾기 전에 Command 객체는 첫번째 인수를 심볼화 할 것이기 때문에, 모든 키는 심볼이어야 한다.

출력물을 처리하기

콘솔 그림

디폴로, 콘솔 출력물 포맷은 :pretty로 설정되어 있다.

SSHKit.config.format = :pretty

그러나, 출력을 최소한만 보이도록 원할 경우 :dot 포맷으로 지정하면 명령 실행의 성패에 따라 빨간색이나 초록색으로 표시될 것이다.

포맷을 사용하지 않고 직접 $stdout으로 출력하기 위해서는 아래와 같이 지정할 수 있다.

SSHKit.config.output = $stdout

출력물의 정보 표시(Output Verbosity)

디폴트 상태에서는 capture()test() 호출시 로그가 남지 않는다. 이 명령들은 종종 백엔드 task에서 환경 설정을 체크하기 위해서 사용된다. Logger::DEBUGCommand 인스턴스에 verbosity 옵션을 지정할 수 있다. 디폴트 구성은 SSHKit.config.output_verbosity= 로 변경할 수 있고 디폴트는 Logger::INFO 이다.

현재 Logger::WARN, ERROR, FATAL 는 사용하지 않는다.

연결 풀링(Connection Pooling)

SSHKit는, 모든 on() 블록에 대해 새로운 SSH 연결을 시도하는 것에 대한 비용을 줄이기 위해서, 단순한 연결 풀(디폴트로 활성화됨)을 사용한다. 용도와 네트워크 상황에 따라서, 상당한 시간 절약을 할 수 있다. 한 테스트에서는, 기본적인 cap deploy 명령이 SSHKit의 최근 버전에 추가된 연결 풀 덕분에 15-20초 정도 더 빠르게 실행되었다.

연결 상태를 활성 상태로 유지하기 위해서, 기존의 풀링된 연결은 30초이상 사용되지 않을 경우 새로운 연결로 대체될 것이다. 이와 같은 타임아웃은 아래와 같이 변경할 수 있다.

SSHKit::Backend::Netssh.pool.idle_timeout = 60 # seconds

연결 풀이 문제를 일으킨다고 생각될 때는, idle_timeout 값을 0으로 설정하므로써 연결 풀 기능을 해제할 수도 있다.

SSHKit::Backend::Netssh.pool.idle_timeout = 0 # disabled

알려진 문제들

  • 느리거나 타이아웃된 연결을 처리하지 못함
  • 느리거나 중단된 원격 명령을 처리하지 못함
  • 백그라운드 작업 처리 기능이 없음
  • 환경 처리기능 없음(sshkit는 이에 대한 걱정을 할 필요없다)
  • host 객체에 임의의 속성을 추가할 수 없음(서버에 roles을 저장하거나 on() 블록에서 유용하게 사용할 수 있는 메타데이타 등)
  • 로그나 경고 메시지를 보여주는 기능이 없음(로그 메시지를 출력으로 넘기는 것이 작동한다) 하나의 로그 객체는, 전역적으로 사용할 수 있어야 하며, 로그 메시지에 대한 관심을 가지는 포맷터들이 인식할 수 있는 LogMessage 형 객체를 발송할 것이다.
  • verbosity를 제어할 수 없음. 명령은 자신에 대한 Logger::LEVEL 을 가져야 한다. 사용자가 생성한 것은 고수준의 정보를 보여줘야 하고, as()within() 로부터 권한 체크를 위해 자동으로 생성되는 명령들은 저수준의 정보를 보여줘야 한다.
  • execute() 류 명령들이 0가 아닌 종료 상태에 대해서 에러를 발생할지 여부를 결정해야 함. 아마도 비슷한 이름을 가진 !(bang) 메소드군은 에러를 발생해야 한다. (아마도 test()는 에러 발생시키지 않고 execute()를 실행하는 방법이고 execute() 류 명령들은 항상 에러를 발생해야 한다.)
  • SSHKit.config.formatter = :pretty 라고 설정할 수 있고, 메소드 setter가 SSHKit::config.output을 기존 출력 스트림을 래핑(wrapping)하는 정확한 포맷터 클래스 인스턴스로 업데이트할 수 있게 한다면 멋질 것이다.
  • 내부적으로 디버깅하기 위한 "trace" 레벨이 없음. 디버그 레벨은 클라이어트 측 디버깅을 위해서 예비해 두어야 하고, 네트워크 연결, 종료, 타임아웃에 대한 로그를 남기 위해서 내부적으로 trace(int -1)를 사용해야 한다.
  • 파일 업로드나 다운로드를 위한 메소드가 없음. 또는 원격 파일로 문자열을 저장하거나 가져오는 메소드가 없음.
  • 네트워크 연결은 중단할 수 없음. 백엔드 abstract 클래스는 비어있지만 다르게 방법으로 구현하여 변경할 수 있는 cleanup 메소드를 포함해야 한다.
  • 연결 풀링 기능이 없음. NetSSH 백엔드의 connection 메소드는, 연결 팩토리에서 연결 객체들을 찾아 볼 수 있도록 쉽게 변경할 수 있어야 하고, 이로써, 여러 개의 on() 블록을 실행할 때 0.5초 정도 시간을 절약할 수 있게 된다.
  • 문서화(YARD 형태)가 필요하다.
  • 모든 명령을 알려진 쉘로 래핑(wrapping)할 수 있어야 함. 즉, execute('uptime')은 일관된 쉘 실행 환경을 유지하기 위해서 sh -c 'uptime'으로 변환되어야 한다.
  • Host.new('user@ip:port')와 같이 호출하는 것을 수용하는 적당한 호스트 파서(parser)가 없음. 이것은 user@hostname:port을 디코딩하지만 IP 주소는 디코딩하지 못할 것이다.
  • Net::SSH가 IOError를 발생(인증 실패와 같이)할 때 에러를 잡아 낼 수 있어야 하고, ConnectionFailed와 같은 에러를 다시 발생할 수 있어야 한다.