리모트 스크립트 제작기
DevOps 가 대두되면서 지속적인 배포(Continuous Delivery) 가 핵심으로 떠오르면서 수만은 서버에 한꺼번에 명령어를 내리고 파일을 전송하고 설정파일을 관리하는 소프트웨어가 인기를 끌고 있다.
대표적으로 Chef, Puppet, Saltstack, Ansible 등을 들수 있는데 이들은 지속적인 배포 뿐만 아니라 시스템과 소프트웨어를 통합해 플랫폼으로서 수많은 서버들의 상태를 동일하게 유지시켜준다.
그런데 몇 해전만 해도 이런것들이 부족했던 시절이 있었는데, 이때 내가 다니던 직장에서 수천대의 서버를 관리해야하는 일이였다. 이러한 소프트웨어가 없던 시절에 나는 개인적으로 이와 비슷한 것을 만들어 사용했는데 그때 당시로 돌아가 어떤 생각으로 만들었었는지 개선했는지를 정리해보고자 한다.
쉘 스크립트
처음에 수십대 서버에 명령어를 보내야했다. 궁리를 한 끝에 생각난 것이 쉘 스크립트로 돌리자는 거였다. 리눅스를 조금 한 사람이면 expect 라는 명령어를 알 것이다. 이 명령어는 어떤 명령어를 날렸을 경우에 돌아오는 답변을 미리 기대해(expect)해서 그것을 파싱하고 자동으로 다시 답변을 해주는 명령어였다.
수백대에 명령어를 보내야 했었는데, 수백대의 서버이름은 파일로 저장해놓고 expect 쉘 스크립트를 작성하면 될 일이였다.
1 2 3 4 5 6 7 8 |
#!/usr/bin/expect -f spawn ssh aspen expect "password: " send "PASSWORD\r" expect "$ " send "ps -ef |grep apache\r" expect "$ " send "exit\r" |
그런데 이게 생각만큼 쉽지 않았다. 먼저 쉘 스크립트를 지금도 잘 모르지만, 예외처리를 하기에 힘들었다. 로그인은 성공했지만 예기치 못하게 접속이 차단되거나 기대했던 프롬프트가 나오지 않아서 명령어가 실행되지 않는등 문제가 많았다.
그래도 그때 당시에는 좋았다. 오류가 나오기도 했지만 100대중 5대 내외여서 나름대로 좋았는데, 사람이 간사한게 처음에는 불편했던것을 감수했지만 시간이 지나자 감수했던 불편함을 어떻게든 없애고 싶은 생각이 들었다. 불편했던 것들은 다음과 같았다.
- 기대하는 프롬프트가 다양하게 나타났다.
- 예외처리를 하기가 힘들었다.
- 정상적인 결과나 오류등을 파일로 저장하고 싶었다.
- 동시에 여러서버들에 실행되길 원했다.
쉘 스크립트말고 다른 프로그래밍 언어를 사용하면 최소한 예외처리나 정상적결과나 오류등을 파일로 저장하는 것이 가능해질것 같았고 평소 사용하던 Python 프로그래밍을 사용해보기로 했다.
expect 프로그래밍 for python
Python은 아주 좋은 언어임에 틀림이 없었다. expect 프로그램을 지원하는 python 라이브러리가 존재했다. pexpect 모듈이 그것이였다.
pexpect 모듈과 Python 이 지원하는 쓰레드(thread) 그리고 로깅(logging) 기능을 조합하면 좋은 꽤나 쓸만한 프로그램이 나올것이 분명해 보였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import pexpect import sys, getpass COMMAND_PROMPT = '[$#] ' TERMINAL_PROMPT = r'Terminal type\?' TERMINAL_TYPE = 'vt100' SSH_NEWKEY = r'Are you sure you want to continue connecting \(yes/no\)\?' def login(host, user, password): child = pexpect.spawn('ssh -l %s %s'%(user, host)) fout = file ("LOG.TXT","wb") child.setlog (fout) i = child.expect([pexpect.TIMEOUT, SSH_NEWKEY, '[Pp]assword: ']) if i == 0: # Timeout print('ERROR!') print('SSH could not login. Here is what SSH said:') print(child.before, child.after) sys.exit (1) if i == 1: # SSH does not have the public key. Just accept it. child.sendline ('yes') child.expect ('[Pp]assword: ') child.sendline(password) |
15번째줄에 보면 기대되는 프롬프트를 적어준다. 위 예제에서는 간단하게 나왔지만 Python이 제공하는 정규표현식을 사용할 수도 있다. 위 예제를 기반으로 쓰레드를 붙이고 로깅을 붙이고 해서 여러서버에 동시에 명령어를 넣을 수 있게 되었다.
나름대로 만족하고 있었는데, 기대되는 프롬프트가 문제였다. 리눅스는 다양한 배포판을 사용하다보니 기대하는 프롬프트가 조금씩 달랐다. 거기다 SSH 접속할때에 가끔씩 인증키가 충돌이 나는 일도 있어서 기대하는 프롬프트를 리스트로 가지고 있어야 했다.
귀찮은게 문제였다. 어떻게 하면 저런것까지 신경 안쓰면서 명령어를 날리고 로깅을 할 수 있을까 하는 문제 말이지…
pxssh 모듈
pxssh 모듈은 귀찮이즘에 적어드는 시기에 찾은 신변기(?) 였다. 이 Python 모듈은 기대하는 프롬프트를 신경쓸 필요가 없었다. 최초로 접속하는 서버일지라도 알아서 HostKey 를 교환했고 프롬프트가 달라도 알아서 해주는 매우 좋은 녀석이였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
s = pxssh.pxssh() try: s.login(host, 'root', '1234', login_timeout=60) except pxssh.ExceptionPxssh, e: flag = 0 s.close() log.error(host+' - '+str(e)) result= RED + 'Failed' + ENDC if flag == 1: for command in self.commandList: s.sendline(command) flag = s.prompt(self.session_timeout) if flag is False: log.error(host+' - '+command+"\n Time out") s.terminate() result= RED + 'TimeOut' + ENDC print host+' - '+s.before log.info(host+' - '+s.before) s.logout() |
pxssh 모듈은 위의 예제처럼 기대되는 프롬프트는 필요가 없다. 대신 한가지 특징이 있는데, pxssh 모듈은 결과값을 before, after 변수에 저장된다. 그래서 19번째 라인처럼 값을 불러오면 된다.
그런데, 한가지 문제가 있었다. 이번에는 파일을 전송하고 싶었던 것이다.
paramiko 모듈
이런저런 고민을 풀면서 일명 자동화 명령어 프로그램을 만드는 동안에 많은 검색을 하게되면서 많은 자료를 모울 수 있다. 그러던중에 paramiko python 모듈을 보게 됐다.
paramiko 는 ssh, sftp 프로토콜을 지원줘서 python 으로 ssh, sftp 프로그래밍이 가능하도록 만들주는 라이브러리였다. 올타쿠나!! 이거야!! 그래서 명령어 전송부터해서 파일 전송을 전부 이것으로 교체하기로하고 만들었지만 문제가 있었다.
paramiko 의 SSHClient 클래스를 지원하는데 이는 SSH를 이용해서 로그인을 할 수 있도록 한것인데, 문제는 pxssh 와 비교해보니 속도면에서 많이 느렸다. (물론 내가 잘못 짠 것일 수도 있다.) 그래서 paramiko 는 파일 전송을 하는데 사용하고 명령어를 쓸때는 pxssh 를 사용하도록 스크립트 두개를 사용하도록 했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
try: transport = paramiko.Transport((host, 22)) transport.connect(username='root', password='1234') sftp = paramiko.SFTPClient.from_transport(transport) sftp.put(self.sourcefile, self.targetfile) sftp.close() transport.close() log.warning(host+' - '+self.targetfile) except Exception, e: log.error(host+' - '+ str(e)) result= RED + 'Failed' + ENDC |
대충 위와같이 프로그램을 짰다.
지금은 Python에서 Fabric 프로그램이라고 많이 사용하고 있다. fabfile.py 에 관련 내용을 작성하면 fab 명령어가 이 파일을 자동으로 인식해 내부에 작성된 메소드들을 실행해주는 방법이다. 파일 전송도 가능하다.
요즘은 Chef, Puppet, Saltstack 과 같은 것들을 사용하면 파일 배포및 명령어도 아주 쉽게 할수 있는 시대에 살지만 아주 간단하게 뭔가를 하고 싶을때에 종종 만들어 두었던 python 프로그램들을 사용하곤 한다.