Deploy via Git

Posted 13.12.2009 um 14:04 von badboy_ - 0 Kommentare

Wie ich bereits hier schrieb, wollte ich das Deployment, also das Übertragen aller Daten meines Blogsystems optimieren. Orientiert habe ich mich dabei vor allem am Setup von Github.

Der Sinn dahinter:

Zuvor hat Capistrano das Handling von verschiedenen Versionen des Codes übernommen und dafür Dateien hin- und herkopiert und Symlinks angelegt.

Da der Code aber sowieso schon in einem git-Repository liegt, kann auch gleich git die Aufgabe der Versionierung übernehmen, eben dafür wurde es ja entwickelt.

Nun aber wie ich das ganze gemacht habe:

$ steht für die Shell des Clients, % für die Shell des Servers

Zuerst mal musste das Repository geklont werden, das hab ich mittels

$ git clone --bare ruby/rails/blog blog.git

auf meinem Laptop erledigt. Als nächstes musste das Repo natürlich auch auf den Server:

$ scp -r blog.git server:/var/git

Auf dem Server habe ich dann eine Gruppe und einen User speziell für git eingerichtet:

% groupadd git
% useradd -g git -s /usr/bin/git-shell -m git

Das letzte Kommando legt den User git an, erstellt das Homeverzeichnis (/home/git) und setzt die Usershell auf git-shell.

Nun muss noch das Verzeichnis für die git-Repos dem richtigen User zugeordnet werden:

% chown -r git:git /var/git

Nun noch seinen SSH-Key hochladen:

$ scp .ssh/id_rsa.pub server:

und auf dem Server in die richtige Datei schreiben:

% mkdir /home/git/.ssh
% cat id_rsa.pub >> /home/git/.ssh/authorized_keys

Nun sollte es möglich sein, das git-Repo zu pushen:

$ cd ruby/rails/blog
$ git remote add origin git@server:/var/git/blog.git
$ git push origin master

Wenn das geklappt hat, ist der größte Teil schon fertig.

Um nun auch die Daten für den Webserver mittels git up-to-date halten zu können, habe ich folgendes gemacht:

% cd /var/www/
% mkdir blog/current blog/cached

Mein apache liefert mittels Passenger die Daten in /var/www/blog/current/ aus (dort liegt meine Rails-App).

Noch ist aber da nichts drin. Also ersteinmal wieder das Repository klonen:

% cd /var/www/blog
% git clone -l /var/git/blog.git current

Der Serverteil ist nun erledigt. Apache sollte nun wieder alles wie gewohnt ausliefern.

Nun zum eigentlichen Deploy-Skript für Capistrano (nur die wichtigsten Teile):

# externe Datei nachladen
load 'config/deploy/symlinks'

# der Ordner, in dem der Blog liegt
set :deploy_to, "var/www/blog"
# Das Repository, nicht weiter verwendet
set :repository, "file:///var/git/blog.git"
# Der Branch, aus dem der Code genommen wird
set :branch, "origin/next"

namespace :deploy do
  desc "deploy the project"
  task :default do
    update
    restart
  end

  desc "update the deployed code."
  task :update_code, :except => { :no_release => true } do
    run "cd #{current_path}; git fetch origin; git reset --hard #{branch}"
  end

  namespace :rollback do
    desc "Rollback a single commit."
    task :code, :except => { :no_release => true } do
      set :branch, "HEAD^"
      deploy.default
    end

    task :default do
      rollback.code
    end
  end

  desc "Tell Passenger to restart the app."
  task :restart do
    run "touch #{current_path}/tmp/restart.txt"
  end
end

Der wichtiges Teil ist der nach "task :update_code":

Hier werden mittels "git fetch origin" erst alle Änderungen eingelesen und dann mittels "git reset --hard origin/next" alle Daten in dem aktuellen Verzeichnis zurückgesetzt.

Das ist schnell und einfach.

Was noch fehlt sind die Daten, die nicht in dem Repo liegen, so z.B. meine database.yml, die nicht versioniert wird.

Zuerst muss die auch auf dem Server landen:

# Erstellen der Ordner:
% mkdir -p /var/www/blog/shared/config
% mkdir -p /var/www/blog/shared/log
$ scp database.yml server:/var/www/blog/shared/config

Und nun kommt die config/deploy/symlinks.rb ins Spiel, die sieht so aus:

set :normal_symlinks, %w(
    config/database.yml
    log
)

set :weird_symlinks, {}

namespace :symlinks do
  desc "Make all the damn symlinks"
  task :make, :roles => :app, :except => { :no_release => true } do
    commands = normal_symlinks.map do |path|
      "rm -rf #{current_path}/#{path} && \
ln -s #{shared_path}/#{path} #{current_path}/#{path}"
    end

    commands += weird_symlinks.map do |from, to|
      "rm -rf #{current_path}/#{to} && \
ln -s #{shared_path}/#{from} #{current_path}/#{to}"
    end

    # needed for some of the symlinks
    run "mkdir -p #{current_path}/tmp"

    run (["cd #{current_path}"]+commands).join(" && ")
  end
end

after "deploy:update_code", "symlinks:make"

Das Skript ist nahezu eins zu eins von Github übernommen, nur das release_path habe ich überall durch current_path ersetzt. Capistrano versucht sonst nämlich einen Pfad wie blog/releases/YYYYMMDD zu benutzen, den es aber bei mir nicht mehr gibt.

Zu guter letzt reicht nun folgendes um den Code auf dem Server aktuell zu halten:

$ cd ruby/rails/blog
$ git checkout next
$ git add .
$ git commit -m 'update'
$ git push origin next
$ cap deploy

Bei mir dauert ein Update so nur noch knappe 2 Sekunden, was wirklich sehr schnell ist.

Und noch ein kleinen Trick habe ich von Github übernommen:

Rails hängt an alle "Assets", also alle Javascript-, Stylesheet- und auch Bilddateien eine ID an, damit Browser diese Daten cachen und eben nur neu laden müssen, wenn sie wirklich verändert wurden, sich also der Timestamp der Datei geändert hat.

Nun habe ich analog zu dem Weg von Github einfach folgendes in meiner config/environments/production.rb stehen:

repo = Grit::Repo.new(RAILS_ROOT)
js = repo.log('next', 'public/javascripts', :max_count => 1).first
css = repo.log('next', 'public/stylesheets', :max_count => 1).first

ENV['RAILS_ASSET_ID'] = js.committed_date > css.committed_date ? js.id : css.id

Nun wird nicht mehr das Datum des letzten Zugriffs auf die Datei verwendet, sondern einfach der SHA1-Hash des letzten Commits, der an den Dateien etwas geändert hat. Ebenfalls simpel und einfach.

Das ganze klappt bislang prima. Einziges Problem: die deploy.rb ist nicht komplett portabel. Das erstmalige Klonen habe ich noch selbst gemacht. Im Skript von Github gibt es dafür einen eigenen Task:

desc "Setup a GitHub-style deployment."
task :setup, :except => { :no_release => true } do
  run "git clone #{repository} #{current_path}"
end

ein

$ cap deploy:setup

sollte nun auch diesen ersten Schritt erledigen.

0 Kommentare:

  1. Was ist 6 plus 6?