Canvas 1 Layer 1

서버스펙을 사용한 도커 이미지 테스트 자동화
RSpec 기반 인프라스트럭처 테스트 프레임워크

들어가며

구성 관리configuration management 도구들은 불변 인프라스트럭처Immutable Infrastructure와 컨테이너를 대표하는 도커Docker와 함께 서버 분야에 많은 변화를 가져왔습니다. 셰프Chef, 퍼핏Puppet, 앤서블Ansible로 대표되는 구성관리 도구들은 독자적인 DSL을 통해서 서버의 이상적인 상태와 상태에 이르는 절차를 정의합니다. 그리고 이 코드를 기반으로 서버의 특정 상태를 몇 번이고 재현해냅니다. 따라서 이를 코드로서의 인프라스트럭처Infrastructure as Code라고 표현하기도 합니다. 서버의 코드화, 여기서 한 단계 더 나아가면 또 다른 흥미로운 아이디어를 만날 수 있습니다. 서버가 코드라면 소프트웨어를 검증하는 기법들을 똑같이 적용할 수 있지 않을까요?

서버스펙Serverspec은 테스트라는 관점에서 이 질문에 답을 하는 도구입니다. 이 글에서는 서버스펙을 사용해 도커 이미지를 테스트하는 방법을 소개합니다.

서버스펙(Serverspec)을 통한 도커 이미지 검증

도커 이미지 검증을 위한 환경을 준비하고 도커 이미지에 대한 테스트를 작성해보겠습니다. 서버스펙에는 프로젝트를 초기화를 돕는 serverspec-init이라는 명령어가 포함되어있습니다. 하지만 여기서는 이 명령어를 사용하지 않고 진행하겠습니다. 루비의 기본적인 구조와 비슷하게 spec 디렉터리를 만들고, 그 아래에 테스트 코드를 추가하겠습니다.

./serverspec-with-docker
├── Gemfile
└── spec
    ├── mongodb_image_spec.rb
    └── spec_helper.rb

디렉터리 구조는 단순합니다. 프로젝트 메인 디렉터리 아래에 spec 디렉터리를 만들고 테스트 설정 파일 spec_helper.rb와 테스트 파일 mongodb_image_spec.rb를 준비합니다.

도커 이미지 준비

여기서는 시스템에 도커가 미리 설치되어있다고 가정합니다. 먼저 도커 명령어로 nginx 공식 이미지를 풀 받습니다.

$ docker pull nginx:latest

젬파일(Gemfile)을 사용한 의존성 관리

스버스펙은 루비를 기반으로 동작하기 때문에 시스템에 루비가 설치되어있어야합니다. 테스트에서 사용하는 의존성 관리를 위해 프로젝트 루트에 다음 내용을 포함한 Gemfile을 생성합니다.

Gemfile은 프로젝트의 의존성을 관리하고, 프로젝트 내에서 사용될 실행 가능한 명령어들을 관리하기 위해 사용됩니다. 이제 의존성을 설치해보겠습니다.

$ bundle install
Fetching gem metadata from https://rubygems.org/.......
Resolving dependencies...
Using diff-lcs 1.2.5
Using excon 0.45.3
...
Using specinfra 2.36.6
Using serverspec 2.19.0
Using bundler 1.7.3
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

설치가 끝났으면 프로젝트 루트 아래에서 rspec 명령어가 작동하는지 실행해봅니다.

$ bundle exec rspec --version
3.3.1

정상적으로 작동하는 것을 확인할 수 있습니다.

spec_helper.rb: 테스트 설정 파일

spec_helper.rb는 프로젝트에 적용되는 R스펙RSpec 설정파일입니다. :host에는 테스트에 사용할 적절한 도커 서버의 주소를 입력합니다. :docker_image는 테스트할 대상을 의미하며, :os는 테스트 대상이 되는 서버의 운영체제를 의미합니다. 서버스펙은 이 옵션을 통해서 테스트에서 사용하는 DSL을 추상화합니다. <USERNAME>에는 적절한 사용자 이름을 넣어줍니다. 여기서는 맥OSmacOS에서 boot2docker를 사용하고 있다고 가정하고 있으며 SSH 키가 다른 곳에 있다면 적절한 위치를 지정해줍니다.

mongodb_image_spec.rb: 몽고DB 이미지 테스트 파일

이 파일에는 테스트 코드를 작성합니다. 테스트 코드는 R스펙 고유의 DSL로 작성되며, 서버스펙은 서버의 상태를 검증하기 위한 기능들을 제공합니다. 루비나 R스펙에 친숙하다면, 이 코드를 보고 어떤 내용인지 바로 알 수 있을 것입니다. 아직 써본 적이 없더라도 영어 문자을 통해 어떤 테스트를 수행하는지 추측해볼 수 있을 것입니다.

