환경 구축 자동화

수동 환경 구축의 위험성

환경 구축을 자동화 하려면 어떤 점이 좋을까요? 일단 수동으로 환경을 구축할 때는 다음과 같은 상황에서 문제가 생길 수 있습니다.

  • 서버를 새로 추가한다
  • 서버에 라이브러리나 미들웨어를 설치한다
  • 개발 환경을 가볍게 추구한다

새로운 서버 추가

  • 서버를 보강하는 방법
방법 설명
스케일업 서버의 CPU와 메모리를 고성능 제품으로 업그레이드하여 처리 성능을 올리는 방법
스케일아웃 서버 수를 늘려서 성능을 올리는 방법

서버에 라이브러리나 미들웨어 설치

서버에 라이브러리나 미들웨어를 추가하는 상황을 생각해봅시다. 당연히 라이브러리의 미들웨어를 설치할 때 시간이 걸리고, 서버 수가 늘어나면 어떤 서버에는 설치하고 어떤 서버에는 설치하지 않는 상황도 발생합니다. 서버마다 같은 라이브러리와 미들웨어를 사용할 때, 같은 버전을 설치했는지 확인하고 싶어도 여러 서버의 버전을 관리해야 하므로 이 또한 큰일입니다.

하지만 환경 구축을 자동화하고, 관리작업을 ansible등 프로비저닝 도구와 서버 상태를 테스트하는 Serverspec에 맡기는 편이 안심할 수 있고 안전합니다.

간단한 개발 환경 구축

지금까지 소개한 사례로 환경 구축을 수동으로 할 때 여러 가지 번거로운 문제가 생길 수 있음을 알 수 있었습니다. 그래서 이런 문제들을 회피하고 환경 구축을 자동화하는 방법을 사용해야 합니다.

Vagrant

Vagrant는 한마디로 VirtualBox나 VMWare, Amazon EC2와 같은 가상화 소프트웨어의 프런트 엔드라고 할 수 있습니다. CUI로 간단히 서버를 시작하거나 정지할 수 있습니다.

Vagrant 도입

$ vagrant --version
VirtualBox 설치
$ VBoxManage -v

Vagrant로 가상 머신 실행 ( CentOS7의 예 )

$ vagrant box add centos7 \
> https://github.com/tommy-muehle/puppet-vagrant-boxes/releases/download/1.1.0/ \ 
> centos-7.0-x86_64.box

vagrant box list 커맨드로 사용할 수 있는 Box 목록을 확인할 수 있습니다.

$ vagrant box list

Box를 사용하여 Vagrant를 초기화합니다.

$ mkdir -p ~/work/vagrant/sample
$ cd ~/work/vagrant/sample
$ vagrant init centos7
가상머신 관리

가상 머신을 일시정지할 때는, vagrant suspend 커맨드, 일시정지에서 복귀할 때는 vagrant resume 커맨드, 셧다운할 때는 vagrant halt 커맨드를 사용합니다. 서버를 다시 실행하려면 vagrant up이라고 하면 됩니다.

$ vagrant suspend
$ vagrant halt

커맨드와 셸 스크립트 실행

이제 가상 머신의 시작, 일시정지, 셧다운 등 작업을 할 수 있습니다. 이외에도 Vagrant를 사용하면 여러 가지 동작을 할 수 있습니다.

예를 들어, 시작할 때 간단한 커맨드를 실행시켜 봅시다. Vagrantfile의 내용에 프로비저닝 처리를 추가해서 vagrant up 합니다.

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
	config.vm.box = "centos7"
	config.vm.provision :shell, inline: "echo foobar"
end

프로비저닝은 처음 시작할 때만 실행합니다. 처리가 필요할 때는 명시적으로 vagrant up –provision으로 지정하거나 프로비저닝만 실행하는 vagrant provision 커맨드를 사용합니다.

프로비저닝을 실행해봅시다.

$ vagrant provision

