Packer와 Ansible을 이용한 표준 AMI 만들기
안녕하세요 저는 작년 우아한테크캠프 2기를 마치고 10월에 시스템신뢰성개발팀에 입사한 김규남이라고 합니다.
이번 글에서는 팀 내에서 Packer와 Ansible을 이용해 AMI를 코드레벨로 어떻게 관리하고 있는지에 대해 공유드리려고 합니다.
용어설명
저는 처음 입사할 당시에 인프라와 관련된 지식이 전혀 없었습니다.
처음 들어보는 용어들이 많았었기에 많이 혼란스러웠는데,
이 글을 보시는 분들 중에도 혹시 저와 같은 분이 계실 수 있어 간략히 용어를 정리하고 넘어가겠습니다.
AMI(Amazon Machine Image)
AMI(Amazon Machine Image)란 아래처럼 인스턴스를 처음 셋업할 때 지정하는 소프트웨어 구성이 기재된 템플릿입니다.
Amazon에서 인스턴스를 셋업했을 때 aws-cli가 설치되어 있는 건 Amazon에서 제공하는 AMI들에 그 부분이 프로비저닝 되어 있기 때문입니다.
프로비저닝(Provisioning)
프로비저닝이란 사용자의 요구에 맞게 서버를 설정해 두었다가 필요 시 서버를 즉시 사용할 수 있는 상태로 미리 준비해 두는 것을 말합니다.
ex) 인스턴스에 JDK를 설치해두거나 MySQL을 설치하고 DB를 생성해두는 작업 등이 여기에 해당됩니다.
Packer
Packer는 여러 플랫폼에 대해 동일한 시스템 이미지를 작성하기위한 오픈 소스 도구입니다.
AWS의 AMI, Azure Image, Google Cloud Image 등을 스크립트 파일을 이용해서 생성할 수 있습니다.
Ansible
Ansible은 프로비저닝에 대한 자동화를 제공하는 소프트웨어입니다.
Ansible은 Docker, Vagrant, EC2등에 대한 프로비저닝을 제공합니다.
yaml 문법으로 작성한 ansible 파일을 통해서 프로비저닝 설정들을 관리할 수 있습니다.
Scale Out
서버의 대수를 늘려서 처리 능력을 향상시키는 것을 말합니다.
ex) 트래픽이 많아 1대의 인스턴스로 처리가 어려울 경우 1대의 인스턴스를 증설해 2대로 요청을 처리하는 경우가 이에 해당합니다.
AWS Auto Scaling Group
인스턴스 조정 및 관리 목적으로 구성된 Amazon EC2 인스턴스들의 묶음입니다.
토이 프로젝트를 진행하거나, 간단한 어플리케이션을 구동 할 때는 단일 인스턴스를 셋업해서 개발을 진행하게 됩니다.
하지만 운영 환경에서는 Scale Out이 빈번하게 일어나기 때문에 Auto Scaling Group을 사용하게 됩니다.
개발 환경 / 아키텍처
AMI를 생성할 때 직접 인스턴스를 생성한 뒤 인스턴스 내부에서 수동으로 작업을 하고 해당 인스턴스를 이용해 AMI를 만들 수도 있습니다.
하지만 그런 경우 해당 AMI에 어떤 내용이 적용되어 있는지에 대해 관리하기 어렵다는 단점이 있습니다.
그런 단점을 극복하고자 HashiCorp의 Packer를 이용해 AMI를 셋업하고, Ansible을 이용해 프로비저닝을 진행하기로 방향을 잡았습니다.
아키텍처
구상했던 아키텍처는 아래 이미지와 같습니다.
-
Jenkins에서 AMI 빌드 Job을 실행한다.
-
Jenkins 서버에서 Github에 코드레벨로 관리되고 있는 Packer와 Ansible의 코드를 pull 받는다
-
Jenkins 서버에서 Packer가 인스턴스를 셋업한다.
-
Ansible을 이용해 셋업된 인스턴스에 프로비저닝 작업을 진행한다.
-
Packer가 프로비저닝이 완료된 인스턴스를 카피해 AMI를 생성한다.
Packer와 Ansible 설치하기
Installation
설치가 잘 진행되지 않는다면, 첨부한 링크의 내용을 참고하셔서 설치를 진행하시면 됩니다.
- Packer
$ brew install packer
or
$ wget https://releases.hashicorp.com/packer/1.3.1/packer_1.3.1_linux_amd64.zip
$ unzip packer_1.3.1_linux_amd64.zip
- Ansible
$ brew install ansible
or
$ sudo pip install ansible
AMI 생성을 위한 Packer 코드 작성하기
Packer Repository의 디렉토리 구조
packer
├── jdk8_ami.json
jdk8_ami.json
{
"_comment": "JDK8 base AMI using Amazon Linux (amzn-ami-hvm-2018.03.0.20180811-x86_64-ebs)",
"variables": {
"ami_name": "{{isotime "060102-1504"}}-base-ami
},
"builders": [
{
"type": "amazon-ebs",
"source_ami": "Source가 될 Image의 id를 지정합니다.",
"vpc_id": "Instance가 올라갈 VPC 명을 지정해주시면 됩니다.",
"subnet_id": "Instance가 올라갈 Subnet 명을 지정합니다.",
"security_group_id": "Instance가 올라가 있는 동안 사용할 Security Group을 지정합니다.",
"instance_type": "t2.small",
"ssh_interface": "private_ip",
"ssh_username": "ec2-user",
"ami_name": "{{user `ami_name` | clean_ami_name}}",
"ami_description": "OpenJDK 8 with Amazon Linux",
"tags": {
"Name": "{{user `ami_name` | clean_ami_name}}",
"BaseAMI_Id": "{{ .SourceAMI }}",
"BaseAMI_Name": "{{ .SourceAMIName }}",
"TEAM": "시스템신뢰성개발팀",
},
"ami_users": ["이 AMI를 공유할 AWS Account ID 목록"]
}
],
"provisioners": [
{
"type": "ansible",
"playbook_file": "playbook/jdk8.yml"
}
]
}
- _comment
말 그대로 comment 용도입니다.
- variables
변수를 선언할 수 있습니다. 위의 코드에서는 ami_name이라는 변수에 생성 시간을 이용해 만들어줄 AMI의 이름을 넣어주었습니다.
시간을 나타내는 부분이 조금 특이한데, 앞에서부터 순서대로 06은 Year 01은 Month 02는 Date 15는 Hour 04는 Minute를 나타냅니다.
자세한 내용은 아래 내용을 참고하시면 됩니다.
Packer Isotime function format
builders
builders 부분은 인스턴스를 생성하고, 그 머신을 AMI로 만드는 코드가 포함되어 있습니다.
- type
type에서는 Packer의 builder 타입을 지정할 수 있습니다.
Packer에서는 구축 전략에 따라 아래 4가지의 builder 타입을 제공합니다.
위 코드에서는 가장 간단하지만 AMI 생성 시간이 조금 오래 걸리는 amazon-ebs를 사용합니다.
amazon-ebs
인스턴스를 생성하고 프로비저닝 작업 이후 새 AMI로 다시 패키징합니다. 가장 간단한 방법입니다.
amazon-instance
인스턴스를 실행 및 프로비저닝 한 후 S3에 업로드해서 AMI를 생성합니다.
amazon-chroot
Chroot 환경을 이용해서 AMI를 생성합니다 인스턴스를 시작 할 필요가 없어서 가장 빠르지만 고급 빌더이기 때문에 신규 사용자에게 권장하지 않습니다.
amazon-ebssurrogate
인스턴스 시작 없이 AMI를 생성합니다. chroot 빌더와 비슷하게 작동합니다. 고급 빌더이기 때문에 마찬가지로 신규 사용자에게 권장하지 않습니다.
- source_ami
베이스가 될 이미지의 id를 지정합니다.
저는 인스턴스 생성 시 AWS에서 제공하는 Base Image의 id를 지정했습니다.
- vpc_id
인스턴스가 생성 될 vpc의 아이디를 지정하면 됩니다.
- subnet_id
인스턴스가 생성 될 subnet의 아이디를 지정하면 됩니다.
- security_group_id
인스턴스에 지정할 Security Group의 아이디 입니다.
여기서 주의해야 하실 점이 있는데요 Security Group을 지정하시지 않으면 디폴트로 22번 포트가 any로 열리게 됩니다.
key가 없어 외부에서 접근은 할 수 없겠지만, 따로 지정해주셔서 외부의 접근을 차단해주시는 게 좋을 것 같습니다.
- instance_type
인스턴스가 올라 갈 타입입니다.
프로비저닝 작업 시 많은 성능을 요하는 작업이 없다면 제일 낮은 타입을 지정해주셔도 무방할 것 같습니다.
- ssh_interface
public_ip, private_ip, public_dns, private_dns로 설정할 수 있습니다.
ssh host를 public ip, private ip, public dns, private dns 어떤 것으로 사용할지를 지정하는 옵션입니다.
- ssh_username
ssh username입니다.
AWS 인스턴스에 접근하기 때문에 ec2-user로 지정했습니다.
- ami_name
말 그대로 식별할 수 있는 ami의 이름입니다.
위에서 변수로 선언한 값을 지정해주고 있습니다.
- tags
AMI에 태그 값을 부여합니다.
- ami_users
여러 AWS 계정이 있을 경우에 대한 엑세스 권한 설정입니다.
기본적으로 Packer가 셋업되는 계정에서는 생성한 AMI에 접근할 수 있습니다.
다른 AWS 계정에 이 AMI를 공유하고 싶으면 각 AWS 계정의 id 정보를 배열로 기입해야 합니다.
- provisioners
프로비저닝을 어떤 방법으로 할 지를 지정하는 부분입니다.
Ansible, Chef, Shell script 등 다양한 방법으로 프로비저닝을 할 수 있습니다.
위 코드에서는 Ansible playbook을 이용합니다.
Ansible을 이용한 프로비저닝 코드 작성하기
프로비저닝 된 내용이 꽤 많고, 회사 코드가 포함되어 있어서 일부 코드와 구조만 간단히 설명드리겠습니다.
Packer/Ansible 코드의 Repository는 아래와 같은 구조를 가지고 있습니다.
playbook 디렉토리 하위에 Role Directory Structure에 맞춰 각각의 프로비저닝 코드를 구성해두었습니다.
packer
├── jdk8_ami.json
playbook
├── jdk8.yml
└── roles
├── common
│ ├── handlers
│ │ └── main.yml
│ └── tasks
│ └── main.yml
├── jdk8
│ └── tasks
│ └── main.yml
├── nginx
│ └── tasks
│ └── main.yml
└── pinpoint
├── files
│ └── pinpoint-bootstrap-1.8.0.jar
└── tasks
└── main.yml
..... 기타 등등
jdk8.yml은 jdk8 ami에 필요한 프로비저닝 관련 role들을 읽고 프로비저닝을 진행하도록 정의되어 있습니다.
- name: Build Amazon Linux Base Image (JDK8)
hosts: all
roles:
- common
- kernel
- limits
- jdk8
- nginx
- pinpoint
//...
- common
common role에서는 디렉토리에 정의된 내용을 읽고 공통적으로 필요한 프로비저닝을 진행합니다.
ntp를 삭제하고, yum update를 진행하며 timezone 설정을 진행하는 프로비저닝 작업들이 common role에 포함됩니다.
playbook/roles/common/tasks/main.yml
- name: remove unused yum packages -ntp
become: yes
yum:
name: ntp
state: absent
- name: update yum packages
yum: list=updates update_cache=true
- name: set Asia/Seoul timezone
become: yes
timezone:
name: Asia/Seoul
//....
- jdk8
jdk8 role에서는 jdk8 버전을 설치하고, 현재 AMI에 jdk 버전을 설정하는 프로비저닝 작업을 진행합니다.
playbook/roles/jdk8/tasks/main.yml
- name: install JDK
become: yes
yum:
name: java-1.8.-----
state: latest
- name: correct java version selected
become: yes
alternatives:
name: java
path: /usr/lib/jvm/jre-1.8..-----.x86_64/bin/java
- nginx
nginx role에서는 nginx와 관련된 패키지들의 설치와 관련된 프로비저닝 작업이 진행됩니다.
playbook/roles/nginx/tasks/main.yml
- name: install nginx and modules
become: yes
yum: pkg={{item}} state=latest
with_items:
- nginx
//...
간단한 role을 몇개 살펴봤는데요, 각각의 role은 아래와 같은 디렉토리 구조에 맞춰서 작성하시면 됩니다.
common/
└── files/
└── templates/
└── tasks/
└── handlers/
└── vars/
└── defaults/
└── meta/
Role과 디렉토리 구조에 관한 자세한 내용은 아래 문서를 참고하실 수 있습니다.
그리고 여기에는 작성되어 있지 않지만, 프로비저닝에 대한 테스트 코드도 작성하실 수 있습니다.
매번 작업 시 마다 AMI를 셋업하고, 그 AMI로 인스턴스를 생성하는 작업은 굉장히 지루하고 불편합니다.
팀 내에서는 Vagrant를 이용해 인스턴스를 셋업하고, 프로비저닝만 하는 방식으로 테스트를 진행했는데요, ServerSpec이나 혹은 다른 라이브러리를 이용해 프로비저닝된 서버에 대한 테스트를 진행하시면 작업 할 때 편하게 진행하실 수 있을 것 같습니다.
코드를 이용해 AMI 빌드하기
Jenkins에서 Packer와 Ansible 코드가 작성된 Git Repository를 지정하고
Execute Shell로 아래 내용을 지정합니다.
packer build packer/jdk8_ami.json
그리고 Job을 실행하면 인스턴스를 생성하고 프로비저닝을 진행합니다.
amazon-ebs:
amazon-ebs: TASK [common : install sysstat] ************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: TASK [common : install git] ****************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: TASK [common : install vim] ****************************************************
amazon-ebs: ok: [default]
amazon-ebs:
amazon-ebs: TASK [common : install telnet] *************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: TASK [common : install jq] *****************************************************
amazon-ebs: changed: [default]
amazon-ebs:
amazon-ebs: TASK [common : Enable service chronyd, and not touch the state] ****************
amazon-ebs: changed: [default]
amazon-ebs:
....
==> amazon-ebs: Adding tags to AMI (ami-0000000000000)...
==> amazon-ebs: Creating AMI tags
amazon-ebs: Adding tag: "TEAM": "시스템신뢰성개발팀"
amazon-ebs: Adding tag: "TYPE": "EC2.ami"
amazon-ebs: Adding tag: "MODULE": "sre-ami"
==> amazon-ebs: Creating snapshot tags
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' finished.
==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
ap-northeast-2: ami-000000000000
인스턴스를 프로비저닝하고 그 인스턴스를 카피해 AMI를 생성합니다.
생성된 AMI를 이용해 인스턴스를 생성하고 접속하면 아래와 같은 화면을 볼 수 있습니다.
__ __ ______ ______ __ __ ______
/\ \ _ \ \/\ __ \/\ __ \/\ \ _ \ \/\ __ \
\ \ \/ ".\ \ \ \/\ \ \ \/\ \ \ \/ ".\ \ \ __ \ Woowabros standard AMI
\ \__/".~\_\ \_____\ \_____\ \__/".~\_\ \_\ \_\
\/_/ \/_/\/_____/\/_____/\/_/ \/_/\/_/\/_/
인스턴스에 아스키 아트를 추가했는데요, 각자의 팀이나 회사의 개성이 드러나는 아스키 아트를 추가하시면 좋을 것 같습니다.
생성된 AMI의 활용
이렇게 만든 AMI는 직접 인스턴스를 생성할 때 Base AMI로 지정하거나, Auto Scaling Group에 사용됩니다.
AWS Auto Scaling Group에는 Launch Configuration이나 Launch Template을 지정해 활용할 수 있는데, Launch Template 혹은 Configuration 생성 시 이 AMI가 사용되게 됩니다.
Java를 사용하는 어플리케이션이 배포되는 Auto Scaling Group이 있다면 JDK8, Pinpoint, 취약점 조치 및 커널 파라미터 튜닝 등이 적용된 AMI를 사용하는 Launch Template을 Auto Scaling Group에 지정해주시면 됩니다.
정리
사내의 인스턴스들에 대한 정보가 노출될 수 있어 상세한 코드를 작성하지 않았지만 AMI에는 커널 파라미터 튜닝, 취약점과 관련된 조치들이 포함되어 있습니다.
AMI가 제대로 관리되고 있지 않다면 커널 파라미터 튜닝이 누락된 인스턴스를 운영에 사용하여 장애가 발생할 수 있고 취약점을 가지고 있는 인스턴스를 사용하게 되어 보안 이슈가 생길 수 있습니다.
이렇게 코드로 AMI에 프로비저닝 될 내용을 작성하고 관리하니, 취약점 조치나 커널 파라미터 튜닝이 어떻게 적용되어 있는지 확인하기가 용이했습니다.
마치며
SRE에 대한 경험이나 지식이 전무한 상태로 들어와 많은 어려움을 겪었었습니다.
인프라나 네트워크에 대한 지식도 전무하고 용어조차 알아듣지 못해서 같이 일하던 팀원분들이 많이 고생하셨을 것 같은데, 도와준 팀원분들에게 감사의 말씀을 전하고 싶습니다.
기술 블로그에 글을 적는건 부담이 된다는 느낌이 들어 입사 후 처음 글을 남기는 것 같습니다.
앞으로 기회가 된다면 신입의 관점에서, 인프라를 접하면서 어려움을 겪으실 분들에게 도움이 될 수 있는 글들을 작성해보고 싶습니다.
입사 초반에 팀원들과 진행했던 내용을 다시 찾아보며 작성한거라 문법이 지금과 다르거나 잘못된 내용이 있을 수 있습니다. 혹시나 잘못되거나 변경된 내용이 있다면 댓글로 남겨주시면 감사하겠습니다.