Making containers with systemd

· Read in about 8 min · (1502 words)

Sometimes, I need to create a laboratory with virtual machines or containers that I can set up easily for testing various technologies. I used to use UML, but this solution doesn’t address my requirements.

In this article, we look at how to set up a laboratory, with systemd spawn for making containers.

Making our first container

To make our first container, we must use systemd, which allows us to manage services. To do so, we must use systemd-nspawn.

First, we create our repository, which contains all of the files for our new system:

$ mkdir ~/lab-test/test-arch

Then, we can create our system. For the ArchLinux users, you must install the package arch-install-scripts for using pacstrap, and with that, you can install your distribution:

$ pacstrap -c -d ~/lab-test/test-arch/ base

For Debian users, you must install debootstrap:

$ mkdir ~/lab-test/test-debian
$ debootstrap --arch=amd64 unstable ~/lab-test/test-debian/

Now that we have installed our system, we can start using it:

$ sudo systemd-nspawn -D ~/lab-test/test-arch

We are in our container, and we have one shell. First of all, you must change the root's password. After that, if you want to manage one service with systemctl, you will get the following error:

# systemctl status systemd-networkd
System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to bus: Host is down

As you see, the error is very clear: systemd isn’t the first process, and we can check that:

# cat /proc/1/status | grep -i Name
Name:    bash

To fix this issue, we must boot up in our container. Exit your container, and boot up with this command:

$ sudo systemd-nspawn -bD ~/lab-test/test-arch

After the booting up sequence, you can log with the root user account, with the new password; then, we check if we can use systemd:

# systemctl status systemd-networkd
* systemd-networkd.service - Network Service
     Loaded: loaded (/usr/lib/systemd/system/systemd-networkd.service; disabled; vendor preset: enabled)
     Active: inactive (dead)
       Docs: man:systemd-networkd.service(8)

If you experience an issue when logging in within your container, you must delete these files /etc/securetty and /usr/share/factory/etc/securetty within your container:

Managing your containers

SystemD uses the package systemd-machined to manage the virtual machines and containers, as follows:

$ machinectl
MACHINE   CLASS     SERVICE        OS   VERSION ADDRESSES
test-arch container systemd-nspawn arch -       -        

1 machines listed.

When a container is initiated, the file /run/systemd/machines/ is created in the repository :

# This is private data. Do not parse.
NAME=test-arch
SCOPE=machine-test\\x2darch.scope
SERVICE=systemd-nspawn
ROOT=/home/geoffrey/lab-test/test-arch
ID=b4227f1d22324c759234b6810f8944e1
LEADER=31643
CLASS=container
REALTIME=1587988574107599
MONOTONIC=11122121091
NETIF=6

Manage your network

To have network access from our container, we must connect it to a bridge. To do that, we use the package bridge-utils and we create one bridge and put in an IP address:

# brctl addbr br0
# ip addr add 192.168.2.1/24 dev br0
# ip link set br0 up

With the command systemd-spawn, we can add some parameters to connect our container to a bridge:

# systemd-nspawn -bD ~/lab-test/test-arch/ --private-network --network-veth --network-bridge=br0

In our container, we can add an IP address and we will use nginx:

