EC2: Add AMI Builder AMI building

Starting in 2015 I have published "AMI Builder AMIs" for FreeBSD/EC2:
These boot into a memory disk, extract a "clean" copy of FreeBSD onto
the root disk, mount it at /mnt, and allow the user to SSH in to make
customizations before creating a new AMI from the "running" instance
(in fact, from the FreeBSD installation which is not running but is
mounted on /mnt).

This provides a much cleaner mechanism for building customized FreeBSD
AMIs than the traditional Linux approach of "launch an EC2 instance,
SSH in and configure it, then try to wipe logs and credentials before
creating an AMI"; and it's easier than building a customized AMI ab
initio by modifying the FreeBSD release-building code.

This commit brings that functionality into the FreeBSD src tree and
into the collection of images built by the release engineering team:
The EC2 "BUILDER" flavour AMI is essentially a "SMALL" flavour AMI with
a compressed "BASE" flavour disk image, plus an init script which
juggles disks around (rerooting into a memory disk and extracting the
"BASE" image onto disk).

Polished by:	bz, emaste
MFC after:	1 week
Sponsored by:	Amazon
Differential Revision:	https://reviews.freebsd.org/D49930

(cherry picked from commit 5842658903)
This commit is contained in:
Colin Percival 2025-04-20 09:38:48 -07:00
parent b93ad61755
commit 925ec43c15
4 changed files with 207 additions and 2 deletions

View file

@ -35,8 +35,9 @@ BASIC-CLOUDINIT_FSLIST?= ufs zfs
BASIC-CLOUDINIT_DESC?= Images for VM with cloudinit disk config support
EC2_FORMAT= raw
EC2_FSLIST?= ufs zfs
EC2_FLAVOURS?= BASE CLOUD-INIT SMALL
EC2_FLAVOURS?= BASE BUILDER CLOUD-INIT SMALL
EC2-BASE_DESC= Amazon EC2 image
EC2-BUILDER_DESC= Amazon EC2 AMI Builder image
EC2-CLOUD-INIT_DESC= Amazon EC2 Cloud-Init image
EC2-SMALL_DESC= Amazon EC2 small image
GCE_FORMAT= raw
@ -112,12 +113,20 @@ CLEANFILES+= ${_CW:tl}.${_FS}.${_FMT}.img \
${_CW:tl}.${_FS}.${_FMT}.raw
${_CW:tu}${_FS:tu}${_FMT:tu}IMAGE= ${_CW:tl}.${_FS}.${_FMT}
# Special handling: EC2 "AMI Builder" images need a copy of the "base" disk
# built first.
.if ${_CW} == EC2-BUILDER
cw-${_CW:tl}-${_FS}-${_FMT}: cw-ec2-base-${_FS}-${_FMT}
.endif
cw-${_CW:tl}-${_FS}-${_FMT}: ${QEMUTGT}
mkdir -p ${.OBJDIR}/${.TARGET}
env TARGET=${TARGET} TARGET_ARCH=${TARGET_ARCH} SWAPSIZE=${SWAPSIZE} \
QEMUSTATIC=${QEMUSTATIC} \
EC2BASEIMG=${.OBJDIR}/${EC2-BASE${_FS:tu}${_FMT:tu}IMAGE} \
${.CURDIR}/scripts/mk-vmimage.sh \
-C ${.CURDIR}/tools/vmimage.subr -d ${.OBJDIR}/${.TARGET} -F ${_FS} \
-C ${.CURDIR}/tools/vmimage.subr -d ${.OBJDIR}/${.TARGET} \
-F ${"${_CW:MEC2-BUILDER}" != "":?ufs:${_FS}} \
-i ${.OBJDIR}/${_CW:tl}.${_FS}.${_FMT}.img -s ${VMSIZE} -f ${_FMT} \
-S ${WORLDDIR} -o ${.OBJDIR}/${${_CW:tu}${_FS:tu}${_FMT:tu}IMAGE} -c ${${_CW:tu}CONF}
touch ${.TARGET}

View file

@ -0,0 +1,57 @@
#!/bin/sh
. ${WORLDDIR}/release/tools/ec2.conf
# Build with a 7.9 GB partition; this is enough for our stripped-down
# base system plus the compressed ec2-base image.
export VMSIZE=8000m
# Flags to installworld/kernel: We don't want debug symbols (kernel or
# userland), 32-bit libraries, tests, or the debugger.
export INSTALLOPTS="WITHOUT_DEBUG_FILES=YES WITHOUT_KERNEL_SYMBOLS=YES \
WITHOUT_LIB32=YES WITHOUT_TESTS=YES WITHOUT_LLDB=YES"
# Packages to install into the image we're creating. In addition to packages
# present on all EC2 AMIs, we install:
# * ec2-scripts, which provides a range of EC2ification startup scripts,
# * isc-dhcp44-client, used for IPv6 network setup, and
# * py-awscli, to make it easier for users to create AMIs.
export VM_EXTRA_PACKAGES="${VM_EXTRA_PACKAGES} ec2-scripts \
isc-dhcp44-client devel/py-awscli"
# Services to enable in rc.conf(5).
export VM_RC_LIST="${VM_RC_LIST} ec2_configinit ec2_ephemeral_swap \
ec2_fetchkey ec2_loghostkey sshd"
vm_extra_pre_umount() {
# Any EC2 ephemeral disks seen when the system first boots will
# be "new" disks; there is no "previous boot" when they might have
# been seen and used already.
touch ${DESTDIR}/var/db/ec2_ephemeral_diskseen
# Configuration common to all EC2 AMIs
ec2_common
# Standard FreeBSD network configuration
ec2_base_networking
# Grab a copy of the ec2-base disk image, and compress it
zstd < ${EC2BASEIMG} > ${DESTDIR}/image.zst
# Disable fortune so we don't have extra noise at login
chmod a-x ${DESTDIR}/usr/bin/fortune
# Install the AMI-building script
install -m 755 ${WORLDDIR}/release/tools/mkami.sh ${DESTDIR}/bin/mkami
# Install an /etc/rc which juggles disks around for us
install -m 755 ${WORLDDIR}/release/tools/rc.amibuilder ${DESTDIR}/etc
# We want to mount from the UFS disk and juggle disks first
cat >> ${DESTDIR}/boot/loader.conf <<-EOF
vfs.root.mountfrom="ufs:/dev/gpt/rootfs"
init_script="/etc/rc.amibuilder"
EOF
return 0
}

