diff options
Diffstat (limited to 'make_ami.sh')
-rwxr-xr-x | make_ami.sh | 325 |
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 | |||
4 | set -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 | |||
12 | die() { | ||
13 | printf '\033[1;31mERROR:\033[0m %s\n' "$@" >&2 # bold red | ||
14 | exit 1 | ||
15 | } | ||
16 | |||
17 | einfo() { | ||
18 | printf '\n\033[1;36m> %s\033[0m\n' "$@" >&2 # bold cyan | ||
19 | } | ||
20 | |||
21 | rc_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 | |||
33 | wgets() ( | ||
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 | |||
43 | validate_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 | |||
54 | fetch_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 | |||
64 | make_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 | |||
73 | setup_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 | ||
89 | setup_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 | |||
107 | fetch_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 | |||
116 | setup_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 | |||
128 | install_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 | |||
156 | create_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 | |||
172 | setup_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 | |||
190 | install_extlinux() { | ||
191 | local target="$1" | ||
192 | |||
193 | chroot "$target" /sbin/extlinux --install /boot | ||
194 | chroot "$target" /sbin/update-extlinux --warn-only | ||
195 | } | ||
196 | |||
197 | setup_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 | |||
206 | setup_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 | |||
218 | enable_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 | |||
227 | create_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 | |||
248 | configure_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 | |||
259 | cleanup() { | ||
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 | |||
278 | main() { | ||
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 | |||
325 | main "$@" | ||