gen_freebsd_zfs_ami.sh
[download]
#!/bin/bash
#
################################################################################
#
# Copyright 2021 Guillermo Ramos <0xwille_at_ gmail dot_com>
#
# Redistribution and use in source and binary forms, with or without modification, are permitted
# provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions
# and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of
# conditions and the following disclaimer in the documentation and/or other materials provided with
# the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to
# endorse or promote products derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
################################################################################
#
# Generate a custom FreeBSD AMI from the images provided by Colin Percival. In particular, change
# the base install to ZFS instead of UFS, following Colin's instructions.
#
# Background: https://www.daemonology.net/blog/2015-11-21-FreeBSD-AMI-builder-AMI.html Instance
# requirements: https://www.patreon.com/posts/freebsd-12-2-ami-43910163 ZFS:
# https://www.daemonology.net/blog/2019-02.html
#
################################################################################
# set -x
AWS_REGION=EU_WEST_3 # Paris
BSD_RELEASE='12.2'
KEYPAIR='planout-key-pair-1' # To access builder instance; must be already ssh-add'ed
SUBNET='subnet-07ab623ab52524547' # Where builder instance will be launched
SG='sg-0f1bdca76004366d6' # SG for the builder AMI; just needs inbound SSH
declare -A BUILDERS
BUILDERS=(
[EU_WEST_3,12.2]='ami-0b6f69df41ae29124'
[EU_WEST_3,13.0]='ami-0ac7800c62d6f5f05'
)
BUILDER_AMI=${BUILDERS[$AWS_REGION,$BSD_RELEASE]}
if [ -z "$BUILDER_AMI" ]; then
echo "No suitable builder AMI defined for $AWS_REGION/$BSD_RELEASE. Edit this script and add it."
exit 1
fi
AMI_NAME="FreeBSD ${BSD_RELEASE}-RELEASE ZFS"
if aws ec2 describe-images --filters "Name=name,Values=${AMI_NAME}" | \
jq -re '.Images[0]' > /dev/null; then
echo "AMI with name '${AMI_NAME}' already exists; exiting."
exit 1
fi
BUILDER_INSTANCE_NAME="freebsd-${BSD_RELEASE}-builder"
echo "============ Generating AMI for FreeBSD ${BSD_RELEASE}-RELEASE (using $BUILDER_AMI from $AWS_REGION)"
instance_status() {
instance_id=$1
aws ec2 describe-instance-status --instance-ids "$instance_id" | jq -r '.InstanceStatuses[0].InstanceStatus.Status'
}
instance_state() {
instance_id=$1
aws ec2 describe-instances --instance-ids "$instance_id" | jq -r '.Reservations[0].Instances[0].State.Name'
}
ami_state() {
ami_id=$1
aws ec2 describe-images --image-ids "$ami_id" | jq -r '.Images[0].State'
}
# Search for already existing instance to avoid duplication
if instance_id=$(aws ec2 describe-instances | jq -er "
.Reservations |
map(.Instances) |
flatten |
map(
select(.State.Name == \"running\" and
(.Tags |
map(select(.Key == \"Name\" and .Value == \"${BUILDER_INSTANCE_NAME}\"))!=[])
)
)[0].InstanceId
"); then
echo "============ Using existing builder instance: $instance_id"
else
echo -n '============ Launching builder instance... '
if instance_id=$(
aws ec2 run-instances \
--image-id "$BUILDER_AMI" \
--count 1 \
--instance-type t3.2xlarge \
--key-name $KEYPAIR \
--subnet-id $SUBNET \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=${BUILDER_INSTANCE_NAME}}]" \
--security-group-ids $SG \
| jq -r '.Instances[0].InstanceId'
); then
echo "done"
else
echo 'ERROR'
exit 1
fi
fi
external_ip=$(
aws ec2 describe-instances --instance-ids "$instance_id" | \
jq -r '.Reservations[0].Instances[0].NetworkInterfaces[0].Association.PublicIp'
)
echo -e "\tid: $instance_id\n\texternal IP: ${external_ip}"
echo -n '============ Waiting for instance to get up...'
while [ "$(instance_status "$instance_id")" != 'ok' ]; do
echo -n '.'
sleep 30
done
echo ' done'
ssh -o StrictHostKeyChecking=accept-new "ec2-user@$external_ip" \
"cat > /tmp/bootstrap.sh; chmod +x /tmp/bootstrap.sh" <<EOF
#!/bin/sh
echo -e "\n============ Move the UFS filesystem safely out of the way..."
mdconfig -a -t swap -s 3G -u 2
newfs /dev/md2
mkdir /mdisk
mount /dev/md2 /mdisk
tar -czf /mdisk/base.tgz --exclude .snap -C /mnt .
umount /mnt
echo -e "\n============ Wipe the old UFS bits out of the way and repartition the disk..."
gpart destroy -F nvd0
dd if=/dev/zero bs=128k of=/dev/nvd0
gpart create -s gpt nvd0
gpart add -a 4k -s 512K -t freebsd-boot nvd0
gpart bootcode -b /boot/pmbr -p /boot/gptzfsboot -i 1 nvd0
gpart add -a 1m -t freebsd-zfs -l disk0 nvd0
echo -e "\n============ Create all of the standard FreeBSD/ZFS datasets..."
zpool create -o altroot=/mnt -O compress=lz4 -O atime=off -m none -f zroot nvd0p2
zfs create -o mountpoint=none zroot/ROOT
zfs create -o mountpoint=/ -o canmount=noauto zroot/ROOT/default
mount -t zfs zroot/ROOT/default /mnt
zfs create -o mountpoint=/tmp -o exec=on -o setuid=off zroot/tmp
zfs create -o mountpoint=/usr -o canmount=off zroot/usr
zfs create zroot/usr/home
zfs create -o setuid=off zroot/usr/ports
zfs create zroot/usr/src
zfs create -o mountpoint=/var -o canmount=off zroot/var
zfs create -o exec=off -o setuid=off zroot/var/audit
zfs create -o exec=off -o setuid=off zroot/var/crash
zfs create -o exec=off -o setuid=off zroot/var/log
zfs create -o atime=on zroot/var/mail
zfs create -o setuid=off zroot/var/tmp
zpool set bootfs=zroot/ROOT/default zroot
echo -e "\n============ Extract FreeBSD back onto the newly-ZFS disk"
tar -xf /mdisk/base.tgz -C /mnt
echo -e "\n============ Get rid of the current contents of fstab(5) + configure settings to support ZFS instead"
: > /mnt/etc/fstab
echo 'zfs_load="YES"' >> /mnt/boot/loader.conf
echo 'kern.geom.label.disk_ident.enable="0"' >> /mnt/boot/loader.conf
echo 'kern.geom.label.gptid.enable="0"' >> /mnt/boot/loader.conf
echo 'vfs.zfs.min_auto_ashift=12' >> /mnt/etc/sysctl.conf
echo 'zfs_enable="YES"' >> /mnt/etc/rc.conf
echo -e "\n============ Done. Shutting down..."
shutdown -p now
EOF
ssh "ec2-user@$external_ip" "su root -c '/tmp/bootstrap.sh'"
echo -n '============ Waiting for builder instance to stop...'
while [ "$(instance_state "$instance_id")" != 'stopped' ]; do
echo -n '.'
sleep 5
done
echo ' done'
echo -n "============ Creating image ${AMI_NAME}..."
ami_id=$(
aws ec2 create-image \
--instance-id "$instance_id" \
--name "$AMI_NAME" \
| jq -r .ImageId
)
while [ "$(ami_state "$ami_id")" != 'available' ]; do
echo -n '.'
sleep 30
done
echo ' done'
echo -n '============ Terminating instance... '
aws ec2 terminate-instances --instance-ids "$instance_id" > /dev/null
echo 'done'