How to build EC2 AMI as Code using HashiCorp Packer

Nont Banditwong
4 min readAug 8, 2024

--

HashiCorp Packer Logo

เราสามารถใช้ Hashicorp Packer ในการสร้าง AWS EC2 AMI หรือ Machine Image ด้วยการเขียนเป็น code ได้ เรียกการทำแบบนี้ว่า Machine Image as Code ข้อดีของการจัดการ Infrastrure ด้วยการเขียนเป็น code นอกจากจะช่วยในเรื่องของการทำซ้ำได้โดยไม่ผิดพลาด และตัว source code เองยังอธิบายขั้นตอนทุกสิ่งทุกอย่างที่เราทำกับ Machine Image นั้น ถ้านำมาใช้ร่วมกับ GitOps และ CI/CD pipeline เราก็จะได้ระบบที่นอกจะช่วยทำงานเหล่านี้ให้อัตโนมัติ ยังมีเครื่องมือในการ audit ได้เป็นอย่างดีอีกด้วย

เครื่องมือของ HashiCorp หลายๆตัวถูกออกแบบมาเพื่อจัดการ Infrastructure ในรูปแบบที่เรียกว่า Immutable infrastructure ซึ่งมีข้อดีในเรื่องของ Consistency สามารถตรวจสอบย้อนกลับได้ง่าย ทำให้มีความปลอดภัยมากกว่า deploy และ rollback ได้เร็ว scale ได้ง่าย จัดการได้ง่าย และสุดท้ายจะทำให้ระบบมีความเสถียรมากกว่าการจัดการ Infrastructure แบบเดิม ถ้านึกไม่ออกว่า Immutable Infrastructure เป็นแบบไหนให้นึกถึง Docker หรือ Container ที่เราใช้กันแพร่หลายในตอนนี้

Packer เป็นเครื่องมือที่ใช้สร้าง Machine Image สำหรับหลายแพลตฟอร์มจากการกำหนดค่า Configuration เพียงครั้งเดียว Packer เครื่องมือขนาดเล็ก สามารถทำงานบนระบบปฏิบัติการหลัก ๆ ทุกระบบ สามารถสร้าง Machine Image สำหรับหลายแพลตฟอร์มได้พร้อมกัน Packer ไม่ได้มาแทนที่เครื่องมือสำหรับทำ Configuration Management เช่น Ansible Chef หรือ Puppet แต่ Packer สามารถใช้เครื่องมือ เช่น Ansible Chef หรือ Puppet เพื่อติดตั้งซอฟต์แวร์ลงบน image ได้

Machine Image คือชุดข้อมูลที่ประกอบไปด้วยระบบปฏิบัติการและซอฟต์แวร์ที่ติดตั้งไว้ล่วงหน้า ซึ่งใช้ในการสร้างเครื่องใหม่ที่รันได้อย่างรวดเร็ว รูปแบบของ Machine Image จะแตกต่างกันไปในแต่ละแพลตฟอร์ม ตัวอย่างเช่น AMI สำหรับ AWS EC2, ไฟล์ VMDK/VMX สำหรับ VMware, OVF exports สำหรับ VirtualBox เป็นต้น

สรุป flow การทำงานของ Packer ได้ตาม diagram ด้านล่าง

Diagram การทำงานของ Packer

Packer เป็น command line tool ขนาดเล็ก การสร้างงาน Packer ทำผ่านคำสั่ง packer โดยมีโครงสร้างของคำสั่งดังนี้

packer [--version] [--help] <command> [<args>]

คำสั่งที่ใช้ได้มีดังนี้:

build สร้าง machine image จาก template
console สร้าง console สำหรับทดสอบ variable interpolation
fix แก้ไข template จาก packer เวอร์ชั่นเก่า
fmt เขียนไฟล์ config ในรูปแบบ HCL2 ใหม่ให้เป็นรูปแบบมาตรฐาน
hcl2_upgrade แปลง JSON template เป็น HCL2
init ติดตั้ง plugins ที่หายไป หรืออัพเกรด plugins
inspect ดู component ของ template
plugins จัดการ Packer plugins และcatalog
validate ตรวจสอบว่า template ถูกต้องหรือไม่
version แสดงเวอร์ชั่นของ Packer

คำสั่งที่ใช้บ่อยจริงๆคือ packer build ส่วน packer init จะใช้ครั้งแรกเพื่อติดตั้ง dependency plugin

Packer Template

