• Ce blog — désormais archivé — est en lecture seule.
    Nouveau blog: William Durand.

Déploiement automatisé avec Capistrano et Git pour symfony et Diem

Hier, j’introduisais mes directives de travail. Aujourd’hui je vais introduire un sujet souvent sensible : la mise en production. C’est une tâche qui peut être critique mais si l’on prend toutes les précautions, nous limitons les risques. On notera bien que le facteur chance est laissé à la porte pour ce genre de manipulation…

La mise en production c’est passer un projet sur le serveur du client pour le rendre accessible aux utilisateurs finaux. Facile ? Non, très souvent les architectures sont différentes et il convient d’utiliser plusieurs environnements. Pour ma part, je dispose de 3 environnements : dev (sur ma machine), test (sur serveur distant) et préprod (sur serveur distant également). Ces environnements me permettent de tester mes projets sur plusieurs OS, plusieurs types de machine et sur deux SGBDR (bases de données) : j’ai pris l’habitude d’effectuer mes tests sous SQLite, j’ai ainsi  deux sons de cloche (en plus de MySQL) me permettant de corriger des étourderies lors de la configuration de la base.

Ok, tout ce speech pourquoi ? Pour pouvoir comprendre mon intérêt à automatiser le process de mise en prod’. En effet, mes trois environnements me garantissent une certaine stabilité de mon application, je n’ai donc pas à jongler avec les serveurs de chacun de mes clients (pas toujours…). Je peux donc déployer (mettre en prod’) mon application presque les yeux fermés. Et ça, ça n’a pas de prix !

Mais je ne joue pas le lanceur de couteaux sans avoir de lames bien aiguisées. J’utilise des outils éprouvés comme Capistrano et Git.

Capistrano est l’outil par excellence pour déployer des applis Rails. Le problème c’est que je ne m’y suis pas encore mis… Mais qu’importe, il déploiera mes applications symfony et Diem. La souplesse de configuration via redéfinition des méthodes permet de déployer à peu près ce que l’on souhaite. De plus, l’interconnexion avec Git se fait sans problème.

Maintenant, courte description :

  • Capistrano va cloner votre projet (Git inside) ;
  • Capistrano peut compresser votre projet (tar) et l’envoyer à vos X serveurs  ;
  • ou Capistrano va demander à X serveurs de récupérer la dernière version de votre projet ;
  • Capistrano va lier (symboliquement) la dernière version en tant que version courante ;
  • Capistrano va migrer les bases de données (la migration ici, ce n’est pas l’histoire avec les oiseaux… C’est plutôt la mise à jour structurelle de vos bases) ;
  • Capistrano va déployer votre appli très rapidement sur autant de serveurs que vous le souhaiter ;
  • Capistrano exécute des commandes avant ou après le déploiement, pratique pour un sf cc ;
  • Enfin, Capistrano ne fait pas le café (il ne faut pas abuser quand même…)

Intéressant non ? Une fois Capistrano installé (explications ici), on va pouvoir commencer à travailler.

Ce que je fais généralement, je crée un répertoire project_name et je tape la commande :

capify .

Ceci me crée un fichier Capfile, un répertoire config/ avec un fichier deploy.rb. C’est ce dernier qui nous intéresse. Le mieux est de partir d’un fichier vide. Pour pouvoir utiliser Git, il faut écrire ceci :

set :scm, "git"
set :repository,  "~/#{application}.git"
set :branch, "master"
set :checkout, "export"
set :deploy_via, :remote_cache

J’indique le chemin de mon dépôt, ici local et la branche (par défaut master). Les paramètres :checkout et :deploy_via ainsi défini permettent de faire un clone complet du projet. Ensuite, quelques paramètres nécessaires :

# Port
set :port, 9999
# App ?
set :application, "project_name"

# Where ?
set :deploy_to, "/var/www/#{application}"

# Which server ?
role :app, "localhost"
role :web, "localhost"
role :db, "localhost", :primary => true

# Who ?
set :user, "myUs3r"
set :password, "p4ssw0rd"

# More config
set :keep_releases, 3
ssh_options[:forward_agent] = true

# Path to php executable
set :php, "/usr/bin/php"

# Symfony application name (used for migrations)
set :sf_app, "front"
# Symfony web directory (www, web, public_html, ...)
set :sf_webdir, "www"

Maintenant, une petite chose à savoir. Capistrano versionne les déploiements et ça, c’est bon ! Mon déploiement plante, rollback automatique vers la version précédente. Je m’aperçois que j’ai fait une grosse faute d’orthographe, je lance un rollback et l’appli passe en version précédente. Pour cela, Capistrano construit sa propre structure dans le répertoire défini par :deploy_to :

