패커(Packer)는 범용적 머신/컨테이너 이미지 생성기이다. 이미지는 일반적으로 가상머신의 특정한 상태를 그대로 저장해서 만들어진다. Packer에서는 Builder 컴포넌트를 통해 다양한 플랫폼을 지원하고, Provisioner 컴포넌트를 통해 다양한 도구로 이미지를 빌드할 수 있다. 이 글은 2015년 9월 5일 네번째 Docker Seoul Meetup에서 발표한 내용을 기반으로 작성되었으며, Packer에 대한 기본적인 기능들과 간단한 사용법에 대해서 소개한다.

패커(Packer)는?

패커(Packer)는 Hashicorp에서 만든 인프라 관리 도구 중에 하나이다. Hashicorp에서는 가상 환경 관리 도구로 유명한 베이그런트(Vagrant)를 비롯해 서버 클러스터 도구 서프(Serf), 서비스 디스커버리 도구 컨설(Consul), 이종 플랫폼 간 리소스 공유를 지원하는 테라폼(Terraform) 등 다양한 인프라 자동화 / 지원 도구들을 만들고 있다. 패커 역시 이러한 인프라 자동화 도구 중 하나로 다양한 플랫폼에서 사용가능한 이미지 생성을 지원한다.

공식 사이트에서는 다음과 같이 소개한다.

Packer is a tool for creating machine and container images for multiple platforms from a single source configuration.

패커(Packer)는 하나의 설정 소스로부터 여러 플랫폼을 지원하는 머신/컨테이너 이미지를 만드는 도구이다. -- Packer offcial website

한 마디로, 범용적인 머신/컨테이너 이미지 생성기이다.

여기서 이미지라는 단어에 주목을 할 필요가 있다. 일반적으로 가상 머신의 이미지는 머신의 특정한 상태를 그대로 저장하고, 나중에 재사용 가능하도록 준비해둔 것이다. 하지만 이러한 이미지에는 몇 가지 문제가 있다. 일단 사용중인 가상머신의 상태를 그대로 저장하기 때문에 이 상태에 이르기까지의 과정은 소실된다. 다르게 말하자면 같은 상태를 재현하는 게 어려워진다. 또한 최신 상태를 저장하기 위해서 계속 이미지를 만들어야 하는데, 이미지들은 서로 아무런 관계도 가지지 않기 때문에 관리 비용은 이미지 수에 비례해서 늘어나게 된다.

패커에서는 이미지를 생성할 때 특정 가상머신의 상태를 그대로 저장하지 않는다. 빌드의 기반이 되는 이미지를 기반으로 프로비저너를 통해서 가상 머신에 패키지 설치와 환경설정 등의 작업을 수행하고, 빌더를 통해서 그 결과를 특정 플랫폼의 이미지로 저장한다. 즉, 이미지 생성 과정에 대한 모든 정보는 코드로 관리되며, 필요에 따라서 같은 과정을 재현해 다양한 플랫폼에서 같은(유사한) 이미지를 만들어 사용할 수 있다.

그렇다면 여기서부터는 프로비저너와 빌더, 그리고 컴포넌트들의 설정을 포함한 Template에 대해서 좀 더 살펴보자.

빌더, 프로비저너, 템플릿

패커에서 가장 중요한 개념은 프로비저너, 빌더, 템플릿 이 세가지다.

빌더로 이미지를 생성할 플랫폼을 설정할 수 있다. 아마존, 디지털 오션과 같은 클라우드는 물론 VirtualBoxVMWare와 같은 가상 머신의 이미지도 지원하며, Docker와 같은 컨테이너 이미지도 생성할 수 있다. 현재 패커에서 지원하는 빌더 목록은 다음과 같다.

Amazon EC2 (AMI)  
DigitalOcean  
Docker  
Google Compute Engine  
Null  
OpenStack  
Parallels  
QEMU  
VirtualBox  
VMware  
Custom  

