Creating an AMI for ARM

AWS has offered A1 EC2 instances powered by Graviton processors since late 2018, and M6g powered by the newer Graviton2 processors reached GA in May 2020. I’ve been excited to use these instances, but have not been able to find many details on how to create a custom image for arm64/aarch64 from AWS or others. I was particularly interested in running CentOS 8, which still doesn’t even have an x86_64 AMI, so I set out to create an image from scratch.

Without CentOS, I found that Red Hat offers RHEL 8, so I began by starting up ami-029ba835ddd43c34f in us-east-1:

aws ec2 run-instances \
  --region 'us-east-1' \
  --image-id 'ami-029ba835ddd43c34f' \
  --security-group-ids 'sg-xxxxxxxxxxxxxxxxx' \
  --key-name 'alan' \
  --instance-type 'a1.medium'

SSH into the instance as ec2-user. Let’s look at the disk:

$ fdisk -l /dev/nvme0n1
Disk /dev/nvme0n1: 10 GiB, 10737418240 bytes, 20971520 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: D4C0BAF2-FA64-4781-90D2-8D9ADE3EA3A8

Device           Start      End  Sectors  Size Type
/dev/nvme0n1p1    2048   411647   409600  200M EFI System
/dev/nvme0n1p2  411648  1460223  1048576  512M Linux filesystem
/dev/nvme0n1p3 1460224 20971486 19511263  9.3G Linux filesystem
$ parted /dev/nvme0n1 print
Model: NVMe Device (nvme)
Disk /dev/nvme0n1: 10.7GB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags: 

Number  Start   End     Size    File system  Name                  Flags
 1      1049kB  211MB   210MB   fat16        EFI System Partition  boot, esp
 2      211MB   748MB   537MB   xfs
 3      748MB   10.7GB  9990MB  xfs

EFI? If you search around for EC2 and (U)EFI, AWS documentation mentions that EFI isn’t used except for importing a particular kind of Windows VHD image with VM Import/Export. Here’s a Stack Overflow post reminding us that EC2 doesn’t support EFI. An FAQ about A1 instances only mentions ACPI tables. I thought I had found some details in this Creating an instance store-backed Linux AMI, but it shows commenting out the UEFI partition in /etc/fstab. We’re left to ourselves to figure it out.

Let’s look at some other AMIs available for A1 and M6g. This is Amazon Linux 2:

$ fdisk -l /dev/nvme0n1
Disk /dev/nvme0n1: 8 GiB, 8589934592 bytes, 16777216 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: C459507E-A682-4B76-AF4A-09F375376D10

Device           Start      End  Sectors Size Type
/dev/nvme0n1p1   22528 16777182 16754655   8G Linux filesystem
/dev/nvme0n1p128  2048    22527    20480  10M EFI System

Partition table entries are not in disk order.
$ parted /dev/nvme0n1 print
Model: NVMe Device (nvme)
Disk /dev/nvme0n1: 8590MB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags: 

Number  Start   End     Size    File system  Name                  Flags
128     1049kB  11.5MB  10.5MB  fat16        EFI System Partition  boot
 1      11.5MB  8590MB  8578MB  xfs          Linux

And, Ubuntu Server 20.04:

$ fdisk -l /dev/nvme0n1
Disk /dev/nvme0n1: 8 GiB, 8589934592 bytes, 16777216 sectors
Disk model: Amazon Elastic Block Store              
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 47852264-02DA-40E3-B781-733CC8102697

Device           Start      End  Sectors  Size Type
/dev/nvme0n1p1  206848 16777182 16570335  7.9G Linux filesystem
/dev/nvme0n1p15   2048   204800   202753   99M EFI System

Partition table entries are not in disk order.
$ parted /dev/nvme0n1 print
Model: Amazon Elastic Block Store (nvme)
Disk /dev/nvme0n1: 8590MB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags: 

Number  Start   End     Size    File system  Name  Flags
15      1049kB  105MB   104MB   fat32              boot, esp
 1      106MB   8590MB  8484MB  ext4

So, despite no documentation about how to do create an AMI for ARM instances, I set out to create a CentOS 8 ARM image from scratch anyhow. After running through the excellent packer-chef-highperf-centos-ami project by Irving Popovetsky on an x86_64 platform, I began working out the differences for aarch64. The key takeaways are using gpt disks and, for Linux, having EFI files available at /boot/efi/EFI/.

To create a CentOS 8 ARM AMI, first launch an EC2 instance using the RHEL8 ARM AMI. Any size A1 or M6g instance will suffice. Add an additional EBS volume with at least 8 GB. Then, ssh in as ec2-user and then switch to root with sudo -i.

We’ll start by setting our chroot mount point and the secondary EBS volume as variables:

export ROOTFS=/rootfs

export DEVICE="/dev/nvme1n1"

Use parted to create the same disk layout as RHEL8:

parted --script "$DEVICE" -- \
  mklabel gpt \
  mkpart primary fat32 1 201MiB \
  mkpart primary xfs 201MiB 713MiB \
  mkpart primary xfs 713MiB -1 \
  set 1 esp on

This left me with three partitions labeled “primary”. I wasn’t able to set empty labels or a label with spaces using parted --script or even parted <<HEREDOC, so I resorted to using expect for use in a non-interactive script:

rpm -q expect || dnf -y install expect

expect <<'EOS'
  set device $env(DEVICE)

  spawn parted "$device"

  expect "(parted) "
  send "name 1 'EFI System Partition'\r"

  expect "(parted) "
  send "name 2\r"
  expect "Partition name? "
  send "''\r"

  expect "(parted) "
  send "name 3\r"
  expect "Partition name? "
  send "''\r"

  expect "(parted) "
  send "quit\r"

  expect eof