# ip addr add 192.168.2.2/24 dev host0
# ip link set host0 up
# systemctl start nginx && systemctl status nginx
* nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
     Active: active (running) since Mon 2020-04-27 17:46:46 CEST; 7ms ago
    Process: 40 ExecStart=/usr/bin/nginx -g pid /run/nginx.pid; error_log stderr; (code=exited, status=0/SUCCESS)
   Main PID: 41 (nginx)
     CGroup: /system.slice/nginx.service
             |-41 nginx: master process /usr/bin/nginx -g pid /run/nginx.pid; error_log stderr;
             `-42 nginx: worker process
[...]

When Nginx has started, we make a request to get an HTML page:

$ curl 192.168.2.2
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Laboratory

Now we look at how to set up a laboratory. In the figure below, you can see the its architecture:

Architecture of our labo Figure XXX: Architecture of our new laboratory

Our server will host the Nginx service. The client will just be an HTTP client. To manage our routers and exchange their routing tables, we will use Bird.

The problem

As you see in the figure above, our routers needed two interfaces: one for the client and one for connecting with the router pair, but it wasn’t possible to make two different veths with the parameters --network-veth and --network-bridge. So, to fix this issue, we must manually create one veth and connect to our bridge:

$ sudo ip link add ve-rt1 type veth peer name ve-rt2
$ sudo brctl addif br1 ve-rt2
$ brctl show
bridge name    bridge id        STP enabled    interfaces
br0        8000.86b54a73aaef    no        
br1        8000.e6b16988d1b0    no        ve-rt2

Then, we will start our container:

$ sudo systemd-nspawn -D router1/ --private-network --network-veth --network-bridge=br0 --network-interface=ve-rt1

Our laboratory

We can now start to set up our laboratory. First at all, we create our routers with the Bird application:

$ mkdir ~/lab-test/{router1,router2}
$ pacstrap -c -d ~/lab-test/router1 base
$ pacstrap -c -d ~/lab-test/router2 base

Then, we start router1 and configure it:

router1# echo 1 > /proc/sys/net/ipv4/ip_forward
router1# ip addr add 91.0.0.1/24 dev host0 && ip link set host0 up
router1# ip addr add 10.0.0.1/24 dev ve-rt1 && ip link set ve-rt1 up
router1# cat /etc/bird.conf
[...]
protocol static static4 {
    ipv4;

    route 73.0.0.0/24 via 10.0.0.2;
}
[...]

We apply the same configuration for router2:

router2# echo 1 > /proc/sys/net/ipv4/ip_forward
router2# ip addr add 91.0.0.2/24 dev host0 && ip link set host0 up
router2# ip addr add 10.0.0.2/24 dev ve-rt2 && ip link set ve-rt2 up
router2# cat /etc/bird.conf
[...]
protocol static static4 {
    ipv4;

    route 91.0.0.0/24 via 10.0.0.1;
}
[...]

Then, we can start our container for the Nginx server and configure it:

server1# ip addr add 73.0.0.2/24 dev host0 && ip link set host0 up
server1# ip route add default via 73.0.0.1
server1# systemctl status nginx
* nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
     Active: active (running) since Thu 2020-04-30 14:49:11 CEST; 15min ago
    Process: 60 ExecStart=/usr/bin/nginx -g pid /run/nginx.pid; error_log stderr; (code=exited, status=0/SUCCESS)
   Main PID: 61 (nginx)
[...]

Then, we configure our client:

client1# ip addr add 91.0.0.2/24 dev host0 && ip link set host0 up
client1# ip route add default via 91.0.0.1

Now, we boot our containers, and we could check if our routers can forward all packets:

router1# ping -c 1 73.0.0.1
PING 73.0.0.1 (73.0.0.1) 56(84) bytes of data.
64 bytes from 73.0.0.1: icmp_seq=1 ttl=64 time=0.098 ms

--- 73.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.098/0.098/0.098/0.000 ms

router1# ping -c 1 73.0.0.2
PING 73.0.0.2 (73.0.0.2) 56(84) bytes of data.
64 bytes from 73.0.0.2: icmp_seq=1 ttl=63 time=0.232 ms

--- 73.0.0.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.232/0.232/0.232/0.000 ms

client1# ping -c 1 73.0.0.2
PING 73.0.0.2 (73.0.0.2) 56(84) bytes of data.
64 bytes from 73.0.0.2: icmp_seq=1 ttl=62 time=0.159 ms

--- 73.0.0.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.159/0.159/0.159/0.000 ms

And finally, we can test a request sent by our client to the server:

client1# curl -v 73.0.0.2
*   Trying 73.0.0.2:80...
* Connected to 73.0.0.2 (73.0.0.2) port 80 (#0)
> GET / HTTP/1.1
> Host: 73.0.0.2
> User-Agent: curl/7.69.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.18.0
[...]

Script

This is the script I used to set up my laboratory:

#!/bin/sh

create_bridge() {
    # Create if not exist
    if [ ! -d /sys/class/net/$1 ]; then
      sudo brctl addbr $1
      sudo ip link set $1 up
    fi    
}

create_veth() {
    if [ ! -d /sys/class/net/$1 ]; then
        sudo ip link add $1 type veth peer name $2
        sudo ip link set $1 up && sudo ip link set $2 up
    fi
    if [ ! -d /sys/class/net/$3/brif/$2 ]
    then
        sudo brctl addif $3 $2
    fi
}

vm1=clt1
vm2=srv1
rt1=rt1
rt2=rt2

# Create bridges
bridge1=br0
bridge2=br1
bridge3=br2
create_bridge $bridge1
create_bridge $bridge2
create_bridge $bridge3

# Create veth
if1=ve-$rt1
if2=ve-$rt2
create_veth $if1 ve-to-$rt2 $bridge2
create_veth $if2 ve-to-$rt1 $bridge2

# Start containers
case $1 in
  clt1)
    sudo systemd-nspawn -bD ~/lab-test/clt1/ \
      --private-network --network-veth --network-bridge=$bridge1
    ;;
  srv1)
    sudo systemd-nspawn -bD ~/lab-test/srv1/ \
      --private-network --network-veth --network-bridge=$bridge3
    ;;
  rt1)
    sudo systemd-nspawn -bD ~/lab-test/router1/ \
      --private-network --network-veth --network-bridge=$bridge1 \
      --network-interface=$if1
    ;;
  rt2)
    sudo systemd-nspawn -bD ~/lab-test/router2/ \
      --private-network --network-veth --network-bridge=$bridge3 \
      --network-interface=$if2
    ;;
esac

The script takes one argument, the name of the container.

To conclude

I admit, I didn’t succeed at booting the container in the background, and it’s complicated to set up a laboratory, because, for each container, I need to open a new terminal. But, I found the systemd-nspawn solution very interesting, and it’s installed by default in the linux system, if you used systemd.