Post Syndicated from Lennart Poettering original https://0pointer.net/blog/walkthrough-for-portable-services-in-go.html
Portable Services Walkthrough (Go Edition)
A few months ago I posted a blog story with a walkthrough of systemd
Portable
Services. The
example service given was written in C, and the image was built with
mkosi
. In this blog story I’d
like to revisit the exercise, but this time focus on a different
aspect: modern programming languages like Go and Rust push users a lot
more towards static linking of libraries than the usual dynamic
linking preferred by C (at least in the way C is used by traditional
Linux distributions).
Static linking means we can greatly simplify image building: if we
don’t have to link against shared libraries during runtime we don’t
have to include them in the portable service image. And that means
pretty much all need for building an image from a Linux distribution
of some kind goes away as we’ll have next to no dependencies that
would require us to rely on a distribution package manager or
distribution packages. In fact, as it turns out, we only need as few
as three files in the portable service image to be fully functional.
So, let’s have a closer look how such an image can be put
together. All of the following is available in this git
repository.
A Simple Go Service
Let’s start with a simple Go service, an HTTP service that simply
counts how often a page from it is requested. Here are the sources:
main.go
— note that I am not a seasoned Go programmer, hence please be
gracious.
The service implements systemd’s socket activation protocol, and thus
can receive bound TCP listener sockets from systemd, using the
$LISTEN_PID
and $LISTEN_FDS
environment variables.
The service will store the counter data in the directory indicated in
the $STATE_DIRECTORY
environment variable, which happens to be an
environment variable current systemd versions set based on the
StateDirectory=
setting in service files.
Two Simple Unit Files
When a service shall be managed by systemd a unit file is
required. Since the service we are putting together shall be socket
activatable, we even have two:
portable-walkthrough-go.service
(the description of the service binary itself) and
portable-walkthrough-go.socket
(the description of the sockets to listen on for the service).
These units are not particularly remarkable: the .service
file
primarily contains the command line to invoke and a StateDirectory=
setting to make sure the service when invoked gets its own private
state directory under /var/lib/
(and the $STATE_DIRECTORY
environment variable is set to the resulting path). The .socket
file
simply lists 8088 as TCP/IP port to listen on.
An OS Description File
OS images (and that includes portable service images) generally should
include an
os-release
file. Usually, that is provided by the distribution. Since we are
building an image without any distribution let’s write our own
version of such a
file. Later
on we can use the portablectl inspect
command to have a look at this
metadata of our image.
Putting it All Together
The four files described above are already every file we need to build
our image. Let’s now put the portable service image together. For that
I’ve written a
Makefile
. It
contains two relevant rules: the first one builds the static binary
from the Go program sources. The second one then puts together a
squashfs
file system combining the following:
- The compiled, statically linked service binary
- The two systemd unit files
- The
os-release
file - A couple of empty directories such as
/proc/
,/sys/
,/dev/
and so on that need to be over-mounted with the respective kernel
API file system. We need to create them as empty directories here
since Linux insists on directories to exist in order to over-mount
them, and since the image we are building is going to be an
immutable read-only image (squashfs
) these directories cannot be
created dynamically when the portable image is mounted. - Two empty files
/etc/resolv.conf
and/etc/machine-id
that can
be over-mounted with the same files from the host.
And that’s already it. After a quick make
we’ll have our portable
service image portable-walkthrough-go.raw
and are ready to go.
Trying it out
Let’s now attach the portable service image to our host system:
# portablectl attach ./portable-walkthrough-go.raw
(Matching unit files with prefix 'portable-walkthrough-go'.)
Created directory /etc/systemd/system.attached.
Created directory /etc/systemd/system.attached/portable-walkthrough-go.socket.d.
Written /etc/systemd/system.attached/portable-walkthrough-go.socket.d/20-portable.conf.
Copied /etc/systemd/system.attached/portable-walkthrough-go.socket.
Created directory /etc/systemd/system.attached/portable-walkthrough-go.service.d.
Written /etc/systemd/system.attached/portable-walkthrough-go.service.d/20-portable.conf.
Created symlink /etc/systemd/system.attached/portable-walkthrough-go.service.d/10-profile.conf → /usr/lib/systemd/portable/profile/default/service.conf.
Copied /etc/systemd/system.attached/portable-walkthrough-go.service.
Created symlink /etc/portables/portable-walkthrough-go.raw → /home/lennart/projects/portable-walkthrough-go/portable-walkthrough-go.raw.
The portable service image is now attached to the host, which means we
can now go and start it (or even enable it):
# systemctl start portable-walkthrough-go.socket
Let’s see if our little web service works, by doing an HTTP request on port 8088:
# curl localhost:8088
Hello! You are visitor #1!
Let’s try this again, to check if it counts correctly:
# curl localhost:8088
Hello! You are visitor #2!
Nice! It worked. Let’s now stop the service again, and detach the image again:
# systemctl stop portable-walkthrough-go.service portable-walkthrough-go.socket
# portablectl detach portable-walkthrough-go
Removed /etc/systemd/system.attached/portable-walkthrough-go.service.
Removed /etc/systemd/system.attached/portable-walkthrough-go.service.d/10-profile.conf.
Removed /etc/systemd/system.attached/portable-walkthrough-go.service.d/20-portable.conf.
Removed /etc/systemd/system.attached/portable-walkthrough-go.service.d.
Removed /etc/systemd/system.attached/portable-walkthrough-go.socket.
Removed /etc/systemd/system.attached/portable-walkthrough-go.socket.d/20-portable.conf.
Removed /etc/systemd/system.attached/portable-walkthrough-go.socket.d.
Removed /etc/portables/portable-walkthrough-go.raw.
Removed /etc/systemd/system.attached.
And there we go, the portable image file is detached from the host again.
A Couple of Notes
-
Of course, this is a simplistic example: in real life services will
be more than one compiled file, even when statically linked. But
you get the idea, and it’s very easy to extend the example above to
include any additional, auxiliary files in the portable service
image. -
The service is very nicely sandboxed during runtime: while it runs
as regular service on the host (and you thus can watch its logs or
do resource management on it like you would do for all other
systemd services), it runs in a very restricted environment under a
dynamically assigned UID that ceases to exist when the service is
stopped again. -
Originally I wanted to make the service not only socket activatable
but also implement exit-on-idle, i.e. add a logic so that the
service terminates on its own when there’s no ongoing HTTP
connection for a while. I couldn’t figure out how to do this
race-freely in Go though, but I am sure an interested reader might
want to add that? By combining socket activation with exit-on-idle
we can turn this project into an excercise of putting together an
extremely resource-friendly and robust service architecture: the
service is started only when needed and terminates when no longer
needed. This would allow to pack services at a much higher density
even on systems with few resources. -
While the basic concepts of portable services have been around
since systemd 239, it’s best to try the above with systemd 241 or
newer since the portable service logic received a number of fixes
since then.
Further Reading
A low-level document introducing Portable Services is shipped along
with systemd.
Please have a look at the blog story from a few months
ago
that did something very similar with a service written in C.
There are also relevant manual pages:
portablectl(1)
and
systemd-portabled(8)
.