SSH autocompletion for EC2 instances
At Affectv we ❤️ automation. We strive to take it one step further: let the computer do the job. Or at least, let the computer make our lives easier. Today we’ll show an example of what we mean.
The pain
Frequently, when we perform any DevOps tasks, we need to ssh
to some of our instances. We may want to check local logs (we only send to Graylog from a certain level), trouble-check some connection issue, or maybe restart a docker container.
This used to involve:
- Go to your favourite browser and log in to the Amazon console.
- Go to the
EC2
tab - Select
Instances
on the sidebar - Search, optionally by product and/or tag
- Choose the correct instance among the candidates
- Copy the public DNS of the instance
- Change to your terminal
- Finally,
ssh
to it
It gets old pretty soon. There has to be a more ergonomic way, don’t you think?
From XKCD
We could start by writing a small script that uses the AWS CLI to only show instances matching a specific tag combination, by using a filter like this:
aws ec2 describe-instances \
--query "Reservations[*].Instances[*].[Tags[?Key=='Name'].Value | \
[0], LaunchTime, PublicDnsName]" \
--filters "Name=tag:Name,Values=kafka-production" --output text
This will print instance name, launch time and public DNS name for all instances with a name matching kafka-production
.
We could get this to automatically ssh
into the first instance, or expect a number to choose the target. But, there’s always a better way.
fzf
fzf is a general-purpose command-line fuzzy finder.
It’s an interactive Unix filter for command-line that can be used with any list; files, command history, processes, hostnames, bookmarks, git commits, etc.
It’s oh, so cool. Our first uses of fzf were just as a zsh+oh-my-zsh extension for better history search. Then we found how easy the API to write your own completions is (although it is still experimental).
Using this API, you can add a completion trigger to any command. For instance, cd <trigger>
followed by TAB
will bring a fzf
search prompt with your current path. You can use any character as <trigger>
, most users of fzf
pick ,
or ,,
.
The approach (under zsh
) to write your own completer is:
- Write a hook for a command
- Generate a list of data related to the command
- Generate the completion result
In the case of our AWS instance completion, the command we want to magically complete is ssh
, so we need to define a function like this
_fzf_complete_ssh() {
ARGS="$@"
This will capture all arguments to ssh
when we trigger completion.
Now we need to actually get the data we want to expose as options. Beware the AWS CLI command now
local machines
machines=$(aws ec2 describe-instances \
--query "Reservations[*].Instances[*].[Tags[?Key=='Name'].Value | \
[0],Tags[?Key=='product'].Value | \
[0], Tags[?Key=='environment'].Value | \
[0], LaunchTime, PublicDnsName]" \
--filters Name=instance-state-name,Values=running --output text | \
tr ' ' '-' | \
column -t -s $'\t')
Writing this AWS command is a pain, there’s no denying.
Basically it translates into
- From all reservations and instances
- Get the tag
Name
- Get the tag
product
- Get the tag
environment
- Get the
launch time
- Get the public DNS
- Filter for instances in the running state
Output format is text, and the *NIX command column
will convert the input into a proper table, from input with a tab as separator.
Now we can bind this expansion to specific commands
if [[ $ARGS == 'ssh '* ]]; then
_fzf_complete "--reverse --multi" "$@" < <(
echo $machines
)
else
eval "zle ${fzf_default_completion:-expand-or-complete}"
fi
These are the magic incantations. The first if
makes sure we are capturing ssh␣
. If we wanted to complete git
branches for the checkout command only, we’d change the condition to ’git co’*
or ’git checkout’*
. The second if
reverts to default zsh
completion data but using fzf
as handler.
Finally, we need to post-process the completion result
_fzf_complete_ssh_post() {
awk '{print $5}'
}
This will add to ssh
the 5th column from the chosen completion, if you are keeping track that’s the DNS.
And that’s it. Get the computer to do the work. Some completion ideas to play with:
git checkout -b␣
completing to branches following your default structure. We followfeature/JIRA_TICKET/description
, using the JIRA API this can be magically completed from the current sprint.docker logs␣
completing with the ids fromdocker ps -a
Have fun, and complete all the things.
The code
_fzf_complete_ssh() {
ARGS="$@"
local machines
machines=$(aws ec2 describe-instances \
--query "Reservations[*].Instances[*].[Tags[?Key=='Name'].Value | \
[0],Tags[?Key=='product'].Value | \
[0], Tags[?Key=='environment'].Value | \
[0], LaunchTime, PublicDnsName]" \
--filters Name=instance-state-name,Values=running --output text | \
tr ' ' '-' | \
column -t -s $'\t')
if [[ $ARGS == 'ssh '* ]]; then
_fzf_complete "--reverse --multi" "$@" < <(
echo $machines
)
else
eval "zle ${fzf_default_completion:-expand-or-complete}"
fi
}
_fzf_complete_ssh_post() {
awk '{print $5}'
}
Copy it somewhere with some_name
, and add source some_name
to your .zshrc
, after loading fzf
.