터미널에 foobar가 출력되었을 것입니다. 이처럼 가상 머신을 시작할 때 임의의 커맨드를 실행 할 수 있습니다. 게다가 단순 커맨드뿐만 아니라 셸 스크립트를 사용할 수도 있습니다.

여러 대의 가상 머신을 한 번에 실행

두 대의 가상 머신 (web1과 web2)을 한 번에 실행해 보겠습니다. 생각보다 간단해서 다음처럼 private_network 설정에서 각각 다른 IP를 지정하면 됩니다. 하지만 이렇게 하면 시작할 때마다 ssh의 포트포워드 포트 번호가 바뀌어서 불편합니다. forwarded_port 설정을 변경해서 호스트 쪽 포트 번호를 임의의 포트 번호로 고정해둡시다.

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
	config.vm.define :web1 do |node|
		node.vm.box="centos7"
		node.vm.network "rivate_network", ip: "192.0.0.1"
		node.vm.network "forwarded_port", guess: 22, host: 2000, id: "ssh"
	end	
	
	config.vm.define :web2 do |node|
		node.vm.box="centos7"
		node.vm.network "private_network", ip: "192.0.0.1"
		node.vm.network "forwarded_port", guess: 22, host: 2001, id: "ssh"
	end
end

가상 머신에 ssh로 로그인

vagrant ssg-config 커맨드는 ‘~/.ssh/config’용 설정을 생성하므로, 이 커맨드를 사용해서 ssh 커맨드로 로그인할 수 있게 하면 편리합니다.

$ vagrant ssh-config
$ vagrant ssg-config >> ~/.ssh/config
$ ssh web1

Amazon EC2 인스턴스 실행

Amazon EC2를 사용해서 AWS에 서버를 실행 해 봅시다.

먼저 Vagrantfile을 작성합니다. 이번에는 vagrant init 커맨드를 실행할 때 인수를 지정하지 않습니다.

$ mkdir ~/work/vagrant/ec2
$ cd !!:$
cd ~/work/vagrant/ec2
$ vagrant init

Provider로 VirtualBox가 아니라 AWS를 사용하려면 vagrant-aws 플러그인이 필요합니다. vagrant plugin install 커맨드로 플러그인을 설치합니다.

$ vagrant plugin install vagrant-aws
$ vagrant box add dummy https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box

dummy라는 이름의 Box를 추가했습니다.

$ vagrant box list
centos7 (virtualbox, 0)
dummy (aws, 0)

나머지 편리한 플러그인 소개

Vagrant 편리한 플러그인을 소개합니다.

sahara 플러그인

sahara는 샌드박스 모드를 유효하게 하는 플러그인입니다.

$ vagrant plugin install sahara
# 샌드박스 모드 실행 
$ vagrant sandbox on 

# 롤백하고 싶을 때 
$ vagrant sandbox rollback

# 확장하고 싶을 때 
$ vagrant sandbox commit

# 샌드박스 모드 종료(커밋하지 않은 변경은 취소된다)
$ vagrant sandbox off

# 샌드박스의 상태 확인
$ vagrant sandbox status
vagrant-cachier 플러그인

프로비저닝 등에서 다운로드한 yum 등 패키지를 캐싱하여 다음부터 다운로드 시간을 줄여 주는 플러그인 입니다.

$ vagrant plugin install vagrant-cachier
global-status

vagrant global-status 커맨드도 꽤 편리합니다. 이전에는 플러그인으로 제공했지만, 최근에는 Vagrant에 포함됐습니다. 이 플러그인을 사용하면 Vagrant에서 다루는 모든 가상 머신의 현재 상태를 표시합니다. 어느 가상 머신이 어느 경로에서 실행 중인지 알 수 있으므로 가상 머신 관리에 사용할 수 있습니다.

Vagrant 가상화 이미지

