Skip to content
This repository was archived by the owner on Aug 22, 2024. It is now read-only.

Commit 9371866

Browse files
authored
Update/add systemd (#1)
* update readme * make code more readable * make variable names more descriptive * add doc comments to functions * make calc_* functions consistent * modularize code * use shared common utilty functions * install to /usr/local and symlink to /usr/bin * isolate upstart installation * isolate systemd installation * use a config file * use trap for stopping log * move service installs * move utils location * create global install script * suppress error messages when detecting init system * echo detected init system * simplify config value retrieval * fix logthis * start service last on installation * add more statistics to logging * add logging of number of devices * make logging messages consistent
1 parent e9415a6 commit 9371866

File tree

13 files changed

+283
-129
lines changed

13 files changed

+283
-129
lines changed

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
# Amazon Elastic Block Store Autoscale
22

3-
This is an example of a small daemon process that monitors a BTRFS filesystem mountpoint and automatically expands it when free space falls below a configured threshold. New [Amazon EBS](https://aws.amazon.com/ebs/) volumes are added to the instance as necessary and the underlying [BTRFS filesystem](http://btrfs.wiki.kernel.org) expands while still mounted. As new devices are added, the BTRFS metadata blocks are rebalanced to mitigate the risk that space for metadata will not run out.
3+
This is an example of a daemon process that monitors a BTRFS filesystem mountpoint and automatically expands it when free space falls below a configured threshold. New [Amazon EBS](https://aws.amazon.com/ebs/) volumes are added to the instance as necessary and the underlying [BTRFS filesystem](http://btrfs.wiki.kernel.org) expands while still mounted. As new devices are added, the BTRFS metadata blocks are rebalanced to mitigate the risk that space for metadata will not run out.
44

55
## Assumptions:
66

7-
1. That this code is running on a AWS EC2 instance
8-
2. The instance has a IAM Instance Profile with appropriate permissions to create and attache new EBS volumes. Ssee the [IAM Instance Profile](#iam_instance_profile) section below for more details
9-
3. That prerequisites are installed on the instance.
7+
1. Code is running on an AWS EC2 instance
8+
2. The instance is using a Linux based OS with either **upstart** or **systemd** system initialization
9+
3. The instance has a IAM Instance Profile with appropriate permissions to create and attach new EBS volumes. See the [IAM Instance Profile](#iam_instance_profile) section below for more details
10+
4. That prerequisites are installed on the instance.
1011

1112
Provided in this repo are:
1213

1314
1. A python [script](bin/create-ebs-volume.py) that creates and attaches new EBS volumes to the current instance
1415
2. The daemon [script](bin/ebs-autoscale) that monitors disk space and expands the BTRFS filesystem by leveraging the above script to add EBS volumes, expand the filesystem, and rebalance the metadata blocks
15-
2. A template for an [upstart configuration file](templates/ebs-autoscale.conf.template)
16-
2. A [logrotate configuration file](templates/ebs-autoscale.logrotate) which should not be needed but may as well be in place for long-running instances.
17-
5. A [initialization script](bin/init-ebs-autoscale.sh) to configure and install all of the above
18-
6. A [cloud-init](templates/cloud-init-userdata.yaml) file for user-data that installs required packages and runs the initialization script. By default this creates a mount point of `/scratch` on a encrypted 20GB EBS volume. To change the mount point, edit the file.
16+
3. Service definitions for [upstart](service/upstart/ebs-autoscale.conf) and [systemd](service/systemd/ebs-autoscale.service)
17+
4. Configuration files for the [service](config/ebs-autoscale.json) and [logrotate](config/ebs-autoscale.logrotate)
18+
5. An [installation script](install.sh) to configure and install all of the above
19+
6. An example [cloud-init](templates/cloud-init-userdata.yaml) script that can be used as EC2 instance user-data for automated installation
1920

2021
## Installation
2122

22-
The easiest way to set up an instance is to provide a launch call with the userdata [cloud-init script](templates/cloud-init-userdata.yaml). Here is an example of launching the [Amazon ECS-Optimized AMI](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html) in us-east-1 using this file:
23+
The easiest way to set up an instance is to provide a launch call with the userdata [cloud-init script](templates/cloud-init-userdata.yaml). Here is an example of launching the [Amazon ECS-Optimized AMI](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html) in us-east-1 using this file:
2324

2425
```bash
2526
aws ec2 run-instances --image-id ami-5253c32d \
@@ -31,6 +32,7 @@ aws ec2 run-instances --image-id ami-5253c32d \
3132
--iam-instance-profile Name=MyInstanceProfileWithProperPermissions
3233
```
3334

35+
that installs required packages and runs the initialization script. By default this creates a mount point of `/scratch` on a encrypted 20GB EBS volume. To change the mount point, edit the file.
3436

3537
## A note on IAM Instance Profile
3638

bin/create-ebs-volume.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import boto3
3737
from botocore.exceptions import ClientError
3838

39-
## TODO: CLI arguments
4039
parameters = argparse.ArgumentParser(description="Create a new EBS Volume and attach it to the current instance")
4140
parameters.add_argument("-s","--size", type=int, required=True)
4241
parameters.add_argument("-t","--type", type=str, default="gp2")
@@ -71,7 +70,7 @@ def get_metadata(key):
7170
return urllib.urlopen(("/").join(['http://169.254.169.254/latest/meta-data', key])).read()
7271

7372

74-
# create a EBS volume
73+
# create an EBS volume
7574
def create_and_attach_volume(size=10, vol_type="gp2", encrypted=True, max_attached_volumes=16, max_created_volumes=256):
7675
instance_id = get_metadata("instance-id")
7776
availability_zone = get_metadata("placement/availability-zone")
@@ -143,5 +142,12 @@ def create_and_attach_volume(size=10, vol_type="gp2", encrypted=True, max_attach
143142

144143
if __name__ == '__main__':
145144
args = parameters.parse_args()
146-
print(create_and_attach_volume(args.size), end='')
145+
print(
146+
create_and_attach_volume(
147+
size=args.size,
148+
vol_type=args.type,
149+
encrypted=args.encrypted
150+
),
151+
end=''
152+
)
147153
sys.stdout.flush()

bin/ebs-autoscale

Lines changed: 72 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,49 +28,57 @@
2828
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
2929
# POSSIBILITY OF SUCH DAMAGE.
3030

31-
if [ "$#" -ne "1" ]; then
32-
echo "USAGE: $0 <MOUNT POINT>"
33-
exit 1
34-
fi
31+
. /usr/local/amazon-ebs-autoscale/shared/utils.sh
3532

36-
logthis () {
37-
echo "[`date`] $1"
38-
}
33+
initialize
34+
35+
MAX_LOGICAL_VOLUME_SIZE=$(get_config_value .limits.max_logical_volume_size)
36+
MAX_EBS_VOLUME_COUNT=$(get_config_value .limits.max_ebs_volume_count)
3937

40-
MP=$1
38+
MOUNTPOINT=$(get_config_value .mountpoint)
4139
BASEDIR=$(dirname $0)
42-
AZ=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone/)
4340

44-
logthis "EBS Autoscaling mountpoint: ${MP}"
41+
starting
42+
trap "stopping; exit" INT TERM KILL
4543

46-
while [ -z "${AZ}" ]; do
47-
logthis "Metadata service did not return AZ. Trying again."
48-
sleep 1
49-
AZ=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone/)
50-
done
51-
RG=$(echo ${AZ} | sed -e 's/[a-z]$//')
52-
logthis "Region = $RG."
53-
IN=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
54-
DRIVE_LETTERS=({a..z})
44+
logthis "EBS Autoscaling mountpoint: ${MOUNTPOINT}"
45+
logthis "Region = $AWS_REGION"
46+
logthis "Availability Zone = $AWS_AZ"
5547

5648
# make sure that this device is mounted.
57-
until [ -d "${MP}" ]; do
49+
until [ -d "${MOUNTPOINT}" ]; do
5850
sleep 1
5951
done
52+
53+
get_num_devices() {
54+
echo $(ls /dev/sd* | grep -v -E '[0-9]$' | wc -l)
55+
}
56+
6057
calc_threshold() {
61-
local num_devices=$(ls /dev/sd* | grep -v -E '[0-9]$' | wc -l)
58+
# calculates percent utilization threshold for adding additional ebs volumes
59+
# as more ebs volumes are added, the threshold level increases
60+
61+
local num_devices=$(get_num_devices)
6262
local threshold=50
63-
if [ "$num_devices" -gt "4" ] && [ "$num_devices" -le "6" ]; then
63+
64+
if [ "$num_devices" -ge "4" ] && [ "$num_devices" -le "6" ]; then
6465
threshold=80
65-
elif [ "$num_devices" -gt "6" ] && [ "$num_devices" -le "10" ]; then
66+
elif [ "$num_devices" -gt "6" ] && [ "$num_devices" -le "10" ]; then
67+
threshold=90
68+
elif [ "$num_devices" -gt "10" ]; then
6669
threshold=90
6770
else
6871
threshold=50
6972
fi
73+
7074
echo ${threshold}
7175
}
76+
7277
calc_new_size() {
73-
local num_devices=$1
78+
# calculates the size to use for new ebs volumes to expand space
79+
# new volume sizes increase as the number of attached volumes increase
80+
81+
local num_devices=$(get_num_devices)
7482
local new_size=150
7583

7684
if [ "$num_devices" -ge "4" ] && [ "$num_devices" -le "6" ]; then
@@ -82,49 +90,68 @@ calc_new_size() {
8290
else
8391
new_size=150
8492
fi
93+
8594
echo ${new_size}
8695
}
8796

8897
add_space () {
89-
local num_devices=$(ls /dev/sd* | grep -v -E '[0-9]$' | wc -l)
90-
if [ "${num_devices}" -ge "16" ]; then
98+
local num_devices=$(get_num_devices)
99+
if [ "${num_devices}" -ge "$MAX_EBS_VOLUME_COUNT" ]; then
91100
logthis "No more volumes can be safely added."
92101
return 0
93102
fi
94-
local curr_size=$(df -BG ${MP} | grep ${MP} | awk '{print $2} ' | cut -d'G' -f1)
95-
if [ "${curr_size}" -lt "16384" ]; then
96-
local vol_size=$(calc_new_size ${num_devices})
97-
logthis "Extending LV ${MP} by ${vol_size}GB"
98103

99-
DV=$(python ${BASEDIR}/create-ebs-volume.py -s ${vol_size})
104+
local curr_size=$(df -BG ${MOUNTPOINT} | grep ${MOUNTPOINT} | awk '{print $2} ' | cut -d'G' -f1)
105+
if [ "${curr_size}" -lt "$MAX_LOGICAL_VOLUME_SIZE" ]; then
106+
local vol_size=$(calc_new_size)
107+
logthis "Extending logical volume ${MOUNTPOINT} by ${vol_size}GB"
108+
109+
DEVICE=$(python ${BASEDIR}/create-ebs-volume.py -s ${vol_size})
100110

101111
exit_status=$?
102112
if [ $exit_status -eq 0 ]; then
103-
logthis "adding volume to filesystem"
104-
btrfs device add ${DV} ${MP}
105-
btrfs balance start -m ${MP}
106-
logthis "Finished extending device."
113+
logthis "Adding device ${DEVICE} to logical volume ${MOUNTPOINT}"
114+
btrfs device add ${DEVICE} ${MOUNTPOINT}
115+
btrfs balance start -m ${MOUNTPOINT}
116+
logthis "Finished extending logical volume"
107117

108118
else
109-
logthis "Error creating or attaching volume"
119+
logthis "Error creating or attaching EBS volume"
110120
fi
111121

112122
fi
113123
}
114124

115-
COUNT=300
125+
# number of event loops between utilization status log lines
126+
# helps to limit the log file size
127+
# utilization detection is not affected by this
128+
LOG_INTERVAL=$(get_config_value .logging.log_interval)
129+
130+
# initialized value for log lines
131+
# report on first run
132+
LOG_COUNT=$LOG_INTERVAL
133+
134+
# time in seconds between event loops
135+
# keep this low so that rapid increases in utilization are detected
136+
DETECTION_INTERVAL=$(get_config_value .detection_interval)
137+
116138
THRESHOLD=$(calc_threshold)
117139
while true; do
118-
F=$(df -BG ${MP} | grep -v Filesystem | awk '{print $5}' | cut -d"%" -f1 -)
119-
if [ $F -ge "${THRESHOLD}" ]; then
120-
logthis "LOW DISK ($F): Adding more."
140+
NUM_DEVICES=$(get_num_devices)
141+
STATS=$(df -BG ${MOUNTPOINT} | grep -v Filesystem)
142+
TOTAL_SIZE=$(echo ${STATS} | awk '{print $2}')
143+
USED=$(echo ${STATS} | awk '{print $3}')
144+
AVAILABLE=$(echo ${STATS} | awk '{print $4}')
145+
PCT_UTILIZATION=$(echo ${STATS} | awk '{print $5}' | cut -d"%" -f1 -)
146+
if [ $PCT_UTILIZATION -ge "${THRESHOLD}" ]; then
147+
logthis "LOW DISK (${PCT_UTILIZATION}%): Adding more."
121148
add_space
122149
fi
123-
if [ "${COUNT}" -ge "300" ]; then
124-
logthis "Threshold -> ${THRESHOLD} :: Used% -> ${F}%"
125-
COUNT=0
150+
if [ "${LOG_COUNT}" -ge "${LOG_INTERVAL}" ]; then
151+
logthis "Devices ${NUM_DEVICES} : Size ${TOTAL_SIZE} : Used ${USED} : Aailable ${AVAILABLE} : Used% ${PCT_UTILIZATION}% : Threshold ${THRESHOLD}%"
152+
LOG_COUNT=0
126153
fi
127154
THRESHOLD=$(calc_threshold)
128-
COUNT=$(expr $COUNT + 1 )
129-
sleep 1
155+
LOG_COUNT=$(expr $LOG_COUNT + 1 )
156+
sleep $DETECTION_INTERVAL
130157
done

config/ebs-autoscale.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"mountpoint": "/scratch",
3+
"detection_interval": 1,
4+
"limits": {
5+
"max_logical_volume_size": 16384,
6+
"max_ebs_volume_count": 16
7+
},
8+
"logging": {
9+
"log_file": "/var/log/ebs-autoscale.log",
10+
"log_interval": 300
11+
}
12+
}

install.sh

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/bin/sh
2+
# Copyright 2018 Amazon.com, Inc. or its affiliates.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
#
7+
# 1. Redistributions of source code must retain the above copyright notice,
8+
# this list of conditions and the following disclaimer.
9+
#
10+
# 2. Redistributions in binary form must reproduce the above copyright
11+
# notice, this list of conditions and the following disclaimer in the
12+
# documentation and/or other materials provided with the distribution.
13+
#
14+
# 3. Neither the name of the copyright holder nor the names of its
15+
# contributors may be used to endorse or promote products derived from
16+
# this software without specific prior written permission.
17+
#
18+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
20+
# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
21+
# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
22+
# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23+
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27+
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
28+
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29+
# POSSIBILITY OF SUCH DAMAGE.
30+
31+
set -e
32+
33+
function printUsage() {
34+
echo "USAGE: $0 <MOUNT POINT> [<DEVICE>]"
35+
}
36+
37+
if [ "$#" -lt "1" ]; then
38+
printUsage
39+
exit 1
40+
fi
41+
42+
MOUNTPOINT=$1
43+
DEVICE=$2
44+
BASEDIR=$(dirname $0)
45+
46+
. ${BASEDIR}/shared/utils.sh
47+
48+
initialize
49+
50+
# Install executables
51+
# make executables available on standard PATH
52+
mkdir -p /usr/local/amazon-ebs-autoscale/{bin,shared}
53+
cp ${BASEDIR}/bin/{create-ebs-volume.py,ebs-autoscale} /usr/local/amazon-ebs-autoscale/bin
54+
chmod +x /usr/local/amazon-ebs-autoscale/bin/*
55+
ln -sf /usr/local/amazon-ebs-autoscale/bin/* /usr/local/bin/
56+
ln -sf /usr/local/amazon-ebs-autoscale/bin/* /usr/bin/
57+
58+
59+
# copy shared assets
60+
cp ${BASEDIR}/shared/utils.sh /usr/local/amazon-ebs-autoscale/shared
61+
62+
63+
## Install configs
64+
# install the logrotate config
65+
cp ${BASEDIR}/config/ebs-autoscale.logrotate /etc/logrotate.d/ebs-autoscale
66+
67+
# install default config
68+
sed -e "s#/scratch#${MOUNTPOINT}#" ${BASEDIR}/config/ebs-autoscale.json > /etc/ebs-autoscale.json
69+
70+
71+
## Create filesystem
72+
if [ -e $MOUNTPOINT ] && ! [ -d $MOUNTPOINT ]; then
73+
echo "ERROR: $MOUNTPOINT exists but is not a directory."
74+
exit 1
75+
elif ! [ -e $MOUNTPOINT ]; then
76+
mkdir -p $MOUNTPOINT
77+
fi
78+
79+
# If a device is not given, or if the device is not valid
80+
# create a new 20GB volume
81+
if [ -z "${DEVICE}" ] || [ ! -b "${DEVICE}" ]; then
82+
DEVICE=$(create-ebs-volume.py --size 20)
83+
fi
84+
85+
# create and mount the BTRFS filesystem
86+
mkfs.btrfs -f -d single $DEVICE
87+
mount $DEVICE $MOUNTPOINT
88+
89+
# add entry to fstab
90+
# allows non-root users to mount/unmount the filesystem
91+
echo -e "${DEVICE}\t${MOUNTPOINT}\tbtrfs\tdefaults\t0\t0" | tee -a /etc/fstab
92+
93+
94+
## Install service
95+
INIT_SYSTEM=$(detect_init_system 2>/dev/null)
96+
case $INIT_SYSTEM in
97+
upstart|systemd)
98+
echo "$INIT_SYSTEM detected"
99+
cd ${BASEDIR}/service/$INIT_SYSTEM
100+
. ./install.sh
101+
;;
102+
103+
*)
104+
echo "Could not install EBS Autoscale - unsupported init system"
105+
exit 1
106+
esac
107+
cd ${BASEDIR}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[Unit]
2+
Description=Amazon EBS Autoscale
3+
After=network-online.target
4+
5+
[Service]
6+
ExecStart=/usr/local/bin/ebs-autoscale
7+
Restart=always
8+
9+
[Install]
10+
WantedBy=multi-user.target

service/systemd/install.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
# install systemd service
4+
cp ebs-autoscale.service /usr/lib/systemd/system/ebs-autoscale.service
5+
6+
# enable the service and start
7+
systemctl daemon-reload
8+
systemctl enable ebs-autoscale.service
9+
systemctl start ebs-autoscale.service

0 commit comments

Comments
 (0)