3

I launch m5.large (nitro-based) EC2 instance from Ubuntu AMI and attach EBS volume. There is systemd as a default init system. As AWS documentation "Making an Amazon EBS Volume Available for Use on Linux" stands, I mount EBS volume within user data:

#!/bin/bash

# Sleep gives the SSD drive a chance to mount before the user data script completes.
sleep 15

mkdir /application

mount /dev/nvme1n1 /application

I need Nginx and provide site configuration for it at EBS volume. For default nginx package with systemd unit file I declare a dependency on the mount with RequiresMountsFor directive within drop-in:

# /lib/systemd/system/nginx.service

[Unit]
Description=A high performance web server and a reverse proxy server
After=network.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/nginx.pid
TimeoutStopSec=5
KillMode=mixed

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/nginx.service.d/override.conf

[Unit]
RequiresMountsFor=/application

[Service]
Restart=always

But this doesn't help to run Nginx only after mount will be completed (in user data) for some reason. I can see the mount unit for /application path, but I don't see Required=application.mount as I'd expect:

$ sudo systemctl show -p After,Requires nginx
Requires=system.slice sysinit.target -.mount
After=sysinit.target -.mount systemd-journald.socket basic.target application.mount system.slice network.target

Nginx service still tries to run before cloud-init completes user data execution, exhausts all attempts to run the service and fails:

Apr 08 15:34:32 hostname nginx[1303]: nginx: [emerg] open() "/application/libexec/etc/nginx/nginx.site.conf" failed (2: No such file or directory) in /etc/nginx/sites-e
Apr 08 15:34:32 hostname nginx[1303]: nginx: configuration file /etc/nginx/nginx.conf test failed
Apr 08 15:34:32 hostname systemd[1]: nginx.service: Control process exited, code=exited status=1
Apr 08 15:34:32 hostname systemd[1]: Failed to start A high performance web server and a reverse proxy server.

I assume the service should be started by systemd on mount notification for the specified path /application. What am I missing?

What is the most flexible and correct way to mount EBS volumes at Ubuntu + systemd?

Tensho
  • 131
  • 1
  • 3
  • Keep reading that document you linked to. The next section, "Automatically Mount an Attached Volume After Reboot" explains what you need to do. This should NOT be in user data, that's not what the documentation says to do, and of course it does not work. – Michael Hampton Apr 08 '19 at 18:59
  • @MichaelHampton, Thank you for your answer. That's not quite what I want. The main idea behind this – **phoenix server**, that is always built from scratch and easy to recreate (or "rise from the ashes") through automated procedures. Ideally, it should never be rebooted. Terraform launches an EC2 instance within Autoscale Group from a golden AMI and attaches EBS volume with the application code and some configuration. So I just want to mount the EBS volume on the first boot and then run Nginx server. – Tensho Apr 08 '19 at 19:22
  • Then it should already be in the `/etc/fstab` in the AMI. – Michael Hampton Apr 09 '19 at 00:47
  • It's hard to predict what the device name should be in `/etc/fstab`, because it depends on the EC2 instance type. For old generation it's `/dev/xvdb` and for new one – `/dev/nvme0n1`. – Tensho Apr 09 '19 at 16:19
  • 1
    I suppose you could put them both in. One will fail and the other will succeed, but either way you get your mount. – Michael Hampton Apr 09 '19 at 16:20
  • @Tensho did you manage to get this working? I’ve come to the same point you have in the OP. I’m now thinking of a new systemd service that only reports success after the ebs volume is mounted… – danw Jun 21 '21 at 10:09
  • @danw Put both mounts to `/etc/fstab` as Michael suggested. – Tensho Jun 22 '21 at 12:20
  • @Tensho according to the docs you can't rely on the device name (if you have multiple EBS volumes), because the device name can change... see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html#identify-nvme-ebs-device – danw Jun 22 '21 at 20:01

1 Answers1

0

Here is my take on a solution that attempts to honour the constraints and limitations mentioned in https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html#identify-nvme-ebs-device. Not currently used in production but that's the aim...

NVMe device names follow the pattern /dev/nvmen, where is the enumeration order, and, for EBS, is 1. Occasionally, devices can respond to discovery in a different order in subsequent instance starts, which causes the device name to change.

The basic idea is to mimic the systemd Requires=foo.mount behaviour by wrapping the EBS mount procedure in a systemd service. Other services that depend on the EBS mount can simply specify that they must be started After the mount service is available.

The functionality builds upon the udev module that sym-links the physical device with the requested device specified when the volume is attached. For example /dev/sdf links to /dev/nvme...). See https://github.com/oogali/ebs-automatic-nvme-mapping and the above guide for more details.

The data-mount.service waits in a sleep loop for the sym-link to appear (allowing you time to attach the volume), then mounts the volume at the defined mount point, formatting it if necessary.

Service to mount the EBS volume

/sbin/ec2-boot-mount-ebs-volume

#!/bin/bash
#
# Copyright 2019 - binx.io B.V.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Based on https://github.com/binxio/ec2-boot-mount-ebs-volume.
#
# Requires the udev rule that automatically creates a sym-link to the actual device. See the note on udev on https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html#identify-nvme-ebs-device

set -e -o pipefail
exec 1> >(logger -s -t $(basename $0)) 2>&1

function wait_for_device {
        while [[ ! -b $(readlink -f $1) ]]; do
            echo "waiting for device $1" >&2
            sleep 5;
        done
}

function label_device {
        label=$(blkid $1 | sed -e 's/.*LABEL="\([^"]*\)".*/\1/')
        if [[ -z $label ]]; then
                echo "INFO: labeling $1 with $2">&2
                e2label $1 $2;
        elif [[ $label != $2 ]]; then
                echo "ERROR: device $1 already has label $label, expected $2">&2; exit 1
        fi
}

function main {
        local device mount_point fstype options
        if [[ $# -ne 4 ]] ;then
                echo "Usage: $(basename $0) device-name mount-point fstype options" >&2
                echo "  waits for the volume, formats it if unformatted and mounts it." >&2
                exit 1
        fi
        device="$1"
        mount_point="$2"
        fstype="$3"
        options="$4"

        wait_for_device $device
        real_device=$(readlink -f $device)
        if [[ $real_device != $device ]] ;then
                echo "INFO: $device appeared as $real_device" >&2
        fi

        if grep -q "^$real_device $mount_point " /proc/mounts; then
                echo "INFO: $real_device already mounted on $mount_point" >&2
                return 0
        fi

        if [[ -z $(blkid $real_device) ]] ; then
                echo "INFO: formatting $real_device" >&2
                mkfs -L $mount_point -t $fstype $real_device
        else
                echo "INFO: $real_device already formatted" >&2
                label_device $real_device $mount_point
        fi

        echo "INFO: mounting $real_device on $mount_point" >&2
        mkdir -p $mount_point
        mount -t $fstype -o "$options" $real_device $mount_point
}

main "$@"

/etc/systemd/system/data-mount.service

[Unit]
Description=Mount Data Volume
After=cloud-init-local.service

[Service]
Type=oneshot
RemainAfterExit=yes
# /dev/sdf should be replaced with the device name you requested when attaching the volume
ExecStart=/sbin/ec2-boot-mount-ebs-volume /dev/sdf /mnt/data ext4 "defaults"
ExecStop=umount -v /mnt/data

[Install]

Other Services

/etc/systemd/system/other-service.service.d/override.conf

[Unit]
# if the data-mount.service stops then this service also needs to stop
BindsTo=data-mount.service
# ensure this service starts after the mount is available
After=data-mount.service
danw
  • 101
  • 2