aboutsummaryrefslogtreecommitdiff
path: root/make_ami.sh
diff options
context:
space:
mode:
Diffstat (limited to 'make_ami.sh')
-rwxr-xr-xmake_ami.sh325
1 files changed, 325 insertions, 0 deletions
diff --git a/make_ami.sh b/make_ami.sh
new file mode 100755
index 0000000..6ad4b0d
--- /dev/null
+++ b/make_ami.sh
@@ -0,0 +1,325 @@
1#!/bin/sh
2# vim:set ts=4:
3
4set -eu
5
6: ${ALPINE_RELEASE:="3.7"} # not tested against edge
7: ${APK_TOOLS_URI:="https://github.com/alpinelinux/apk-tools/releases/download/v2.8.0/apk-tools-2.8.0-x86_64-linux.tar.gz"}
8: ${APK_TOOLS_SHA256:="da21cefd2121e3a6cd4e8742b38118b2a1132aad7f707646ee946a6b32ee6df9"}
9: ${ALPINE_KEYS:="http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/alpine-keys-2.1-r1.apk"}
10: ${ALPINE_KEYS_SHA256:="7b2d1e9a00324c8eee49785dc22355be02534201e77473ba9762027e1a475cc7"}
11
12die() {
13 printf '\033[1;31mERROR:\033[0m %s\n' "$@" >&2 # bold red
14 exit 1
15}
16
17einfo() {
18 printf '\n\033[1;36m> %s\033[0m\n' "$@" >&2 # bold cyan
19}
20
21rc_add() {
22 local target="$1"; shift # target directory
23 local runlevel="$1"; shift # runlevel name
24 local services="$*" # names of services
25
26 local svc; for svc in $services; do
27 mkdir -p "$target"/etc/runlevels/$runlevel
28 ln -s /etc/init.d/$svc "$target"/etc/runlevels/$runlevel/$svc
29 echo " * service $svc added to runlevel $runlevel"
30 done
31}
32
33wgets() (
34 local url="$1" # url to fetch
35 local sha256="$2" # expected SHA256 sum of output
36 local dest="$3" # output path and filename
37
38 wget -T 10 -q -O "$dest" "$url"
39 echo "$sha256 $dest" | sha256sum -c > /dev/null
40)
41
42
43validate_block_device() {
44 local dev="$1" # target directory
45
46 lsblk -P --fs "$dev" >/dev/null 2>&1 || \
47 die "'$dev' is not a valid block device"
48
49 if lsblk -P --fs "$dev" | grep -vq 'FSTYPE=""'; then
50 die "Block device '$dev' is not blank"
51 fi
52}
53
54fetch_apk_tools() {
55 local store="$(mktemp -d)"
56 local tarball="$(basename $APK_TOOLS_URI)"
57
58 wgets "$APK_TOOLS_URI" "$APK_TOOLS_SHA256" "$store/$tarball"
59 tar -C "$store" -xf "$store/$tarball"
60
61 find "$store" -name apk
62}
63
64make_filesystem() {
65 local device="$1" # target device path
66 local target="$2" # mount target
67
68 mkfs.ext4 "$device"
69 e2label "$device" /
70 mount "$device" "$target"
71}
72
73setup_repositories() {
74 local target="$1" # target directory
75
76 mkdir -p "$target"/etc/apk/keys
77 cat > "$target"/etc/apk/repositories <<-EOF
78 http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_RELEASE/main
79 http://dl-cdn.alpinelinux.org/alpine/v$ALPINE_RELEASE/community
80 EOF
81}
82
83# This is mostly a temporary measure because some required packages have not
84# yet been accepted upstream. This can be removed when the following pull
85# requests are merged:
86#
87# - https://github.com/alpinelinux/aports/pull/2962
88# - https://github.com/alpinelinux/aports/pull/2961
89setup_staging_repos() {
90 local target="$1" # target directory
91
92 echo "https://mcrute-build-artifacts.s3.us-west-2.amazonaws.com/alpine-packages/$ALPINE_RELEASE/testing" >> "$target"/etc/apk/repositories
93
94 cat > "$target"/etc/apk/keys/mcrute-5a3eecec.rsa.pub <<-EOF
95 -----BEGIN PUBLIC KEY-----
96 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5fW5dyTqgs9Yf93xKn5U
97 cYzY9t//M3TAaiDWH7rFxqBqTGnVGkP9QAGqsbXyoo/JpIalazkOfm/1L+XaK7NI
98 IUD/8KxfrnBW53cc/KOkPcGAga36aTBz/HmLQQvjWcizPxWepjdfvAnRTMV69Oud
99 zaRPGKx8nCRqLy1YFAEXn+zpHRh+OHCzzQFlkJop+2PCXqDFaMWC7+oWwrqFs1i0
100 CXc4pq5oT6vAQyt6pUwN85sLVxtxXSt5G5ALYzQtaIj7IAR3jGlwU26wOAv5YP7z
101 xn/Z1ebQsPbAl3rw48v2T2ohPEX2TUtUq4OuwOG+z1pi3woIGOlOFVAP3k6lm8Z9
102 9QIDAQAB
103 -----END PUBLIC KEY-----
104 EOF
105}
106
107fetch_keys() {
108 local target="$1"
109 local tmp="$(mktemp -d)"
110
111 wgets "$ALPINE_KEYS" "$ALPINE_KEYS_SHA256" "$tmp/alpine-keys.apk"
112 tar -C "$target" -xvf "$tmp"/alpine-keys.apk etc/apk/keys
113 rm -rf "$tmp"
114}
115
116setup_chroot() {
117 local target="$1"
118
119 mount -t proc none "$target"/proc
120 mount --bind /dev "$target"/dev
121 mount --bind /sys "$target"/sys
122
123 # Don't want to ship this but it's needed for bootstrap. Will be removed in
124 # the cleanup stage.
125 install -Dm644 /etc/resolv.conf "$target"/etc/resolv.conf
126}
127
128install_core_packages() {
129 local target="$1"
130
131 # Most from: https://git.alpinelinux.org/cgit/alpine-iso/tree/alpine-virt.packages
132 #
133 # acct - installed by some configurations, so added here
134 # aws-ena-driver-hardened - required for ENA enabled instances
135 # e2fsprogs - required by init scripts to maintain ext4 volumes
136 # linux-hardened - can't use virthardened because it's missing NVME support
137 # mkinitfs - required to build custom initfs
138 # sudo - to allow alpine user to become root, disallow root SSH logins
139 # tiny-ec2-bootstrap - to bootstrap system from EC2 metadata
140 chroot "$target" apk --no-cache add \
141 acct \
142 alpine-mirrors \
143 aws-ena-driver-hardened \
144 chrony \
145 e2fsprogs \
146 linux-hardened \
147 mkinitfs \
148 openssh \
149 sudo \
150 tiny-ec2-bootstrap \
151 tzdata
152
153 chroot "$target" apk --no-cache add --no-scripts syslinux
154}
155
156create_initfs() {
157 local target="$1"
158
159 # Create ENA feature for mkinitfs
160 # Submitted upstream: https://github.com/alpinelinux/mkinitfs/pull/19
161 echo "kernel/drivers/net/ethernet/amazon" > \
162 "$target"/etc/mkinitfs/features.d/ena.modules
163
164 # Enable ENA and NVME features these don't hurt for any instance and are
165 # hard requirements of the 5 series and i3 series of instances
166 sed -Ei 's/^features="([^"]+)"/features="\1 nvme ena"/' \
167 "$target"/etc/mkinitfs/mkinitfs.conf
168
169 chroot "$target" /sbin/mkinitfs $(basename $(find "$target"/lib/modules/* -maxdepth 0))
170}
171
172setup_extlinux() {
173 local target="$1"
174
175 # Must use disk labels instead of UUID or devices paths so that this works
176 # across instance familes. UUID works for many instances but breaks on the
177 # NVME ones because EBS volumes are hidden behind NVME devices.
178 #
179 # Enable ext4 because the root device is formatted ext4
180 #
181 # Shorten timeout because EC2 has no way to interact with instance console
182 sed -Ei -e "s|^[# ]*(root)=.*|\1=LABEL=/|" \
183 -e "s|^[# ]*(default_kernel_opts)=.*|\1=|" \
184 -e "s|^[# ]*(modules)=.*|\1=sd-mod,usb-storage,ext4|" \
185 -e "s|^[# ]*(default)=.*|\1=hardened|" \
186 -e "s|^[# ]*(timeout)=.*|\1=1|" \
187 "$target"/etc/update-extlinux.conf
188}
189
190install_extlinux() {
191 local target="$1"
192
193 chroot "$target" /sbin/extlinux --install /boot
194 chroot "$target" /sbin/update-extlinux --warn-only
195}
196
197setup_fstab() {
198 local target="$1"
199
200 cat > "$target"/etc/fstab <<-EOF
201 # <fs> <mountpoint> <type> <opts> <dump/pass>
202 LABEL=/ / ext4 defaults,noatime 1 1
203 EOF
204}
205
206setup_networking() {
207 local target="$1"
208
209 cat > "$target"/etc/network/interfaces <<-EOF
210 auto lo
211 iface lo inet loopback
212
213 auto eth0
214 iface eth0 inet dhcp
215 EOF
216}
217
218enable_services() {
219 local target="$1"
220
221 rc_add "$target" default sshd chronyd networking tiny-ec2-bootstrap
222 rc_add "$target" sysinit devfs dmesg mdev hwdrivers
223 rc_add "$target" boot modules hwclock swap hostname sysctl bootmisc syslog
224 rc_add "$target" shutdown killprocs savecache mount-ro
225}
226
227create_alpine_user() {
228 local target="$1"
229
230 # Allow members of the wheel group to sudo without a password. By default
231 # this will only be the alpine user. This allows us to ship an AMI that is
232 # accessible via SSH using the user's configured SSH keys (thanks to
233 # tiny-ec2-bootstrap) but does not allow remote root access which is the
234 # best-practice.
235 sed -i '/%wheel .* NOPASSWD: .*/s/^# //' "$target"/etc/sudoers
236
237 # There is no real standard ec2 username across AMIs, Amazon uses ec2-user
238 # for their Amazon Linux AMIs but Ubuntu uses ubuntu, Fedora uses fedora,
239 # etc... (see: https://alestic.com/2014/01/ec2-ssh-username/). So our user
240 # and group are alpine because this is Alpine Linux. On instance bootstrap
241 # the user can create whatever users they want and delete this one.
242 chroot "$target" /usr/sbin/addgroup alpine
243 chroot "$target" /usr/sbin/adduser -h /home/alpine -s /bin/sh -G alpine -D alpine
244 chroot "$target" /usr/sbin/addgroup alpine wheel
245 chroot "$target" /usr/bin/passwd -u alpine
246}
247
248configure_ntp() {
249 local target="$1"
250
251 # EC2 provides an instance-local NTP service syncronized with GPS and
252 # atomic clocks in-region. Prefer this over external NTP hosts when running
253 # in EC2.
254 #
255 # See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-time.html
256 sed -i 's/^server .*/server 169.254.169.123/' "$target"/etc/chrony/chrony.conf
257}
258
259cleanup() {
260 local target="$1"
261
262 # Sweep cruft out of the image that doesn't need to ship or will be
263 # re-generated when the image boots
264 rm -f \
265 "$target"/var/cache/apk/* \
266 "$target"/etc/resolv.conf \
267 "$target"/root/.ash_history \
268 "$target"/etc/*-
269
270 umount \
271 "$target"/dev \
272 "$target"/proc \
273 "$target"/sys
274
275 umount "$target"
276}
277
278main() {
279 [ "$#" -ne 1 ] && { echo "usage: $0 <block-device>"; exit 1; }
280
281 device="$1"
282 target="/mnt/target"
283
284 validate_block_device "$device"
285
286 [ -d "$target" ] || mkdir "$target"
287
288 einfo "Fetching static APK tools"
289 apk="$(fetch_apk_tools)"
290
291 einfo "Creating root filesystem"
292 make_filesystem "$device" "$target"
293
294 setup_repositories "$target"
295
296 einfo "Fetching Alpine signing keys"
297 fetch_keys "$target"
298
299 setup_staging_repos "$target"
300
301 einfo "Installing base system"
302 $apk add --root "$target" --update-cache --initdb alpine-base
303
304 setup_chroot "$target"
305
306 einfo "Installing core packages"
307 install_core_packages "$target"
308
309 einfo "Configuring and enabling boot loader"
310 create_initfs "$target"
311 setup_extlinux "$target"
312 install_extlinux "$target"
313
314 einfo "Configuring system"
315 setup_fstab "$target"
316 setup_networking "$target"
317 enable_services "$target"
318 create_alpine_user "$target"
319 configure_ntp "$target"
320
321 einfo "All done, cleaning up"
322 cleanup "$target"
323}
324
325main "$@"