패커를 사용한 도커 이미지와 AMI 만들기(feat. Ansible)도커 이미지와 AMI 이미지 빌드
패커Packer는 가상머신, 컨테이너 이미지 생성기입니다. 이미지는 일반적으로 가상머신의 특정한 상태를 그대로 저장해서 만들어집니다. 패커에서는 빌더Builder 컴포넌트를 통해 다양한 플랫폼을 지원하며, 프로비저너Provisioner 컴포넌트를 통해 다양한 도구로 이미지를 빌드할 수 있습니다. 이 글은 네번째 도커 서울 밋업Docker Seoul Meetup에서 발표한 내용을 기반으로 작성된 글로, 패커에 대한 기본적인 기능들과 사용법에 대해서 소개합니다.
패커(Packer): 범용 이미지 빌드 도구
패커는 하시코프Hashicorp에서 만든 인프라 관리 도구 중에 하나입니다. 하시코프에서는 가상 환경 관리 도구로 유명한 베이그런트Vagrant를 시작으로 서버 클러스터 도구 서프Serf, 서비스 디스커버리 도구 컨설Consul, 코드로서의 인프라스트럭처를 구현한 테라폼Terraform 등 다양한 인프라 자동화 도구들을 오픈소스로 만들고 있습니다. 패커 역시 이러한 인프라 자동화 도구 중 하나로 다양한 플랫폼에서 사용가능한 이미지를 동적으로 생성할 수 있게 도와줍니다.
공식 사이트에서는 패커를 다음과 같이 소개합니다.
패커Packer는 하나의 설정 소스로부터 여러 플랫폼을 지원하는 머신/컨테이너 이미지를 만드는 도구이다.
여기서 이미지라는 단어에 주목을 할 필요가 있습니다. 가상 머신의 이미지는 머신의 특정한 상태를 그대로 저장하고, 나중에 재사용 가능하도록 준비해둔 것입니다. 하지만 이 방식으로 생성된 이미지에는 몇 가지 문제가 있습니다. 먼저 사용중인 가상머신의 상태를 그대로 저장하기 때문에 이 상태에 이르기까지의 과정은 소실 됩니다. 다르게 말하자면 이미지의 현재 상태를 처음부터 다시 재현하는 게 거의 불가능합니다. 또한 최신 상태를 저장하기 위해서 계속 이미지를 만들어야 하는데, 관리 비용은 이미지 수에 비례해서 늘어나게 됩니다.
패커에서는 이미지를 만들 때 가상머신의 특정 상태를 저장하는 방법을 사용하지 않습니다. 먼저 베이스 이미지를 기반으로 프로비저너를 통해서 가상 머신에 패키지 설치와 환경설정 등의 작업을 수행합니다. 그리고 프로비저닝이 끝난 상태를 빌더를 통해서 특정 플랫폼에서 사용 가능한 이미지로 저장합니다. 즉, 최종 결과물을 저장하는 것이 아니라 이미지 생성 과정에 대한 모든 정보는 코드로 관리합니다. 따라서 같은 과정을 재현해 다양한 플랫폼에서 같은(유사한) 이미지를 만들어 사용하는 것도 가능합니다.
여기서부터는 프로비저너와 빌더, 그리고 컴포넌트들의 설정을 포함한 템플릿Template에 대해서 좀 더 살펴보겠습니다.
빌더(Builder), 프로비저너(Provisioner), 템플릿(Template)
패커에서 가장 중요한 개념은 프로비저너, 빌더, 템플릿 이 세가지입니다.
빌더로 이미지를 생성할 플랫폼을 지정할 수 있습니다. 아마존 웹 서비스Amazon Web Service, 디지털 오션Digital Ocean과 같은 클라우드는 물론 버추얼박스VirtualBox와 VM웨어VMWare와 같은 가상 머신의 이미지, 도커Docker 등 컨테이너 이미지도 생성할 수 있다. 현재 패커에서 지원하는 빌더 목록은 다음과 같습니다다.
- 아마존 EC2Amazon EC2 AMI
- 디지털오션DigitalOcean
- 도커Docker
- 구글 컴퓨트 엔진Google Compute Engine
- 오픈스택OpenStack
- 패러럴스Parallels
- QEMU
- 버추얼박스VirtualBox
- VM웨어VMware
- Custom
- Null
다음으로 프로비저너는 이미지 생성할 때 사용할 빌드 도구를 의미합니다. 셸스크립트와 같은 원시적인 방법은 물론 앤서블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 파일로 이미지를 빌드합니다. 템플릿은 JSON 포맷으로 작성된 설정 파일입니다. 이 설정 파일은 빌더와 프로비저너를 비롯한 개별 컴포넌트 설정들로 구성됩니다. 템플릿의 기본적인 포맷은 다음과 같습니다.
빌더와 프로비저너를 지정하고, 세부 설정을 작성합니다. 여기서 빌더와 프로비저너 설정이 배열 안에 들어가 있는 걸 볼 수 있습니다. 빌더가 다수 지정되면 다양한 플랫폼의 이미지를 같은 프로비저너를 통해서 생성할 수 있습니다. 프로비저너가 다수 지정되면 각각의 프로비저너를 순차적으로 실행합니다. 예를 들어 셸스크립트로 아주 기본적인 설정을 한 다음 앤서블 플레이북을 적용하는 것이 가능합니다.
셸스크립트(shellscript)로 도커(Docker) 이미지 빌드
이제 실전입니다. 패커를 사용해서 이미지를 생성해보겠습니다. 여기서는 셸스크립트를 사용해 도커 이미지를 만듭니다. 보통 도커 이미지는 Dockerfile
을 사용해서 빌드합니다. 하지만 패커를 사용하면 Dockerfile
을 작성하지 않고도, 셸스크립트나 구성 관리Configuration Management 도구를 사용해 도커 이미지를 작성할 수 있습니다.*
* 굳이 패커를 사용해 도커 이미지를 만들 필요가 있을까요? 꼭 패커를 써야할 이유는 없습니다. 단 이런 경우는 생각해볼 수 있습니다. 이미 CM 툴을 잘 사용하고 있는데 도커를 도입하는 경우라면, 패커가 가뭄의 단비처럼 느껴질 것입니다.
여기서는 wget
이 설치된 ubuntu
이미지를 만들어봅니다. 이를 Dockerfile
로 작성하면 다음과 같습니다.
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y wget
이와 같은 역할을 하는 패커 템플릿(설정 파일)을 작성해보겠습니다. 기본적인 구조는 아래와 같습니다.
{
"builders": [{
"type": "docker"
// ...
}],
"provisioners": [{
"type": "shell"
// ...
}],
"post-processors": [{
"type": "docker-import"
// ...
}]
}
먼저 첫번째 빌더 타입으로 docker
를 지정합니다. 여기서는 도커 이미지만 만들기 때문에 하나의 빌더만을 지정합니다. 프로비저너로는 shell
을 지정합니다. 따로 설명하지 않았지만 도커 이미지를 빌드 할 때는 post-processors
를 사용해야합니다. docker
빌더는 기본적으로 압축파일로 이미지를 생성합니다. 이를 도커에서 사용하려면 docker-import
포스트 프로세서를 통해 생성한 이미지를 임포트해야 하기 때문입니다.
하나씩 구체적인 설정을 살펴보겠습니다. 먼저 빌더는 다음과 같이 설정합니다.
image
에는 빌드 시 사용할 베이스 이미지를 지정합니다. 이 이미지를 기반으로 새로운 이미지를 빌드합니다. export_path
는 도커 이미지를 내보낼 경로(파일명)을 지정합니다. 다음으로 프로비저너를 살펴보겠습니다.
type
에는 shell
을 지정합니다. 다음으로 inline
속성에 실행할 명령어들을 차례로 담은 배열로 지정합니다. inline
속성을 사용하면 실행하고자 하는 명령을 설정 파일에 바로 작성할 수 있으며, 별도의 스크립트를 지정하고자 하는 경우에는 script
(문자열, 하나의 스크립트)나 scripts
(배열, 여러개의 스크립트) 속성을 사용하면 됩니다. 여기서는 inline
속성에 지정된 대로 패키지 리스트를 업데이트하고 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
root@79b1000960e5:/# wget --version
bash: wget: command not found
wget
명령어가 존재하지 않습니다. 그렇다면 이번에는 방금 빌드한 이미지를 테스트해보겠습니다.
$ docker run -it nacyot/ubuntu:wget bash
root@2767bc99a7fc:/# wget --version
GNU Wget 1.15 built on linux-gnu.
wget
명령어가 존재합니다! 의도한 대로 이미지가 만들어졌습니다. 여기까지 패커를 통해서 첫번째 이미지를 만들어보았습니다.
앤서블 플레이북(Ansible Playbook)으로 아마존 머신 이미지(AMI) 빌드하기
이번에는 다른 조합으로 이미지를 만들어보겠습니다. 이번에는 앤서블로 아마존 머신 이미지를 빌드해봅니다. 템플릿은 다음과 같은 형식으로 작성합니다.
{
"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_key
와 secret_key
에는 아마존 API 인증 정보를 입력합니다. region
은 이미지가 생성되는 지역, source_ami
는 이미지를 생성할 베이스 이미지(ami-cbf90ecb
는 아마존 리눅스입니다), instance_type
은 이미지를 빌드할 때 사용할 인스턴스 타입, ssh_username
에는 SSH 사용자 이름*, 마지막으로 ami_name
에는 새로 생성될 이미지 이름을 지정합니다
* 기본 사용자 이름은 일반적으로 베이스 이미지에 따라 결정됩니다. 아마존 리눅스에서는 ec2-user
, 우분투에서는 ubuntu
가 됩니다.
패커의 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를 가지고 새로운 인스턴스를 실행할 수 있다.
* 여기서는 ami-12345678
은 실제 AMI의 이름이 아닙니다. 빌드 결과에서 실제 값을 확인해야합니다.
패커를 사용하면 AMI 이미지를 빌드하는 것도 도커를 빌드하는 것과 크게 다르지 않습니다. 이번에는 도커 컨테이너가 아니라, AWS 위에서 인스턴스를 실행하고 그 위에서 프로비저닝을 수행합니다. 그리고 그 결과를 AMI 이미지로 만듭니다. 이번에도 성공하든 실패하든 임시 설정들을 삭제하고 인스턴스를 종료하는 것은 패커가 보장해줍니다. 패커 빌드 중에 강제 종료를 하더라도, 실행한 인스턴스가 종료될 때까지 기다립니다.
결론
여기까지 패커를 통해서 간단한 이미지를 만들어보았습니다. 눈치 챈 사람이 있을 지도 모르겠지만 패커는 베이그런트와 매우 비슷한 구조를 가지고 있습니다. 베이그런트에는 프로바이더와 프로비저너라는 개념이 있는데, 이는 각각 빌더와 프로비저너에 대응합니다. 따라서 패커 이미지 빌드를 실행하기에 앞서, 베이그런트로 빌드 환경을 미리 구성해보는 것도 얼마든지 가능합니다. 베이그런트가 프로비저너로 개발환경을 구축한다면, 패커는 프로비저너로 이미지를 생성합니다.
하시코프의 도구들을 보면 단순히 하나의 플랫폼만을 위한 유틸리티가 아니라 다양한 플랫폼과 도구를 지원하는 경우가 많습니다. 베이그런트Vagrant도 그렇고, 다양한 벤더의 서비스들을 조합해주는 테라폼Terraform 역시 이런 특징을 공유하고 있습니다. 그리고 패커도 그렇습니다. 패커 역시 특정 플랫폼의 이미지를 만드는 도구가 아니라, 다양한 플랫폼에 대한 이미지 생성 과정을 추상화해주는 역할을 합니다. 이러한 특징 덕분에 플랫폼 간 이동을 쉽게 해주며, 인스턴스 기반 환경에서 컨테이너 기반으로 이동하는 가교 역할을 하는 것도 가능합니다. 또한 이 전체를 코드로서 관리할 수 있기 때문에, 재현 가능성도 높아지고 관리 역시 쉬워집니다.
패커는 이미지를 만든다는 단순한 개념에서 출발하지만, 서비스들 사이를 이어주는 강력한 도구라고 할 수 있습니다.
같이 보면 좋은 자료들
- 네 번째 도커 서울 밋업 라이트닝 토크 - Packer를 통한 AMI 자동 빌드 시스템 구축 - Speaker Deck