Packer Templates กำหนดพฤติกรรมของ Packer โดยบอก Packer ว่าจะใช้ plugins (builders, provisioners, post-processors) อะไร ตั้งค่าอย่างไร และรันตามลำดับไหน template นี้มีเครื่องมือ variable injection ที่ยืดหยุ่น และ built-in functions เพื่อช่วยปรับแต่ง builds

เดิม Packer ใช้ template แบบ JSON แต่กำลังเปลี่ยนไปใช้ HCL2 ซึ่งเป็นภาษาเดียวกับที่ Terraform และผลิตภัณฑ์อื่นๆ ของ HashiCorp HCL2 มีความยืดหยุ่น เป็นโมดูลาร์ และกระชับกว่า JSON เดิม แม้ Packer จะยังรองรับ JSON template แต่บางฟีเจอร์ใหม่จะใช้ได้เฉพาะ HCL เท่านั้น

ตั้งแต่ Packer เวอร์ชั่น 1.7.0 เป็นต้นไป HCL2 กลายเป็นวิธีที่แนะนำอย่างเป็นทางการในการเขียน Packer configuration

Plugins

ใน Packer template จะประกอบไปด้วย plugins หลายๆแบบ แต่ที่ใช้บ่อยคือ builders data sources และ provisioners plugins และ ซึ่งเป็น built-in plugins ที่สามารถใช้งานได้โดยไม่ต้องติดตั้งเพิ่มเติม นอกจากนี้เราสามารถติดตั้ง plugins ภายนอกเพื่อให้ Packer ทำงานได้โดยไม่ต้องแก้ไขซอร์สโค้ดหลัก Packer

built-in ปลั๊กอิน

  • Packer ใช้ปลั๊กอินที่เรียกว่า builders เพื่อสร้างเครื่อง (Instance) และ Image ศึกษาเพิ่มเติมได้จาก Builders
  • Data Sources plug-in ทำหน้าที่ดึงข้อมูลมาใช้ใน template ศึกษาเพิ่มเติมได้จาก Data Sources
  • Provisioners plug-in ติดตั้งและกำหนดค่า Image ของเครื่องหลังจากบูต ศึกษาเพิ่มเติมได้จาก Provisioners
  • Post-processors plug-in ทำงานเพิ่มเติมหลังจาก provisioning ศึกษาเพิ่มเติมได้จาก Post-Processors

Packer Template

Prerequisite: ก่อนที่จะใช้ Packer ในการสร้าง EC2 AMI ได้ต้องทำการ setup ให้ AWS CLI สามารถติดต่อ AWS และทำการสั่งสร้าง EC2 Instance รวมถึง AMI ได้ก่อน ซึ่งจะไม่ขอกล่าวถึงในที่นี้

ทดลองสร้างไฟล์ Packer template ชื่อ main.pkr.hcl

# main.pkr.hcl
packer {
required_plugins {
amazon = {
version = ">= 1.3.2"
source = "github.com/hashicorp/amazon"
}
}
}

source "amazon-ebs" "example" {
ami_name = "packer-example {{timestamp}}"
instance_type = "t2.micro"
region = "ap-southeast-1"
source_ami = "ami-0fd76827946882950"
ssh_interface = "public_ip"
ssh_username = "ec2-user"
}

build {
sources = ["source.amazon-ebs.example"]

provisioner "shell" {
inline = ["sudo yum -y update"]
}
}

โค้ดนี้ใช้ Packer เพื่อสร้าง AMI (Amazon Machine Image) ใหม่บน AWS โดยใช้ AMI ที่มีอยู่คือ AMI ID: ami-0fd76827946882950 เป็นต้นแบบ

ในตัวอย่างใช้ Image ของ Amazon Linux 2023 x86_64 architecture (minimal AMI) ล่าสุดโดย Query จาก AWS Systems Manager Parameter Store ด้วยคำสั่ง AWS CLI ด้านล่าง

aws ssm get-parameters --region "ap-southeast-1" --names /aws/service/ami-amazon-linux-latest/al2023-ami-minimal-kernel-default-x86_64 --query 'Parameters[0].[Value]' --output text

ส่วนประกอบหลักของโค้ด

  1. packer { ... }
  • บล็อกนี้กำหนดการตั้งค่าทั่วไปสำหรับ Packer
  • required_plugins: กำหนดปลั๊กอินที่จำเป็นสำหรับการสร้าง AMI ในกรณีนี้คือปลั๊กอิน amazon ซึ่งใช้สำหรับการทำงานกับ AWS
  • version: กำหนดเวอร์ชันของปลั๊กอินที่ต้องการ
  • source: กำหนดแหล่งที่มาของปลั๊กอิน