다음으로 프로비저너는 이미지 빌드에 사용할 도구를 의미한다. 셸스크립트와 같은 원시적인 방법은 물론 앤서블(Ansible), 셰프(Chef)로 대표되는 Configuration Management 도구를 지원한다. 이러한 도구들을 사용해 이미지를 원하는 상태로 만들 수 있다. 패커에서는 다양한 프로비저너를 지원하기 때문에 기존에 사용하던 도구를 그대로 사용하기 쉽다. 현재 패커에서 지원하는 프로비저너 목록은 다음과 같다.

Remote Shell  
Local Shell  
File Uploads  
PowerShell  
Windows Shell  
Ansible  
Chef Client  
Chef Solo  
Puppet Masterless  
Puppet Server  
Salt  
Windows Restart  
Custom  

Packer에서는 위에서 살펴본 프로비저너와 빌더를 조합해서 사용한다. 그리고 이들 설정을 담은 템플릿(Template) 파일로 이미지를 빌드한다. Template은 json 포맷으로 작성된 설정파일이다. 이 설정파일은 빌더(Builder)프로비저너(Provisioner)를 비롯한 개별 컴포넌트 설정들로 구성된다. 템플릿의 기본적인 포맷은 다음과 같다.

{
  "builders": [{
    // ...
  }],
  "provisioners": [{
    // ...
  }]
}

빌더와 프로비저너를 지정하고, 세부 설정을 작성한다. 여기서 빌더와 프로비저너 설정이 배열 안에 들어가 있는 걸 볼 수 있다. 빌더가 다수 지정되면 다양한 플랫폼의 이미지를 같은 프로비저너를 통해서 생성할 수 있다. 프로비저너가 다수 지정되면 각각의 프로비저너를 순차적으로 실행한다. 예를 들어 셸스크립트로 아주 기본적인 설정을 하고, 앤서블 플레이북을 적용할 수 있다.

셸스크립트(shellscript)로 도커(Docker) 이미지 빌드하기

패커를 사용해서 이미지를 생성해보자. 여기서는 셸스크립트를 사용해 도커 이미지를 만들어 본다. 보통 Docker 이미지는 Dockerfile을 작성해서 빌드한다. 하지만 Packer를 사용하면 Dockerfile을 사용하지 않고도, 셸스크립트나 형상 관리(Configuration Management) 도구들로 도커 이미지를 작성할 수 있다2.

(굳이 패커를 사용해 도커 이미지를 만들 필요가 있을까? 꼭 패커를 써야할 이유는 없다. 단 이런 경우는 생각해볼 수 있다. 이미 CM 툴을 잘 사용하고 있는데 도커를 도입하는 경우라면, 패커가 가뭄의 단비처럼 느껴질 것이다.)

여기서는 wget이 설치된 ubuntu 이미지를 만들어본다. 이를 Dockerfile로 작성하면 다음과 같다.

FROM ubuntu:14.04

RUN apt-get update  
RUN apt-get install -y wget  

이와 같은 역할을 하는 Packer 템플릿(설정 파일)을 작성해보자. 기본적인 구조는 다음과 같다.

{
  "builders": [{
    "type": "docker"
    // ...
  }],
  "provisioners": [{
    "type": "shell"
    // ...
  }],
  "post-processors": [{
    "type": "docker-import"
    // ...
  }]
}

먼저 첫번째 빌더 타입으로 docker를 지정한다. 여기서는 도커 이미지만 만들기 때문에 하나의 빌더만을 지정한다. 프로비저너로는 shell을 지정한다. 따로 소개하지 않았지만 도커 이미지를 빌드 할 때는 post-processors를 사용해야한다. docker 빌더는 기본적으로 압축파일로 이미지를 생성한다. 이를 도커에서 사용하려면 docker-import 포스트 프로세서를 통해 생성한 이미지를 임포트해야 한다.

하나씩 구체적인 설정을 살펴보자. 먼저 빌더는 다음과 같이 설정한다.

