Inside packer-builder-arm-image

As someone who has worked embedded project on occasion, I needed to a consistent way to automatically prepare embedded images, pre-loaded with code (especially in the world of cheap SD-cards). The challenge is ofcourse, that embedded images use a different CPU architecture. That's why packer-builder-arm-image was created.

Overview

packer-builder-arm-image brings the benefits for packer, to the world embedded linux arm images, and allows you do modify them directly from your laptop or cloud VM. This means that you can now modify ARM images using native ARM tools on your x86 machine. Let's see how!

The packer-build-arm-image performs the following:

  1. Download and verifies an existing OS Image (raspbian for example)
  2. Extract it and mount it
  3. Modify it via shell scripts and file uploads (using arm binaries!)

The packer builder arm leverages existing technologies and integrates them together. Let's see how in detail.

Downloads an existing OS Image

The plugin leverages existing packer code for downloading ISOs (hence all the iso terminology in the config). this lets us use images from a local or a remote location, and have their checksum validated. This feature makes easy to distribute packer configuration and have it Just Work™.

Extract & Mount

Extract

SD card images are usually compressed. If that's the case, they are automatically extracted so they can be mounted. When extracting xz images, we try to use the native xz binary for maximum performance, falling back to a go library if not available.

In the end of the process, we leave the result uncompressed so it is ready for flashing. Note that the original image file is never modified - a copy is made.

Mount

Before mounting, we allow the user to optionally increase the size of the last partition (if the free space in the original image is not sufficient). We do that by reading and modifying the partition table for the image.

Once we have the final image files with the correct partition sizes, time to mount them.

In order to mount the image locally (so we can edit its file system), we use kpartx. kpartx reads the partition table from the image and maps each partition to device files using the kernel's device mapper. we then mount these devices.

Where do we mount them too? Well, if its a known image type (BBB or raspbian) we have presets for the mount points. If not, the list of mount points can be provided by the user in the configuation.

Image modification

Once everything is mounted, we can chroot in and start modifying the image. We even have most of the code for handling chroot in packer already (in the aws provisioner. the code was copied to prevent pulling in aws dependencies). The one problem remains that all the images inside the chroot are arm binaries. Let's see how we solve that.

To allow users to run the arm utilities in the image (e.g. apt-get install ...), We use the kernel's binfmt and qemu-static-arm. the kernel binfmt allows us to have arm binaries automatically executed through qemu. The qemu-static-arm is copied inside the image, so it's available when chrooting, and removed once all customizations are done.

If the user wants to provide custom arguments for qemu, we compile a static C qemu wrapper, that execs qemu with the arguments the user provided.

Summary

That's it!

All the steps above now enable using packer's shell and file provisioners to prepare you custom image automcatically. Checkout the examples in the repo. with the set of tools above you can create and modify embedded \ IoT images automatically. I personally used this to create images with default passwords disabled for extra network security. During the provisioning phase I added my ssh public key to the image, so I can still login remotely.