여기에는 3개의 테스트가 있습니다. 서버스펙에서는 리소스라는 개념을 사용해서 테스트를 수행합니다. 먼저 첫번째 테스트에서 쓰인 리소스는 file입니다. 이를 통해서 /etc가 디렉터리인지 검증합니다. 그 다음으로 두번째 테스트에서는 process 리소스를 통해서 mongod 프로세스가 실행중인지 검증합니다. 마지막으로 port 리소스를 통해서 27017 포트가 대기중인지 검증합니다. 서버스펙은 서버를 검증하기 위한 다양한 리소스를 제공하고 있으며 더 많은 리소스들에 대해서는 서버스펙 공식 사이트에서 찾아볼 수 있습니다.

R스펙(RSpec)으로 테스트 실행하기

이제 테스트 코드를 모두 작성했으니, 테스트가 정상적으로 작동하는지 실행하는 일만 남았습니다. 프로젝트 루트 디렉터에서 테스트를 실행해보겠습니다.

$ rspec .
tutum/mongodb IMage
  File "/etc"
    should be directory
  Process "mongod"
    should be running
  Port "27017"
    should be listening

Finished in 6.18 seconds (files took 1.14 seconds to load)
3 examples, 0 failures

모든 테스트가 성공했습니다! 테스트를 통해서 /etc 디렉터리가 존재하고, mongod 프로세스가 실행되고 있으며, 27017 포트가 대기중인 것을 확인했습니다.

테스트 주도 인프라스트럭처(Test Driven Infrastructure)

여기까지 서버스펙을 통해서 이미 만들어져있는 도커 이미지를 검증하는 방법에 대해서 살펴보았습니다. 그렇다면 소프트웨어 개발과 마찬가지로 Dockerfile을 만드는 과정 전체를 테스트해보는 것은 어떨까요?

물론 가능합니다. 여기서는 메모리 기반 키-밸류 데이터스토어 레디스Redis 이미지를 직접 만들어가면서 테스트를 해보겠습니다.

디렉터리 구조

./serverspec-with-docker
├── Dockerfile
├── Gemfile
├── Guardfile
└── spec
    ├── nacyot_redis_image_spec.rb
    └── spec_helper.rb

spec_helper.rb: 테스트 설정 파일

먼저 spec_helper.rb은 앞선 예제를 그대로 사용하되, 이미지 부분을 주석처리하거나 삭제해줍니다.

nacyot_redis_image_spec.rb: 레디스 이미지 테스트 파일

레디스 서버가 정상적으로 작동하는 지 확인하기 위한 테스트를 준비한다.

여기서 작성한 테스트들 역시 기본적인 R스펙 문법으로 작성되었으며 서버스펙의 리소스들을 사용하고 있다.

예제 코드에서 주목할만한 부분이 있습니다. 이전에는 없었던 before(:all) 절이 추가되었는데, 이 부분은 테스트를 실행하기에 앞서 한 번 실행됩니다. 여기서 하는 일은 프로젝트 루트의 Dockerfile로부터 도커 이미지를 생성하고, 테스트 대상 이미지를 동적으로 지정하는 일입니다. 이 때 이미지 이름이 아니라, 빌드로부터 반환되는 이미지 ID 값을 사용합니다. 이 방법을 사용해 별도로 빌드 명령을 수행하지 않아도, 테스트를 수행할 때마다 자동적으로 빌드를 하고 테스트를 수행할 수 있습니다.

가드(Guard)를 통한 자동 테스트 구현

이번 프로젝트에서는 가드Guard를 사용해 Dockerfile과 테스트 파일의 변경을 감지하고 테스트할 수 있도록 합니다. 이를 위해 Gemfile을 다음과 같이 수정합니다.

source 'https://rubygems.org'

gem 'serverspec'
gem 'docker-api'
gem 'guard'
gem 'guard-rspec'

프로젝트 루트 디렉터리에서 추가한 의존성을 설치해줍니다.

$ bundle install

그리고 아래와 같이 Guardfile을 작성합니다.

guard :rspec, cmd: "bundle exec rspec" do
  require 'guard/rspec/dsl'
  watch(Guard::RSpec::Dsl.new(self).rspec.spec_files)
  watch(%r{^Dockerfile$}) { 'spec/nacyot_redis_image_spec.rb' }
end

Guardfile 파일은 spec 디렉터리 아래의 파일이나 dockerfile이 변경되었을 때 테스트를 자동적으로 실행하라는 내용을 담고 있습니다.

이제 별도의 터미널을 실행해서 프로젝트 루트 디렉터리에서 다음 명령어를 실행하면 파일이 변경될 때마다 테스트가 자동적으로 실행됩니다.