하드웨어 위에 호스트 OS가 있고, 그 위에 하이퍼바이저라는 가상 머신을 에뮬레이트하는 기능이 있습니다. 하이퍼바이저 위에서 가상 머신을 실행하고, 그 안에서 게스트 OS를 동작합니다. 하이퍼바이저에 해당하는 것이 VirtualBox나 VMware, Amazon EC2 등 가상화 소프트웨어입니다.

Ansible

Ansible은 프로비저닝 도구 중 하나로, 매우 문턱이 낮아 간편하게 도입할 수 있는 것이 특징입니다. ssh 로그인만 할 수 있으면 대상 서버에는 아무것도 필요 없습니다.

Ansible 도입

$ vagrant init centos7

그리고 다음처럼 Vagrantfile을 설정합니다.

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
	config.vm.define :server1 do |node|
		node.vm.box="centos7"
		node.vm.network "rivate_network", ip: "192.0.0.1"
		node.vm.network "forwarded_port", guess: 22, host: 2000, id: "ssh"
	end	
	
	config.vm.define :server2 do |node|
		node.vm.box="centos7"
		node.vm.network "private_network", ip: "192.0.0.1"
		node.vm.network "forwarded_port", guess: 22, host: 2001, id: "ssh"
	end
end

나머지는 vagrant up 커맨드를 실행하면 됩니다. 이것으로 server1과 server2라는 가상 머신 두 대를 시작합니다. 다음처럼 ssh로 로그인할 수 있게 합시다.

$ vagrant ssg-config >> ~/.ssh/config
$ ssh server1

이 상태로는 어느 서버에 접속했는지 쉽게 알 수 없어 불편하므로, 가상 머신을 시작할 때 프로비저닝으로 호스트 이름을 설정하는 처리를 추가하겠습니다.

VAGRANTFILE_API_VERSION = "2"
change_hostname = <<SCRIPT
sudo hostname $1
sudo echo HOSTNAME=$1 >> /etx/sysconfig/network
sudo service network restart
SCRIPT

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
	config.vm.define :server1 do |node|
		node.vm.box="centos7"
		node.vm.network "rivate_network", ip: "192.0.0.1"
		node.vm.network "forwarded_port", guess: 22, host: 2000, id: "ssh"
		node.vm.provision :shell so |s|
			s.inline = change_hostname
			s.args = ['server1']
		end
	end	
	
	config.vm.define :server2 do |node|
		node.vm.box="centos7"
		node.vm.network "private_network", ip: "192.0.0.1"
		node.vm.network "forwarded_port", guess: 22, host: 2001, id: "ssh"
		node.vm.provision :shell do |s|
			s.inline = change_hostname
			s.args = ['server2']
		end
	end
end

인벤토리 파일 준비

이번에는 host 이름으로 인벤토리 파일을 만들었습니다. 임의의 그룹을 정의할 수도 있으므로, server1과 server2 양쪽을 포함하는 그룹을 ‘all-server’로, server1만 포함하는 그룹을 ‘dev-servers’로 했습니다.

[all-server]
server[1:2]

[dev-servers]
server1

간단한 처리 실행

예를 들어, Ansible을 사용해서 server1에 ping을 날려 봅시다. -i 옵션으로 인벤토리 파일을 지정하고, 그 다음에 대상으로 할 서버를 지정합니다.

$ ansible -i hosts server1 -m ping

server1뿐만 아니라 server2에서도 한꺼번에 ping을 실행하려면, 인벤토리에 지정한 그룹 이름인 all-servers나 all이라는 특별한 키워드를 지정해야합니다.

$ ansible -i hosts all-servers -m ping

playbook으로 복잡한 처리 실행

좀 더 복잡한 처리를 실행하려면, ansible-playbook 커맨드를 사용합니다.

MySQL을 설치하는 상황을 생각해 보겠습니다. 우선 MySQL을 설치해서 실행하는 설정 파일을 준비합니다. 예를 들어 mysql-playbook.yml이라고 합시다.