/var/www/project_name
  `_ current (symlink)
   |_ releases
   |_ shared

current est le lien symbolique qui pointe toujours vers la version active de l’application. Le répertoire releases/ possède X répertoires correspondant aux différents déploiements passés. Vous l’aurez compris, le lien symbolique current pointe vers l’un de ces répertoires. Le répertoire shared/ quant à lui contient des fichiers partagés par toutes les releases. Exemple : le fichier databases.yml de symfony qui définit les identifiants de connexion à la base de données ou encore le répertoire uploads/ qui contient des données utilisateurs.

On peut désormais personnaliser les actions de déploiement. Pour cela, on va redéfinir les actions de :deploy, qui contient les tâches de déploiement appelées lors de tout déploiement.

namespace (:deploy) do

  desc < <-DESC
    [internal] Overriding original task to fit to symfony project needs
  DESC
  task :finalize_update, :except => { :no_release => true } do
    # Fix permissions
    run "cd #{latest_release} && find * -type f -exec chmod 644 {} \\;"
    run "cd #{latest_release} && find * -type d -exec chmod 705 {} \\;"
 
    run < <-CMD
      rm -rf #{latest_release}/log &&
      ln -s #{shared_path}/log #{latest_release}/log
    CMD

    run <<-CMD
      rm -rf #{latest_release}/cache &&
      ln -s #{shared_path}/cache #{latest_release}/cache
    CMD

    stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
    asset_paths = %w(images css js).map { |p| "#{latest_release}/#{sf_webdir}/#{p}" }.join(" ")
    run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }
  end
 
  desc < <-DESC
    Overriding original task to exclude restart
  DESC
  task :default do
    update
  end
 
  desc <<-DESC
    Overriding original task to use symfony migrations
  DESC
  task :migrations do
    update
    #sf.migrate
  end  
 
  after "deploy:update", 'deploy:customize'
 
  desc <<-DESC
    Custom tasks
  DESC
  task :customize do
    sf.symlinks
    sf.remove_dev_environments
    # clear cache
    sf.cc
    # Diem
    dm.cc
    dm.setup
  end
 
end

<

p style= »text-align: justify; »>

Après déploiement, on va créer fixer les droits, les répertoires de logs et de cache. Ensuite on met à jour la base de données (Doctrine migration). Pour finir on exécute une série de tâches « custom » qui permettent de supprimer les contrôleurs de développement (frontend_dev, backend_dev, …), de recréer les liens symboliques pour les fichiers partagés, de vider le cache symfony ou Diem, …

Voyons la suite de commandes pour symfony :

namespace (:sf) do
 
  desc < <-DESC
    Run the "doctrine migration" task
  DESC
  task :migrate do
    run "cd #{current_path} && #{php} symfony doctrine:generate-migrations-diff && #{php} symfony doctrine:migrate"    
  end

  desc <<-DESC
    Run the "symfony cc" task
  DESC
  task :cc do
    run "cd #{current_path} && #{php} symfony cc"
    run "cd #{current_path} && rm -rf cache/*"
  end

  desc <<-DESC
    Create symlink to symfony specific targets
  DESC
  task :symlinks do
    # symlink to database.yml
    run "rm -rf #{current_path}/config/databases.yml"
    run "ln -s #{shared_path}/databases.yml #{current_path}/config/databases.yml"
   
    # symlink to uploads
    run "rm -rf #{current_path}/#{sf_webdir}/uploads"
    run "ln -s #{shared_path}/uploads #{current_path}/#{sf_webdir}/uploads"
     
    # symlink to .htaccess
    run "rm -rf #{current_path}/#{sf_webdir}/.htaccess"
    run "ln -s #{shared_path}/.htaccess #{current_path}/#{sf_webdir}/.htaccess"

    # symlink to app.yml
    run "rm -rf #{current_path}/apps/#{sf_app}/config/app.yml"
    run "ln -s #{shared_path}/app.yml #{current_path}/apps/#{sf_app}/config/app.yml"
  end
 
  desc <<-DESC
    Remove DEV environments
  DESC
  task :remove_dev_environments do
    run "rm -rf #{current_path}/#{sf_webdir}/*dev.php"    
  end
 
  desc <<-DESC
    Disable symfony application
  DESC
  task :disable do
    run "cd #{current_path} && #{php} symfony project:disable #{sf_app} prod"    
  end
 
  desc <<-DESC
    Enable symfony application
  DESC
  task :enable do
    run "cd #{current_path} && #{php} symfony project:enable #{sf_app} prod"    
  end  
end

<

p style= »text-align: justify; »>Et voilà la suite de commandes pour Diem :

namespace (:dm) do

  desc < <-DESC
    Run the "Diem setup" task
  DESC
  task :setup do
    run "cd #{current_path} && #{php} symfony dm:setup"    
  end

  desc <<-DESC
    Clear Diem cache and fix permissions
  DESC
  task :cc do
    run "rm -rf #{current_path}/#{sf_webdir}/cache"
    run "mkdir #{current_path}/#{sf_webdir}/cache"
    run "chmod 777 #{current_path}/#{sf_webdir}/cache"    
  end
 
end

<

p style= »text-align: justify; »>Et voilà ! Un simple cap deploy fait tout le reste. Vous pouvez ajouter autant de serveurs que vous le souhaiter et lancer également toutes les commandes qui vous font plaisir.

Si vous avez des problèmes d’hébergement, exemple chez OVH qui ne laisse pas d’accès externe sur leurs serveurs Plan (…), vous pouvez utiliser la configuration suivante qui va archiver votre projet et l’envoyer en SFTP :

set :deploy_via, :copy

# Use copy to bypass firewall...
set :copy_strategy, :export
set :copy_cache, "/tmp/#{application}"
set :copy_exclude, [".git/*"]
set :copy_compression, :gzip

Cette configuration est largement inspirée de ces articles :

Pour terminer, hier je vous parlais d’Hudson, un serveur d’intégration continue. En liant les deux, j’obtiens une automatisation complète de mon « after code« . C’est-à-dire que j’écris mon code comme un barbu (et mes tests). Lorsque j’ai bien commité et que je push mes modifs, Hudson voit ces mises à jour, il va relancer les tests. Si tout passe la build estampillée « success » va être déployée automatiquement par Capistrano.

Et moi dans l’histoire ? Je n’ai rien à faire. Au pire, je suis alerté par mail si un test ne passe pas et si la build ne peut être déployée, Capistrano fera un rollback automatiquement sur sa dernière version stable. Manquerait plus qu’une cafetière USB et une tâche Hudson pour lancer le café et là… utopie !

A noter qu’un projet nommé Capifony a vu le jour depuis quelques temps. C’est un Capistrano réécrit pour gérer nativement les projets symfony, je n’ai pas testé mais cela me semble une excellente option : http://github.com/everzet/capifony. Pour Diem, j’ai vu passé quelque chose dans le Google Groups, à suivre donc.

Vous pouvez, si vous le voulez, utiliser Murder, un projet développé par Twitter pour déployer via Bittorent leurs applications sur tous leurs serveurs. C’est pas une mauvaise idée si vous avez beaucoup de serveurs, leurs temps de déploiements sont assez phénoménales. En parlant de temps, mes temps de déploiements sont de l’ordre de 2 minutes…

C’est tout, pour le moment :-)

  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • FriendFeed
  • LinkedIn
  • MySpace
  • Netvibes
  • PDF
  • Ping.fm
  • RSS
  • Technorati
  • viadeo FR
  • Wikio
  • Yahoo! Buzz
Publié dans Diem Project, symfony, Sysadmin | Mots-clefs : , , , , , | Commentaires fermés

Faut-il tester… les tests ?

Avec ce titre très accrocheur, vous allez surement penser que je vais vous parler de tests et vous avez raison… Mais de quels tests et pourquoi d’abord ?

Il y a un presque deux ans maintenant, j’apprenais à écrire des tests unitaires et fonctionnels en Java, à l’aide de JUnit, JMeter, et compagnie. J’ai appris à écrire des scénarios de tests avec un logiciel dont je ne me souviens plus du nom, une alternative gratuite à HP Test Director. On écrit des scénarios de tests, des gens suivent les instructions et fournissent le résultat qu’ils obtiennent. C’est bien gentil tout ça mais je n’ai pas 50 personnes sous la main pour les faire exécuter mes tests et ce n’est pas mon associé qui voudrait suivre mes procédures…

Comme mon boulot consiste à écrire uniquement des sites web, j’ai cherché un outil qui pourrait faire ce boulot à ma place. Il ne m’a pas fallu longtemps pour trouver Selenium, une suite d’outils permettant d’enregistrer un enchaînement d’actions dans le navigateur, de rejouer cet enchaînement autant qu’on le souhaite, sur plusieurs navigateurs, en parallèle (test de « scalabilité« ), …

C’est donc l’outil parfait pour gérer les scénarios fonctionnels. J’enregistre mes tests et je les rejoue. Je vérifie ainsi que mon appli réponds correctement et que je n’ai rien cassé en modifiant une partie du code. Exemple avec symfony, la mise à jour du schema.yml.

D’ailleurs avec symfony, on peut faire des tests fonctionnels et c’est une chose que je ne faisais pas. Pour être franc, je ne faisais pas trop de tests en PHP, enfin si mais pas formellement. Maintenant cette période est terminée. J’adopte une nouvelle approche, l’approche TDD que j’expliciterai plus tard. En symfony, on peut donc faire des tests fonctionnels avec les classes sfTestFunctional et sfBrowser (qui simule un navigateur). Pourquoi utiliser Selenium alors ? Et bien parce que ces tests sont bons mais ne remplacent pas Selenium, par exemple, on ne peut pas tester les appels Ajax. Voici un aperçu d’un test fonctionnel écrit en symfony :

/**
* Functional test file
*
* @author William DURAND
*/


include(dirname(__FILE__).'/../../bootstrap/functional.php');
include(dirname(__FILE__).'/../../bootstrap/doctrine.php');

$browser = new sfTestFunctional(new sfBrowser());

// Auto-generated
$test = $browser->test();
$conn = Doctrine::getConnectionByTableName('Commande');

$conn->beginTransaction();

// Get the form
$browser
  ->call('/commande/new', 'GET', array())
  ->with('request')->begin()
    ->isParameter('module', 'commande')
    ->isParameter('action', 'new')
  ->end()
  ->with('response')->begin()
    ->info('1.1 - Get the "Commande" form')
    ->isStatusCode(200)
    ->checkElement('#content > p', '#Nouvelle commande#')
  ->end()
  ;

C’est très intuitif, les tests sont formés de « blocs » commençant par with() et terminant par end(). Il y a déjà beaucoup d’articles sur le sujet donc je ne m’y étendrait pas. Par contre petit tuyau, pour aller plus vite j’utilise le plugin swFunctionalTestGenerationPlugin qui permet de générer un « stub » de test fonctionnel symfony directement depuis les actions que l’on effectue dans la navigateur, pratique !

Il n’y a pas que les tests fonctionnels dans la vie, il y a également les tests unitaires. Comme JUnit pour Java, on a le droit à PHPUnit pour PHP mais je ne l’ai pas encore essayé. J’utilise Lime, un framework de tests embarqué dans symfony, écrit par Maître @fapbot. Au lieu des classiques assertTrue(), assertFalse(), on dispose des méthodes is(), isnt(), … Comme pour les tests fonctionnels, l’écriture de tests unitaires est très intuitive.

Grâce à ces tests unitaires, mon code est contrôlé et je n’ai plus à me soucier d’éventuels effets de bord. Je sais exactement comment se comportent mes méthodes et mes objets. Je blinde ainsi mon modèle, ce qui fait une chose de moins à me soucier. Je passe 50% de mon temps à écrire du code dans mes actions et vues, 20% de mon temps à écrire du code dans mon modèle, 20% à écrire mes tests et le reste à boire du café… Je ne me soucis pas du reste, mon environnement de test est complètement découplé de mon environnement de dev et l’environnement de prod est distant.

Sauf que… lancer les tests c’est long et j’aime pas trop attendre. Je me suis tourné vers des outils qui font le boulot à ma place. C’est ce que l’on appelle des serveurs d’intégration continue. Je regrette de ne pas avoir pu tester Sismo, le serveur d’intégration continue (CI) de Sensio Labs (vous savez, le même que plus haut, décidément !).

J’ai testé Hudson et phpUnderControl (cruiseControl). Pour ce dernier, je n’en suis pas très satisfait avec symfony et Lime. Il fonctionne bien, l’utilisation de Ant fait plaisir mais je n’ai pas pu profiter de toutes les possibilités de ce serveur. Je l’ai donc laissé tombé au profit d’Hudson. C’est un serveur plaisant, rapide à prendre en main, très lisible et parfaitement customisable avec le nombre incommensurable de plugins dont il dispose. Pour savoir comment le faire fonctionner avec symfony, on lit cet article de Nicolas Perriault. Pour ma part, je n’utilise pas SVN mais Git, il suffit d’installer le plugin qui va bien.

Voilà, sachant que mon Gmail est tout le temps ouvert et que j’ai le checker qui va bien dans Chrome, je suis alerté assez rapidement par mail si je casse tout. Encore quelque chose de moins à me soucier. C’est ce qu’on appelle, je crois, l’efficience et la productivité s’en porte beaucoup mieux.

Il me reste cependant quelque chose à éclaircir : TDD. Test Driven Development ou Développement piloté par les tests. Pour faire simple, avant de développer une fonctionnalité, je réfléchis. Oui mais je réfléchis deux fois puis trois fois même. C’est-à-dire que je cherche comment l’écrire, comment elle pourrait mal se comporter et comment la tester. Ensuite, je prends le chemin inverse. J’écris mes tests, je veille à son comportement lors de son écriture. Et voilà, ma fonctionnalité est écrite, les tests passent, je continue mon travail. Je ne passe pas 30 minutes à chercher ce qui ne va pas…

Au fait, pourquoi cet intitulé d’article ? C’est une interrogation qui vient des cours de qualité logiciel que j’ai pu avoir auparavant. J’ai toujours gardé cette phrase à l’esprit lorsqu’on me parle de tests. La question n’est pas si simple qu’elle en a l’air. Si vous avez une idée, les commentaires sont fait pour cela. Moi, je garde mon idée ;)

En résumé, j’ai passé de bonnes vacances :-D

  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • FriendFeed
  • LinkedIn
  • MySpace
  • Netvibes
  • PDF
  • Ping.fm
  • RSS
  • Technorati
  • viadeo FR
  • Wikio
  • Yahoo! Buzz
Publié dans Boulot, Ma vie, symfony | Mots-clefs : , , , , , | Commentaires fermés

L’erreur « suexec policy violation » avec symfony (chez OVH…)

Bonjour, je laisse ici un petit rappel pour avoir lutté quelques heures avec une erreur 500, au début incompréhensible… Il s’agit de l’erreur :

suexec policy violation: see suexec log for more details

Cette erreur engendre une erreur 500. C’est un peu dommage… Il s’agit simplement d’un problème de droits sur les fichiers mais aussi sur les dossiers.

Les fichiers doivent posséder les droits 644 (-rw-r–r–) et les dossiers les droits 705 (drwx—r-x). Pour les fichiers, ces droits sont classiques, pour les dossiers on peut surement faire mieux mais là, ça marche, c’est testé. Pour faire ça vite :

find * -type f -exec chmod 644 {} \;
find * -type d -exec chmod 705 {} \;

Ensuite, on passe un coup de project:permissions et le tour est joué. On notera que la seule commande :

php symfony project:permissions

ne suffit pas à résoudre les problèmes de droits.

On fixera les droits du .htaccess à 604. Voilà :-)

  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • FriendFeed
  • LinkedIn
  • MySpace
  • Netvibes
  • PDF
  • Ping.fm
  • RSS
  • Technorati
  • viadeo FR
  • Wikio
  • Yahoo! Buzz
Publié dans Serveur, symfony | Mots-clefs : , | Commentaires fermés

Dédibox v3 : Nginx + PHP-FPM + Apache2 + APC + Memcached + MySQL

Bonjour :-)

J’ai récemment pris une Dédibox v3 et pour le moment j’en suis très satisfait. On m’a pas mal sollicité ces derniers temps pour que j’explique comment j’ai configuré ce serveur ainsi que les services que j’ai pu mettre en place. C’est l’objet de cet article.

Premièrement, j’utilise Debian depuis toujours et c’est logiquement ce qui tourne sur la Dédibox (en version 64 bits).

Le serveur HTTP frontal n’est pas Apache2 mais Nginx (wiki) que j’ai appréhendé seulement depuis l’obtention de ce serveur. Et je dois avouer que j’en suis très satisfait, terriblement surpris d’ailleurs. Apache2 est bien en place mais sert de proxy à Nginx.

Nginx est couplé à php-fpm. La paire formée est excellente et permet l’utilisation de Memcached ou APC sans soucis. Notons que l’excellent repository Dotdeb fournit un package .deb de php-fpm. Ce repository est certainement un must-have puisque Debian n’est pas forcément très en avance sur les versions de PHP. Pour ma part, j’ai installé, via ce repository, PHP 5.3.2 ainsi que la dernière version d’APC (3.1.2 via pecl). Comme je l’ai dit plus haut j’ai installé Memcached avec l’extension PHP qui va bien. Passons à la configuration.

Nginx

user www-data;
worker_processes 1;

error_log  /var/log/nginx/error.log;
pid        /var/run/nginx.pid;

events {
worker_connections 1024;
}

http {
include       /etc/nginx/mime.types;
default_type  application/octet-stream;

client_max_body_size 8M;

sendfile            on;
keepalive_timeout   15;
tcp_nodelay         on;

# Enable Gzip compression
gzip on;
gzip_disable "MSIE [1-6].(?!.*SV1)";
gzip_vary on;
gzip_comp_level 3;
gzip_proxied any;
gzip_types text/plain text/html text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_buffers 16 8k;

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

Le paramètre worker_processes dépend de votre processeur (multi-coeurs ou non). Dans le cas d’une Dédibox, on ne dispose que d’un processeur mono-coeur donc on fixe ce paramètre à 1. Ensuite j’active la compression gzip, ce qui permettra de gagner un peu en performances (moins de données à envoyer d’un coup = plus de données envoyées en même temps).

Nginx va servir tous les fichiers statiques, les « assets » mais également les pages en cache. Il s’interface très bien avec Memcached. Il est aussi capable de servir n’importe quel fichier sur disque.

Voici les deux autres fichiers de configuration de Nginx : proxy.conf pour configurer Apache2 en proxy et fastcgi_params pour php-fpm.

# /etc/nginx/proxy.conf
proxy_redirect                  off;
proxy_set_header                Host                    $host;
proxy_set_header                X-Real-IP               $remote_addr;
proxy_set_header                X-Forwarded-For         $proxy_add_x_forwarded_for;
client_max_body_size            10m;
client_body_buffer_size         128k;
proxy_connect_timeout           90;
proxy_send_timeout              90;
proxy_read_timeout              90;
proxy_buffer_size               4k;
proxy_buffers                   4 32k;
proxy_busy_buffers_size         64k;
proxy_temp_file_write_size      64k;
# /etc/nginx/factcgi_params
fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

# Custom settings
fastcgi_connect_timeout 60;
fastcgi_send_timeout 180;
fastcgi_read_timeout 180;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
fastcgi_intercept_errors on;

Pour utiliser Apache2 en proxy ou php-fpm, il suffit d’insérer quelques lignes dans vos configurations de vhosts. C’est suffisamment détaillé ailleurs, je n’y reviendrai donc pas.

APC

Voilà ma configuration APC, fonctionne bien pour Symfony. Pour certaines applis dont le code laisse à désirer, je suggère de passer le paramètre include_once_override à 0. Cette config provient en grande partie de chez Romain Cambien.

[apc]
apc.enabled=1

; 1 segments of 256Mo
apc.shm_segments=1
apc.shm_size=256

; No optimization
apc.optimization=0

; Never expire
apc.ttl=0
apc.user_ttl=0

; workaround for CLI
apc.enable_cli=1

; Symfony make lot of path lookups, try to optimize
apc.include_once_override=1
apc.canonicalize=1

; Wait 2 seconds to rebuild cache
apc.file_update_protection=2

PHP

La configuration de PHP est celle par défaut aux best practices près (short_tags off, magic quote gpc off, …). J’ai surtout calqué sur les « requirements » de Diem. A noter que l’on gère 3 fichiers php.ini : pour Apache2, pour php-fpm et pour cli.

Memcached

Aucun besoin de modifier la configuration par défaut. Par contre, on peut modifier la taille (64Mo par défaut) si besoin.

Apache2

Pour utiliser Apache2 en proxy, on doit nécessairement lui attribuer un port différent du port par défaut (80). Pour cela, on modifie le fichier /etc/apache2/ports.conf :

# If you just change the port or add more ports here, you will likely also
# have to change the VirtualHost statement in
# /etc/apache2/sites-enabled/000-default
# This is also true if you have upgraded from before 2.2.9-3 (i.e. from
# Debian etch). See /usr/share/doc/apache2.2-common/NEWS.Debian.gz and
# README.Debian.gz

Listen 9999
NameVirtualHost *:9999
# SSL name based virtual hosts are not yet supported, therefore no
# NameVirtualHost statement here
Listen 443

J’ai choisi le prefork MPM d’Apache2, la directive KeepAlive est désactivé car Apache2 ne sert que du PHP. Après quelques tests, voici la configuration retenue :

# Timeout: The number of seconds before receives and sends time out.
Timeout 10

# KeepAlive: Whether or not to allow persistent connections (more than
# one request per connection). Set to "Off" to deactivate.
KeepAlive Off

# MaxKeepAliveRequests: The maximum number of requests to allow
# during a persistent connection. Set to 0 to allow an unlimited amount.
# We recommend you leave this number high, for maximum performance.
MaxKeepAliveRequests 100

# KeepAliveTimeout: Number of seconds to wait for the next request from the
# same client on the same connection.
KeepAliveTimeout 2

# prefork MPM
# StartServers: number of server processes to start
# MinSpareServers: minimum number of server processes which are kept spare
# MaxSpareServers: maximum number of server processes which are kept spare
# MaxClients: maximum number of server processes allowed to start
# MaxRequestsPerChild: maximum number of requests a server process serves
StartServers          8
MinSpareServers       5
MaxSpareServers      15
MaxClients           80
MaxRequestsPerChild   0

MySQL

En SGBD, j’ai installé MySQL et PostgreSQL qui ne sert pas pour le moment. La configuration MySQL est d’origine à l’exception d’un Ramdisk pour le répertoire temporaire. L’explication pour la création d’un Ramdisk est expliqué ici : http://www.willdurand.fr/optimiser-son-application-web-en-jouant-sur-php-mysql-et-apache2/. MySQL est également protégé par une couche Memcached, ce qui permet de faire tenir une charge correcte sans envoyer MySQL dans les choux.

Postfix

Configuré en « Site Internet », rien n’a été touché et les mails transitent bien. Je n’ai pas besoin de faire de gestion de mail autre.

Getmail

Dernière petite chose qui n’avait pas été relevé dans l’article précédent : http://www.willdurand.fr/sauvegarde-incrementale-avec-rsync-fichiers-et-base-de-donnees-mysql/, la sauvegarde de mes emails. Tout est dit ici. J’envoie toutes mes sauvegardes sur un autre serveur privé et inaccessible excepté le temps du transfert.

Voilà.

  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • FriendFeed
  • LinkedIn
  • MySpace
  • Netvibes
  • PDF
  • Ping.fm
  • RSS
  • Technorati
  • viadeo FR
  • Wikio
  • Yahoo! Buzz
Publié dans Serveur, Sysadmin | Mots-clefs : , , , , , , , , | Commentaires fermés

Sauvegarde incrémentale avec rsync : fichiers et base de données MySQL

Bonjour, suite à mon passage en dédié (Dédibox v3) et à sa configuration aujourd’hui terminée, j’ai trouvé nécessaire de m’occuper de la sauvegarde des données. Pour cela, je me suis aidé de plusieurs sources :

Sauvegarde des bases MySQL

Je sauvegarde toutes les bases de données sur une durée d’une semaine. Voici le script, il est possible de le coupler à un envoi de la sauvegarde via FTP. Je conserve ainsi toutes mes sauvegardes sur un serveur distant et complètement déconnecté de ce dont je vais vous parler ensuite.

#!/bin/bash

#---------------------------------------------------------------#
# Paramétrage de la connection MySQL                            #
#---------------------------------------------------------------#

#Nom de l'utilisateur qui lance le backup
user=<user>
#Machine sur laquelle on se connecte
host=<host>
#Mot de passe de l'utilisateur de backup
pass=<password>

# Outil de dump
MYSQLDUMP=mysqldump
#Outil de check
MYSQLCHECK=mysqlcheck
# Options passées à MYSQLDUMP
OPTIONS="--add-drop-database  --add-drop-table --complete-insert --routines --triggers --max_allowed_packet=250M --force"

#---------------------------------------------------------------#
# Paramétrage de la sauvegarde                                  #
#---------------------------------------------------------------#

# Répertoire temporaire pour stocker les backups
TEMPORAIRE="/tmp/bkp_mysql"

# Nom du serveur
MACHINE="$(hostname)"

# Date courante
DATE="$(date +"%d-%m-%Y")"
# Date de conservation maximum
DATE2=`date --date '1 week ago' "+%d-%m-%Y"`

# Nom des fichiers de backup
# Répertoire de destination du backup
DESTINATION="/home/backuper/mysql"
FICHIER_BACKUP=$MACHINE"_BACKUP_MYSQL_"$DATE".tar.gz"
FICHIER_EFFACER=$MACHINE"_BACKUP_MYSQL_"$DATE2".tar.gz"

#Informations FTP
LOGIN_FTP=<user_ftp>
PASS_FTP=<pass_ftp>
HOST_FTP=<host_ftp>

#---------------------------------------------------------------#
# Process de sauvegarde                                         #
#---------------------------------------------------------------#

# Création du répertoire temporaire
if [ -d $TEMPORAIRE ];
then
  echo "Le repertoire temporaire existe.";
else
  mkdir $TEMPORAIRE;
fi

# On construit la liste des bases de données
BASES="$(mysql -u $user -h $host -p$pass -Bse 'show databases')"

# On lance le dump des bases
for db in $BASES
do
  #On lance un check et une analyse pour chaque base de données
  $MYSQLCHECK -u $user -h $host -p$pass -c -a $db
  # On lance un mysqldump pour chaque base de données
  $MYSQLDUMP -u $user -h $host -p$pass $OPTIONS $db -R > $TEMPORAIRE"/"$MACHINE"-"$db"-"$DATE".sql";
done

# Création de l'archive contenant tout les dump
#Cette archive est stockée dans le dossier défini pour la sauvegarde
if [ -d $DESTINATION ];
then
    cd $TEMPORAIRE
    tar -cvzf $DESTINATION"/"$FICHIER_BACKUP *
fi

# On transfere l'archive par FTP
cd $DESTINATION
ftp -v -n $HOST_FTP < <SCRIPT
user $LOGIN_FTP $PASS_FTP
bin
put $FICHIER_BACKUP
bye
SCRIPT

# On supprime le fichier de plus de x jours
if [ -f $DESTINATION"/"$FICHIER_EFFACER ]; then
    rm $DESTINATION"/"$FICHIER_EFFACER
fi

# On suprime le répertoire temporaire
if [ -d fichier ]; then
  rm -Rf $TEMPORAIRE
fi

Sauvegarde incrémentale des fichiers avec rsync

Je sauvegarde mes fichiers à la semaine de la façon suivante :

  • sauvegarde dans un répertoire nommé : <Année>-<Numéro de semaine>
  • dans ce répertoire j’ai un dossier main qui contient l’intégralité des fichiers. Cette sauvegarde est faite le lundi.
  • j’ai 6 répertoires nommés : <jour>_<date au format jj-mm-aaaa> contenant les sauvegardes incrémentales de la semaine.

J’ai modifié le script pour ajouter une conservation des sauvegardes de 4 semaines.

#!/bin/bash

# - By default, the full backup is made monday, you can specify another day
#   by entering a number from 1 to 7 (1 is monday, 2 is tuesday ...)
#FULL_DAY=1

# - BACKUP_LIST is a text file where you specify files/directories you
#   want to backup (one per line), the paths on this file must be relative
#   to the path set in BACKUP_ROOT
BACKUP_ROOT=/
BACKUP_LIST="/home/backuper/scripts/backup_list_files"

# Excludes file
# - Contains one wildcard pattern per line of files to exclude
# - This is a rsync exclude file. See the rsync man page.  
EXCLUDES_LIST="/home/backuper/scripts/backup_exclude_files"

# Hostname or IP adress of the remote backup server
BACKUP_SRV=

# Port SSH
SSH_PORT=

# Root directory that stores backups on the remote server
ARCHIVE_ROOT=~/files

# Remote user. Used by ssh. This is the user who connects to the remote backup server
# Must have write access on $ARCHIVE_ROOT
REMOTE_USER=

# Mail address for status updates
#  - This is used to email you a status report
#  - Comment it if you don't want to mail the report
MAILADDR=

# Log file
LOGFILE="/home/backuper/backup-files.log"

# HOSTNAME
#  This is used for creating a directory specific to this host on the remote host
HOSTNAME="$(hostname)"

#########################################
# From here on out, you probably don't  #
#   want to change anything unless you  #
#   know what you're doing.             #
#########################################

if [ -f $LOGFILE ]
then
  rm $LOGFILE
  touch $LOGFILE
fi

# Calculate WEEK_NUM
# - This is the week number of year, it depends on the variable FULL_DAY
#  the value of FULL_DAY is considered as the first day of week
# - By default the command `date +%W` considers monday as first day of week, so if the day of
#  the full backup is different from monday then we must make additional operations
if [ $FULL_DAY ] && [ $FULL_DAY -le 7 ] && [ `date +%u` -ge $FULL_DAY ] ; then
  WEEK_NUM=`expr $(date +%W) + 1`
else
  WEEK_NUM=`date +%W`
fi

# ARCHIVE_DIR
#  This is the directory (full path) of the current week backups (on the remote host)
#  The name of directory indicates the number of the week in the year (e.g 2007-32)
ARCHIVE_DIR="$ARCHIVE_ROOT/$HOSTNAME/`date +%Y`-$WEEK_NUM"

# Directory which holds our current datastore
CURRENT=main

# Directory which we save incremental changes to (e.g monday_06-08-2007)
INCREMENTDIR=`date +%A_%d-%m-%Y`

# Options to pass to rsync
# -r for recursivity must be explicite in spite of -a because --files-from (see man rsync)
OPTIONS="--force --ignore-errors --delete --delete-excluded \
--exclude-from=$EXCLUDES_LIST --backup --backup-dir=$ARCHIVE_DIR/$INCREMENTDIR \
-arhv --files-from=$BACKUP_LIST"


# Our actual rsyncing function
do_rsync()
{
   if [ $SSH_PORT ] ; then
    rsync -e "ssh -p $SSH_PORT" $OPTIONS $BACKUP_ROOT $REMOTE_USER@$BACKUP_SRV:$ARCHIVE_DIR/$CURRENT >> $LOGFILE 2>&1
   else
    rsync $OPTIONS $BACKUP_ROOT $REMOTE_USER@$BACKUP_SRV:$ARCHIVE_DIR/$CURRENT >> $LOGFILE 2>&1
   fi
   return
}

# Our post rsync accounting function
do_accounting()
{
  if [ $1 ] ; then
    echo "Backup accounting for day $INCREMENTDIR on $HOSTNAME:" > /tmp/rsync_script_tmpfile
    echo >> /tmp/rsync_script_tmpfile

    if [ $SSH_PORT ] ; then
      ssh -p $SSH_PORT $REMOTE_USER@$BACKUP_SRV du -s $ARCHIVE_DIR/* >> /tmp/rsync_script_tmpfile
    else
      ssh $REMOTE_USER@$BACKUP_SRV du -s $ARCHIVE_DIR/* >> /tmp/rsync_script_tmpfile
    fi

    echo >> /tmp/rsync_script_tmpfile
    cat $LOGFILE | grep "rsync: " >> /tmp/rsync_script_tmpfile

    echo "Mail $1 -s "$HOSTNAME Backup Report" < /tmp/rsync_script_tmpfile"
    mail $1 -s "$HOSTNAME Backup Report" < /tmp/rsync_script_tmpfile

    echo "rm /tmp/rsync_script_tmpfile"
    rm /tmp/rsync_script_tmpfile
  fi
}

# Clean old backups
do_clean()
{
    # Nb weeks
    NB_WEEKS_MAX=4
    LIMIT=`expr $WEEK_NUM - $NB_WEEKS_MAX`

    if [ $LIMIT -le 0 ]
    then
        NB=`expr $NB_WEEKS_MAX - $WEEK_NUM`
        NB=`expr 52 - $NB`
        LAST_YEAR=`expr $(date +%Y) - 1`
        TO_DELETE_DIR="$ARCHIVE_ROOT/$HOSTNAME/$LAST_YEAR-$NB"

        if [ -d $TO_DELETE_DIR ] ; then
          echo "rm -rf $TO_DELETE_DIR"  
    rm -rf $TO_DELETE_DIR
        fi
    else
        TO_DELETE_DIR="$ARCHIVE_ROOT/$HOSTNAME/`date +%Y`-$LIMIT"

        if [ -d $TO_DELETE_DIR ] ; then
          echo "rm -rf $TO_DELETE_DIR"  
    rm -rf $TO_DELETE_DIR
        fi
    fi
}

# Some error handling and/or run our backup and accounting
if [ $BACKUP_LIST ]; then
  if [ $EXCLUDES_LIST ]; then
    if [ -f $BACKUP_LIST ]; then
      if [ -f $EXCLUDES_LIST ] ; then
        # make sure our backup tree exists
        if [ $SSH_PORT ] ; then
    ssh -p $SSH_PORT $REMOTE_USER@$BACKUP_SRV install -d $ARCHIVE_DIR/$CURRENT
  else
    ssh $REMOTE_USER@$BACKUP_SRV install -d $ARCHIVE_DIR/$CURRENT
  fi
        echo >> $LOGFILE
  echo "=========================================================" >> $LOGFILE
        echo "`date` : Backup started." >> $LOGFILE
        do_rsync
  if [ $? -eq 0 ] ; then
    echo "OK. Backup succeeded" | tee -a $LOGFILE
          do_clean
    do_accounting $MAILADDR
    exit 0
  else
    echo "Backup failed. See $LOGFILE for debug info." | tee -a $LOGFILE
    do_accounting $MAILADDR
    exit 1
  fi
      else
        echo "$EXCLUDES_LIST: File not found or not a regular file. Backup aborted" | tee -a $LOGFILE;
  exit 1
      fi
    else
      echo "$BACKUP_LIST: File not found or not a regular file. Backup aborted" | tee -a $LOGFILE
      exit 1
    fi
  else
    echo "You must edit the script file and set the variable EXCLUDES_LIST" | tee -a $LOGFILE
    exit 1
  fi
else
  echo "You must edit the script file and set the variable BACKUP_LIST" | tee -a $LOGFILE
  exit 1
fi

exit 2

Je sauvegarde ces fichiers :

var/www
var/log
etc/apache2
etc/apt/sources.list
etc/fstab
etc/crontab
etc/hostname
etc/hosts
etc/group
etc/memcached.conf
etc/mysql
etc/nginx
etc/php5
etc/phpmyadmin
etc/postfix
etc/ssh
etc/fail2ban
home

Et j’exclue ceux-là :

home/backuper/backup-files.log
home/backuper/files
etc/mysql/debian.cnf
etc/ssh/ssh_host*
etc/phpmyadmin/htpasswd.setup
.*_history
.Xauthority
.lesshst
.ssh

Automatisation

J’ai rajouté dans mon crontab, deux lignes pour lancer les sauvegardes différées de 2h :

0 1 * * * /home/backuper/scripts/backup_mysql.sh
0 3 * * * /home/backuper/scripts/backup_files.sh

  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • FriendFeed
  • LinkedIn
  • MySpace
  • Netvibes
  • PDF
  • Ping.fm
  • RSS
  • Technorati
  • viadeo FR
  • Wikio
  • Yahoo! Buzz
Publié dans Serveur, Sysadmin | Mots-clefs : , , , | Commentaires fermés