Capistrano is a neat remote server automation and deployment tool. You write commands in an extended Rake DSL (Ruby) and cap will run them over SSH on your target servers.
It comes with some nifty built-in commands for deploying code from a VCS and prints pretty output, among other things.
The downside is that it requires you to install and know Ruby. Also, isn’t the best language for shell commands Bash? (Be quiet, Zsh users.)
So here’s an experiment to see how much of Capistrano we can quickly recreate in Bash. So far it’s got:
- pretty output
- built-in command to deploy from VCS
- supports SSH to multiple servers (but not parallel)
- supports multiple environment configs
- arbitrary user commands and config
usage: bashistrano [options] [command] [target]
Arguments:
command The bash function to run (e.g. deploy, rollback)
target The target environment (e.g. local, staging, production)
Options:
-h --help Print this message
-v --verbose Print a lot
This has been a learning experience rather than an attempt to make a serious tool, so here’s some of the features of the code. It’s also a literate program, i.e. the entire program is extracted from this README with org-babel
.
The method here is to build up a script with string substitution, loop over the remote servers and pipe it into ssh
. It’s a surprisingly little known fact that you can feed strings and heredocs into ssh
.
#!/usr/bin/env bash
<<license_header>>
usage="$0 - remote automation tool
<<usage>>
"
<<parse_arguments>>
<<load_user_config>>
<<load_user_commands>>
for server in $servers
do
read -r -d '' script <<-EOF
<<remote_script>>
EOF
$ssh_cmd ${user}@${server} "$script"
done
The script takes a command name and a target environment. The default command just prints a greeting.
while getopts 'hv' opt
do
case "$opt" in
v) set -x;;
*) echo "$usage"; exit 0;;
esac
done
shift $((OPTIND-1))
command=${1:-main} # Default to a command called "main"
target=${2:-local} # Default to fucking up your own box
Configuration for each target environment is located at config/<target environment>
. These are just shell scripts that get sourced, so put in normal variables (and do any special logic required to initialize these).
Some post-processing of the config is done to provide defaults. The reason ssh
is given the -tt
option to force a TTY is that it seems to be the only way to get coloured output back.
source "config/default"
source "config/$target"
ssh_cmd=${ssh_cmd:-'ssh -tt'}
deploy_dir=${deploy_dir:-/opt/deploy}
timestamp=$(date +%Y-%m-%dT%H:%M:%S)
repo=${repo:-git@github.com:cbowdon/bashistrano.git}
Users can add arbitrary commands as functions in shell scripts in the same directory. The files are basically sourced, nothing clever here.
# TODO Could reduce data transfer by filtering for command (and sourced files)
user_commands=$(find . -name "*.sh" | grep -v "config/" | xargs cat)
User functions can take advantage of some of the helpers defined in the remote script such as message
for nice(ish) log messages.
The remote script is built by just substituting in the user config and commands. Anything that needs to be considered a variable at the other end is escaped. A few helper functions are rolled in too.
# Make some vars available to user commands
cmd_host=$HOSTNAME
cmd_user=$USER
# Make user's own config available to user commands
$(cat "config/default")
$(cat "config/$target")
PATH="\${remote_PATH:-\$PATH}"
# Make some helper functions available to user commands
message () {
local fg_cyan="\$(tput setaf 6)"
local reset="\$(tput sgr0)"
echo "\${fg_cyan}[${user}@${server}]\${reset} \$1"
}
# Default command, can be redefined by user
main () {
message "Hello, world"
}
# Default deploy command
deploy () {
mkdir -p ${deploy_dir}/releases -x 0755
git clone $repo ${deploy_dir}/releases/${timestamp}
if [ -L ${deploy_dir}/current ]
then
ln -sfn $(readlink ${deploy_dir}/current) ${deploy_dir}/rollback
fi
ln -sfn ${deploy_dir}/releases/${timestamp} ${deploy_dir}/current
}
message "Connected"
# Define all user commands
$user_commands
message "Running '$command' on $server"
$command
The deploy command hasn’t been well-tested to be honest, since I’d expect almost every user to require their own variation anyway.
I haven’t gone out of my way for portability, but have attempted to stick to POSIX most of the time so porting shouldn’t be too much effort.
# Bashistrano - a remote server automation and deployment tool
# Copyright (C) 2017 Chris Bowdon
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.