provisioning: Use AWS CLI to automate provisioning

The previous steps for standing up a new host were somewhat manual.
This further scripts the process, by using the AWS CLI to start the
instance, and pass it a "user data" script to provision itself upon
boot.  This results in a hands-off provisioning process which
completes in 5min.

Additional settings are required for `~/.zulip-install-server.conf`.
It is not suited for all roles, as it assumes one instance type and
security group value.  Additionally, not all of the post-provision
process is currently automated -- Nagios SSH key verification, for
instance, is still a manual step.  There are also additional steps for
database or frontend servers.  Regardless, this is a move toward
automated provisioning.
This commit is contained in:
Alex Vandiver
2020-07-15 18:54:57 +00:00
committed by Tim Abbott
parent 29c66cf7c2
commit a2fc823c3f
3 changed files with 219 additions and 119 deletions

View File

@@ -1,136 +1,120 @@
#!/usr/bin/env bash
set -e
server=$1
type=$2
hostname=$3
branch=$4
if [ -z "$hostname" ]; then
echo "USAGE: $0 server type hostname [branch]"
SERVER=$1
ROLES=$2
BRANCH=$3
if [ -z "$SERVER" ] || [ -z "$ROLES" ]; then
echo "USAGE: $0 server roles [branch]"
echo
echo "Installs an empty Ubuntu server in AWS with a Zulip server role."
echo "* hostname is the current hostname/IP of the server"
echo "* type is a list of puppet rules to be passed to scripts/lib/install"
echo " E.g. 'zulip::base,zulip::apt_repository,zulip::postgres_common'"
echo "* hostname is to be the server's external hostname."
echo "* branch is used to override the default branch to install from."
echo "Reads configuration from $HOME/.zulip-install-server.conf."
exit 1
fi
if ! echo "$hostname" | grep -q zulip; then
echo "USAGE: $0 server type hostname [branch]"
echo "Hostname must have zulip in it."
echo
echo " * server is the local part of the hostname (e.g. postgres0)"
echo " * roles is a list of puppet rules to be passed to scripts/lib/install"
echo " E.g. 'zulip::base,zulip::apt_repository,zulip::postgres_common'"
echo " * branch is used to override the default branch to install from."
echo
echo "Reads configuration from $HOME/.zulip-install-server.conf, which should look like:"
echo
echo "[repo]"
echo "repo_url=git@github.com:zulip/zulip.git"
echo "branch=master"
echo "[aws]"
echo "zone_id=Z2U988IEXAMPLE"
echo "security_groups=sg-01234567"
echo "image_id=ami-0dc45e3d9be6ab7b5"
echo "instance_type=m4.large"
echo "ssh_secret_id=prod/git/deploy"
exit 1
fi
set -x
zulip_ssh_config_file="$HOME/.zulip-install-server.conf"
amazon_key_file=$(crudini --get "$zulip_ssh_config_file" ssh amazon_key_file)
if ! [ -e "$amazon_key_file" ]; then
echo "You need the amazon ssh key at $amazon_key_file"
cd "$(dirname "$0")"
source ./bootstrap-awscli
zulip_install_config_file="$HOME/.zulip-install-server.conf"
if [ ! -f "$zulip_install_config_file" ]; then
echo "No configuration file found in $zulip_install_config_file"
exit 1
fi
REPO_URL=$(crudini --get "$zulip_install_config_file" repo repo_url)
if [ -z "$BRANCH" ]; then
BRANCH=$(crudini --get "$zulip_install_config_file" repo default_branch)
fi
AWS_ZONE_ID=$( crudini --get "$zulip_install_config_file" aws zone_id)
SECURITY_GROUPS=$(crudini --get "$zulip_install_config_file" aws security_groups)
AMI_ID=$( crudini --get "$zulip_install_config_file" aws image_id)
INSTANCE_TYPE=$( crudini --get "$zulip_install_config_file" aws instance_type)
SSH_SECRET_ID=$( crudini --get "$zulip_install_config_file" aws ssh_secret_id)
# Verify it doesn't exist already
ZONE_NAME=$($AWS route53 get-hosted-zone --id "$AWS_ZONE_ID" | jq -r '.HostedZone.Name' )
HOSTNAME="$SERVER.${ZONE_NAME%?}" # Remove trailing .
EXISTING_RECORDS=$($AWS route53 list-resource-record-sets \
--hosted-zone-id "$AWS_ZONE_ID" \
--query "ResourceRecordSets[?Name == '$HOSTNAME.']" \
| jq '. | length')
if [ "$EXISTING_RECORDS" != "0" ]; then
echo "$HOSTNAME already exists!"
exit 1
fi
server_private_key_file=$(crudini --get "$zulip_ssh_config_file" ssh server_private_key_file)
if ! [ -e "$server_private_key_file" ]; then
echo "You need a server ssh key at $server_private_key_file"
exit 1
fi
# Build up the provisioning script
BOOTDATA=$(mktemp)
{
echo "#!/bin/bash"
echo "SERVER=$SERVER"
echo "HOSTNAME=$HOSTNAME"
echo "ROLES=$ROLES"
echo "REPO_URL=$REPO_URL"
echo "BRANCH=$BRANCH"
echo "SSH_SECRET_ID=$SSH_SECRET_ID"
sed '/^AWS=/ r ./bootstrap-awscli' bootstrap-aws-installer
} >> "$BOOTDATA"
if [ -n "${zulip_confdir-}" ]; then
zulipconf_file="$zulip_confdir/zulip.conf"
secrets_file="$zulip_confdir/zulip-secrets.conf"
settings_file="$zulip_confdir/settings.py"
fi
if [ -z "$secrets_file" ]; then
echo "Specify secrets_file via environment."
exit 1
fi
TAGS="[{Key=Name,Value=$SERVER},{Key=role,Value=\"$ROLES\"}]"
INSTANCE_DATA=$($AWS ec2 run-instances \
--iam-instance-profile 'Name="EC2ProdInstance"' \
--image-id "$AMI_ID" \
--instance-type "$INSTANCE_TYPE" \
--security-group-ids "$SECURITY_GROUPS" \
--tag-specifications "ResourceType=instance,Tags=$TAGS" \
--user-data "file://$BOOTDATA")
INSTANCEID=$(echo "$INSTANCE_DATA" | jq -r .Instances[0].InstanceId)
zulip_repo=$(crudini --get "$zulip_ssh_config_file" repo repo_url)
# Wait for public IP assignment
PUBLIC_DNS_NAME=""
while [ -z "$PUBLIC_DNS_NAME" ]; do
sleep 1
PUBLIC_DNS_NAME=$($AWS ec2 describe-instances --instance-ids "$INSTANCEID" \
| jq -r .Reservations[0].Instances[0].PublicDnsName )
done
if [ -z "$branch" ]; then
branch=$(crudini --get "$zulip_ssh_config_file" repo default_branch)
fi
VIRTUALENV_NEEDED=$(if echo "$type" | grep -q app_frontend; then echo -n yes; else echo -n no; fi)
# Force RSA keys. We do this because the ECDSA key is not printed on syslog,
# and our puppet configuration does not use ECDSA. If we don't do this,
# we'll get key errors after puppet apply.
SSH_OPTS=(-o 'HostKeyAlgorithms=ssh-rsa')
set +e
ssh "${SSH_OPTS[@]}" "$server" -i "$amazon_key_file" -lubuntu -o "ControlMaster=no" /bin/bash <<EOF
sudo mkdir -p ~root/.ssh && sudo cp .ssh/authorized_keys /root/.ssh/authorized_keys
sudo sed -i 's/disable_root: true/disable_root: false/' /etc/cloud/cloud.cfg
sudo mkdir -p /etc/zulip
EOF
# shellcheck disable=SC2029 disable=SC2087
ssh "${SSH_OPTS[@]}" "$server" -i "$amazon_key_file" -lroot /bin/bash <<EOF
# Set the hostname early
echo "$hostname" > /etc/hostname
hostname "$hostname"
sed -i 's/localhost$/localhost $hostname/' /etc/hosts
# Add the hostname to the zone
ROUTE53_CHANGES=$(mktemp)
cat > "$ROUTE53_CHANGES" <<EOF
{
"Comment": "Add the $HOSTNAME CNAME record",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "$HOSTNAME",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [{"Value": "$PUBLIC_DNS_NAME"}]
}
}
]
}
EOF
$AWS route53 change-resource-record-sets --hosted-zone-id "$AWS_ZONE_ID" --change-batch "file://$ROUTE53_CHANGES"
rm "$ROUTE53_CHANGES"
set -e
# Give server its SSH keys
scp "${SSH_OPTS[@]}" -i "$amazon_key_file" "$server_private_key_file" root@"$server":/root/.ssh/id_rsa
scp "${SSH_OPTS[@]}" -i "$amazon_key_file" "$server_private_key_file".pub root@"$server":/root/.ssh/id_rsa.pub
scp "${SSH_OPTS[@]}" -i "$amazon_key_file" "$secrets_file" root@"$server":/etc/zulip/zulip-secrets.conf
if [ -e "$zulipconf_file" ]; then
scp "${SSH_OPTS[@]}" -i "$amazon_key_file" "$zulipconf_file" root@"$server":/etc/zulip/zulip.conf
fi
if [ -e "$settings_file" ]; then
scp "${SSH_OPTS[@]}" -i "$amazon_key_file" "$settings_file" root@"$server":/etc/zulip/settings.py
fi
# shellcheck disable=SC2029 disable=SC2087
ssh "${SSH_OPTS[@]}" "$server" -i "$amazon_key_file" -lroot /bin/bash <<EOF
set -x
# Finish setting up the SSH private key
chmod 600 /root/.ssh/id_rsa
# Delete the ubuntu user
if grep -q '^ubuntu:' /etc/passwd; then
userdel ubuntu
fi
# Make sure root doesn't have a password
passwd -d root
apt-get update
apt-get -y upgrade
cd "\$(mktemp -d)"
# Get GitHub known_hosts setup
# TODO: Replace this with hardcoding the actual GitHub keys
ssh -oStrictHostKeyChecking=no git@github.com </dev/null >/dev/null || true
if ! [ -e "zulip" ]; then
# need to install git to clone the repo
apt-get install -y git crudini
git clone "$zulip_repo" zulip
fi
cd zulip
git fetch
git checkout origin/$branch
# The main Zulip production install script can take things from here!
env VIRTUALENV_NEEDED=$VIRTUALENV_NEEDED PUPPET_CLASSES="$type" \
./scripts/setup/install --self-signed-cert --no-init-db --no-overwrite-settings
EOF
scp "${SSH_OPTS[@]}" -i "$amazon_key_file" "$server_private_key_file" root@"$server":/home/zulip/.ssh/id_rsa
scp "${SSH_OPTS[@]}" -i "$amazon_key_file" "$server_private_key_file".pub root@"$server":/home/zulip/.ssh/id_rsa.pub
# shellcheck disable=SC2029
ssh "${SSH_OPTS[@]}" "$server" -i "$amazon_key_file" -lroot /bin/bash <<EOF
chown zulip:zulip /home/zulip/.ssh/id_rsa*
chmod 600 /home/zulip/.ssh/id_rsa*
EOF
set +x
cat <<EOF
Done.
EOF
echo
echo
echo ">>> Install started successfully! Provisioning takes 5-6min."
echo " sleep 360 && ssh root@$HOSTNAME"