---
- hosts: dev-servers # 대상 서버를 지정한다 
  sudo: yes
  tasks: 
  - name: add repository
    yum: name=http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm state-present
    
  
  - name: install mysql
    yum: name=mysql-server enablerepo=epel,rpmforge state-installed # 설치된 상태로 한다
  - name: start mysql
    service: name=mysqld state=running enabled=yes # 시작된 상태로 한다

실행 커맨드

$ ansible-playbook -i hosts mysql-playbook.yml

멱등성 확보

이번에는 server2에도 MySQL을 설치하는 경우를 생각해보겠습니다.

---
- hosts: all-servers # 이곳을 변경한다 
  sudo: yes
  tasks:
  - name: add repository
    yum: name=http://repo.mysql.com/mysql-community-release-el7-t.noarch.rpm state=present
    
    
  - name: install mysql
    yum: name=mysql-server enablerepo=epel,rpmforge state=installed # 설치된 상태로 한다 
  - name: start mysql 
    service: name=mysqld state=running enabled=yes # 시작된 상태로 한다

실행 커맨드

$ andible-playbook -i hosts mysql-playbook.yml

파일 끝 추가

yum 모듈과 service 모듈을 사용한 경우라면 멱등성을 확보하므로 괜찮지만, shell 모듈로 임의의 커맨드를 실행할 때는 멱등성을 확보하지 않으므로 구현하는 쪽에서 멱등성을 확보할 필요가 있습니다. 예를 들어, 다음 playbook을 실행하면 실행할 때마다 파일 끝에 test라는 출력을 추가합니다.

---
- hosts: dev-servers
  tasks: 
  - shell: echo test >> /tmp/foobar

이래서는 곤란합니다. 그래서 이때는 다음처럼 register 모듈과 when 모듈을 사용합니다.

---
- hosts: dev-servers
  tasks: 
  - shell: echo test >> /tmp/foobar
    register: result
  - shell: echo test >> /tmp/foobar
    when: result.stdout.find('test') == -1

다음에서는 테스트에서 when 모듈을 사용하여 여기서 지정한 조건을 만족할 때만 /tmp/foobar 끝에 test라는 문자열을 추가하도록 지정했습니다. result.stdout.find(‘test’)는 표준 출력 안에 test 문자열이 포함되어 있으면 그 인덱스를, 포함되어 있지 않으면 -1을 반환합니다. 이렇게 지정하면 /tmp/foobar에 test 문자열이 없을 때만 끝에 test를 추가하는 조건부 처리가 가능합니다.

먼저 /tmp/foobar에 test가 없을 때를 확인해 보자. 테스트를 위해 server1에 /tmp/foobar라는 이름으로 빈 파일을 하나 만듭니다.

$ ssh server1 # server1에 접속해서 
$ touch /tmp/foobar # 빈파일을 만든다
$ logout
$ ansible-playbook -i hosts shell-playbook.yml

나머지 편리한 모듈 소개

changed_when 모듈은 어떤 때 changed로 표시할지 조건을 지정합니다. 또, debug 모듈은 특정 변수 이름의 내용과 메시지를 출력할 수 있습니다.

--- 
- hosts: dev-servers
  tasks:
  - shell: cat /tmp/foobar
    register: result
    changed_when: False
  - shell: echo test >> /tmp/foobar
    when: result.stdout.find('test') == -1
  - debug: var=

Serverspec

서버의 상태를 테스트하는 프레임워크인 Serverspec은 프로비저닝 결과를 제대로 설정했는지 확인할 때 도움을 줍니다. Ruby로 구현되어 있으며, Ruby 테스트 프레임워크인 RSpec에 따른 형태로 기술할 수 있습니다.

Serverspec 도입

$ gem install serverspec

테스트 드리븐 프로비저닝

아파치용 테스트 템플릿을 생성했지만, 이번에는 아파치가 아니라 Nginx를 사용할 것이므로 다음처럼 httpd를 nginx로 변경합니다.

require 'spec_helper'

