Serverspec을 활용한 인프라 구성 검증 자동화

Serverspec 개요

Serverspec은 인프라스트럭처의 실제 상태가 의도한 구성과 일치하는지 검증하는 Ruby 기반 테스팅 프레임워크다. Ansible, Puppet, Chef 등의 구성 관리 도구가 적용된 결과를 코드로 검증하여 인프라 테스트를 자동화한다.

주요 특징

  • 테스트 코드 자체가 문서 역할 수행
  • 재사용 가능한 검증 스크립트 작성
  • 코드 리뷰를 통한 테스트 케이스 검증
  • 직관적인 DSL(Domain Specific Language) 제공
  • coderay 젬 설치 시 HTML 리포트 생성 가능
  • 로컬 실행 및 SSH 원격 실행 모두 지원
  • 설정 파일 기반 및 대화형 초기화 방식 제공

공식 자료

  • 공식 사이트: https://serverspec.org/
  • 학습 자료: https://serverspec.org/tutorial.html
  • 소스 저장소: https://github.com/mizzy/serverspec
  • 젬 버전: https://rubygems.org/gems/serverspec/versions/

설치 과정

Ruby 2.2.6 이상 버전이 사전에 설치되어 있어야 한다. 버전이 낮으면 의존성 해결에 실패한다.

$ gem install serverspec
Fetching rspec-support-3.9.0.gem
Fetching diff-lcs-1.3.gem
Fetching sfl-2.3.gem
Fetching multi_json-1.14.1.gem
Fetching rspec-expectations-3.9.0.gem
Fetching specinfra-2.82.4.gem
Fetching net-ssh-5.2.0.gem
Fetching net-scp-2.0.0.gem
Fetching rspec-core-3.9.0.gem
Fetching rspec-its-1.3.0.gem
Fetching rspec-mocks-3.9.0.gem
Fetching rspec-3.9.0.gem
Fetching serverspec-2.41.5.gem
Successfully installed sfl-2.3
Successfully installed net-ssh-5.2.0
Successfully installed net-scp-2.0.0
Successfully installed specinfra-2.82.4
Successfully installed multi_json-1.14.1
Successfully installed diff-lcs-1.3
Successfully installed rspec-support-3.9.0
Successfully installed rspec-expectations-3.9.0
Successfully installed rspec-core-3.9.0
Successfully installed rspec-its-1.3.0
Successfully installed rspec-mocks-3.9.0
Successfully installed rspec-3.9.0
Successfully installed serverspec-2.41.5
Parsing documentation for sfl-2.3
Installing ri documentation for sfl-2.3
Parsing documentation for net-ssh-5.2.0
Installing ri documentation for net-ssh-5.2.0
Parsing documentation for net-scp-2.0.0
Installing ri documentation for net-scp-2.0.0
Parsing documentation for specinfra-2.82.4
Installing ri documentation for specinfra-2.82.4
Parsing documentation for multi_json-1.14.1
Installing ri documentation for multi_json-1.14.1
Parsing documentation for diff-lcs-1.3
Couldn't find file to include 'Contributing.rdoc' from README.rdoc
Couldn't find file to include 'License.rdoc' from README.rdoc
Installing ri documentation for diff-lcs-1.3
Parsing documentation for rspec-support-3.9.0
Installing ri documentation for rspec-support-3.9.0
Parsing documentation for rspec-expectations-3.9.0
Installing ri documentation for rspec-expectations-3.9.0
Parsing documentation for rspec-core-3.9.0
Installing ri documentation for rspec-core-3.9.0
Parsing documentation for rspec-its-1.3.0
Installing ri documentation for rspec-its-1.3.0
Parsing documentation for rspec-mocks-3.9.0
Installing ri documentation for rspec-mocks-3.9.0
Parsing documentation for rspec-3.9.0
Installing ri documentation for rspec-3.9.0
Parsing documentation for serverspec-2.41.5
Installing ri documentation for serverspec-2.41.5
Done installing documentation for sfl, net-ssh, net-scp, specinfra, multi_json, diff-lcs, rspec-support, rspec-expectations, rspec-core, rspec-its, rspec-mocks, rspec, serverspec after 15 seconds
13 gems installed

