Laravel Envoy - How to Automate the Boring Stuff

cover
10 Jul 2024

Laravel Envoy is a tool for executing common tasks you run on your remote servers.

I believe Envoy is underrated; I don't see it being used very often even though I have always found it super useful. In this article, we will explore how Envoy can help increase your productivity🚀.

Laravel Envoy is not exclusive to Laravel developers or limited to Laravel projects, anyone can use it ❤️.

First, let's discuss two key concepts in Envoy:

  1. Tasks: This represents a specific action like updating the server or cloning a repository.

  2. Stories: which is a collection of Tasks.

That's all you need to know for now; you can always read about all the features in the docs.

What Are We Automating?

In this article, we will automate 2 things most developers do when deploying their applications:

  1. Configuring Nginx.

  2. Generating SSH keys and adding them to GitHub to be able to access private repositories.

Yes I know, most of the time Envoy is used for CI/CD workflows, but it can do EVERYTHING, literally.

Envoy Setup

First, let's create a directory:

mkdir tuto && cd $_ 

$ take tuto if you are using zsh.

Install Envoy by running the following command:

composer require laravel/envoy --dev

Make sure you have composer installed.

Now, create a file called Envoy.blade.php. Yes, we will be using the Blade syntax, super cool right?

touch Envoy.blade.php

That's it! All you need is a single script. It doesn't have to be specific to Laravel or any Laravel-related project. Let's start automating! 😁

Configure Nginx

We have a brand new server, and we want to set up Nginx. If we break down the process, it would be like this:

  1. Update the server
  2. Install Nginx
  3. Configure Nginx

That's exactly what we will be doing with Envoy; think of each step as a Task and the entire process as a Story.

So, let's translate what we have just said into a story:

@servers(['web' => '[email protected]', 'local' => '127.0.0.1'])

@story('setup-nginx')
    update-server
    install-nginx
    copy-nginx-stub
    configure-nginx
@endstory

The @servers directive is used to specify the servers on which we will be running our tasks later on.

Now, we can proceed to define each task 😁

Our first task update-server will ensure that the server's packages and dependencies are up to date:

@task('update-server', ['on' => ['web']])
    echo "Updating server..."

    apt update && apt upgrade -y
@endtask

The second task install-nginx will install Nginx on our server:

@task('install-nginx', ['on' => ['web']])
    echo "Installing nginx..."

    apt install nginx -y

    rm /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default

    touch /etc/nginx/sites-available/{{ $application_name }}.conf

    ln -s /etc/nginx/sites-available/{{ $application_name }}.conf /etc/nginx/sites-enabled/{{ $application_name }}.conf
@endtask

Notice that we have removed the default Nginx link and created a new one for our application, with the name coming from the $application_name variable.

To be able to use that variable, you need to declare it, so we need to include the @setup directive:

@setup
    $application_name = 'your-application-name';
@endsetup

Now, we can move to the third task copy-nginx-stub. In my case, I am deploying a Laravel application, so I will use the Nginx configuration file provided by the docs, with a few tweaks. If you're deploying a different application, you can apply the same concept to your own configuration file.

In the directory we just created, run the following command:

mkdir stubs; nano stubs/nginx.conf

Then, paste the following content into the editor, save it, and exit:

server {
    listen 80;
    listen [::]:80;
    server_name public_ip;
    root /var/www/app_name/public;
 
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
 
    index index.php;
 
    charset utf-8;
 
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }
 
    error_page 404 /index.php;
 
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
 
    location ~ /\.(?!well-known).* {
        deny all;
    }
}

The public_ip and app_name are placeholders for now and will be automatically updated with our variables.

Let's move on to writing the task itself:

@task('copy-nginx-stub', ['on' => 'local'])
    scp -P{{ $production_port }} -r ./stubs/nginx.conf
    {{ $production_host }}:/etc/nginx/sites-available/{{ $application_name }}.conf
@endtask

This task will be executed on our local machine instead of the remote server. We specify this using 'on' => 'local'.

And don't forget to update the @setup directive with the necessary variables:

@setup
    $application_name = 'your-application-name';
    $production_port = 22;
    $production_host = '[email protected]';
@endsetup

The fourth and final task configure-nginx will update the placeholders, so we can serve the application correctly:

@task('configure-nginx', ['on' => 'web'])
    echo "Configuring nginx..."

    cd /etc/nginx/sites-available/

    sed -i 's/app_name/{{ $application_name }}/g' {{ $application_name }}.conf
    sed -i 's/public_ip/{{ $production_ip }}/g' {{ $application_name }}.conf
@endtask

Note the cd command. This is because each task is executed separately, so it always starts from the home directory of the remote server.

We have already created the symlink when installing Nginx, we don't have to worry about it now.

And we are done with this section! Your script should look like this:

@servers(['web' => '[email protected]', 'local' => '127.0.0.1'])

@setup
    $application_name = 'your-application-name';
    $production_port = 22;
    $production_host = '[email protected]';
@endsetup

@story('setup-nginx')
    update-server
    install-nginx
    copy-nginx-stub
    configure-nginx
@endstory

@task('update-server', ['on' => ['web']])
    echo "Updating server..."

    apt update && apt upgrade -y
@endtask

@task('install-nginx', ['on' => ['web']])
    echo "Installing nginx..."

    apt install nginx -y

    rm /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default

    touch /etc/nginx/sites-available/{{ $application_name }}.conf

    ln -s /etc/nginx/sites-available/{{ $application_name }}.conf /etc/nginx/sites-enabled/{{ $application_name }}.conf