# Nginx가 설치되어 있을 것
describe package('nginx') do 
	it { shoud be_installed }
end

# Nginx가 자동 실행하도록 설정되어 있을 것
# Nginx가 실행 중일 것
describe service('nginx') do 
	it { should be_enabled }
	it { should be_running }
end 

# 80번 포트가 열려 있을 것
describe port(80) do 
	it { should be_listening }
end

테스트의 실행

$ rake spec

모든 테스트가 실패할것입니다. server1에는 아직 Nginx가 설치되어 있지 않으니 당연한 결과입니다.

그럼, Nginx를 설치하고 그상태에서 테스트하면 성공한느지 확인해 봅시다.

--- 
- hosts: dev-servers
  sudo: yes
  tasks: 
  - name: install nginx
    yum: name=nginx state=installed
  - name: start nginx
    service: name=nginx state=running enabled=yes
$ ansible-playbook -i hosts nginx-playbook.yml

Docker

Docker는 Go 언어로 기술한 가상화를 구현하는 소프트웨어입니다. Docker를 사용하면 Heroku나 CircleCI처럼 배포와 테스트를 할 때마다 다른 환경을 준비할 수 있습니다.

Docker 가상화 이미지

Vagrant와 Docker는 어떻게 다를까? 둘 다 가상화 기술이지만 가상화의 방법이 다르다.

  • Vagrant와 Docker의 차이
Vagrant Docker
하이퍼바이저형 컨테이너형
호스트 OS와 다른 OS를 다룰 수 있다 호스트 OS와 같은 OS만 처리할 수 있다
애뮬레이트하는 만큼 시작에 시간이 걸린다 바로 시작한다

Docker 도입

$ mkdir ~/work/vagrant/docker
$ cd !!:$
$ vagrant init centos7
$ vagrant up

이제 가상 머신을 시작했습니다. vagrant ssh 커맨드로 로그인한 후 Docker를 설치합니다.

$ sudo yum update
$ sudo vi /etc/yum.repos.d/docker.repo

# 다음 내용을 입력하고 저장한다
[dockerrepo]
name=Docker Repository
baseurl=https://yum.dockerproject.org/repo/main/centos/7
enabled=1
gpgcheck=1
gpgkey=https://yum.dockerproject.org/gpg

# Docker를 설치한다
$ sudo yum -y install docker-engine

다음 커맨드로 Docker를 실행하고 자동 실행 설정도 합니다.

$ sudo systemctl start docker 
$ sudo systemctl enable docker

Docker 테스트

docker run 커맨드로 임의의 Docker 이미지에 임의의 처리를 실행할 수 있습니다. 예를 들어, ‘Hello World’를 표시해 보겠습니다.

$ docker run centos /bin/echo "Hello World"

centos라는 Docker 이미지에서 컨테이너를 생성하여 ‘Hello World’를 출력하고, 출력이 끝나면 바로 컨테이너를 종료합니다.

  • 주요옵션
옵션 설명
-d 백그라운드에서 실행한다.
-i 컨테이너 표준 입력을 연다. /bin/bash 등에서 컨테이너를 조작할 때 지정한다.
-t tty(단말 디바이스)를 확보한다. /bin/bash 등에서 컨테이너를 조작할 때 지정한다.
-p -p<호스트 포트="" 번호="">:<컨테이너 포트="" 번호=""> 포트 매핑을 설정한다.

셸에서의 Nginx 설치 커맨드

$ docker run -i -t centos /bin/bash
Docker 이미지를 만든다

Nginx를 설치한 상태를 Docker이미지로 저장해 보겠습니다.

$ docker images

컨테이너 목록은 docker ps 커맨드로 표시할 수 있습니다.

$ docker ps -a

‘docker commit <컨테이너 ID=""> <임의의 이름="">'으로 지정하면 컨테이너의 Docker 이미지를 원하는 일므으로 저장할 수 있습니다.