프로젝트 초기화

CentOS 환경에서 로컬 모드로 실행할 경우, 검증 스크립트는 spec/localhost/*_spec.rb 경로에 위치한다. serverspec-init 명령으로 생성되는 샘플 파일을 참고하여 테스트를 작성한다.

$ mkdir ~/infra-test && cd ~/infra-test
$ serverspec-init
Select OS type:

  1) UN*X
  2) Windows

Select number: 1

Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 2

 + spec/
 + spec/localhost/
 + spec/localhost/sample_spec.rb
 + spec/spec_helper.rb
 + Rakefile
 + .rspec

생성된 디렉터리 구조:

.
├── Rakefile
└── spec
    ├── localhost
    │   └── sample_spec.rb
    └── spec_helper.rb

2 directories, 3 files

기본 샘플 테스트 내용:

$ cat spec/localhost/sample_spec.rb
require 'spec_helper'

describe package('httpd'), :if => os[:family] == 'redhat' do
  it { should be_installed }
end

describe package('apache2'), :if => os[:family] == 'ubuntu' do
  it { should be_installed }
end

describe service('httpd'), :if => os[:family] == 'redhat' do
  it { should be_enabled }
  it { should be_running }
end

describe service('apache2'), :if => os[:family] == 'ubuntu' do
  it { should be_enabled }
  it { should be_running }
end

describe service('org.apache.httpd'), :if => os[:family] == 'darwin' do
  it { should be_enabled }
  it { should be_running }
end

describe port(80) do
  it { should be_listening }
end

실행 예시

Rake를 통한 테스트 실행

Rakefile이 있는 디렉터리에서 명령을 실행해야 한다.

$ rake spec
/usr/local/rvm/rubies/ruby-2.5.5/bin/ruby -I/usr/local/rvm/gems/ruby-2.5.5/gems/rspec-support-3.9.0/lib:/usr/local/rvm/gems/ruby-2.5.5/gems/rspec-core-3.9.0/lib /usr/local/rvm/gems/ruby-2.5.5/gems/rspec-core-3.9.0/exe/rspec --pattern spec/localhost/\*_spec.rb

Package "httpd"
  is expected to be installed

Service "httpd"
  is expected to be enabled (FAILED - 1)
  is expected to be running (FAILED - 2)

Port "80"
  is expected to be listening

Failures:

  1) Service "httpd" is expected to be enabled
     On host `localhost'
     Failure/Error: it { should be_enabled }
       expected Service "httpd" to be enabled
       /bin/sh -c systemctl\ --quiet\ is-enabled\ httpd
       
     # ./spec/localhost/sample_spec.rb:12:in `block (2 levels) in <top (required)>'

  2) Service "httpd" is expected to be running
     On host `localhost'
     Failure/Error: it { should be_running }
       expected Service "httpd" to be running
       /bin/sh -c systemctl\ is-active\ httpd
       unknown

     # ./spec/localhost/sample_spec.rb:13:in `block (2 levels) in <top (required)>'

Finished in 0.14409 seconds (files took 0.65376 seconds to load)
4 examples, 2 failures

Failed examples:

rspec ./spec/localhost/sample_spec.rb:12 # Service "httpd" is expected to be enabled
rspec ./spec/localhost/sample_spec.rb:13 # Service "httpd" is expected to be running

Nginx 환경 검증 예시

Ansible 연동을 통한 인프라 구성 및 검증 자동화 사례다.

$ cat site.yml
---
- hosts: webservers
  become: yes
  connection: local
  roles:
    - common
    - nginx
    - serverspec
    - serverspec_sample

디렉터리 구조:

roles/serverspec_sample/
├── files
│   └── serverspec_sample
│       ├── Rakefile
│       └── spec
│           ├── localhost
│           └── spec_helper.rb
├── meta
│   └── main.yml
├── tasks
│   └── main.yml
├── templates
│   ├── nginx_spec.rb.j2
│   └── web_spec.rb.j2
└── vars
    └── main.yml

8 directories, 7 files

Ansible 실행 및 검증 파일 배포:

$ ansible-playbook -i development site.yml
...
TASK [serverspec_sample : distribute serverspec suite] ***********************
changed: [localhost]

TASK [serverspec_sample : distribute spec file] ******************************
changed: [localhost]

PLAY RECAP *******************************************************************
localhost                  : ok=9    changed=2    unreachable=0    failed=0

배포된 검증 스크립트:

$ tree /tmp/serverspec_sample/
/tmp/serverspec_sample/
├── Rakefile
└── spec
    ├── localhost
    │   └── web_spec.rb
    └── spec_helper.rb

2 directories, 3 files

$ cat /tmp/serverspec_sample/spec/localhost/web_spec.rb
require 'spec_helper'

describe package('nginx') do
  it { should be_installed }
end

describe service('nginx') do
  it { should be_enabled }
  it { should be_running }
end

describe port(80) do
  it { should be_listening }
end

describe file('/usr/share/nginx/html/index.html') do
  it { should be_file }
  it { should exist }
  its(:content) { should match /^Hello, development ansible!!$/ }
end

첫 실행 결과 - 콘텐츠 불일치 오류:

$ cd /tmp/serverspec_sample && rake spec

Package "nginx"
  is expected to be installed

Service "nginx"
  is expected to be enabled
  is expected to be running

Port "80"
  is expected to be listening

File "/usr/share/nginx/html/index.html"
  is expected to be file
  is expected to exist
  content
    is expected to match /^Hello, development ansible!!$/ (FAILED - 1)

Failures:

  1) File "/usr/share/nginx/html/index.html" content is expected to match /^Hello, development ansible!!$/
     On host `localhost'
     Failure/Error: its(:content) { should match /^Hello, development ansible!!$/ }
       expected "hello, development ansible\n" to match /^Hello, development ansible!!$/
       Diff:
       @@ -1,2 +1,2 @@
       -/^Hello, development ansible!!$/
       +hello, development ansible

Finished in 0.23396 seconds (files took 0.64631 seconds to load)
7 examples, 1 failure

템플릿 수정 후 재배포:

$ cat roles/nginx/templates/index.html.j2
Hello, {{ env }} ansible!!

$ ansible-playbook -i development site.yml
...
TASK [nginx : replace index.html] ********************************************
changed: [localhost]

PLAY RECAP *******************************************************************
localhost                  : ok=9    changed=1    unreachable=0    failed=0

최종 검증 성공:

$ cd /tmp/serverspec_sample && rake spec

Package "nginx"
  is expected to be installed

Service "nginx"
  is expected to be enabled
  is expected to be running

Port "80"
  is expected to be listening

File "/usr/share/nginx/html/index.html"
  is expected to be file
  is expected to exist
  content
    is expected to match /^Hello, development ansible!!$/

Finished in 0.19861 seconds (files took 0.61345 seconds to load)
7 examples, 0 failures

문제 해결

Ruby 버전 오류

gem 설치 시 Ruby 2.2.6 미만 버전에서는 다음 오류가 발생한다:

$ gem install serverspec
...
ERROR:  Error installing serverspec:
    net-ssh requires Ruby version >= 2.2.6.

$ ruby -v
ruby 2.0.0p648 (2015-12-16) [x86_64-linux]

해결 방안: RVM을 통한 Ruby 업그레이드

yum으로는 최신 Ruby를 설치할 수 없으므로 RVM(Ruby Version Manager)을 사용한다.

$ gem sources -a http://mirrors.aliyun.com/rubygems/
$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
$ curl -sSL https://get.rvm.io | bash -s stable
$ source /etc/profile.d/rvm.sh
$ rvm install 2.5

설치 확인:

$ ruby -v
ruby 2.5.5p157 (2019-03-15 revision 67260) [x86_64-linux]

HTML 리포트 생성

coderay 젬을 설치하면 테스트 결과를 HTML 형식으로 출력할 수 있다.

$ gem install coderay

적절한 포맷터 설정을 통해 시각적인 테스트 리포트를 생성할 수 있다.

태그: Serverspec ruby infrastructure-as-code Ansible Configuration-Testing

7월 6일 01:29에 게시됨