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
:
-
Tasks: This represents a specific action like updating the server or cloning a repository.
-
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:
-
Configuring Nginx.
-
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 usingzsh
.
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:
- Update the server
- Install Nginx
- 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:
-
Generate SSH keys
-
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 😛😛