Deploying with Idephix

Deploying a PHP application can be done in many ways, this recipe shows you our best strategy for a generic PHP application, and it is composed of several steps:

  • Preparing the local project
  • Preparing the remote server
  • Syncing the project to the server
  • Linking shared files across releases (configuration files, cache, logs, etc)
  • Switching the symlink for the new release, finalizing the deploy

The recipe organize your code on the server using a directory hierarchy borrowed from Capistrano:

├── current -> /var/www/my_app_name/releases/20150120114500/
├── releases
│   ├── 20150080072500
│   ├── 20150090083000
│   ├── 20150100093500
│   ├── 20150110104000
│   └── 20150120114500
└── shared
    └── <linked_files and linked_dirs>

So you can keep multiple releases on the server and switch the current release just creating a symlink to the actual one you want to make current. This allows you to easily rollback from one release to another.

idxfile.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php

function deploy(Idephix\Context $context, $go = false)
{
    $sharedFiles = $context->get('deploy.shared_files', array());
    $sharedFolders = $context->get('deploy.shared_folders', array());
    $remoteBaseDir = $context->get('deploy.remote_base_dir');
    $rsyncExclude = $context->get('deploy.rsync_exclude');
    $repository = $context->get('deploy.repository');
    $deployBranch = $context->get('deploy.branch');
    $nextRelease = "$remoteBaseDir/releases/" . time();
    $linkedRelease = "$remoteBaseDir/current";
    $localArtifact = '.deploy';
    $context->prepareArtifact($localArtifact, $repository, $deployBranch, $go);
    $context->prepareSharedFilesAndFolders($remoteBaseDir, $sharedFolders, $sharedFiles, $go);
    try {
        $context->remote("cd $remoteBaseDir && cp -pPR `readlink {$linkedRelease}` $nextRelease");
    } catch (\Exception $e) {
        $context->output()->writeln('<info>First deploy, sending the whole project</info>');
    }
    $dryRun = $go ? '' : '--dry-run';
    $context->rsyncProject($nextRelease, $localArtifact . '/', $rsyncExclude, $dryRun, $go);
    $context->linkSharedFilesAndFolders($sharedFiles, $sharedFolders, $nextRelease, $remoteBaseDir, $go);
    $context->switchToNextRelease($remoteBaseDir, $nextRelease, $go);
}

function prepareArtifact(Idephix\Context $context, $localArtifact, $repository, $deployBranch, $go = false)
{
    $context->local(
        "
        rm -Rf {$localArtifact} && \\
        git clone {$repository} {$localArtifact} && \\
        cd {$localArtifact} && \\
        git fetch && \\
        git checkout --force {$deployBranch} && \\
        composer install --no-dev --prefer-dist --no-progress --optimize-autoloader --no-interaction
        ",
        !$go
    );
}

function prepareSharedFilesAndFolders(Idephix\Context $context, $remoteBaseDir, $sharedFolders, $sharedFiles, $go = false)
{
    $context->remote(
        "mkdir -p {$remoteBaseDir}/releases && \\
         mkdir -p {$remoteBaseDir}/shared",
        !$go
    );
    foreach ($sharedFolders as $folder) {
        $context->remote("mkdir -p {$remoteBaseDir}/shared/{$folder}", !$go);
    }
    foreach ($sharedFiles as $file) {
        $sharedFile = "{$remoteBaseDir}/shared/{$file}";
        $context->remote("mkdir -p `dirname '{$sharedFile}'` && touch \"$sharedFile\"", !$go);
    }
}
function linkSharedFilesAndFolders(Idephix\Context $context, $sharedFiles, $sharedFolders, $nextRelease, $remoteBaseDir, $go = false)
{
    foreach (array_merge($sharedFiles, $sharedFolders) as $item) {
        $context->remote("rm -r $nextRelease/$item", !$go);
        $context->remote("ln -nfs $remoteBaseDir/shared/$item $nextRelease/$item", !$go);
    }
}

function switchToNextRelease(Idephix\Context $context, $remoteBaseDir, $nextRelease, $go = false)
{
    $context->remote(
        "
        cd $remoteBaseDir && \\
        ln -nfs $nextRelease current",
        !$go
    );
}

These tasks are based on several configuration options that you can define in your idxrc.php file:

idxrc.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

$environments = array(
    'prod' => array(
        'hosts' => array('127.0.0.1'),
        'ssh_params' => array(
            'user' => 'ideato',
//            'password'             => '',
//            'public_key_file'      => '',
//            'private_key_file'     => '',
//            'private_key_file_pwd' => '',
//            'ssh_port'             => '22'
        ),
        'deploy' => array(
            'repository' => './',
            'branch' => 'origin/master',
            'shared_files' => array('app/config/parameters.yml'),
            'shared_folders' => array('app/cache', 'app/logs'),
            'remote_base_dir' => '/var/www/testidx',
            'rsync_exclude' => './rsync_exclude.txt',
        )
    ),
);

return
    array(
        'envs' => $environments,
        'ssh_client' => new \Idephix\SSH\SshClient(),
        'extensions' => array(
            'rsync' => new \Idephix\Extension\Project\Rsync(),
        ),
    );