February 21, 2015

Deploying with Envoy (Cast)

We'll learn to use Laravel's Envoy to handle deployment over SSH.We'll use Laravel's Envoy to deploy a PHP application to a production server. This will make a new release directory, clone the repository into it, run any needed build steps, and then finally swap the new code out with the older code, so that Nginx and PHP-FPM serve the new code..

Setup

The remote server is at IP address 104.236.85.162. It has a user deployer, and I created a local SSH key pair to connect to it. That key pair is called id_deployex.

To generate this key pair, I used the following command:

ssh-keygen -t rsa -b 4096 -C "fideloper@gmail.com" -f id_deployex

To copy that to remove server (since I'm on a Vagrant server, not my Mac, which may not have this command), I can use the following to copy the public key into the deployer user's authorized_keys file:

# Since not running on mac, we have this command
# This will ask for the user's password to log in with, and assumes
# the server allows password-based authentication
ssh-copy-id -o "PubkeyAuthentication no" deployer@104.236.85.162

On the remote (production) server, we create SSH key to use within GitHub as a Deploy Key. This lets us push/pull to the GitHub repository from the production server, which we'll use to deploy new code.

# Create key, by default named "id_rsa"
ssh-keygen -t rsa -b 4096 -C "fideloper@gmail.com"

# Echo out the public key,
# so we can copy it and paste it into GitHub
cat ~/.ssh/id_rsa.pub

Then we can get to using Envoy with our Laravel application.

Install Envoy Globally

# Install envoy globally. May need to ensure that
# the composer vendor/bin directory is in your PATH
# so the envoy command is found
composer global require "laravel/envoy=~1.0"

# fails, if the PATH variable is not set with composer's
# bin directory
which envoy

vim ~/.profile

If the which envoy test fails (if there's no output saying the path to the envoy command), edit .profile, .bashrc or whatever is appropriate for your system. Within this file, we'll append the following near the bottom. Adjust the file path as needed for your development server.

# Prepend the composer bin directory to your PATH
# This path is specific to my vagrant server
export PATH=/home/vagrant/.composer/vendor/bin:$PATH

Finally we can create an envoy file for our project:

cd /vagrant/app
vim Envoy.blade.php

Here's a sample we can use to test that Envoy can connect to our server:

@servers(['web' => 'deploy-ex'])

<?php $whatever = 'hola, whatever'; ?>

@task('deploy', ['on' => 'web'])
    echo {{ $whatever }}
@endtask

To run that:

envoy run deploy

Now we'll employ a deployment strategy with Envoy:

@servers(['web' => 'deploy-ex'])

<?php
$repo = 'git@github.com:Servers-for-Hackers/deploy-ex.git';
$release_dir = '/var/www/releases';
$app_dir = '/var/www/app';
$release = 'release_' . date('YmdHis');
?>

@macro('deploy', ['on' => 'web'])
    fetch_repo
    run_composer
    update_permissions
    update_symlinks
@endmacro

@task('fetch_repo')
    [ -d {{ $release_dir }} ] || mkdir {{ $release_dir }};
    cd {{ $release_dir }};
    git clone {{ $repo }} {{ $release }};
@endtask

@task('run_composer')
    cd {{ $release_dir }}/{{ $release }};
    composer install --prefer-dist;
@endtask

@task('update_permissions')
    cd {{ $release_dir }};
    chgrp -R www-data {{ $release }};
    chmod -R ug+rwx {{ $release }};
@endtask

@task('update_symlinks')
    ln -nfs {{ $release_dir }}/{{ $release }} {{ $app_dir }};
    chgrp -h www-data {{ $app_dir }};
@endtask

Run that and test it via:

envoy run deploy

A notable command is the creation of the symlink to make the new code files live. This is the ln command with the -nfs flags used:

  • -s - Create a symbolic link.
  • -f - Force the creation of the symlink even if a file, directory, or symlink already exists at that location (it will unlink, aka delete, the target directory!).
  • -n - If the target directory or file is a symlink, don't follow it. Often used with the -f flag.

After testing that, we add new route to app/Http/routes.php, thus changing the code to simulate a real development-deployment cycle.

Route::get('/test', function()
{
    Return Request::header();
});

Update the remote GitHub repository:

git add --all
git commit -m "added new test route"
git push origin master

Then we can re-run the deployment script and see if it worked:

envoy run deploy

Notes and Resources

Here is the Envoy Repository and Documentation

Create the Laravel application within Vagrant:

cd /vagrant
composer create-project laravel/laravel app

Nginx Config on Vagrant and production server (the only difference between dev and production is only the server_name directive value).

server {
    listen 80;

    root /vagrant/app/public;

    index index.html index.htm index.php app.php app_dev.php;

    # Make site accessible from ...
    server_name 192.168.22.45.xip.io vaprobash.dev;

    access_log /var/log/nginx/vagrant.com-access.log;
    error_log  /var/log/nginx/vagrant.com-error.log error;

    charset utf-8;

    location / {
        try_files $uri $uri/ /app.php?$query_string /index.php?$query_string;
    }

    location = /favicon.ico { log_not_found off; access_log off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    # pass the PHP scripts to php5-fpm
    # Note: .php$ is susceptible to file upload attacks
    # Consider using: "location ~ ^/(index|app|app_dev|config).php(/|$) {"
    location ~ .php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+.php)(/.+)$;
        # With php5-fpm:
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param LARA_ENV local; # Environment variable for Laravel
        fastcgi_param HTTPS off;
    }

    # Deny .htaccess file access
    location ~ /\.ht {
        deny all;
    }
}

All Topics