├── README.md ├── config └── launch.sh ├── fs ├── fs.sh └── rootfs.Dockerfile ├── installer └── install_fc.sh ├── util └── github.sh └── wizard /README.md: -------------------------------------------------------------------------------- 1 | # 🔥🧙 Firecracker MicroVM Wizard 2 | 3 | Simplifying the Firecracker VM setup process. Inspired by [gruchalski](https://gruchalski.com/). 4 | 5 | --- 6 | 7 | [![DigitalOcean Referral Badge](https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%201.svg)](https://www.digitalocean.com/?refcode=ed8d462f1268&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) 8 | 9 | _If my work is helpful to you, please consider using my Digital Ocean referral code above for DO credit_ 10 | 11 | # Table of contents 12 | 13 | - [Usage](#usage) 14 | - [Setting up Firecracker from scratch on Linux](#setting-up-firecracker-from-scratch-on-linux) 15 | - [Basic setup & pre-reqs](#basic-setup--pre-reqs) 16 | - [Configuring your custom image](#configuring-your-custom-image) 17 | - [Generate the VM configuration details](#generate-the-vm-configuration-details) 18 | - [Run the MVM](#run-the-mvm) 19 | - [Accessing the machine via SSH](#accessing-the-machine-via-ssh) 20 | 21 | # Usage 22 | 23 | ```sh 24 | git clone git@github.com:Schachte/Firecracker-VM-Helper.git && \ 25 | cd Firecracker-VM-Helper && \ 26 | ./wizard 27 | ``` 28 | 29 | ``` 30 | 🧙🔥 Firecracker Setup Wizard 31 | 32 | 1) Download Linux kernel binary 33 | 2) Install Firecracker 34 | 3) Install Docker 35 | 4) Setup Firecracker filesystem 36 | 5) Setup Firecracker config JSON 37 | 6) Run Firecracker 38 | 7) Exit 39 | Pick a number ➡️ 40 | ``` 41 | 42 | # Setting up Firecracker from scratch on Linux 43 | 44 | ⚠️ This entire repo _only_ runs on Linux. You can rent a VM, but it must support virtualization (ie. Google Cloud, Digital Ocean, EC2). 45 | 46 | ℹ️ You can skip this and just run all the defaults without issue. I'm just adding context below in case you want to make custom modifications. 47 | 48 | ## Basic setup & pre-reqs 49 | 1. Download the Linux kernel binary via option `1` of the wizard 50 | 2. Install Firecracker onto the `PATH` view option `2` of the wizard 51 | 3. Install Docker (if you don't already have) via option `3` of the wizard 52 | 53 | ## Configuring your custom image 54 | 55 | You're free to modify this as you please, but there is a minimal alpine image located in `fs/rootfs.Dockerfile`. This will auto-configure and boot sshd on init so you can SSH into the VM once it boots up. Password-based login is disabled. 56 | 57 | You can modify the Dockerfile if needed, but to generate the image, we will leverage Docker, but extract the filesystem from it then remove the image from the local filesystem since it will no longer be needed. 58 | 59 | 4. Configure the image for Firecracker via option `4` of the wizard 60 | 61 | ## Generate the VM configuration details 62 | 63 | There are a few ways to launch a VM, we will be launching the VM via the `firecracker` binary giving it a JSON path to the dynamically generated configuration. There will be several questions asked during config generation: 64 | 65 | ``` 66 | Enter TAP IP Address (default: 172.16.0.1/24) 67 | 68 | Enter Firecracker IP Address (default: 172.16.0.2/24) 69 | 70 | Enter guest MAC address (default: 02:FC:00:00:00:05) 71 | 72 | Enter VM memory usage (default: 128 mib) 73 | 74 | Enter TAP name (default: tap-fc-dev) 75 | ``` 76 | 77 | In this case, at least for testing, defaults should work for you out of the box. I'm automatically generating a TAP interface within the configuration setup. This is the network bridge used between the host and the VM to control the flow of packets in and out of the virtual machine. 78 | 79 | 5. Generate config via option `5` of the wizard. 80 | 81 | ## Run the MVM 82 | 83 | 6. Run option `6` and the micro-VM will start and present a login. 84 | 85 | > If you'd like to just run the VM manually and in the background so you can SSH within the same terminal session, then run: 86 | 87 | ```sh 88 | export CONFIG_LOCATION=/firecracker/configs 89 | $ sudo firecracker --no-api --config-file $CONFIG_LOCATION/alpine-config.json 90 | ``` 91 | 92 | Note: You _will_ be presented with a login screen. This is useless based on the default config in the Dockerfile because I've disabled logging in via password. Kill the session and use SSH to access the VM. 93 | 94 | # Accessing the machine via SSH 95 | 96 | ```sh 97 | ssh -i ~/.ssh/hacker alpine@172.16.0.2 98 | ``` 99 | 100 | You can change the details of the key name in `fs/fs.sh` and the user login username via the Dockerfile in `fs/rootfs.Dockerfile` 101 | 102 | ```sh 103 | root@vm:~# ssh -i ~/.ssh/hacker alpine@172.16.0.2 104 | 105 | The authenticity of host '172.16.0.2 (172.16.0.2)' can't be established. 106 | ED25519 key fingerprint is SHA256:0vI9w7kxgB8hX/J3N5jRWXo5CNxlmryoGYTQEpGpOoI. 107 | This key is not known by any other names 108 | Are you sure you want to continue connecting (yes/no/[fingerprint])? yes 109 | 110 | Warning: Permanently added '172.16.0.2' (ED25519) to the list of known hosts. 111 | Welcome to Alpine! 112 | 113 | The Alpine Wiki contains a large amount of how-to guides and general 114 | information about administrating Alpine systems. 115 | See . 116 | 117 | You can setup the system with the command: setup-alpine 118 | 119 | You may change this message by editing /etc/motd. 120 | 121 | 172:~$ 122 | ``` 123 | -------------------------------------------------------------------------------- /config/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function gen_config() { 4 | echo "Enter TAP IP Address (default: 172.16.0.1/24)" 5 | read input 6 | tap_ip=${input:="172.16.0.1"} 7 | 8 | echo "Enter Firecracker IP Address (default: 172.16.0.2/24)" 9 | read input 10 | firecracker_ip=${input:="172.16.0.2"} 11 | 12 | echo "Enter guest MAC address (default: 02:FC:00:00:00:05)" 13 | read input 14 | guest_mac=${input:="02:FC:00:00:00:05"} 15 | 16 | echo "Enter VM memory usage (default: 128 mib)" 17 | read input 18 | mem=${input:="128"} 19 | 20 | echo "Enter TAP name (default: tap-fc-dev)" 21 | read input 22 | tap_name=${input:="tap-fc-dev"} 23 | 24 | MASK_LONG="255.255.255.0" 25 | MASK_SHORT="/24" 26 | KERNEL_BOOT_ARGS="ro console=ttyS0 noapic reboot=k panic=1 pci=off nomodules random.trust_cpu=on" 27 | KERNEL_BOOT_ARGS="${KERNEL_BOOT_ARGS} ip=${firecracker_ip}::${tap_ip}:${MASK_LONG}::eth0:off" 28 | 29 | # just _in case_ it already exists, we will just wipe it 30 | sudo ip link del "$tap_name" 2> /dev/null || true 31 | 32 | # Add a tap device to act as a bridge between the microVM 33 | # and the host. 34 | sudo ip tuntap add dev $tap_name mode tap 35 | 36 | # The subnet is 172.16.0.0/24 and so the 37 | # host will be 172.16.0.1 and the microVM is going to be set to 38 | # 172.16.0.2 39 | sudo ip addr add $tap_ip$MASK_SHORT dev $tap_name 40 | sudo ip link set $tap_name up 41 | ip addr show dev $tap_name 42 | 43 | # Set up IP forwarding and masquerading 44 | 45 | # Change IFNAME to match your main ethernet adapter, the one that 46 | # accesses the Internet - check "ip addr" or "ifconfig" if you don't 47 | # know which one to use. 48 | IFNAME=eth0 49 | 50 | # Enable IP forwarding 51 | sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward" 52 | 53 | # Enable masquerading / NAT - https://tldp.org/HOWTO/IP-Masquerade-HOWTO/ipmasq-background2.5.html 54 | sudo iptables -t nat -A POSTROUTING -o $IFNAME -j MASQUERADE 55 | sudo iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 56 | sudo iptables -A FORWARD -i $tap_name -o $IFNAME -j ACCEPT 57 | 58 | 59 | 60 | echo "✅ TAP network created" 61 | 62 | cat < /firecracker/configs/alpine-config.json 63 | { 64 | "boot-source": { 65 | "kernel_image_path": "$2/kernel", 66 | "boot_args": "${KERNEL_BOOT_ARGS}" 67 | }, 68 | "drives": [ 69 | { 70 | "drive_id": "rootfs", 71 | "path_on_host": "$1/alpine-base-root.ext4", 72 | "is_root_device": true, 73 | "is_read_only": false 74 | } 75 | ], 76 | "network-interfaces": [ 77 | { 78 | "iface_id": "eth0", 79 | "host_dev_name": "$tap_name", 80 | "guest_mac": "$guest_mac" 81 | } 82 | ], 83 | "machine-config": { 84 | "vcpu_count": 1, 85 | "mem_size_mib": 128 86 | } 87 | } 88 | EOF 89 | } -------------------------------------------------------------------------------- /fs/fs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | build_dir="/tmp/alpine-build" 5 | dockerfile="/firecracker/fs/Dockerfile" 6 | filesystem_target="/firecracker/fs/alpine-base-root.ext4" 7 | key_file="hacker" 8 | image_tag="local/alpine-base:latest" 9 | pre_build_dir=$(pwd) 10 | 11 | echo "🔑 Generating a keypair..." 12 | set +e 13 | ssh-keygen -t rsa -b 4096 -C "alpine@firecracker" -f "${HOME}/.ssh/${key_file}" 14 | set -e 15 | 16 | echo "📁 Creating build directory..." 17 | mkdir -p "${build_dir}" && cd "${build_dir}" 18 | echo "✅ Created at ${build_dir}" 19 | 20 | echo "🔑 -> 📁 Copying public key to the build directory..." 21 | cp "${HOME}/.ssh/${key_file}.pub" "${build_dir}/key.pub" 22 | echo "✅ Public key has been copied to ${build_dir}/key.pub" 23 | 24 | echo "🐋 Building Docker image..." 25 | cp "${dockerfile}" "${build_dir}/Dockerfile" 26 | docker build -t "${image_tag}" . 27 | retVal=$? 28 | cd "${pre_build_dir}" 29 | rm -r "${build_dir}" 30 | 31 | if [ $retVal -ne 0 ]; then 32 | echo " ==> build failed with status $?" 33 | exit $retVal 34 | fi 35 | 36 | echo "💾 Creating file system..." 37 | mkdir -p "${build_dir}/fsmnt" 38 | dd if=/dev/zero of="${build_dir}/rootfs.ext4" bs=1M count=500 39 | mkfs.ext4 "${build_dir}/rootfs.ext4" 40 | echo "Mounting file system..." 41 | sudo mount "${build_dir}/rootfs.ext4" "${build_dir}/fsmnt" 42 | 43 | echo "🏁 Starting container from new image ${image_tag}..." 44 | CONTAINER_ID=$(docker run --rm -v ${build_dir}/fsmnt:/export-rootfs -td ${image_tag} /bin/sh) 45 | 46 | echo "📋 Copying Docker file system..." 47 | docker exec ${CONTAINER_ID} /bin/sh -c 'for d in home; do tar c "/$d" | tar x -C /export-rootfs; done; exit 0' 48 | docker exec ${CONTAINER_ID} /bin/sh -c 'for d in bin dev etc lib root sbin usr; do tar c "/$d" | tar x -C /export-rootfs; done; exit 0' 49 | docker exec ${CONTAINER_ID} /bin/sh -c 'for dir in proc run sys var; do mkdir /export-rootfs/${dir}; done; exit 0' 50 | 51 | echo "Unmounting file system..." 52 | sudo umount "${build_dir}/fsmnt" 53 | 54 | echo "Removing docker container..." 55 | docker stop $CONTAINER_ID 56 | 57 | echo "Moving file system..." 58 | mv "${build_dir}/rootfs.ext4" "${filesystem_target}" 59 | 60 | echo "Cleaning up build directory..." 61 | rm -r "${build_dir}" 62 | 63 | echo "Removing Docker image..." 64 | docker rmi ${image_tag} 65 | 66 | echo "" 67 | echo "✅ File system written to ${filesystem_target}!" 68 | echo "" 69 | -------------------------------------------------------------------------------- /fs/rootfs.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.13 2 | 3 | RUN apk update 4 | USER root 5 | RUN apk add openrc openssh sudo util-linux \ 6 | && ssh-keygen -A \ 7 | && mkdir -p /home/alpine/.ssh \ 8 | && addgroup -S alpine && adduser -S alpine -G alpine -h /home/alpine -s /bin/sh \ 9 | && echo "alpine:$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n1)" | chpasswd \ 10 | && echo '%alpine ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/alpine \ 11 | && ln -s agetty /etc/init.d/agetty.ttyS0 \ 12 | && echo ttyS0 > /etc/securetty \ 13 | && rc-update add agetty.ttyS0 default \ 14 | && rc-update add devfs boot \ 15 | && rc-update add procfs boot \ 16 | && rc-update add sysfs boot \ 17 | && rc-update add local default 18 | COPY ./key.pub /home/alpine/.ssh/authorized_keys 19 | RUN chown -R alpine:alpine /home/alpine \ 20 | && chmod 0740 /home/alpine \ 21 | && chmod 0700 /home/alpine/.ssh \ 22 | && chmod 0400 /home/alpine/.ssh/authorized_keys \ 23 | && mkdir -p /run/openrc \ 24 | && touch /run/openrc/softlevel \ 25 | && rc-update add sshd \ 26 | && echo 'AllowTcpForwarding yes' >> /etc/ssh/sshd_config 27 | -------------------------------------------------------------------------------- /installer/install_fc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function install() { 3 | set -eu 4 | install_dir=/firecracker/releases 5 | bin_dir=/usr/bin 6 | release_url="https://github.com/firecracker-microvm/firecracker/releases" 7 | latest=$(basename $(curl -fsSLI -o /dev/null -w %{url_effective} ${release_url}/latest)) 8 | arch=`uname -m` 9 | 10 | if [ -d "${install_dir}/${latest}" ]; then 11 | echo "${latest} already installed" 12 | else 13 | echo "downloading firecracker-${latest}-${arch}.tgz to ${install_dir}" 14 | curl -o "${install_dir}/firecracker-${latest}-${arch}.tgz" -L "${release_url}/download/${latest}/firecracker-${latest}-${arch}.tgz" 15 | pushd "${install_dir}" 16 | 17 | echo "decompressing firecracker-${latest}-${arch}.tgz in ${install_dir}" 18 | tar -xzf "firecracker-${latest}-${arch}.tgz" 19 | rm "firecracker-${latest}-${arch}.tgz" 20 | 21 | echo "linking firecracker ${latest}-${arch} & jailer" 22 | sudo ln -sfn "${install_dir}/release-${latest}-x86_64/firecracker-${latest}-${arch}" "${bin_dir}/firecracker" 23 | sudo ln -sfn "${bin_dir}/jailer-${latest}-x86_64-${arch}" "${bin_dir}/jailer" 24 | 25 | echo "firecracker ${latest}-${arch}: ready" 26 | firecracker --help | head -n1 27 | fi 28 | } -------------------------------------------------------------------------------- /util/github.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | read -p "Enter your GitHub email address: " email 3 | echo "Generating a new SSH key for $email" 4 | ssh-keygen -t ed25519 -C "$email" 5 | 6 | eval "$(ssh-agent -s)" 7 | ssh-add ~/.ssh/id_ed25519 8 | if ! command -v xclip > /dev/null; then 9 | echo "xclip not found, installing..." 10 | sudo apt-get install -y xclip 11 | fi 12 | 13 | echo "Copy the below public key to your clipboard and add to Github" 14 | cat ~/.ssh/id_ed25519.pub 15 | echo "Your new SSH public key has been copied to the clipboard, you can now add it to your GitHub account" -------------------------------------------------------------------------------- /wizard: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | KERNEL_LOCATION=/firecracker/kernel 4 | FILESYSTEM_LOCATION=/firecracker/fs 5 | CONFIG_LOCATION=/firecracker/configs 6 | 7 | download_kernel() { 8 | print_message "🐧️ Downloading Linux kernel binary..." 9 | ARCH="$(uname -m)" 10 | wget -O $KERNEL_LOCATION/kernel https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.5/${ARCH}/vmlinux-5.10.186 11 | print_message "✅ Kernel binary has downloaded and been written to $KERNEL_LOCATION/kernel" 12 | } 13 | 14 | # Creates the ext4 filesystem for the VM 15 | # You can think of this similar to a Docker image - which is what we build the fs from! 16 | create_fs() { 17 | print_message "💿 Preparing filesystem preparation..." 18 | cp $PWD/fs/rootfs.Dockerfile $FILESYSTEM_LOCATION/Dockerfile 19 | . $PWD/fs/fs.sh 20 | } 21 | 22 | install_docker() { 23 | print_message "🐋 Installing Docker... This might take a while as we will setup all dependencies" 24 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 25 | sudo add-apt-repository \ 26 | "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ 27 | $(lsb_release -cs) \ 28 | stable" 29 | 30 | sudo apt-get update 31 | sudo apt-get install \ 32 | bison \ 33 | build-essential \ 34 | flex \ 35 | git \ 36 | libelf-dev \ 37 | libncurses5-dev \ 38 | libssl-dev -y 39 | 40 | sudo apt-get install \ 41 | apt-transport-https \ 42 | ca-certificates \ 43 | curl \ 44 | gnupg-agent \ 45 | software-properties-common -y 46 | 47 | sudo apt-get install \ 48 | docker-ce \ 49 | docker-ce-cli \ 50 | containerd.io -y 51 | sudo groupadd docker # this may report that the group already exists 52 | sudo usermod -aG docker $USER 53 | 54 | print_message "✅ Docker has been installed succcessfully" 55 | } 56 | 57 | # Generates the Firecracker configuration file 58 | setup_cfg() { 59 | source $PWD/config/launch.sh 60 | print_message "Configuring VM with kernel path of $KERNEL_LOCATION/kernel & $FILESYSTEM_LOCATION/alpine-base-root.ext4" 61 | gen_config $FILESYSTEM_LOCATION $KERNEL_LOCATION 62 | print_message "✅ Configuration JSON written to $CONFIG_LOCATION/alpine-config.json" 63 | } 64 | 65 | # Puts Firecracker on path 66 | install_fc() { 67 | print_message "🔥 Beginning Firecracker installation" 68 | source $PWD/installer/install_fc.sh 69 | install 70 | print_message "✅ Firecracker successfully installed onto your PATH (/usr/bin/firecracker)" 71 | } 72 | 73 | run_fc() { 74 | print_message "🔥 Preparing to run Firecracker..." 75 | print_message "If this step fails, then just remove the socket and re-run..." 76 | firecracker --no-api --config-file $CONFIG_LOCATION/alpine-config.json 77 | print_message "✅ Firecracker running!" 78 | } 79 | 80 | display_menu() { 81 | echo "🧙🔥 Firecracker Setup Wizard" 82 | echo "" 83 | echo "1) Download Linux kernel binary" 84 | echo "2) Install Firecracker" 85 | echo "3) Install Docker" 86 | echo "4) Setup Firecracker filesystem" 87 | echo "5) Setup Firecracker config JSON" 88 | echo "6) Run Firecracker" 89 | echo "7) ✨ Setup everything" 90 | echo "8) Exit" 91 | } 92 | 93 | print_message() { 94 | local message=$1 95 | local len=${#message} 96 | local dashes=$(printf '%*s' "$len" | tr ' ' '-') 97 | 98 | echo "$dashes" 99 | echo "$message" 100 | echo "$dashes" 101 | } 102 | 103 | setup_everything() { 104 | print_message "EXPERIMENTAL: Automating the entire setup process!" 105 | download_kernel 106 | install_fc 107 | install_docker 108 | create_fs 109 | setup_cfg 110 | print_message "✅ Setup complete - You may now run Firecracker from the wizard menu" 111 | } 112 | 113 | check_root() { 114 | if [ "$(id -u)" != "0" ]; then 115 | print_message "👋 This script must be run as root!" 116 | exit 1 117 | fi 118 | } 119 | 120 | clear 121 | # sudo only for this script! 122 | check_root 123 | 124 | # pre-gen the necessary directories 125 | print_message "⚠️ Creating the necessary directories underneath /firecracker/*" 126 | mkdir -p /firecracker/{kernel,fs,configs,releases} 127 | 128 | # USER needs r/w privleges for /dev/kvm 129 | print_message "⚠️ Installing the acl package before we begin so we can set permissions for /dev/kvm" 130 | sudo apt-get install -y acl 131 | sudo setfacl -m u:${USER}:rw /dev/kvm 132 | 133 | while true; do 134 | display_menu 135 | read -p "Pick a number ➡️ " choice 136 | case $choice in 137 | 1) download_kernel ;; 138 | 2) install_fc ;; 139 | 3) install_docker ;; 140 | 4) create_fs ;; 141 | 5) setup_cfg ;; 142 | 6) run_fc ;; 143 | 7) setup_everything ;; 144 | 8) break ;; 145 | *) echo "Invalid option, please try again." ;; 146 | esac 147 | done --------------------------------------------------------------------------------