{
  "type": "docker",
  "image": "ubuntu:14.04",
  "export_path": "nacyot-ubuntu-wget.tar"
}

image는 빌드에서 사용할 베이스 이미지를 지정한다. 이 이미지를 기반으로 새로운 이미지를 빌드한다. export_path는 도커 이미지를 내보낼 경로(파일명)을 지정한다.

다음으로 프로비저너를 살펴보자.

{
  "type": "shell",
  "inline": [
    "apt-get update",
    "apt-get install -y wget"
  ]
}

type에는 shell을 지정한다. 다음으로 inline 속성에 실행할 명령어들을 차례로 담은 배열로 지정한다. inline 속성을 사용하면 실행하고자 하는 명령을 설정 파일에 바로 작성할 수 있으며, 별도의 스크립트를 지정하고자 하는 경우에는 script(문자열, 하나의 스크립트)나 scripts(배열, 여러개의 스크립트) 속성을 사용하면 된다. 여기서는 inline 속성에 지정된 대로 패키지 리스트를 업데이트하고 wget을 설치한다.

{
  "type": "docker-import",
  "repository": "nacyot/ubuntu",
  "tag": "wget"
}

마지막으로 생성한 도커 이미지를 도커에 임포트하도록 post-processors 속성을 사용한다. repository 속성에는 새로운 이미지의 이름을 지정하고 tag에는 태그를 지정한다. 예제와 같이 지정하면 nacyot/ubuntu:wget 이미지가 만들어진다.

이제 조각나있는 설정 파일들을 한 데 모아보자.

{
  "builders": [{
    "type": "docker",
    "image": "ubuntu:14.04",
    "export_path": "nacyot-ubuntu-wget.tar"
  }],
  "provisioners": [{
      "type": "shell",
      "inline": [
        "apt-get update",
        "apt-get install -y wget"
      ]
  }],
  "post-processors": [{
      "type": "docker-import",
      "repository": "nacyot/ubuntu",
      "tag": "wget"
  }]
}

설정파일을 모두 작성했다. 실제로 빌드를 수행해보자.

$ packer build ./template.json
docker output will be in this color.

==> docker: Creating a temporary directory for sharing data...
==> docker: Pulling Docker image: ubuntu:14.04
docker: 14.04: Pulling from ubuntu

...

==> docker: Exporting the container
==> docker: Killing the container: f0e28c4f
==> docker: Running post-processor: docker-import
    docker (docker-import): Importing image: Container
    docker (docker-import): Repository: nacyot/ubuntu:wget
    docker (docker-import): Imported ID: 6b773d2f
Build 'docker' finished.  

성공적으로 빌드가 끝났다(boot2docker에서는 문제가 생길 수 있다. boot2docker에서는 스크립트가 볼륨을 통해서 정상적으로 주입되지 않는다. 이를 해결할 수 없는 것은 아니지만, 리눅스 환경이나 도커가 설치된 가상 머신에 직접 들어가서 패커를 사용할 것을 권장한다).

빌드 과정을 좀 더 자세히 살펴보도록 하자. 먼저 주어진 베이스 이미지로 머신이나 컨테이너를 실행한다. 현재 환경에서는 먼저 ubuntu:14.04 이미지를 풀해서 받아온 후, 이 이미지로 컨테이너를 실행한다. 그리고 프로비저너를 통해 컨테이너 안에서 스크립트를 실행(혹은 배포를 진행)하고 정상적으로 종료되면 그 상태를 이미지로 저장한다. 패커는 빌드가 성공하든 실패하든 빌드를 위해 준비한 설정들을 삭제하고, 머신/컨테이너 종료를 보장한다. 이 예제에서는 ubuntu:14.04를 기반으로 컨테이너를 실행하고 셸스크립트로 프로비저닝을 한다. 이를 통해 wget이 설치가 되고나면 컨테이너를 종료하고 이미지를 저장한다. 마지막으로 docker-import를 통해서 이 이미지를 nacyot/ubuntu:wget 이름으로 도커에 임포트한다.

