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'