Packer 를 이용한 EC2 AMI 생성하기
Packer 는 Terraform 으로 유명한 HashiCorp 에서 만든 프로비저닝(Provisioning) 툴 이다. Packer 는 다양한 IT 리소스를 지원하기 위해서 플러그인(Plugin) 제공하며 사용해야 한다. AWS 에 EC2 AMI 생성을 위해서 Packer 의 AWS 플러그인을 사용해야 한다.
AWS EC2 는 운영체제를 기반으로 생성되는 자원임으로 운영체제에 대한 설정도 있어야 한다. OS 의 많은 설정들을 Packer 는 Ansible 을 이용해 해준다. 따라서 Packer 를 이용한 EC2 AMI 를 생성하기 위해서는 Ansible 도 할 수 있어야 한다. 당연히 Packer 에서 Ansible 을 사용하기 위해서는 플러그인을 이용해야 한다.
Python3.12 설치
Ansible 이 필요한데, 되도록이면 Python3.12 를 설치하는게 좋다. Ansible 을 가장 최신판으로 사용하길 원하기 때문인데, Ansible 을 이용해서 OS 설정을 자동화 해야하는데 최신의 리눅스에 각종 설정컴포넌트들은 최신판에 대부분 구현 된다.
그렇다고 Ansible 을 무조건 최신판을 사용하는게 능사는 아니다. 최신의 Ansible 의 경우에는 ‘yum’ builtin 모듈이 제거 됐다. 레드햇 기반의 배포판의 경우에는 이제 dnf 를 다 사용함으로 인해서 yum 을 제거한 것인데, 과거의 레드햇 기반 배포판을 여전히 사용하고 있다면 최신판 사용은 권장되지 않는다.
Ansible 은 Python 에 기반함으로 Python 을 설치해서 pip 명령어를 이용해 설치해주면 된다. 물론 Virtual Environment 를 만들어서 해줘도 된다.
디렉토리 구조
검색을 해보면 Packer 를 사용하기 위해 디렉토리 구조를 정의하기도 한다. 하지만 그렇게 전문적으로 하는게 아닌지라, 디렉토리 구조를 별도로 정의하지 않는다.
대신, Packer 를 위한 파일과 Ansible 을 위한 파일 그리고 Python 에 가상디렉토리 환경을 위해서 각각 디렉토리를 정의해줄 필요는 있다.
1 2 |
]$ ls -d .ansible .python3 .packer .ansible .packer .python3 |
Ansible 의 디렉토리 구조는 Packer 설정에서 Ansible 의 루트 디렉토리를 지정해줌으로써 Packer 가 인식하게 된다. 디렉토리 구조는 대략 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
]$ tree -p -d . ├── [drwx------] cp ├── [drwxrwxr-x] inventory │ └── [drwxrwxr-x] production ├── [drwxr-xr-x] library ├── [drwxrwxr-x] module_utils ├── [drwxr-xr-x] roles │ ├── [drwxr-xr-x] redhat │ │ ├── [drwxr-xr-x] files │ │ ├── [drwxr-xr-x] tasks │ │ └── [drwxr-xr-x] templates └── [drwx------] tmp |
전형적인 Ansible 의 표준적인 구조다. library 에는 커스텀 모듈을 작성해서 넣으면 된다. 이러한 디렉토리 구조와 더불어서 Ansible 을 위한 설정 파일은 대략 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[defaults] library = ./library module_utils = ./module_utils roles_path = ./roles local_tmp = ./tmp remote_user = ec2-user remote_port = 22 transport = smart module_lang = C module_set_locale = False #host_key_checking = False #timeout = 10 [privilege_escalation] #become = False become_method = sudo become_user = root become_ask_pass = False |
복잡하고 많은 설정은 필요하지 않다. 기본적인 설정만 있으면 동작하는데 무리가 없다. 접속자 ID 나 포트는 어짜피 Packer 에서 정의된것을 사용하기 때문에 별 다른 의미가 없다. 디렉토리 구조는 Packer 사용하는데도 적용되니 적절하게 지정해 줘야 한다.
AWS Role
만일 맥을 사용하거나 리눅스를 데스크탑으로 사용하고 있다면 AWS EC2 AMI 생성을 위한 IAM 계정에 Access Key, Secret Key 를 가지고 있을 것이다. IAM 계정에는 다음과 같이 AWS EC2 AMI 생성을 위한 정책(Policy) 가 적용되어 있어야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:AttachVolume", "ec2:AuthorizeSecurityGroupIngress", "ec2:CopyImage", "ec2:CreateImage", "ec2:CreateKeypair", "ec2:CreateSecurityGroup", "ec2:CreateSnapshot", "ec2:CreateTags", "ec2:CreateVolume", "ec2:DeleteKeyPair", "ec2:DeleteSecurityGroup", "ec2:DeleteSnapshot", "ec2:DeleteVolume", "ec2:DeregisterImage", "ec2:DescribeImageAttribute", "ec2:DescribeImages", "ec2:DescribeInstances", "ec2:DescribeInstanceStatus", "ec2:DescribeRegions", "ec2:DescribeSecurityGroups", "ec2:DescribeSnapshots", "ec2:DescribeSubnets", "ec2:DescribeTags", "ec2:DescribeVolumes", "ec2:DetachVolume", "ec2:GetPasswordData", "ec2:ModifyImageAttribute", "ec2:ModifyInstanceAttribute", "ec2:ModifySnapshotAttribute", "ec2:RegisterImage", "ec2:RunInstances", "ec2:StopInstances", "ec2:TerminateInstances", "iam:PassRole", "iam:GetInstanceProfile" ], "Resource": "*" } ] } |
대략 위와같은 권한을 가지고 있으면 된다.
Packer 파일 작성
한가지 주의해야할 것이 있다. Packer 파일은 Json 형식과 HCL 형식 두가지를 지원한다. HCL 은 HashiCorp 에서 만든 문법인데, 당연히 이것을 권장하고 있다. Json 형식도 사용하지 못할 정도는 아니여서 충분히 사용이 가능하다. 그럼에도 HCL 형식을 사용하는 이유는 HCL 이 단순하게 메타 프로그래밍 수준이 아니라 각종 함수(Function) 을 제공하고 이것을 HCL 파일에서 사용할 수 있다는데 있다. 예를들어 다음과 같은 것이 있다.
1 2 3 4 5 6 7 8 9 10 11 12 |
locals { timestamp = formatdate("YYYYMMDDhhmm", timestamp()) } source "amazon-ebs" "aws_ec2_ami" { region = var.aws_region vpc_id = var.vpc_id subnet_id = var.subnet_id source_ami = var.source_ami ami_name = "${var.ami_name}-${local.timestamp}" ami_description = var.ami_description instance_type = var.instance_type |
formatdate 함수를 이용해서 날짜를 생성하고 이것을 AMI 파일 이름으로 사용하도록 설정했는데, Json 형식에서는 formatdate 함수를 사용할 수 없다.
또, HCL 형식으로 Packer 를 이용할 경우에 프로비져닝별로 디렉토리를 생성하는 것이 좋다. Packer 는 HCL 에 대해서 파일 이름을 미리 정의해 놓고 있다. varialbes.pkr.hcl 의 경우에는 변수 선언을 위한 파일이고, build.pkr.hcl 의 경우에는 빌드를 위한 파일 이름으로 미리 정의해 놓고 있다. 이러한 파일 이름을 미리 정의하게 되면 디렉토리내에서 자동으로 인식을 하게 되어서 편리하다.
HCL 파일로 작성
여기서는 그런 디렉토리를 생성하지 않고 빌드를 위한 파일과 변수 파일 두가지로 나뉘는 구조로 작성을 할 예정이다. 이렇게 한 후에 Packer 명령어에서 옵션과 인수값으로 파일 두개를 던져주면 실행이 된다.
일단 빌드 파일은 EC2 AMI 를 생성하기 위한 정보들을 가지고 있고 변수 파일은 빌드 파일내에서 변수들의 값을 가지는 파일이라고 보면 된다. 빌드 파일의 기본적인 구조는 대략 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# Packer 플러그인 셋업 packer { required_plugins { amazon = { source = "github.com/hashicorp/amazon" version = "~> 1" } ansible = { version = "~> 1" source = "github.com/hashicorp/ansible" } } } # 변수 타입만 정의. default 값도 정의가능하지만 다른 변수 파일에서 값을 정의해 오버라이드 시킴. variable "aws_region" { type = string } variable "vpc_id" { type = string } variable "subnet_id" { type = string } ......... ......... # Packer 빌드를 위한 소스 설정 부분 source "amazon-ebs" "aws_ec2_ami" { region = var.aws_region vpc_id = var.vpc_id subnet_id = var.subnet_id source_ami = var.source_ami ami_name = "${var.ami_name}-${local.timestamp}" ...................... ...................... } # Packer 빌드를 위한 설정으로 Ansible 을 이용함으로 이에 대한 설정을 해준다. build { sources = [ "source.amazon-ebs.aws_ec2_ami" ] provisioner "ansible" { playbook_file = ".ansible/${var.playbook}.yml" use_proxy = "false" user = "ec2-user" ansible_env_vars = [ "ANSIBLE_CONFIG=.ansible/ansible.cfg" ] extra_arguments = [ "-v", "--extra-vars", "os=${var.os} namespace=${var.namespace}" ] } } |
코멘트로 달았지만 세가지 부분으로 나뉠 수 있다. HCL 은 세부분을 각각의 파일로 나뉘어 작성하는 것도 가능하다. 하지만 복잡하고 크게하지 않을 거라면 이렇게 파일 하나에 다 담아도 된다.
중요하게 봐야 하는 부분이 변수를 선언하는 부분이다. 변수 선언과 함께 Default 값도 정의할 수 있다. 변수 할당을 위한 별도의 파일을 작성하는데 변수 선언부분은 반드시 있어야 한다. 이 부분이 없으면 Undefined Variables 에러가 발생할 것이다.
두번째로 눈여겨 봐야하는 부분은 extra_arguments 부분인데, 이는 Ansible 을 위한 옵션이라고 보면 된다. Ansible 은 Packer 가 명령행으로 실행을 시켜준다. 이때에 Ansible 에 명령행 옵션을 주고 싶다면 위와 같이 주면 된다. Ansible 은 명령행 옵션을 받아서 다양한 동작을 하게 되는데, 위 예제에서 –extra-vars 옵션을 사용해서 값을 주고 있다. 이 값들은 Ansible 내에서 변수로 사용할 수 있다. 심지여 Jinja 템플릿에서도 사용이 가능하다. 위 예제에서 os, namespace 변수를 주고 있는데, 값을 Packer 변수 파일에 값으로 넣어서 넘겨주는 구조를 취했다.
이렇게 작성한 파일을 ec2_ami.pkr.hcl 파일로 저장했다.
변수 설정을 위한 파일은 변수에 값을 다 넣어야 하는 파일을 말한다. Packer 메뉴얼에는 *.pkrvars.hcl 확장자를 가진 파일로 설명하고 있다. 이 파일은 형식이란게 별도로 없고 그냥 왼쪽에는 변수 오른쪽에는 값을 적어서 넣으면 되는 구조다.
1 2 3 4 5 |
instance_type = "c6g.large" ebs_optimized = "true" ssh_interface = "private_ip" ssh_port = "22" ssh_username = "ec2-user" |
여기에 적혀있는 변수는 ec2_ami.pkr.hcl 파일에서 정의한 변수명이여야 한다. 선언되었지만 값이 없는 경우에는 에러가 발생한다.
이렇게 작성한 파일을 web_ami.pkrvars.hcl 로 저장했다.
이제 파일은 다 작성이 되었다. 이렇게 작성된 파일은 다음과 같은 명령어로 실행하면 EC2 AMI 를 생성해 준다.
1 |
packer build -var-file=".packer/web_ami.pkrvars.hcl" .packer/ec2_ami.pkr.hcl |
위와 같이 변수 파일과 실행파일을 명령행 옵션과 값으로 주면 실행을 해준다.
JSON 파일로 작성
JSON 파일 형식은 HashiCorp 에서 더 이상 지원하지 않는 방향으로 가고 있다. 현시점(2024.11) 에서 그나마 지원이 되어서 잘 작동되었다. (너무 기본적인 것만 사용해서 그런지 아무런 문제가 없었다.)
JSON 파일 형식도 여러파일로 쪼갤 수 있을지 모르겠지만, 그냥 하나의 파일에 모든것을 담아도 된다. 인터넷을 검색해보면 대부분 파일 하나에 모두 집어 넣고 사용하는 예제가 많다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
{ "variables": { "aws_region": "ap-northeast-2", "instance_type": "c6g.large", "ebs_optimized": "true", "ssh_interface": "private_ip", "ssh_port": "22", "ssh_username": "ec2-user", ................ }, "builders": [ { "type": "amazon-ebs", "region": "{{user `aws_region`}}", "vpc_id": "{{user `vpc_id`}}", "subnet_id": "{{user `subnet_id`}}", "source_ami":"{{user `source_ami`}}", "ami_name": "{{user `ami_name`}}", "ebs_optimized": "{{user `ebs_optimized`}}", "ami_description": "{{user `ami_description`}}", "instance_type": "{{user `instance_type`}}", "imds_support": "v2.0", "metadata_options": { "http_endpoint": "enabled", "http_tokens": "required" }, "security_group_ids": [ "sg-0a20", "sg-0a6e" ], "ssh_interface": "{{user `ssh_interface`}}", "ssh_port": "{{user `ssh_port`}}", "ssh_username": "{{user `ssh_username`}}", ...................... } ], "provisioners": [ { "type": "ansible", "playbook_file": ".ansible/ec2_ami.yml", "use_proxy": "false", "user": "ec2-user", "ansible_env_vars": [ "ANSIBLE_CONFIG=.ansible/ansible.cfg" ], "extra_arguments": [ "-v", "--extra-vars", "os=redhat namespace=web" ] } ] } |
하나의 파일을 전부 다 넣었다. 변수를 선언하고 이것을 다른데서 사용하기 위해서 user 를 사용하고 있다는 것을 눈여겨 봐야 한다. 이렇게 하나의 파일에 전부 작성을 했으면 다음과 같이 실행할 수 있다.
1 |
pakcer build ec2_ami.json |
packer 는 EC2 인스턴스를 생성하고, Ansible 을 이용해 EC2 에 각종 설정을 하고, EC2 를 중지하고, AMI 를 생성, EC2 삭제하는 절차를 수행하면서 AMI 를 생성해 준다.