@endtask

@task('copy-nginx-stub', ['on' => 'local'])
    scp -P{{ $production_port }} -r ./stubs/nginx.conf
    {{ $production_host }}:/etc/nginx/sites-available/{{ $application_name }}.conf
@endtask

@task('configure-nginx', ['on' => 'web'])
    echo "Configuring nginx..."

    cd /etc/nginx/sites-available/

    sed -i 's/app_name/{{ $application_name }}/g' {{ $application_name }}.conf
    sed -i 's/public_ip/{{ $production_ip }}/g' {{ $application_name }}.conf
@endtask

SSH Key Configuration

Now that we configured Nginx, and it is ready to serve our application, we need to generate SSH keys and add the public key to GitHub so we can pull the private repositories.

For this, we will use GitHub REST API, so before we start, you need to create a token.

When creating your token, ensure that you only select the "admin:public_key" scope.

Now that you created your token, let's begin by defining some variables:

@setup
    $ssh_key = '~/.ssh/id_rsa_github';
    $github_api_key = 'your-github-token';
    $email = '[email protected]';
@endsetup

At this point, you might be wondering about the steps involved in this process. Well, we can break it down into two steps:

  1. Generate SSH keys

  2. Copy the public key to GitHub

Once again, this process will be our story:

@story('setup-ssh-keys')
    generate-ssh-keys
    add-ssh-keys-to-github
@endstory

The first task, generate-ssh-keys, can be done by running a single command:

@task('generate-ssh-keys', ['on' => ['web']])
    echo "Generating ssh keys..."

    ssh-keygen -t ed25519 -f {{ $ssh_key }} -N '' -q -C "{{ $email }}"
@endtask

Once we have generated our SSH keys, we can add the public key to GitHub using the GitHub API. This can be done with a single request:

@task('add-ssh-keys-to-github', ['on' => ['web']])
    echo "Adding ssh keys to github..."

    key=$(cat {{ $ssh_key }}.pub)

    curl --request POST \
    --url https://api.github.com/user/keys \
    --header 'Accept: application/vnd.github+json' \
    --header 'Authorization: Bearer {{ $github_api_key }}' \
    --header 'Content-Type: application/json' \
    --header 'X-GitHub-Api-Version: 2022-11-28' \
    --data '{
    "title": "[Envoy] Public key",
    "key": "'"$key"'"
    }'
@endtask

And that's it! If you visit your GitHub developer settings, you should see your newly created key.

By combining the two sections, your final script should look like this:

@servers(['web' => '[email protected]', 'local' => '127.0.0.1'])

@setup
    $application_name = 'your-application-name';
    $production_port = 22;
    $production_host = '[email protected]';
    $ssh_key = '~/.ssh/id_rsa_github';
    $github_api_key = 'your-github-token';
    $email = '[email protected]';
@endsetup

@story('setup-nginx')
    update-server
    install-nginx
    copy-nginx-stub
    configure-nginx
@endstory

@story('setup-ssh-keys')
    generate-ssh-keys
    add-ssh-keys-to-github
@endstory

@task('update-server', ['on' => ['web']])
    echo "Updating server..."

    apt update && apt upgrade -y
@endtask

@task('install-nginx', ['on' => ['web']])
    echo "Installing Nginx..."

    apt install nginx -y

    rm /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default

    touch /etc/nginx/sites-available/{{ $application_name }}.conf

    ln -s /etc/nginx/sites-available/{{ $application_name }}.conf /etc/nginx/sites-enabled/{{ $application_name }}.conf
@endtask

@task('copy-nginx-stub', ['on' => 'local'])
    scp -P{{ $production_port }} -r ./stubs/nginx.conf
    {{ $production_host }}:/etc/nginx/sites-available/{{ $application_name }}.conf
@endtask

@task('configure-nginx', ['on' => 'web'])
    echo "Configuring nginx..."

    cd /etc/nginx/sites-available/

    sed -i 's/app_name/{{ $application_name }}/g' {{ $application_name }}.conf
    sed -i 's/public_ip/{{ $production_ip }}/g' {{ $application_name }}.conf
@endtask

@task('generate-ssh-keys', ['on' => ['web']])
    echo "Generating ssh keys..."

    ssh-keygen -t ed25519 -f {{ $ssh_key }} -N '' -q -C "{{ $email }}"
@endtask

@task('add-ssh-keys-to-github', ['on' => ['web']])
    echo "Adding ssh keys to github..."

    key=$(cat {{ $ssh_key }}.pub)

    curl --request POST \
    --url https://api.github.com/user/keys \
    --header 'Accept: application/vnd.github+json' \
    --header 'Authorization: Bearer {{ $github_api_key }}' \
    --header 'Content-Type: application/json' \
    --header 'X-GitHub-Api-Version: 2022-11-28' \
    --data '{
    "title": "creating from script",
    "key": "'"$key"'"
    }'
@endtask

Conclusion

We learned how to use Envoy to automate common tasks. It's a powerful tool with even more capabilities than we've explored here. Don't limit yourself to just deploying your applications; Envoy can automate ANY terminal command, literally anything that comes to mind.

The next time you find yourself repeating the same commands, consider using it, I promise you it does way more than CI/CD 😛😛