자, 이제 이미지가 생성된 것을 확인해보자.

$ docker images
REPOSITORY    TAG  IMAGE ID     CREATED              VIRTUAL SIZE  
nacyot/ubuntu wget 6b773d2f87b4 About a minute ago   190.5 MB  

nacyot/ubuntu:wget 이미지가 추가되었다. 그렇다면 정말로 wget이 설치되었는지 테스트해보자. 먼저 기본 ubuntu:14.04 이미지에 wget이 없다는 것을 확인해본다.

$ docker run -it ubuntu:14.04 bash
[email protected]:/# wget --version  
bash: wget: command not found  

wget 명령어가 존재하지 않는다. 그렇다면 방금 빌드한 이미지를 테스트해보자.

$ docker run -it nacyot/ubuntu:wget bash
[email protected]:/# wget --version  
GNU Wget 1.15 built on linux-gnu.  

wget 명령어가 존재한다! 정상적으로 이미지가 만들어졌다. 여기까지 패커를 통해서 첫번째 이미지를 만들어보았다.

앤서블 플레이북(Ansible Playbook)으로 아마존 머신 이미지(AMI) 빌드하기

다른 조합으로 이미지를 만들어보자. 이번에는 앤서블로 아마존 머신 이미지를 빌드해본다. Template은 다음과 같은 형식으로 작성한다.

{
  "builders": [{
    "type": "amazon-ebs"
    // ...
  }],
  "provisioners": [{
    "type": "ansible-local"
    // ...
  }]
}

빌더와 프로비저너 설정도 작성한다. 빌더 설정은 다음과 같다.

{
  "type": "amazon-ebs",
  "access_key": "<AWS_ACCESS_KEY>",
  "secret_key": "<AWS_SECRET_KEY>",
  "region": "ap-northeast-1",
  "source_ami": "ami-cbf90ecb",
  "instance_type": "m3.medium",
  "ssh_username": "ec2-user",
  "ami_name": "CustomImage {{isotime | clean_ami_name}}"
}

도커 빌더보다 좀 더 복잡해보인다. 하지만 아마존에 익숙한 사람이라 생서하지는 않을 설정들일 것이다.

먼저 type에는 amazon-ebs를 지정했다. 그리고 access_keysecret_key에는 아마존 API 인증 정보를 입력한다. region은 이미지가 생성되는 지역, source_ami는 이미지를 생성할 베이스 이미지(ami-cbf90ecb는 아마존 리눅스이다), instance_type은 이미지를 빌드할 때 사용할 인스턴스 타입, ssh_username에는 ssh 사용자 이름(보통은 이미지에 따라 결정된다. 아마존 리눅스에서는 ec2-user), 마지막으로 ami_name에는 새로 생성될 이미지 이름을 지정한다.

packer의 json에서는 몇 가지 미리 정의되어있는 변수들을 사용할 수 있다. 이미지 이름에서 사용하는 isotime은 시간을 출력하며 | 다음의 clean_ami_name 필터를 통해서 이미지에서 사용할 수 없는 기호들을 미리 제거한다.

다음으로 프로비저너를 살펴보자.

{
  "type": "ansible-local",
  "playbook_file" : "ansible/playbook.yml",
  "playbook_dir": "/Users/../ansible"
}

앤서블을 프로비저너로 사용하고자 하는 경우에는 앤서블 플레이북을 미리 작성해야한다. type에는 ansible-local을 지정하고, 플레이북의 경로를 지정한다. 주제를 벗어나므로 여기서는 앤서블에 대해서는 추가로 설명하지 않는다. 김용환 님의 발표나 다른 자료를 참조하기 바란다.

전체 템플릿은 다음과 같다.

