diff --git a/.ageboxreg.yml b/.ageboxreg.yml new file mode 100644 index 0000000..15bdc58 --- /dev/null +++ b/.ageboxreg.yml @@ -0,0 +1,3 @@ +file_ids: +- overlay/zdt/configs/access.conf +version: "1" diff --git a/.agekeys b/.agekeys new file mode 100644 index 0000000..c714e30 --- /dev/null +++ b/.agekeys @@ -0,0 +1 @@ +age1z42dmf0cluvuyp2jz9gzkf2ly9afxqmp9cy6dy22fwak32uhjszscn25k4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2472157 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +overlay/zdt/configs/access.conf diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f0c3f88 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +OVERLAY := $(shell pwd)/overlay +# FILTER := --only 3.15 kubezero --skip aarch64 +FILTER := --only 3.15 --skip aarch64 +STEP := publish + +all: build + +build: + cd alpine-cloud-images && ./build $(STEP) --clean --revise $(FILTER) --custom $(OVERLAY)/zdt --vars $(OVERLAY)/zdt/zdt.hcl + +clean: + rm -rf alpine-cloud-images/work diff --git a/alpine-cloud-images/clouds/aws.py b/alpine-cloud-images/clouds/aws.py index 436caa7..aa9ef42 100644 --- a/alpine-cloud-images/clouds/aws.py +++ b/alpine-cloud-images/clouds/aws.py @@ -152,14 +152,23 @@ class AWSCloudAdapter(CloudAdapterInterface): # import snapshot from S3 log.info('Importing EC2 snapshot from %s', s3_url) - ss_import = ec2c.import_snapshot( - DiskContainer={ + _import_opts = { + 'DiskContainer': { 'Description': description, # https://github.com/boto/boto3/issues/2286 'Format': 'VHD', 'Url': s3_url } - # NOTE: TagSpecifications -- doesn't work with ResourceType: snapshot? - ) + } + # NOTE: TagSpecifications -- doesn't work with ResourceType: snapshot? + + # For some reason the import_snapshot boto function cannot handle setting KmsKeyId to default / empty + # so we need to set it conditionally + if ic.encryption_key_id: + _import_opts['Encrypted'] = True + _import_opts['KmsKeyId'] = ic.encryption_key_id + + ss_import = ec2c.import_snapshot(**_import_opts) + ss_task_id = ss_import['ImportTaskId'] while True: ss_task = ec2c.describe_import_snapshot_tasks( @@ -315,6 +324,8 @@ class AWSCloudAdapter(CloudAdapterInterface): Name=source.name, SourceImageId=source_id, SourceRegion=source_region, + Encrypted=True if ic.encryption_key_id else False, + KmsKeyId=ic.encryption_key_id ) except Exception: log.warning('Skipping %s, unable to copy image:', r, exc_info=True) @@ -343,6 +354,7 @@ class AWSCloudAdapter(CloudAdapterInterface): if fresh: tags.published = datetime.utcnow().isoformat() + tags.Name = tags.name # because AWS is special image.create_tags(Tags=tags.as_list()) # tag image's snapshot, too @@ -358,14 +370,15 @@ class AWSCloudAdapter(CloudAdapterInterface): ) # apply launch perms - log.info('%s: Applying launch perms to %s', r, image.id) - image.reset_attribute(Attribute='launchPermission') - image.modify_attribute( - Attribute='launchPermission', - OperationType='add', - UserGroups=perms['groups'], - UserIds=perms['users'], - ) + if perms['groups'] or perms['users']: + log.info('%s: Applying launch perms to %s', r, image.id) + image.reset_attribute(Attribute='launchPermission') + image.modify_attribute( + Attribute='launchPermission', + OperationType='add', + UserGroups=perms['groups'], + UserIds=perms['users'], + ) # set up AMI deprecation ec2c = image.meta.client diff --git a/alpine-cloud-images/configs/bootstrap/cloudinit.conf b/alpine-cloud-images/configs/bootstrap/cloudinit.conf index bd35331..4883c4e 100644 --- a/alpine-cloud-images/configs/bootstrap/cloudinit.conf +++ b/alpine-cloud-images/configs/bootstrap/cloudinit.conf @@ -1,5 +1,5 @@ # vim: ts=2 et: -name = [cloudinit] +# name = [cloudinit] # start cloudinit images with 3.15 EXCLUDE = ["3.12", "3.13", "3.14"] @@ -11,4 +11,4 @@ packages { } services.default.cloud-init-hotplugd = true -scripts = [ setup-cloudinit ] \ No newline at end of file +scripts = [ setup-cloudinit ] diff --git a/alpine-cloud-images/configs/firmware/bios.conf b/alpine-cloud-images/configs/firmware/bios.conf index c1d9602..4b8b17f 100644 --- a/alpine-cloud-images/configs/firmware/bios.conf +++ b/alpine-cloud-images/configs/firmware/bios.conf @@ -1,6 +1,6 @@ # vim: ts=2 et: -name = [bios] +#name = [bios] bootloader = extlinux packages.syslinux = --no-scripts -qemu.firmware = null \ No newline at end of file +qemu.firmware = null diff --git a/audit_grants.sh b/audit_grants.sh new file mode 100755 index 0000000..56a7ae6 --- /dev/null +++ b/audit_grants.sh @@ -0,0 +1,13 @@ +#!/bin/bash +#set -x + + +for r in $(aws ec2 describe-regions --query "Regions[].{Name:RegionName}" --output text); do + + keyAlias="arn:aws:kms:${r}:533404190593:alias/zdt/amis" + keyArn=$(aws kms describe-key --region $r --key-id $keyAlias --output json 2>/dev/null | jq -r '.KeyMetadata.Arn') + + if [ -n "$keyArn" ]; then + aws kms list-grants --region $r --key-id $keyArn --output json | jq '.Grants[]' + fi +done diff --git a/cleanup_amis.sh b/cleanup_amis.sh new file mode 100755 index 0000000..ad1a31e --- /dev/null +++ b/cleanup_amis.sh @@ -0,0 +1,22 @@ +#!/bin/bash +#set -x + +TAG_FILTER="Name=tag:project,Values=zdt-alpine" + +#for r in $(aws ec2 describe-regions --query "Regions[].{Name:RegionName}" --output text); do +for r in eu-central-1 us-west-2; do + amis=$(aws ec2 describe-images --region $r --owners self --output json --filters $TAG_FILTER | jq -r '.Images[].ImageId') + for a in $amis; do + aws ec2 deregister-image --region $r --image-id $a && echo "Deleted AMI $a in $r" + done + + amis=$(aws ec2 describe-images --region $r --owners self --output json --filters Name=state,Values=failed | jq -r '.Images[].ImageId') + for a in $amis; do + aws ec2 deregister-image --region $r --image-id $a && echo "Deleted AMI $a in $r" + done + + snapshots=$(aws ec2 describe-snapshots --region $r --owner-ids self --output json --filters $TAG_FILTER | jq -r '.Snapshots[].SnapshotId') + for s in $snapshots; do + aws ec2 delete-snapshot --snapshot-id $s --region $r && echo "Deleted snapshot $s in $r" + done +done diff --git a/overlay/zdt/configs/access.conf.agebox b/overlay/zdt/configs/access.conf.agebox new file mode 100644 index 0000000..d89258c --- /dev/null +++ b/overlay/zdt/configs/access.conf.agebox @@ -0,0 +1,5 @@ +age-encryption.org/v1 +-> X25519 ZT6m1CYk0KfJbxayb1X65OgPL6U4lnVgr90fSOiHNTA +aAo+pQyd8gS9Y2cYufu9rAsSCDr+hmjfRa2h5HtkEZw +--- JlxAy916xCRYxSIeTbFzmU9U6+TYOFSVwDMx30m8i/w +ѳuP#@h9˚Cϐ mm>' kd6qƁSť \ No newline at end of file diff --git a/overlay/zdt/configs/common-packages.conf b/overlay/zdt/configs/common-packages.conf new file mode 100644 index 0000000..81c5414 --- /dev/null +++ b/overlay/zdt/configs/common-packages.conf @@ -0,0 +1,16 @@ +bash = true +jq = true +yq = true +logrotate = true +iptables = true +syslog-ng-json = true +podman = true +wireguard-tools = true +lvm2 = true +socat = true +ethtool = true +nvme-cli = true +xfsprogs = true +dhclient = true +monit = true +#prometheus-node-exporter = true diff --git a/overlay/zdt/configs/common-services.conf b/overlay/zdt/configs/common-services.conf new file mode 100644 index 0000000..730bd58 --- /dev/null +++ b/overlay/zdt/configs/common-services.conf @@ -0,0 +1,14 @@ +sysinit { + cgroups = true +} + +boot { + syslog = null + syslog-ng = true +} + +default { + local = true + monit = true + crond = true +} diff --git a/overlay/zdt/configs/images.conf b/overlay/zdt/configs/images.conf new file mode 120000 index 0000000..1db5ce2 --- /dev/null +++ b/overlay/zdt/configs/images.conf @@ -0,0 +1 @@ +zdt.conf \ No newline at end of file diff --git a/overlay/zdt/configs/kubezero.conf b/overlay/zdt/configs/kubezero.conf new file mode 100644 index 0000000..bb987dc --- /dev/null +++ b/overlay/zdt/configs/kubezero.conf @@ -0,0 +1,24 @@ +# vim: ts=2 et: + +description = [ "- https://kubezero.com" ] +name = [ kubezero-1.22.8 ] +size = 2G + +scripts = [ setup-common ] +packages { include required("common-packages.conf") } +services { include required("common-services.conf") } + +WHEN { + kubezero { + scripts = [ setup-kubernetes ] + } +} + +WHEN { + aws { + packages { + aws-cli = true + py3-boto3 = true + } + } +} diff --git a/overlay/zdt/configs/minimal.conf b/overlay/zdt/configs/minimal.conf new file mode 100644 index 0000000..eb0fdf9 --- /dev/null +++ b/overlay/zdt/configs/minimal.conf @@ -0,0 +1,17 @@ +# vim: ts=2 et: + +description = [ "- https://zero-downtime.net/cloud" ] +name = [ minimal ] + +scripts = [ setup-common ] +packages { include required("common-packages.conf") } +services { include required("common-services.conf") } + +WHEN { + aws { + packages { + aws-cli = true + py3-boto3 = true + } + } +} diff --git a/overlay/zdt/configs/zdt.conf b/overlay/zdt/configs/zdt.conf new file mode 100644 index 0000000..5c199d5 --- /dev/null +++ b/overlay/zdt/configs/zdt.conf @@ -0,0 +1,88 @@ +# vim: ts=2 et: + +project = zdt-alpine +kubeversion = 1.21 + +# all build configs start with these +Default { + project = ${project} + + # image name/description components + encryption_key_id = null + name = [ zdt-alpine ] + description = [ "ZeroDownTime Alpine Images" ] + + motd { + welcome = "Welcome to Alpine!" + + wiki = "The Alpine Wiki contains a large amount of how-to guides and general\n"\ + "information about administrating Alpine systems.\n"\ + "See ." + + version_notes = "Release Notes:\n"\ + "* " + release_notes = "* > "$TARGET/etc/apk/repositories" + +# Fix dhcp to set MTU properly +install -o root -g root -Dm644 -t $TARGET/etc/dhcp $SETUP/dhclient.conf +echo 'Setup dhclient' + +# Enable SSH keepalive +sed -i -e "s/^[\s#]*TCPKeepAlive\s.*/TCPKeepAlive yes/" -e "s/^[\s#]*ClientAliveInterval\s.*/ClientAliveInterval 60/" $TARGET/etc/ssh/sshd_config +echo 'Enabled SSH keep alives' + +# CgroupsV2 +sed -i -e "s/^[\s#]*rc_cgroup_mode=.*/rc_cgroup_mode=\"unified\"/" $TARGET/etc/rc.conf + +# Setup syslog-ng json logging +cp $SETUP/syslog-ng.conf $TARGET/etc/syslog-ng/syslog-ng.conf +cp $SETUP/syslog-ng.logrotate.conf $TARGET/etc/logrotate.d/syslog-ng + +# Install cloudbender shutdown hook +cp $SETUP/cloudbender.stop $TARGET/etc/local.d +mkdir -p $TARGET/etc/cloudbender/shutdown.d + +# Install tools +cp $SETUP/route53.py $TARGET/usr/local/bin + +# Install ps_mem +wget -q -O $TARGET/usr/local/bin/ps_mem.py https://raw.githubusercontent.com/pixelb/ps_mem/master/ps_mem.py +sed -i -e 's,#!/usr/bin/env python,#!/usr/bin/env python3,' $TARGET/usr/local/bin/ps_mem.py +chmod +x $TARGET/usr/local/bin/ps_mem.py +echo 'Installed ps_mem.py' + +printf '\n# Zero Down Time config applied' diff --git a/overlay/zdt/scripts/setup-kubernetes b/overlay/zdt/scripts/setup-kubernetes new file mode 100755 index 0000000..e986905 --- /dev/null +++ b/overlay/zdt/scripts/setup-kubernetes @@ -0,0 +1,30 @@ +#!/bin/sh -eu +# vim: ts=4 et: + +[ -z "$DEBUG" ] || [ "$DEBUG" = 0 ] || set -x + +SETUP=/tmp/setup.d +TARGET=/mnt + +KUBE_VERSION=1.22 +AWS_IAM_VERSION=0.5.6 + +# Enable ZDT repo +echo "@kubezero https://cdn.zero-downtime.net/alpine/v${VERSION}/kubezero" >> "$TARGET/etc/apk/repositories" +install -o root -g root -Dm600 -t $TARGET/etc/apk/keys $SETUP/stefan@zero-downtime.net-61bb6bfb.rsa.pub + +apk -U --root "$TARGET" --no-cache add \ + cri-tools@kubezero \ + cri-o@kubezero=~$KUBE_VERSION \ + kubelet@kubezero=~$KUBE_VERSION \ + kubectl@kubezero=~$KUBE_VERSION + +# aws-iam-authenticator +wget -qO $TARGET/usr/local/bin/aws-iam-authenticator https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/v${AWS_IAM_VERSION}/aws-iam-authenticator_${AWS_IAM_VERSION}_linux_amd64 +chmod +x $TARGET/usr/local/bin/aws-iam-authenticator +echo "Installed aws-iam-authenticator binary version $AWS_IAM_VERSION" + +# Pre-load container images +# echo 'Pre-loaded Kubernetes control container images' + +printf '\n\n# Zero Down Time config applied' diff --git a/overlay/zdt/scripts/setup.d/cloudbender.stop b/overlay/zdt/scripts/setup.d/cloudbender.stop new file mode 100755 index 0000000..d84fd44 --- /dev/null +++ b/overlay/zdt/scripts/setup.d/cloudbender.stop @@ -0,0 +1,15 @@ +# Include dynamic config setting create at boot +[ -r /etc/cloudbender/rc.conf ] && . /etc/cloudbender/rc.conf + +rm -f /tmp/shutdown.log + +for cmd in $(ls /etc/cloudbender/shutdown.d/* | sort); do + . $cmd 1>>/tmp/shutdown.log 2>&1 +done + +[ $DEBUG -eq 1 ] && SHUTDOWNLOG="$(cat /tmp/shutdown.log)" + +[ -n "$RC_REBOOT" ] && ACTION="rebooting" || ACTION="terminated" +[ -z "$DISABLE_SCALING_EVENTS" ] && cloudbender_sns_alarm.sh "Instance $ACTION" "" Info "$SHUTDOWNLOG" + +sleep ${SHUTDOWN_PAUSE:-0} diff --git a/overlay/zdt/scripts/setup.d/dhclient.conf b/overlay/zdt/scripts/setup.d/dhclient.conf new file mode 100644 index 0000000..12b6b25 --- /dev/null +++ b/overlay/zdt/scripts/setup.d/dhclient.conf @@ -0,0 +1,12 @@ +# Borrowed from Ubuntu 20.04LTS minimal EC2 AMi + +option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; + +send host-name = gethostname(); +request subnet-mask, broadcast-address, time-offset, routers, + domain-name, domain-name-servers, domain-search, host-name, + dhcp6.name-servers, dhcp6.domain-search, dhcp6.fqdn, dhcp6.sntp-servers, + netbios-name-servers, netbios-scope, interface-mtu, + rfc3442-classless-static-routes, ntp-servers; + +timeout 300; diff --git a/overlay/zdt/scripts/setup.d/route53.py b/overlay/zdt/scripts/setup.d/route53.py new file mode 100755 index 0000000..fe9a01e --- /dev/null +++ b/overlay/zdt/scripts/setup.d/route53.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +import sys +import boto3 +import json +import argparse + + +def update_dns(record_name, ips=[], ttl=180, action="UPSERT", record_type='A'): + route53 = boto3.client("route53") + zone_id = route53.list_hosted_zones_by_name( + DNSName=".".join(record_name.split(".")[1:]) + )["HostedZones"][0]["Id"] + + changeset = { + "Changes": [ + { + "Action": action, + "ResourceRecordSet": { + "Name": record_name, + "Type": record_type, + "TTL": ttl, + "ResourceRecords": [], + }, + } + ] + } + for ip in ips: + changeset["Changes"][0]["ResourceRecordSet"]["ResourceRecords"].append( + {"Value": ip} + ) + + route53.change_resource_record_sets(HostedZoneId=zone_id, ChangeBatch=changeset) + + +parser = argparse.ArgumentParser(description='Update Route53 entries') +parser.add_argument('--fqdn', dest='fqdn', action='store', required=True, + help='FQDN for this record') +parser.add_argument('--record', action='append', required=True, + help='Value of a record') +parser.add_argument('--type', dest='record_type', action='store', default='A', + help='Record type') +parser.add_argument('--ttl', dest='ttl', action='store', default=180, type=int, + help='TTL of the entry') +parser.add_argument('--delete', dest='delete', action='store_true', + help='delete entry') + +args = parser.parse_args() +action = "UPSERT" +if args.delete: + action = "DELETE" + +print(args) +update_dns(args.fqdn, args.record, action=action, ttl=args.ttl, record_type=args.record_type) diff --git a/overlay/zdt/scripts/setup.d/stefan@zero-downtime.net-61bb6bfb.rsa.pub b/overlay/zdt/scripts/setup.d/stefan@zero-downtime.net-61bb6bfb.rsa.pub new file mode 100644 index 0000000..74a7edb --- /dev/null +++ b/overlay/zdt/scripts/setup.d/stefan@zero-downtime.net-61bb6bfb.rsa.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6BP/2VTRKfWmGtcJKf10 +tHrjiOir0BUqxTlYRwOtRv2iSs2aNaxs89sH+ZCNGxao1n+zBijhI2UFbp/nxGO5 +ftCPZicirASBmFN0XMg94nCt/vz+KCYjU+ASqlM/4uRFk0zf+loknzLgyyGD3SUT +tR9NCsOsZWN4sRTGDAAkseCqPOTsG/7c7bDWaEr1Gq2LQdV12KU1OqkSoR+aH9lk +xBdKMIgXssHiTQZevgMo515Z5kqaMBsOojpNUNjq7sPHmpKFlJJ93Id0QfH9duPk +0oWzT5XJdh6lrilYDAU4Bs4QNVGr1i27dQXRL57m5Gp1u705rwNjUmzwpZtCStwd +YwIDAQAB +-----END PUBLIC KEY----- diff --git a/overlay/zdt/scripts/setup.d/syslog-ng.conf b/overlay/zdt/scripts/setup.d/syslog-ng.conf new file mode 100644 index 0000000..1aa6e59 --- /dev/null +++ b/overlay/zdt/scripts/setup.d/syslog-ng.conf @@ -0,0 +1,16 @@ +# syslog-ng, format all json into messages +# https://www.syslog-ng.com/technical-documents/doc/syslog-ng-open-source-edition/3.23/administration-guide/63#TOPIC-1268643 + +@version: 3.30 +@include "scl.conf" + +options { chain_hostnames(off); flush_lines(0); use_dns(no); use_fqdn(no); + dns_cache(no); owner("root"); group("adm"); perm(0640); + stats_freq(0); bad_hostname("^gconfd$"); frac-digits(6); +}; + +source s_sys { system(); internal();}; + +destination d_mesg { file("/var/log/messages" template("$(format-json time=\"$UNIXTIME\" facility=\"$FACILITY\" host=\"$LOGHOST\" ident=\"$PROGRAM\" pid=\"$PID\" level=\"$PRIORITY\" message=\"$MESSAGE\")\n")); }; + +log { source(s_sys); destination(d_mesg); }; diff --git a/overlay/zdt/scripts/setup.d/syslog-ng.logrotate.conf b/overlay/zdt/scripts/setup.d/syslog-ng.logrotate.conf new file mode 100644 index 0000000..cd481e7 --- /dev/null +++ b/overlay/zdt/scripts/setup.d/syslog-ng.logrotate.conf @@ -0,0 +1,13 @@ +/var/log/messages +{ + rotate 2 + missingok + notifempty + compress + maxsize 64M + daily + sharedscripts + postrotate + invoke-rc.d syslog-ng reload > /dev/null + endscript +} diff --git a/overlay/zdt/zdt.hcl b/overlay/zdt/zdt.hcl new file mode 100644 index 0000000..3913dd4 --- /dev/null +++ b/overlay/zdt/zdt.hcl @@ -0,0 +1,10 @@ +qemu = { + "boot_wait": { + "aarch64": "15s", + "x86_64": "15s" + } + cmd_wait = "5s" + ssh_timeout = "20s" + memory = 1024 +} +