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:

  1. Go to your favourite browser and log in to the Amazon console.
  2. Go to the EC2 tab
  3. Select Instances on the sidebar
  4. Search, optionally by product and/or tag
  5. Choose the correct instance among the candidates
  6. Copy the public DNS of the instance
  7. Change to your terminal
  8. 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:

  1. Write a hook for a command
  2. Generate a list of data related to the command
  3. 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 follow feature/JIRA_TICKET/description, using the JIRA API this can be magically completed from the current sprint.
  • docker logs␣ completing with the ids from docker 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.