65
release/tools/mkami.sh Normal file
View file

@ -0,0 +1,65 @@
#!/bin/sh -e
#
# Copyright (c) 2015 Colin Percival
#
# SPDX-License-Identifier: BSD-2-Clause
#
# mkami.sh: Create an AMI from the currently running EC2 instance.
#
export PATH=$PATH:/usr/local/bin
NAME=$1
if [ -z "$NAME" ]; then
echo "usage: mkami <AMI name> [<AMI description>]"
exit 1
fi
DESC=$2
if ! [ -z "$DESC" ]; then
DESCOPT="--description '$DESC'"
fi
# Get the instance ID and region from the EC2 Instance Metadata Service:
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
TMPFILE=`mktemp`
fetch -qo $TMPFILE http://169.254.169.254/latest/dynamic/instance-identity/document
INST=`awk -F \" '/"instanceId"/ { print $4 }' $TMPFILE`
REGION=`awk -F \" '/"region"/ { print $4 }' $TMPFILE`
rm $TMPFILE
CMD="aws --region $REGION ec2 create-image --instance-id $INST --output text --no-reboot --name '$NAME' $DESCOPT"
# Unmount the new system image
if mount -p | grep -q '/mnt.*ufs'; then
echo -n "Unmounting new system image..."
sync
umount /mnt
sync
sleep 5
sync
echo " done."
elif mount -p | grep -q '/mnt.*zfs'; then
echo -n "Unmounting new system image..."
sync
zfs umount -a
zfs umount zroot/ROOT/default
sync
sleep 5
sync
echo " done."
fi
if eval "$CMD" --dry-run 2>&1 |
grep -qE 'UnauthorizedOperation|Unable to locate credentials'; then
echo "This EC2 instance does not have permission to create AMIs."
echo "Launch an AMI-builder instance with an appropriate IAM Role,"
echo "create an AMI from this instance via the AWS Console, or run"
echo "the following command from a system with the necessary keys:"
echo
echo "$CMD"
exit
fi
echo -n "Creating AMI..."
AMINAME=`eval "$CMD"`
echo " done."
echo "AMI created in $REGION: $AMINAME"

View file

@ -0,0 +1,74 @@
#!/bin/sh
#
# Copyright (c) 2025 Colin Percival
#
# SPDX-License-Identifier: BSD-2-Clause
#
# rc.amibuilder: Juggle disks to reroot into a memory disk and install a clean
# copy of FreeBSD onto the root disk.
#
# Do nothing if init(8) is in the middle of rerooting
if ps -p 1 -o command | grep -q reroot; then
exit 0
fi
# Figure out which partition we boot from
BOOTPART=$(sysctl -n kern.geom.conftxt |
grep -E 'PART|gpt/rootfs' |
grep -B 1 gpt/rootfs |
awk '{ print $3 }' |
head -1)
BOOTDISK=${BOOTPART%%p*}
BOOTPARTNUM=${BOOTPART##*p}
# First pass: Copy ourselves into a memory disk and reroot into it
if ! [ -c /dev/md0 ]; then
# Create a memory disk of appropriate size and copy the disk
echo "Copying FreeBSD into memory disk..."
DISKBYTES=$(diskinfo ${BOOTDISK} | awk '{print $3}')
mdconfig -a -t swap -s ${DISKBYTES}b -u 0
dd if=/dev/${BOOTDISK} of=/dev/md0 bs=1M
# Reboot into the memory disk we just created
echo "Rebooting into memory disk..."
kenv vfs.root.mountfrom="ufs:/dev/md0p${BOOTPARTNUM}"
reboot -r
# Lose a race against init
sleep 10
exit 1
fi
# Second pass: Extract a clean copy of FreeBSD onto the physical disk
echo "Installing base FreeBSD image..."
sysctl kern.geom.debugflags=16
zstdcat < /image.zst | dd bs=1M of=/dev/${BOOTDISK}
# Mount the clean image
if gpart show ${BOOTDISK} | grep -q freebsd-ufs; then
mount /dev/${BOOTPART} /mnt
else
zpool import -aNR /mnt
zfs mount zroot/ROOT/default
zfs mount -a
fi
# Provide instructions for when the user logs in
mount -w /
cat >/etc/motd.template <<EOF
Welcome to the FreeBSD AMI builder!
FreeBSD `uname -r` is now installed onto the disk /dev/${BOOTDISK},
and mounted at /mnt. Make any further changes you wish, then run
# mkami <AMI name> [<AMI description>]
to create the AMI. Don't forget to shut down this instance when
you're done!
EOF
mount -o ro /
# After we exit, the boot proceeds with init spawning /etc/rc normally
exit 0