Support the ongoing development of Laravel.io โ†’
Article Hero Image

Laravel Envoy - Automate the boring stuff

17 Feb, 2024 8 min read

Photo by Lenny Kuhne on Unsplash

Laravel Envoy

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: which 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 application:

  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 times 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 setup Nginx. If we break down the process it would be like:

  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 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 for 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 ๐Ÿ˜›๐Ÿ˜›

Last updated 3 weeks ago.

driesvints liked this article

1
Like this article? Let the author know and give them a clap!
oussamamater (Oussama Mater) I'm a software engineer and CTF player. I use Laravel and Vue.js to turn ideas into applications ๐Ÿš€

Other articles you might like

Article Hero Image November 18th 2024

Laravel Custom Query Builders Over Scopes

Hello ๐Ÿ‘‹ Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...

Read article
Article Hero Image November 19th 2024

Access Laravel before and after running Pest tests

How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....

Read article
Article Hero Image November 11th 2024

๐Ÿฃ Sushi โ€” Your Eloquent model driver for other data sources

In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.