{
  "builders": [{
    "type": "amazon-ebs",
    "access_key": "<AWS_ACCESS_KEY>",
    "secret_key": "<AWS_SECRET_KEY>",
    "region": "ap-northeast-1",
    "source_ami": "ami-cbf90ecb",
    "instance_type": "m3.medium",
    "ssh_username": "ec2-user",
    "ami_name": "CustomImage {{isotime | clean_ami_name}}"
  }],
  "provisioners": [{
    "type": "ansible-local",
    "playbook_file" : "ansible/playbook.yml",
    "playbook_dir": "/Users/../ansible"
  }]
}

그럼 템플릿이 완성되었으니 빌드를 해보자.

$ packer build ./template.json
amazon-ebs output will be in this color.

==> amazon-ebs: Inspecting the source AMI...
==> amazon-ebs: Creating temporary keypair: packer 55e9b978-5a49...
==> amazon-ebs: Creating temporary security group for this instance...
==> amazon-ebs: Authorizing SSH access on the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
    amazon-ebs: Instance ID: i-12345678
==> amazon-ebs: Waiting for instance (i-12345678) to become ready...

...

==> amazon-ebs: Stopping the source instance...
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: CustomImage 2015-09-04T15-32-08Z
    amazon-ebs: AMI: ami-12345678
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:

ap-northeast-1: ami-12345678  

성공적으로 ami가 만들어졌다(여기서는 ami-12345678으로 대체한다). 이제 이 ami를 가지고 새로운 인스턴스를 실행할 수 있다.

AMI 이미지를 빌드하는 것도 도커를 빌드하는 것과 크게 다르지 않다. 이번에는 AWS 위에서 인스턴스를 실행하고 그 위에서 프로비저닝을 수행한다. 그리고 그 결과를 AMI 이미지로 만든다. 이번에도 성공하든 실패하든 임시 설정들을 삭제하고 인스턴스를 종료하는 것은 패커가 보장한다. 패커 빌드 중에 강제 종료를 하더라도, 실행한 인스턴스가 종료될 때까지 기다린다

발표 자료 - Packer를 통한 AMI 자동 빌드 시스템 구축

다음은 도커 서울 밋업에서 발표한 내용이다.

결론

여기까지 패커를 통해서 간단한 이미지를 만들어보았다. 눈치 챈 사람이 있을 지도 모르겠지만 패커는 베이그런트와 매우 비슷하다. 베이그런트에는 프로바이더와 프로비저너라는 개념이 있는데, 이는 각각 빌더와 프로비저너에 대응한다. 따라서 패커 이미지 빌드를 실행하기에 앞서, 베이그런트로 빌드 환경을 미리 구성해보는 것도 얼마든지 가능하다. 베이그런트는 프로비저닝으로 개발환경을 구축한다면, 패커에서는 프로비저닝으로 이미지를 생성한다.

Hashicorp의 도구들을 보면 단순히 하나의 플랫폼만을 위한 유틸리티가 아니라 다양한 플랫폼과 도구를 지원하는 경우가 많다. 베이그런트도 그렇고, 다양한 벤더의 서비스들을 조합해주는 테라폼(Terraform) 역시 그렇다. 그리고 패커도 그렇다. 패커 역시 단순히 특정 플랫폼의 이미지를 만드는 도구가 아니라, 다양한 플랫폼에 대한 이미지 생성 과정을 추상화해준다. 이러한 특징 덕분에 플랫폼 간 이동을 쉽게 해주며, 인스턴스 기반 환경에서 컨테이너 기반으로 이동하는 가교 역할을 하는 것도 가능하다. 또한 이 전체를 코드로서 관리할 수 있기 때문에, 재현 가능성도 높아지고 관리 역시 쉬워진다.

이미지를 만든다는 단순한 개념에서 출발하지만, 패커는 서비스들 사이를 이어주는 강력한 도구이다. NACYOT



이 글이 도움이 되셨나요?

Feedly에서 Remotty 블로그 구독하기
페이스북에서 Remotty 구독하기