├── Dockerfile ├── Makefile ├── README.md ├── build-fs ├── fs-base └── go │ ├── Dockerfile │ ├── build-image │ ├── inittab │ ├── interfaces │ ├── resolv.conf │ └── start-vm └── start-fire /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11.4-alpine3.8 as builder 2 | 3 | RUN apk --no-cache add curl git make build-base \ 4 | && echo "Pulling firectl binary from Github." \ 5 | && git clone https://github.com/firecracker-microvm/firectl \ 6 | && cd firectl && make && chmod u+x firectl \ 7 | && cp firectl /usr/bin/firectl \ 8 | && apk del curl git make build-base --no-cache 9 | 10 | FROM alpine:3.8 11 | MAINTAINER Swarvanu Sengupta 12 | ENV container docker 13 | 14 | RUN apk add curl qemu-system-x86_64 libvirt \ 15 | && apk add libvirt-daemon dbus polkit \ 16 | && apk add qemu-img bash iproute2 e2fsprogs 17 | 18 | RUN curl -LOJ https://github.com/firecracker-microvm/firecracker/releases/download/v0.13.0/firecracker-v0.13.0 \ 19 | && mv firecracker-v0.13.0 /usr/local/bin/firecracker \ 20 | && chmod u+x /usr/local/bin/firecracker 21 | 22 | RUN curl -fsSL -o /tmp/micro-vmlinux.bin https://s3.amazonaws.com/spec.ccfc.min/img/hello/kernel/hello-vmlinux.bin 23 | 24 | COPY --from=builder /usr/bin/firectl /usr/local/bin/firectl 25 | COPY start-fire /usr/local/bin/start-fire 26 | RUN chmod u+x /usr/local/bin/start-fire 27 | 28 | VOLUME /rootfs 29 | 30 | ENTRYPOINT ["/usr/local/bin/start-fire"] 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: basefs docker-firecracker 2 | 3 | docker-firecracker: 4 | docker build -t s8sg/docker-firecracker:0.1.0 . 5 | docker tag s8sg/docker-firecracker:0.1.0 s8sg/docker-firecracker:latest 6 | 7 | basefs: 8 | echo "Building go fs base" 9 | docker build -t s8sg/microvm-base-go:0.1.0 ./fs-base/go 10 | docker tag s8sg/microvm-base-go:0.1.0 s8sg/microvm-base-go:latest 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Firecracker 2 | 3 | Generic container for launching a firecracker microVM inside a Docker container 4 | 5 | > * It attaches to the VM as many NICs as the docker container has 6 | > * Outputs serial console to stdio, thus visible using docker logs 7 | 8 | ### Getting Started : 9 | 10 | ### Build the docker container 11 | ```sh 12 | git clone https://github.com/s8sg/docker-firecracker 13 | cd docker-firecracker 14 | make 15 | ``` 16 | 17 | ### Build your root fs using fs-base `currently only supported GO` 18 | ```sh 19 | ./build-fs ./my-go-app my_root_fs 20 | ``` 21 | 22 | ### Running 23 | ```sh 24 | docker run \ 25 | -td \ 26 | --privileged \ 27 | -v /path_to/my_root_fs:/rootfs/image \ 28 | -p : \ 29 | s8sg/docker-firecracker 30 | ``` 31 | -------------------------------------------------------------------------------- /build-fs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 2 ]; then 4 | echo "Illegal number of parameters" 5 | echo "build " 6 | exit 1 7 | fi 8 | 9 | if [ $1 == 'help' ]; then 10 | echo "build " 11 | exit 0 12 | fi 13 | 14 | CODEDIR="$(readlink -f $1)" 15 | docker run --rm --privileged -v $CODEDIR:/opt/code s8sg/microvm-base-go:latest 16 | 17 | ROOTFSDIR="$(readlink -f $2)" 18 | mv $CODEDIR/rootfs $ROOTFSDIR 19 | echo "Root filesystem created as: $ROOTFSDIR" 20 | -------------------------------------------------------------------------------- /fs-base/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | RUN cd /tmp && \ 4 | wget http://dl-cdn.alpinelinux.org/alpine/v3.8/releases/x86_64/alpine-minirootfs-3.8.1-x86_64.tar.gz 5 | 6 | COPY inittab /tmp/overlay/etc/inittab 7 | COPY interfaces /tmp/overlay/etc/network/interfaces 8 | COPY start-vm /tmp/overlay/start-mvm.sh 9 | COPY resolv.conf /tmp/overlay/etc/resolv.conf 10 | 11 | COPY build-image /build-image 12 | CMD ["/bin/bash", "/build-image"] 13 | -------------------------------------------------------------------------------- /fs-base/go/build-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # run this container like so 4 | # docker run --rm --privileged -v /path/to/code:/go/src/gopath/to/code firecracker-builder 5 | 6 | set -ex 7 | 8 | 9 | dd if=/dev/zero of=/tmp/rootfs bs=1M count=50 10 | mkfs.ext4 /tmp/rootfs 11 | 12 | mkdir /tmp/tmp 13 | # mknod /dev/loop0 b 7 0 14 | mount /tmp/rootfs /tmp/tmp -o loop 15 | 16 | tar xzf /tmp/alpine-minirootfs-3.8.1-x86_64.tar.gz -C /tmp/tmp 17 | 18 | CODEDIR=${CODEDIR:-/opt/code} 19 | 20 | cp -r /tmp/overlay/* /tmp/tmp/ 21 | 22 | cd $CODEDIR 23 | go get -d -v ./... 24 | CGO_ENABLED=0 go build -o /tmp/tmp/usr/local/bin/program 25 | 26 | cat > /tmp/tmp/prepare.sh < /etc/resolv.conf 26 | 27 | # Set up IP and GATEWAY 28 | ifconfig eth0 up 29 | ifconfig eth0 $IP 30 | route add default gw $GATEWAY 31 | 32 | # UNDO configuration mount 33 | umount /mnt/hostconffs-mount 34 | rm -rf /mnt/hostconffs-mount 35 | 36 | #echo "Restarting networking" 37 | /etc/init.d/networking restart 38 | 39 | # run program. the firecracker compiler would place this here. 40 | echo Running user program 41 | /usr/local/bin/program 42 | 43 | # shutdown firecracker 44 | echo User program is all done. rebooting. 45 | reboot 46 | -------------------------------------------------------------------------------- /start-fire: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is the entrypoint for firecracker microVM configuration and 4 | # attaches network bridges to the VM and starts microVM with the 5 | # selected options. 6 | 7 | # Refer to README.md for further info. 8 | 9 | # If no arguments, the VM will lauch with default values for vCPU 10 | # count (=1) and memory size (=128 MiB) 11 | 12 | 13 | : ${DEBUG:='N'} 14 | : ${LAUNCHER:='/usr/local/bin/firectl'} 15 | : ${FIRECTL_KERNEL:='--kernel=/tmp/micro-vmlinux.bin'} 16 | : ${FIRECTL_KERNEL_OPTS:='--kernel-opts=console=ttyS0 noapic reboot=k panic=1 pci=off nomodules rw'} 17 | : ${FIRECTL_ROOT_DRIVE:='--root-drive=/rootfs/image'} 18 | 19 | log () { 20 | case "$1" in 21 | INFO | WARNING | ERROR ) 22 | echo "$1: ${@:2}" 23 | ;; 24 | DEBUG) 25 | [[ $DEBUG -eq 1 ]] && echo "$1: ${@:2}" 26 | ;; 27 | *) 28 | echo "-- $@" 29 | ;; 30 | esac 31 | } 32 | 33 | # ContainsElement: checks if first parameter is among the array given as second parameter 34 | # returns 0 if the element is found in the list and 1 if not 35 | # usage: containsElement $item $list 36 | 37 | containsElement () { 38 | local e 39 | for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done 40 | return 1 41 | } 42 | 43 | # Generate random MAC address 44 | genMAC () { 45 | hexchars="0123456789ABCDEF" 46 | end=$( for i in {1..8} ; do echo -n ${hexchars:$(( $RANDOM % 16 )):1} ; done | sed -e 's/\(..\)/:\1/g' ) 47 | echo "FE:05$end" 48 | } 49 | 50 | # atoi: Returns the integer representation of an IP arg, passed in ascii 51 | # dotted-decimal notation (x.x.x.x) 52 | atoi() { 53 | IP=$1 54 | IPnum=0 55 | for (( i=0 ; i<4 ; ++i )) 56 | do 57 | ((IPnum+=${IP%%.*}*$((256**$((3-${i})))))) 58 | IP=${IP#*.} 59 | done 60 | echo $IPnum 61 | } 62 | 63 | # itoa: returns the dotted-decimal ascii form of an IP arg passed in integer 64 | # format 65 | itoa() { 66 | echo -n $(($(($(($((${1}/256))/256))/256))%256)). 67 | echo -n $(($(($((${1}/256))/256))%256)). 68 | echo -n $(($((${1}/256))%256)). 69 | echo $((${1}%256)) 70 | } 71 | 72 | cidr2mask() { 73 | local i mask="" 74 | local full_octets=$(($1/8)) 75 | local partial_octet=$(($1%8)) 76 | 77 | for ((i=0;i<4;i+=1)); do 78 | if [ $i -lt $full_octets ]; then 79 | mask+=255 80 | elif [ $i -eq $full_octets ]; then 81 | mask+=$((256 - 2**(8-$partial_octet))) 82 | else 83 | mask+=0 84 | fi 85 | test $i -lt 3 && mask+=. 86 | done 87 | 88 | echo $mask 89 | } 90 | 91 | # Generates and returns a new IP and MASK in a superset (inmediate wider range) 92 | # of the given IP/MASK 93 | # usage: getNonConflictingIP IP MASK 94 | # returns NEWIP MASK 95 | getNonConflictingIP () { 96 | local IP="$1" 97 | local CIDR="$2" 98 | 99 | let "newCIDR=$CIDR-1" 100 | 101 | local i=$(atoi $IP) 102 | let "j=$i^(1<<(32-$CIDR))" 103 | local newIP=$(itoa j) 104 | 105 | echo $newIP $newCIDR 106 | } 107 | 108 | # generates unused, random names for macvlan or bridge devices 109 | # usage: generateNetDevNames DEVICETYPE 110 | # DEVICETYPE must be either 'macvlan' or 'bridge' 111 | # returns: 112 | # - bridgeXXXXXX if DEVICETYPE is 'bridge' 113 | # - macvlanXXXXXX, macvtapXXXXXX if DEVICETYPE is 'macvlan' 114 | generateNetdevNames () { 115 | devicetype=$1 116 | 117 | local netdevinterfaces=($(ip link show | awk "/$devicetype/ { print \$2 }" | cut -d '@' -f 1 | tr -d :)) 118 | local randomID=$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 6 | head -n 1) 119 | 120 | # check if the device already exists and regenerate the name if so 121 | while containsElement "$devicetype$randomID" "${netdevinterfaces[@]}"; do randomID=$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 6 | head -n 1); done 122 | 123 | echo "$randomID" 124 | } 125 | 126 | # Crate tap device with interface mac and ip 127 | setupTap () { 128 | set -x 129 | local iface="$1" 130 | local tapName="$2" 131 | 132 | 133 | NAMESERVER=`grep "nameserver " /etc/resolv.conf | cut -f 2 -d ' '` 134 | NAMESERVERS=`echo ${NAMESERVER[*]} | sed "s/ /,/g"` 135 | NETWORK_IP="${NETWORK_IP:-$(echo 172.$((RANDOM%(31-16+1)+16)).$((RANDOM%256)).$((RANDOM%(254-2+1)+2)))}" 136 | NETWORK_SUB=`echo $NETWORK_IP | cut -f1,2,3 -d\.` 137 | NETWORK_GW="${NETWORK_GW:-$(echo ${NETWORK_SUB}.1)}" 138 | 139 | 140 | ip tuntap add mode tap $tapName 141 | 142 | dnsmasq --user=root \ 143 | --dhcp-range=$NETWORK_IP,$NETWORK_IP \ 144 | --dhcp-option=option:router,$NETWORK_GW \ 145 | --dhcp-option=option:dns-server,$NAMESERVERS 146 | 147 | ip addr add $NETWORK_GW/24 dev $tapName 148 | ip link set dev "$tapName" up 149 | 150 | iptables -t nat -A POSTROUTING -o $iface -j MASQUERADE 151 | iptables -I FORWARD 1 -i $tapName -j ACCEPT 152 | iptables -I FORWARD 1 -o $tapName -m state --state RELATED,ESTABLISHED -j ACCEPT 153 | 154 | iptables -t nat -A PREROUTING -d $IP -j DNAT --to-destination $NETWORK_IP 155 | 156 | 157 | # Append MAC and respective IP address to the net-conf file 158 | echo -e "IP=$NETWORK_IP" >> /tmp/net-conf 159 | echo -e "GATEWAY=$NETWORK_GW" >> /tmp/net-conf 160 | } 161 | 162 | 163 | # Setup tap device to connect to the VM for the given interface 164 | # and setup the ip addresses 165 | configureNetworks () { 166 | local i=0 167 | 168 | local GATEWAY=$(ip r | grep default | awk '{print $3}') 169 | local IP 170 | 171 | /sbin/sysctl -w net.ipv4.conf.all.forwarding=1 172 | 173 | # Create the net-conf file that will be mounted as env drive 174 | for iface in "${local_ifaces[@]}"; do 175 | 176 | IPs=$(ip address show dev $iface | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/) 177 | IPs=($IPs) 178 | MAC=$(ip link show $iface | awk '/ether/ { print $2 }') 179 | log "DEBUG" "Container original MAC address: $MAC" 180 | 181 | IP=${IPs[0]} 182 | 183 | local CIDR=$(ip address show dev $iface | awk "/inet $IP/ { print \$2 }" | cut -f2 -d/) 184 | 185 | # use container MAC address ($MAC) for tap device 186 | # and generate a new one for the local interface 187 | ip link set $iface down 188 | ip link set $iface address $(genMAC) 189 | ip link set $iface up 190 | 191 | FIRECTL_NET_OPTS="" 192 | 193 | local deviceID=$(generateNetdevNames "bridge") 194 | local tapName="tap-$deviceID" 195 | 196 | # setup the tap device for connecting the VM 197 | setupTap $iface $tapName 198 | # firectl configuration: 199 | FIRECTL_NET_OPTS="$FIRECTL_NET_OPTS --tap-device=$tapName/$MAC" 200 | 201 | log "DEBUG" "tapName: $tapName" 202 | let i++ 203 | 204 | done 205 | } 206 | 207 | # Create docker host conatiner configuration image file that will be mounted as a drive 208 | createHostConfDrive() { 209 | drivePath="/tmp/hostconf" 210 | 211 | dd if=/dev/zero of=$drivePath bs=1M count=2 212 | mkfs.ext4 $drivePath 213 | 214 | mkdir /tmp/hostconffs-mount 215 | mount $drivePath /tmp/hostconffs-mount -o loop 216 | cp /tmp/net-conf /tmp/hostconffs-mount/net-conf 217 | cp /etc/hosts /tmp/hostconffs-mount/hosts 218 | cp /etc/resolv.conf /tmp/hostconffs-mount/resolv.conf 219 | umount /tmp/hostconffs-mount 220 | 221 | FIRECTL_DRIVE_OPTS="$FIRECTL_DRIVE_OPTS --add-drive=$drivePath:ro" 222 | } 223 | 224 | echo "Starting Configuration... \n" 225 | 226 | FIRECTL_DRIVE_OPTS="" 227 | 228 | # Debugging mode 229 | if [ "$1" = "bash" ]; then 230 | export DEBUG=1 231 | export ENABLE_DHCP=$ENABLE_DHCP 232 | export DNS_SERVERS=$DNS_SERVERS 233 | exec bash 234 | fi 235 | 236 | case "$DEBUG" in 237 | [Yy1]* ) DEBUG=1;; 238 | [Nn0]* ) DEBUG=0;; 239 | * ) log "ERROR" "DEBUG incorrect or undefined. It must be one of [Yy1Nn0]"; exit 1;; 240 | esac 241 | 242 | 243 | # Get all interfaces: 244 | local_ifaces=($(ip link show | grep -v noop | grep state | grep -v LOOPBACK | awk '{print $2}' | tr -d : | sed 's/@.*$//')) 245 | local_bridges=($(brctl show | tail -n +2 | awk '{print $1}')) 246 | # Get non-bridge interfaces: 247 | for i in "${local_bridges[@]}" 248 | do 249 | local_ifaces=(${local_ifaces[@]//*$i*}) 250 | done 251 | 252 | 253 | sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward" 254 | 255 | configureNetworks 256 | 257 | createHostConfDrive 258 | 259 | log "INFO" "Launching Firecracker" 260 | log "DEBUG" "Launching $LAUNCHER $FIRECTL_KERNEL $FIRECTL_KERNEL_OPTS $FIRECTL_ROOT_DRIVE $FIRECTL_DRIVE_OPTS $@ $FIRECTL_NET_OPTS" 261 | 262 | eval exec $LAUNCHER $FIRECTL_KERNEL $FIRECTL_KERNEL_OPTS $FIRECTL_ROOT_DRIVE $FIRECTL_DRIVE_OPTS "$@" $FIRECTL_NET_OPTS 263 | --------------------------------------------------------------------------------