# bundle exec guard
09:47:59 - INFO - Guard::RSpec is running
09:47:59 - INFO - Guard is now watching at '/Users/.../docker-with-serverspec'
[1] guard(main)>

첫번째 이터레이션: 이미지 생성하기

먼저 빈 Dockerfile에 베이스 이미지를 지정해준다.

FROM ubuntu:14.04

가드는 이 변화를 감지하고 자동으로 테스트를 수행할 것입니다. 앞서 이야기했듯이 테스트가 실행되면 자동적으로 이미지가 빌드되므로 따로 이미지 빌드를 실행하지 않아도 됩니다.

nacyot/redis Image
  File "/etc"
    should be directory
  Package "redis-server"
    should be installed (FAILED - 1)
  File "/usr/bin/redis-server"
    should be executable (FAILED - 2)
  Process "redis-server"
    should be running (FAILED - 3)
  Port "6379"
    should be listening (FAILED - 4)

Finished in 5.23 seconds (files took 0.30381 seconds to load)
5 examples, 4 failures

Failed examples:

rspec ./spec/nacyot_redis_image_spec.rb:14 # nacyot/redis Image Package "redis-server" should be installed
rspec ./spec/nacyot_redis_image_spec.rb:18 # nacyot/redis Image File "/usr/bin/redis-server" should be executable
rspec ./spec/nacyot_redis_image_spec.rb:22 # nacyot/redis Image Process "redis-server" should be running
rspec ./spec/nacyot_redis_image_spec.rb:26 # nacyot/redis Image Port "6379" should be listening

etc 디렉터리의 존재를 확인하는 첫번째 테스트만 성공하고, 나머지 테스트가 실패했습니다!

두번째 이터레이션: apt-get을 사용해 redis 패키지 설치하기

Dockerfile 끝에 다음 내용을 추가합니다.

RUN sed -i 's/archive.ubuntu.com/ftp.daum.net/g' /etc/apt/sources.list

RUN \
  apt-get update &&\
  apt-get install -y redis-server

이번에도 자동적으로 테스트가 실행됩니다.

nacyot/redis Image
  File "/etc"
    should be directory
  Package "redis-server"
    should be installed
  File "/usr/bin/redis-server"
    should be executable
  Process "redis-server"
    should be running (FAILED - 1)
  Port "6379"
    should be listening (FAILED - 2)

Finished in 7.42 seconds (files took 0.29263 seconds to load)
5 examples, 2 failures

Failed examples:

rspec ./spec/nacyot_redis_image_spec.rb:22 # nacyot/redis Image Process "redis-server" should be running
rspec ./spec/nacyot_redis_image_spec.rb:26 # nacyot/redis Image Port "6379" should be listening

redis-server 패키지의 설치를 검증하는 두번째, 세번째 테스트가 통과했습니다!

세번째 이터레이션: redis 실행하기

Dockerfile 끝에 다음 내용을 추가해준다.

CMD redis-server

다시 자동적으로 테스트가 실행됩니다.

nacyot/redis Image
  File "/etc"
    should be directory
  Package "redis-server"
    should be installed
  File "/usr/bin/redis-server"
    should be executable
  Process "redis-server"
    should be running
  Port "6379"
    should be listening

Finished in 6.94 seconds (files took 0.29334 seconds to load)
5 examples, 0 failures

테스트가 모두 통과했습니다! 이를 통해 이 이미지를 사용하면 레디스(redis) 서버가 정상적으로 실행되는 것을 보장할 수 있습니다.

테스트 코드

여기서 사용한 프로젝트와 테스트 코드는 serverspec_tutorial에서 확인할 수 있습니다.

결론

인프라가 정상적인 상태에 있다는 것을 증명하는 것은 아주 중요합니다. 그럼에도 불구하고 이러한 테스트는 대부분 자동화되어있지 않습니다. 스버스펙은 원래 SSH를 사용해 구성 관리 도구와 함께 사용을 목적으로 만들어진 도구입니다. 이 훌륭한 도구는 단순히 기존 서버 환경 뿐만 아니라, 빌드를 통해 완성된 이미지를 구성하는 도커와 같은 컨테이너 시스템을 테스트하는 데도 적격입니다. 이를 통해 소프트웨어 테스트 뿐만 아니라 소프트웨어를 탑재한 이미지가 배포되기 전에 정상적으로 작동하는지, 필요한 파일들을 제대로 포함하고 있는지까지 함께 검증하는 것이 가능합니다. 또한 서버가 코드처럼 다뤄질 수 있다면, 테스트 자동화는 물론 저장소와 연동해서 CI를 통해 지속적인 통합 역시 가능해집니다.