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