EOS

Format the partitions the same as RHEL8:

# /
mkfs.xfs -f "${DEVICE}p3"

# /boot
mkfs.xfs -f "${DEVICE}p2"

# /boot/efi
mkfs.fat -F 16 "${DEVICE}p1"

Mount these three partitions and requisite special mounts for a functional chroot:

# Chroot Mount /
mkdir -p "$ROOTFS"
mount "${DEVICE}p3" "$ROOTFS"

# Chroot Mount /boot
mkdir -p "$ROOTFS/boot"
mount "${DEVICE}p2" "$ROOTFS/boot"

# Chroot Mount /boot/efi
mkdir -p "$ROOTFS/boot/efi"
mount "${DEVICE}p1" "$ROOTFS/boot/efi"

# Special filesystems
mkdir -p "$ROOTFS/dev" "$ROOTFS/proc" "$ROOTFS/sys"
mount -o bind          /dev     "$ROOTFS/dev"
mount --types devpts   devpts   "$ROOTFS/dev/pts"
mount --types tmpfs    tmpfs    "$ROOTFS/dev/shm"
mount --types proc     proc     "$ROOTFS/proc"
mount --types sysfs    sysfs    "$ROOTFS/sys"
mount --types selinuxfs selinuxfs "$ROOTFS/sys/fs/selinux"

Initialize the RPM database and install centos-release-8 and centos-repos-8 for aarch64:

rpm --root="$ROOTFS" --initdb

release_pkg_latest="$( curl --silent https://mirrors.edge.kernel.org/centos/8/BaseOS/aarch64/os/Packages/ | grep --only-matching 'centos-release-8[^"]*.rpm' | sort --unique --version-sort | tail -1 )"
release_pkg_url="https://mirrors.edge.kernel.org/centos/8/BaseOS/aarch64/os/Packages/$release_pkg_latest"

repos_pkg_latest="$( curl --silent https://mirrors.edge.kernel.org/centos/8/BaseOS/aarch64/os/Packages/ | grep --only-matching 'centos-repos-8[^"]*.rpm' | sort --unique --version-sort | tail -1 )"
repos_pkg_url="https://mirrors.edge.kernel.org/centos/8/BaseOS/aarch64/os/Packages/$repos_pkg_latest"

rpm --root="$ROOTFS" --nodeps -ivh "$release_pkg_url"
rpm --root="$ROOTFS" --nodeps -ivh "$repos_pkg_url"

Run an update to make sure the packages installed successfully:

Note: use “–nogpgcheck” so users of the resulting AMI still need to confirm GPG key usage

dnf --installroot="$ROOTFS" --nogpgcheck -y update

Create the /etc/fstab file with our partitions, again, like the one from RHEL8:

cat > "${ROOTFS}/etc/fstab" <<EOF

#
# /etc/fstab
#
# Accessible filesystems, by reference, are maintained under '/dev/disk/'.
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info.
#
# After editing this file, run 'systemctl daemon-reload' to update systemd
# units generated from this file.
#
UUID=$( lsblk "${DEVICE}p3" --noheadings --output uuid ) /                       xfs     defaults        0 0
UUID=$( lsblk "${DEVICE}p2" --noheadings --output uuid ) /boot                   xfs     defaults        0 0
UUID=$( lsblk "${DEVICE}p1" --noheadings --output uuid )          /boot/efi               vfat    defaults,uid=0,gid=0,umask=077,shortname=winnt 0 2
EOF

From what I can tell, the file /etc/default/grub does not come from an RPM but is generated. Comparing the x86_64 versions of CentOS 7 and RHEL 7, they appear to be the same there, so I copy the one from RHEL8:

mkdir "${ROOTFS}/etc/default"

cp -av /etc/default/grub "${ROOTFS}/etc/default/grub"

I referenced create_base_ami8.sh and CentOS-7-x86_64-hvm.ks for the package selections. Because the chroot has all required mounts and both /etc/fstab and /etc/default/grub are set up, the post-installation for the kernel will take care of most of the bootloader steps without complicated manual intervention:

yum --installroot="$ROOTFS" --nogpgcheck -y install \
  --exclude="iwl*firmware" \
  --exclude="libertas*firmware" \
  --exclude="plymouth*" \
  "@Minimal Install" \
  centos-gpg-keys \
  cloud-init \
  cloud-utils-growpart \
  dracut-config-generic \
  efibootmgr \
  grub2 \
  kernel \
  shim \
  yum-utils

yum --installroot="$ROOTFS" -C -y remove firewalld --setopt="clean_requirements_on_remove=1"

yum --installroot="$ROOTFS" -C -y remove linux-firmware

# This is currently failing on the dependency "timedatex" having a cpio package problem for both aarch64 and x86_64, but chrony still installs.
yum --installroot="$ROOTFS" --nogpgcheck -y install chrony || true

Complete bootloader setup and verify that grubby can detect a default kernel (Note: there is no need to run grub2-install on an EFI system):

chroot "$ROOTFS" grub2-mkconfig -o /etc/grub2-efi.cfg

chroot "$ROOTFS" grubby --default-kernel

That’s it for the EFI-specific portion of creating an AMI from scratch. Everything else is as needed, such as setting up /etc/hosts, disabling first boot, etc.

If you’d like to create CentOS 8 AMIs for both ARM and x86_64, you can find chroot-bootstrap.sh and the Packer ebs-surrogate file I wrote at gist.github.com/alanivey/68712e6172b793037fbd77ebb3112c3f.

Leave a comment at GitHub, or reach out @alanivey and please let me know if you found this helpful.