2. source "amazon-ebs" "example" { ... }

  • บล็อกนี้กำหนดแหล่งที่มาของ AMI ที่จะใช้ในการสร้าง AMI ใหม่โดยใช้ Builder plug-in ชื่อ amazon-ebs ซึ่งสามารถสร้าง Amazon AMI จาก ไดรฟ์ EBS Builder plug-in นี้สร้าง AMI โดยการสร้าง EC2 Instance จาก AMI ต้นทาง ติดตั้ง packge ลงบนเครื่องที่รันอยู่ (Provisioning) และจากนั้นสร้าง AMI จากเครื่องนั้น โดยทั้งหมดนี้ทำในบัญชี AWS ของเราเอง Builder จะสร้างค่ key-pair ชั่วคราว security-group ฯลฯ ที่ช่วยให้เข้าถึงอินสแตนซ์ชั่วคราวในขณะที่กำลังสร้าง Image ซึ่งจะช่วยลดความยุ่งยากในการกำหนดค่า configuration
  • ami_name: ชื่อของ AMI ใหม่ที่จะสร้าง โดยใช้ {{timestamp}} เพื่อเพิ่มเวลาปัจจุบันลงในชื่อ
  • instance_type: ประเภทของอินสแตนซ์ EC2 ที่จะใช้ในการสร้าง AMI
  • region: ภูมิภาค AWS ที่จะสร้าง AMI
  • source_ami: AMI ที่มีอยู่ที่จะใช้เป็นต้นแบบ
  • ssh_interface: กำหนดวิธีการเชื่อมต่อ SSH ไปยังอินสแตนซ์ EC2 ในกรณีนี้คือ public_ip ซึ่งหมายถึงการใช้ IP สาธารณะ
  • ssh_username: ชื่อผู้ใช้สำหรับการเชื่อมต่อ SSH

3. build { ... }

  • บล็อกนี้กำหนดขั้นตอนการสร้าง AMI
  • sources: กำหนดแหล่งที่มาของ AMI ที่จะใช้ในการสร้าง
  • provisioner "shell" { ... }: กำหนดคำสั่ง shell ที่จะรันบนอินสแตนซ์ EC2 ก่อนที่จะสร้าง AMI
  • inline: กำหนดคำสั่ง shell ที่จะรัน

คำอธิบายโค้ด

โค้ดนี้จะสร้าง AMI ใหม่บน AWS โดยใช้ AMI ที่มีอยู่เป็นต้นแบบ (ami-0fd76827946882950) ในภูมิภาค ap-southeast-1 โดยใช้อินสแตนซ์ EC2 ประเภท t2.micro ก่อนที่จะสร้าง AMI คำสั่ง sudo yum -y update จะรันบนอินสแตนซ์ EC2 เพื่ออัปเดตแพ็คเกจ

ก่อนทำการสร้าง AMI ให้ run คำสั่ง init เพื่อติดตั้ง plugins

packer init .

จากนั้น run คำสั่ง build เพื่อสร้าง AMI

packer build .

ผลการ run เป็นดังภาพด้านล่าง

และจะพบว่ามี AMI ถูกสร้างขึ้นใหม่ซึ่งเราสามารถเอาไปใช้เพื่อสร้าง EC2 Instance ได้ต่อไป

ในการทำงานจริงขั้นตอนของการ Provisioning จะซับซ้อนกว่าในตัวอย่างซึ่งเพียงแค่ทำการ upgrade package ด้วยคำสั่ง sudo yum -y update เช่นเตรียม Image สำหรับระบบที่ซับซ้อนอย่าง Apache Kafka ซึ่งอาจมีขั้นตอนซึ่งลำพังการใช้ shell provisioner ในตัวอย่างอาจจะไม่สามารถทำได้ง่าย ในกรณีนี้บ่อยครั้งใช้ Ansible Provisioner เข้ามาช่วย หรือบางครั้งเราสามารถใช้ Ansible Role ที่มีอยู่แล้วในการเตรียม Image

ในบทความหน้าจะลองเอาตัวอย่างของการใช้ Packer เพื่อสร้าง AMI สำหรับใช้ในการทำ Bitbucket self-hosted runner ในรูปแบบของ EC2 Spot-Fleet ที่ใช้สำหรับรองรับการทำงานของ CI/CD Pipeline มาเล่าให้ฟัง

--

--

Nont Banditwong

Cloud Engineering Specialist, Software Developer, System Engineer, Photographer and Learner