1
0
mirror of https://gitlab.alpinelinux.org/alpine/cloud/tiny-cloud.git synced 2025-12-15 11:22:43 +03:00

Introducing Tiny Cloud

This commit is contained in:
Jake Buchholz Göktürk 2022-01-02 10:15:20 -08:00
parent 0bfdd16977
commit 07f0e646e8
14 changed files with 402 additions and 167 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*~
*.bak
*.swp
.vscode/

View File

@ -1,4 +1,4 @@
Copyright (c) 2017-2020 Michael Crute
Copyright (c) 2017-2021 Jake Buchholz Göktürk, Michael Crute
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

View File

@ -2,4 +2,5 @@ PREFIX?=/
.PHONY: install
install:
install -Dm 755 tiny-ec2-bootstrap $(PREFIX)/etc/init.d/tiny-ec2-bootstrap
cp -r etc $(PREFIX)
cp -r lib $(PREFIX)

165
README.md
View File

@ -1,70 +1,125 @@
# Tiny EC2 Bootstrapper
# Tiny Cloud
This is designed to do the minimal amount of work required to bootstrap an EC2
instance based on the local settings assigned at boot time as well as the
user's configured settings. This is, in-concept, similar to
[cloud-init](https://cloudinit.readthedocs.io/en/latest/) but trades features
and cloud platform support for small size and limited external dependencies.
The Tiny Cloud bootstrapper performs critical initialization tasks for cloud
instances during their first boot. Unlike the more popular and feature-rich
[cloud-init](https://cloudinit.readthedocs.io/en/latest), Tiny Cloud seeks to
do just what is necessary with a small footprint and minimal dependencies.
A direct descendant of [tiny-ec2-bootstrap](
https://gitlab.alpinelinux.org/alpine/cloud/tiny-ec2-bootstrap), Tiny Cloud
works with multiple cloud providers. Currently, the following are supported:
* AWS (Amazon Web Services)
* Azure (Microsoft Azure)
* GCP (Google Cloud Platform)
* OCI (Oracle Cloud Infrastructure)
## Features
The following actions will occur ***only once***, during the initial boot of an
instance:
* expand the root filesystem to use all available root device space, during the
**sysinit** runlevel
* set the instance's hostname from instance metadata
* install SSH keys from instance metadata to the cloud user account's
`authorized_keys` file (the user must already exist)
* save the instance user-data to a file and if it's a script, execute it at the
end of the **default** runlevel
Optional features, which may not be universally necessary:
* manage symlinks from NVMe block devices to `/dev/xvd` and `/dev/sd` devices
(i.e. AWS Nitro instances)
* manage secondary IPv4 and IPv6 addresses on network interfaces
## Requirements
The most important feature of this bootstrapper is the very limited set of
dependencies. In-fact, this works with just BusyBox (provided ash and wget
are built in) and a couple utilities for expanding the root filesystem.
The full list of required dependencies are:
As Tiny Cloud is meant to be tiny, it has very few dependencies:
* Busybox (`ash`, `wget`, etc.)
* `partx`
* `resize2fs`
* `sfdisk`
- bash-like shell (e.g. bash, dash, ash)
- wget
- sfdisk
- partx
- resize2fs
Tiny Cloud has been developed specifically for use with the
[Alpine Cloud Images](https://gitlab.alpinelinux.org/alpine/cloud/alpine-cloud-images)
project, and as such, it is currently tailored for use with [Alpine Linux](
https://alpinelinux.org), the [OpenRC](https://github.com/OpenRC/openrc) init
system, and the `ext4` root filesystem. If you would like to see Tiny Cloud
supported on additional distributions, init systems, and/or filesystems, please
open an issue with your request -- or better yet, submit a merge request!
## Supported Features and Environments
## Installation
cloud-init has support for many different cloud providers. This project only
supports EC2; [EC2 metadata
service](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html)
is a hard requirement of using this bootstrapper. All of the data for the
supported features below is sourced from the EC2 instance metadata service
which runs on every EC2 instance at IP 169.254.169.254.
cloud-init also has a very rich feature set with support for adding users,
installing packages, and many other things. This bootstrap does not support
those things. Instead it supports:
- setting system hostname
- installing the instance's SSH keys in the EC2 user's authorized_keys file
- running any script-like user data (must start with #!)
- disabling root and the EC2 user's password
- expanding root partition to available disk space
These steps only run once. After the initial bootstrap the bootstrapper script
is a no-op. To force the script to run again at boot time remove the file
`/var/lib/cloud/.bootstrap-complete` and reboot the instance.
The default EC2 user is `alpine`; this can be overriden with a
`/etc/conf.d/tiny-ec2-bootstrap` containing...
Typically, Tiny Cloud is installed and configured when building a cloud image,
and is available on Alpine Linux as the [`tiny-cloud`](
https://pkgs.alpinelinux.org/packages?name=tiny-cloud) APK...
```
EC2_USER="otheruser"
apk install tiny-cloud
```
The EC2 user *must* already exist in the AMI -- `tiny-ec2-bootstrap` will
**NOT** add the user automatically.
This will install the necessary init scripts, libraries, etc. plus any missing
dependencies.
## User Data
Alternately, you can download a release tarball, and use `make` to install it.
User data is provided at instance boot time and can be any arbitrary string of
data. The bootstrapper will consider any user data that begins with the ASCII
characters `#!` to be a script. It will write the entire contents of the user
data to `/var/lib/cloud/user-data.sh`, make the file executable, and execute
the file piping any output to `/var/log/cloud-bootstrap.log`.
Next, enable the three primary init scripts...
```
rc-update add tiny-cloud-early sysinit
rc-update add tiny-cloud default
rc-update add tiny-cloud-final default
```
The user data script can do anything it pleases with the instance. It will be
run as root and networking will be up. No other guarantees about system state
are made at the point the script runs.
## Configuration
## Assumptions
Tiny Cloud looks expects configuration to be found at
[`/etc/conf.d/tiny-cloud`](etc/conf.d/tiny-cloud), which documents all
tuneable parameters (and their defaults).
- This was written for Alpine Linux; use on other distributions has not been
tested.
However, because Tiny Cloud does not do auto-detection, you ***must*** set a
value for `CLOUD` indicating which cloud provider which will be used when
instantiating the image. Current valid values are `aws`, `azure`, `gcp`, and
`oci`.
- The script is run by OpenRC.
## Operation
The first time an instance boots -- either freshly instantiated from an image,
or after installation on an existing instance -- Tiny Cloud sets up the
instance in three phases...
### Early Phase
The `tiny-cloud-early` init script does not depend on the cloud provider's
Instance MetaData Service (IMDS), and does therefore does not have a dependency
on networking. During this "early" phase, the root filesystem is expanded, and
any necessary `mdev` rules for device hotplug are set up.
### Main Phase
The main `tiny-cloud` init script *does* depend on the cloud provider's IMDS
data, and sets up instance's hostname and the cloud user's SSH keys before
`sshd` starts.
### Final Phase
`tiny-cloud-final` should be the very last init script to run in the
**default** runlevel, and saves the instance's user data to
`/var/lib/cloud/user-data`.
If the user data is a script that starts with `#!` (aka "[shebang](
https://en.wikipedia.org/wiki/Shebang_(Unix))"), it will be executed; its
output (combined STDOUT and STDERR) is saved to `/var/log/cloud/user-data.log`
and the exit code can be found in `/var/log/cloud/user-data.exit`.
If all went well, the very last thing `tiny-cloud-final` does is touch
`/var/lib/cloud/.bootstrap-complete` into existence.
### Further Reboots
After the initial bootstrap of an instance, the init scripts are largely a
no-op.
To force the init scripts to re-run on the next boot...
```
rm -f /var/lib/cloud/.bootstrap-complete
```
If you're instantiating an instance in order to create a new cloud image
(using [Packer](https://packer.io), or some other means), you will need to
remove this file before creating the image to ensure that instances using the
new image will also run Tiny Cloud init scripts during their first boot.

10
etc/conf.d/tiny-cloud Normal file
View File

@ -0,0 +1,10 @@
# tiny-cloud configuration
# REQUIRED: The instance's cloud provider (valid: aws, azure, gcp, oci)
CLOUD=
# User account where instance SSH keys are installed
#CLOUD_USER=alpine
# Number of seconds an AWS IMDS token is valid
#IMDS_TOKEN_TTL=5

51
etc/init.d/tiny-cloud Normal file
View File

@ -0,0 +1,51 @@
#!/sbin/openrc-run
# vim:set ft=sh noet ts=4:
description="Tiny Cloud Bootstrap - main phase"
depend() {
need net
before sshd
}
_update_hostname() {
local fqdn=$(imds_hostname)
local host="${fqdn%%\.*}"
echo "$host" > /etc/hostname
hostname -F /etc/hostname
echo -e "127.0.1.1\t$fqdn $host" >> /etc/hosts
}
_set_ssh_keys() {
local user="$1"
local pwent=$(getent passwd "$user")
local group=$(echo "$pwent" | cut -d: -f4)
local ssh_dir="$(echo "$pwent" | cut -d: -f6)/.ssh"
local keys_file="$ssh_dir/authorized_keys"
if [ ! -d "$ssh_dir" ]; then
mkdir -p "$ssh_dir"
chmod 700 "$ssh_dir"
fi
touch "$keys_file"
chmod 600 "$keys_file"
chown -R "$user:$group" "$ssh_dir"
imds_ssh_keys > "$keys_file"
}
start() {
[ -f "/var/lib/cloud/.bootstrap-complete" ] && return 0
[ -d "/var/lib/cloud" ] || mkdir -p /var/lib/cloud
source /var/lib/tiny-cloud/imds
CLOUD_USER="${CLOUD_USER:-alpine}"
ebegin "Setting Instance Hostname"; _update_hostname; eend $?
ebegin "Installing SSH Keys for $CLOUD_USER User"
_set_ssh_keys "$CLOUD_USER"
eend $?
# TODO: background tiny-cloud-network watcher?
}

View File

@ -0,0 +1,36 @@
#!/sbin/openrc-run
# vim:set ft=sh noet ts=4:
description="Tiny Cloud Bootstrap - early phase"
depend() {
before mdev
}
_expand_root_partition() {
local mountpoint=$(busybox mountpoint -n / | cut -d' ' -f1)
local volume=$(echo "$mountpoint" |
sed -Ee "s/(nvme\d+n\d|(xv|s)d[a-z])p?\d?$/\1/"
)
if [ "$mountpoint" != "$volume" ]; then
partition=$(echo "$volume" | sed -Ee "s/.*(\d+)$/\2/")
einfo "Expanding root partition to volume size..."
echo ", +" | sfdisk -q --no-reread -N "$partition" "$volume"
einfo "Updating kernel with new partition table..."
partx -u "$volume"
fi
einfo "Resizing root filesystem..."
resize2fs "$mountpoint"
}
start() {
[ -f "/var/lib/cloud/.bootstrap-complete" ] && return 0
[ -d "/var/lib/cloud" ] || mkdir -p /var/lib/cloud
source /etc/conf.d/tiny-cloud # or use auto-sourced tiny-cloud-early?
ebegin "Expanding Root Partition"; _expand_root_partition; eend $?
# TODO: _setup mdev things, if applicable
}

View File

@ -0,0 +1,37 @@
#!/sbin/openrc-run
# vim:set ft=sh noet ts=4:
description="Tiny Cloud Bootstrap - final phase"
depend() {
after *
provide cloud-final
}
_save_userdata() {
imds_userdata > "$USERDATA"
}
_run_userdata() {
local log="/var/log/user-data.log"
local exit="/var/log/user-data.exit"
chmod +x "$USERDATA"
{ "$USERDATA" 2>& 1; echo $? > "$exit"; } | tee "$log"
return $(cat "$exit")
}
start() {
[ -f "/var/lib/cloud/.bootstrap-complete" ] && return 0
[ -d "/var/lib/cloud" ] || mkdir -p /var/lib/cloud
source /lib/tiny-cloud/imds
USERDATA="/var/lib/cloud/user-data"
ebegin "Saving Instance UserData"; _save_userdata; eend $?
if head -n1 "$USERDATA" | grep -q '^#!/'; then
ebegin "Executing UserData Script"; _run_userdata; eend $?
fi
}

28
lib/tiny-cloud/aws/imds Normal file
View File

@ -0,0 +1,28 @@
# AWS Instance MetaData Service related variables and functions
# vim:set ft=sh noet ts=4:
IMDS_HEADER="X-aws-ec2-metadata-token"
IMDS_TOKEN_TTL_HEADER="X-aws-ec2-metadata-token-ttl-seconds"
IMDS_TOKEN_TTL=${IMDS_TOKEN_TTL:-5}
IMDS_URL="http://169.254.169.254/latest"
IMDS_HOSTNAME="meta-data/hostname"
IMDS_SSH_KEYS="meta-data/public-keys"
IMDS_USERDATA="user-data"
_imds_token() {
echo -ne "PUT /latest/api/token HTTP/1.0\r\n"\
"$IMDS_TOKEN_TTL_HEADER: $IMDS_TOKEN_TTL\r\n\r\n" |
nc 169.254.169.254 80 | tail -n 1
}
_imds_header() {
echo "$IMDS_HEADER: $(_imds_token)"
}
# digs deeper than the default
imds_ssh_keys() {
for key in $(imds "$IMDS_SSH_KEYS"); do
imds "$IMDS_SSH_KEYS/${key%=*}/openssh-key"
done | sort -u
}

26
lib/tiny-cloud/azure/imds Normal file
View File

@ -0,0 +1,26 @@
# Azure metadata-related variables and functions
# vim:set ft=sh noet ts=4:
IMDS_HEADER="Metadata"
IMDS_QUERY="?format=text&api-version=2021-05-01"
IMDS_URL="http://169.254.169.254/metadata/instance"
IMDS_HOSTNAME="compute/name"
IMDS_SSH_KEYS="compute/publicKeys"
IMDS_USERDATA="compute/userData"
_imds_header() {
echo "$IMDS_HEADER: true"
}
# dig deeper than default
imds_ssh_keys() {
for key in $(imds "$IMDS_SSH_KEYS"); do
imds "$IMDS_SSH_KEYS/${key}/keyData"
done | sort -u
}
# decode userdata value
imds_userdata() {
imds "$IMDS_USERDATA" | base64 -d | _enforce_ending_newline
}

24
lib/tiny-cloud/gcp/imds Normal file
View File

@ -0,0 +1,24 @@
# Google Cloud metadata-related variables and functions
# vim:set ft=sh noet ts=4:
IMDS_HEADER="Metadata-Flavor"
IMDS_URL="http://169.254.169.254/computeMetadata/v1"
IMDS_HOSTNAME="instance/hostname"
IMDS_SSH_KEYS="
project/attributes/ssh-keys
instance/attributes/ssh-keys
"
IMDS_USERDATA="instance/attributes/user-data"
_imds_header() {
echo "$IMDS_HEADER: Google"
}
# merge project and instance keys
imds_ssh_keys() {
for ssh_keys in $IMDS_SSH_KEYS; do
# ignore errors and strip leading '<login>:'
_imds "$ssh_keys" | cut -d: -f2- | _enforce_ending_newline
done | sort -u
}

60
lib/tiny-cloud/imds Normal file
View File

@ -0,0 +1,60 @@
# tiny-cloud Instance MetaData Service related functions and variables
# vim:set ft=sh ts=4 noet:
CONF_DIR="/etc/conf.d"
LIB_DIR="/lib/tiny-cloud"
### load configuration
[ -f "$CONF_DIR"/tiny-cloud ] && source "$CONF_DIR"/tiny-cloud
### configuration defaults
CLOUD="${CLOUD:-unknown}"
CLOUD_LOGIN="${CLOUD_LOGIN:-alpine}"
if [ ! -d "$LIB_DIR/$CLOUD" ]; then
echo "ERROR: Unknown Cloud '$CLOUD'" >&2
exit 1
fi
IMDS_HEADER=
IMDS_QUERY=
IMDS_URL=
IMDS_HOSTNAME=
IMDS_SSH_KEYS=
IMDS_USERDATA=
### default/common functions
_imds() {
wget --quiet --header "$(_imds_header)" --output-document - \
"$IMDS_URL/$1/$IMDS_QUERY"
}
_enforce_ending_newline() { sed '$a\'; }
imds() {
set -o pipefail
_imds "$1" | _enforce_ending_newline
RV=$?
set +o pipefail
[ $RV != 0 ] && echo "ERROR: $RV" >&2
return $RV
}
imds_hostname() {
imds "$IMDS_HOSTNAME"
}
imds_userdata() {
imds "$IMDS_USERDATA"
}
imds_ssh_keys() {
imds "$IMDS_SSH_KEYS"
}
# cloud-specific things (potentially overriding the above)
[ -f "$LIB_DIR/$CLOUD/metadata" ] && source "$LIB_DIR/$CLOUD"/imds

13
lib/tiny-cloud/oci/imds Normal file
View File

@ -0,0 +1,13 @@
# OCI metadata-related variables and functions
# vim:set ft=sh noet ts=4:
IMDS_HEADER="Authorization"
IMDS_URL="http://169.254.169.254/opc/v2"
IMDS_HOSTNAME="instance/hostname"
IMDS_SSH_KEYS="instance/metadata/ssh_authorized_keys"
IMDS_USERDATA="instance/metadata/userdata"
_imds_header() {
echo "$IMDS_HEADER: Bearer Oracle"
}

View File

@ -1,110 +0,0 @@
#!/sbin/openrc-run
# vim:set ft=sh noet ts=4:
description="Provides EC2 cloud bootstrap"
# override in /etc/conf.d/tiny-ec2-bootstrap
EC2_USER=${EC2_USER:-alpine}
IMDS2_TOKEN_TTL=${IMDS2_TOKEN_TTL:-5}
depend() {
need net
provide cloud-final
}
_get_metadata_token() {
echo -ne "PUT /latest/api/token HTTP/1.0\r\nX-aws-ec2-metadata-token-ttl-seconds: $IMDS2_TOKEN_TTL\r\n\r\n" |
nc 169.254.169.254 80 | tail -n 1
}
_get_metadata() {
local uri="$1"
wget -qO - --header "X-aws-ec2-metadata-token: $(_get_metadata_token)" \
"http://169.254.169.254/latest/$uri" 2>/dev/null
}
_update_hostname() {
local ec2_fqdn="$(_get_metadata meta-data/hostname)"
local short_hostname="${ec2_fqdn%%\.*}"
echo "$short_hostname" > /etc/hostname
hostname -F /etc/hostname
echo -e "127.0.1.1\t$ec2_fqdn $short_hostname" >> /etc/hosts
}
_set_ssh_keys() {
local user="$1"
local group="$(getent passwd "$user" | cut -d: -f4)"
local ssh_dir="$(getent passwd "$user" | cut -d: -f6)/.ssh"
local keys_file="$ssh_dir/authorized_keys"
if [ ! -d "$ssh_dir" ]; then
mkdir -p "$ssh_dir"
chmod 755 "$ssh_dir"
fi
[ -f "$keys_file" ] && rm "$keys_file"
touch "$keys_file"
chmod 600 "$keys_file"
chown -R "$user:$group" "$ssh_dir"
for key in $(_get_metadata meta-data/public-keys/); do
_get_metadata "meta-data/public-keys/${key%=*}/openssh-key/" >> "$keys_file"
done
}
_run_userdata() {
local user_data="$(_get_metadata user-data)"
if printf '%s' "$user_data" | head -n1 | grep -q '^#!/'; then
printf '%s' "$user_data" >/var/lib/cloud/user-data.sh
chmod +x /var/lib/cloud/user-data.sh
local log_file=/var/log/cloud-bootstrap.log
local ec_file=/var/log/cloud-bootstrap.exit
{ /var/lib/cloud/user-data.sh 2>&1 ; echo $? >"$ec_file"; } | tee "$log_file"
ec=$(cat "$ec_file")
echo "User Data Script Exit Status: $ec"
return "$ec"
fi
}
_resize_root_partition() {
local mountpoint="$(busybox mountpoint -n / | cut -d' ' -f1)"
# mountpoint is the second partition...
if echo "$mountpoint" | cut -d' ' -f1 | grep -qE '/(nvme\d+n\d+p|xvd[a-z]+)2$'; then
local volume="$(echo "$mountpoint" | sed -Ee 's/(nvme\d+n\d+|xvd[a-z]+)p?2/\1/')"
einfo "Expanding root partition to volume size..."
echo ", +" | sfdisk -q --no-reread -N 2 "$volume"
einfo "Updating kernel with new partition table..."
partx -u "$volume"
fi
einfo "Resizing..."
resize2fs "$mountpoint"
}
_lock_root_account() {
passwd -l root
}
_disable_password() {
echo "$1:*" | chpasswd -e
}
start() {
# Don't bootstrap if the host has already been bootstrapped
[ -f "/var/lib/cloud/.bootstrap-complete" ] && return 0
[ -d "/var/lib/cloud" ] || mkdir -p /var/lib/cloud
ebegin "Locking root account"; _lock_root_account; eend $?
ebegin "Disabling $EC2_USER password"; _disable_password "$EC2_USER"; eend $?
ebegin "Expanding root partition"; _resize_root_partition; eend $?
ebegin "Setting ec2 hostname"; _update_hostname; eend $?
ebegin "Setting ec2 user ssh keys"; _set_ssh_keys "$EC2_USER"; eend $?
ebegin "Running ec2 user data script"; _run_userdata; eend $?
touch "/var/lib/cloud/.bootstrap-complete"
}