$ docker commit e07 sasata299/nginx
Docker 이미지로 컨테이너를 시작한다
$ docker run -d -p 80:80 sasata299/nginx /usr/sbin/nginx -g 'deamon off;'

Vagrantfile에 private_network 설정이 되어 있으면, 브라우저로도 Nginx의 실행을 확인할 수 있습니다.

컨테이너 관리

Docker 컨테이너를 정지하고 삭제해 보겠습니다.

$ docker stop b80ea2d658cd

docker rm 커맨드를 사용하면 컨테이너를 완전히 삭제합니다.

$ docker rm b80ea2d658cd
Docker 이미지 관리

Docker 이미지의 삭제는 docker rmi 커맨드를 사용합니다.

$ docker rmi <이미지명>

Dockerfile로 Docker 이미지 만들기

간단히 Docker 이미지를 만드는 방법을 실험해 보겠습니다.

$ echo test > index.html

그리고 Dockerfile은 다음 내용으로 준비합니다. Nginx를 설치하고 작성한 index.html을 /usr/share/nginx/html(DocumentRoot)에 배치하는 처리를 설정합니다.

FROM centos
MAINTAINER yoon <zizou0812@gmail.com>
RUN rpm -Uvh \
	http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
RUN yum install -y nginx
ADD index.html /usr/share/nginx/html/

docker build 커맨들르 사용하면, Dockerfile을 바탕으로 Docker 이미지를 만들 수 있습니다. 여기서는 ‘yoon/nginx’라는 이름의 버전 0.1로 지정했습니다.

$ docker build -t yoon/nginx:0.1 .

Docker 이미지 공유

Docker 이미지 공유는 ‘Docker Hub’에서 할 수 있습니다.

사용자 등록이 끝났으면, docker login 커맨드로 로그인합니다.

$ docker login 

이제 docker push 커맨드로 이미지를 등록해보겠습니다

$ docker push yoon/nginx

Docker Hub에 등록한 상태에서 docker search 커맨드로 검색해 봅시다. 등록한 Docker 이미지를 찾을 수 있을 것입니다. 이것으로 이미지를 전세계에서 누구나 이용할 수 있게 되었습니다.

  • docker 커맨드 목록
커맨드 설명
run Docker 이미지를 가져와서 컨테이너를 시작하고, 그 안에서 임의의 처리를 실행한다. 종료 후 컨테이너를 종료한다.
images Docker 이미지 목록을 표시한다.
ps 현재 실행 중인 컨테이너 목록을 표시한다. -a 옵션을 사용하면 정지된 컨테이너도 표시한다.
stop 지정한 컨테이너를 정지한다.
rm 지정한 컨테이너를 삭제한다.
rmi 지정한 Docker 이미지를 삭제한다.
build Dockerfile을 사용하여 Docker 이미지를 만든다.
commit 컨테이너를 Docker 이미지로 저장한다.
pull Docker 이미지를 리포지터리에서 가져온다.
push Docker 이미지를 리포지터리에 등록한다.
search Docker 이미지를 리포지터리에서 검색한다.

Docker로 CI 테스트

Docker의 예로 Jenkins에서 CI 테스트 환경을 Docker로 만든 후 매번 클린 환경으로 테스트하는 방법을 자주 사용합니다.

Java 설치

Jenkins는 Java에서 동작하므로, yum 커맨드를 사용하여 Java를 설치합니다.

$ sudo yum -y install java-1.7.0-openjdk
$ sudo yum -y install wget
Jenkins 설치
$ sudo wget -0 /etc/yum.repos.d/jenkins.repo http://pkh.jenkins-ci.org/redhat/jenkins.repo
$ sudo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key
$ sudo yum -y install jenkins

자동 실행 설정

$ sudo service Jenkins start
$ sudo chkconfig Jenkins on
Jenkins 사용자를 docker 그룹에 추가
$ sudo gpasswd -a Jenkins docker 
Jenkins 설정
FROM yoon/nginx:0.1
RUN git clone <임으의 리포지터리>