├── 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 | [](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
--------------------------------------------------------------------------------