30.08.12 Continuous Deployment Part 1

Erstellen eines testbaren Produkts

Continuous Deployment bedeutet im Kern, jederzeit in der Lage zu sein, den derzeitigen Arbeitstand ausliefern zu können. Dazu muss die Software für automatische Tests in einer Form bereitgestellt werden, die möglichst nah an der Form ist, in der sie später auch genutzt werden soll. Und dazu gehört auch das Umfeld, in dem die Software später laufen soll. Je näher dieses Umfeld am Umfeld des Produktionseinsatzes ist, um so geringer sind die Risiken und Überraschungen bei der Inbetriebnahme.

Im Fall, dass die Software auf einem Server in der Cloud bei Amazon laufen soll, ist die nahe liegende Form, in der die Software inklusive Umfeld für Tests geliefert werden kann, ein Amazon Machine Image (AMI).

Automatisierung

Zum Erstellen solch eines AMIs geht man z.B. so vor, dass man einen Server mit einem 'vanilla' Linux startet, alle benötigte Software installiert, und den Stand der Maschine nach der Installation als AMI einfriert. Startet man mit dem endstandenen AMI einen neuen Server, so hat er den Stand des Servers, auf dem die installation durchgeführt wurde zum Zeitpunkt, als das image erstellt wurde.

Automatisches Erstellen von AMIs, mit Skripten, die wie Quellcode versioniert werden, führen zu einem nachvollziehbaren Umfeld, dass sich sicher in Details ändern lässt und bis auf wenige Details identisch zum Umfeld im Betrieb ist.

Hier wollte ich mir für ein aktuelles Projekt eine Basis schaffen und ein Ruby-Skript schreiben, das einen Server aufsetzt, auf dem sich Sioux bauen und testen lässt und auf dem anschließend noch die Applikation installiert wird. Als Ausgangspunkt dient ein Amazon-Linux-Image, auf dem dann ganz einfach via yum ein paar Tools, wie GCC, Git, Ruby, Rake etc. installiert werden. Dann noch eben Boost installiert, und schon können die sourcen von Sioux via git auf den Server 'gecloned' und die Software gebaut und getestet werden. Da ich soetwas noch nie gemacht hatte, habe ich mal ganz konservativ 3-8 Tage Aufwand geschätzt.

Viel zu große Schritte

Da die Installation von Boost, Ruby und Sioux, selbst wenn alles gut läuft, über eine Stunde dauert, hatte ich mir überlegt, die Installation auf zwei Skripte zu verteilen: Ein basic image, mit Komponenten, die sich nicht so häufig ändern, und darauf aufbauend ein application image, das auf dem basic image aufsetzt und dort die sich häufiger ändernde, eigentliche Applikation installiert. Zwei Skripte, die zwei unterschiedliche AMIs erstellen; in beiden AMIs werden die Versionen aller zugrunde liegenden Skripte vermerkt. Die Versionen der Skripte werden direkt über die Quellcodeverwaltung (Git) ermitelt.

Anfänglich kam ich so schnell vorran, dass ich Hoffnung hatte, eher am Drei-Tages-Ende, mit meinem Aufwand zu landen. Aber dann kamen sie doch noch, diese fiesen Kleinigkeiten, die einen Tage kosten. Das macht kein Spaß, und die gewonnenen Erkenntnisse sind meist nur von geringem Wert für spätere Projekte.

Das erste basic image lief am Ende über eine Stunde. Jedes Problem am Ende des Skripts ließ sich nur schwerr untersuchen, ständiges Auskommentieren von Teilen des Skripts und händisches Testen von Teil-Schritten führten immer wieder dazu, dass das Skript am Ende doch noch irgendwo eine Macke hatte und sich dies auch erst am Ende der Laufzeit zeigte. Als dann das zweite application image auf dem basic image aufsetzen sollte, stellte sich heraus, dass doch noch etwas fehlte, und jede kleine Änderung führte wieder zu stundenlangen Wartezeiten.

Am Ende war ich dann doch bei den 8 Tagen.

Viele kleine Schritte

Jedes Problem am Ende des langen Skript führt dazu, dass das gesamte korrigierte Skript wiederholt ausgeführt werden muss. Mit mehreren kleineren Skripten würde jedes erfolgreich ausgeführte Skript als Basis für das nächste Skript dienen. Tritt bei einem kleinen Skript ein Problem auf, so muss nur das kleine Skript untersucht, geändert und erneut ausgeführt werden. Ein Einfuss auf das Ergebnis vorheriger Skripte ist ja ausgeschlossen.

Das Prinzip ist einfach: Jedes Skript enthält ein paar Schritte, die zur gesamten Installation beitragen. Jedes Skript ist versioniert. Alle Skripte hängen direkt linear voneinander ab. Modeliert man die Abhängigkeiten mit einem build tool wie Rake (so etwas wie Make, MMS, BJam etc.), kann das build tool anhand der Versionen der Skripte und der vorhandenen AMIs selbst ermitteln, welche Skripte noch auszuführen sind.

Versionierung

Jedes Skript bassiert auf dem AMI, das von den vorgelagerten Skripten in deren Versionen erzeugt wird. Bei drei Skripten A, B und C, die alle in der Version 1.0 vorliegen, muss vor der Ausführung von A C laufen und vor C muss B laufen. C erzeugt ein AMI, in dem die aktuelle Version von C vermerkt wird. Im AMI von B wird die Version von B und C vermerkt und im Ergebnis von A schließlich die Versionen von A, B und C. Ändert sich im aktuell betrachteten repository z.B. die Version von B auf 1.1, so hängt A von B in der Version 1.1 und C in der Version 1.0 ab. Das AMI für C 1.0 ist vorhanden, B kann also auf diesem AMI aufbauen und ein AMI erzeugen, mit den Skriptversionen C 1.0 und B 1.1.

Jedes Skript baut auf dem Ergbnis des Skripts auf, dass den vorherigen Installationsschritt beschreibt. Mit jeder Änderung eines Skripts müssen alle folgenden Skripte neu ausgeführt werden. Mit zwei Ausnahmen: Das erste Skript baut auf einem fest konfiguriertem AMI auf (z.B. dem 'vanilla' Linux). Das zuletzt ausgeführte Skript (im Beispiel A), wird in der Regel die eigentliche Applikation installieren und hängt damit vom Skript selber und der Applikation ab.

Automatisierung mit Kiel

Mit dem Ruby Gem Kiel lässt sich das Erstellen von Rake tasks (ähnlich einem make targets) zur Automatisierung von Installationen automatisieren:

Kiel::image [ A:, B:, C: ], { base_image: 'ami-123123' } 

Der Aufruf der Funktion Kiel::image erzeugt drei Rake tasks: A, B und C. Jeder task lässt genau ein Skript von Capistrano ausführen. Capistrano ist ein Tool, mit dem sich Anweisungen auf mehreren Rechnern ausführen lassen. Kiel verwendet Capistrano, um einen einfachen Zugriff über SSH auf den zu installierenden Rechner zu bekommen. Der Name des Skripts leitet sich vom Namen des tasks ab (task A führt A.rb aus, tasks B führt B.rb aus usw.). Dies lässt sich bei Bedarf überschreiben. Das Skript wird aber nur ausgeführt, wenn das zu erzeugende AMI noch nicht vorhanden ist.

Die Versionen der Dateien B.rb, C.rb und des gesamten repositories werden über Git ermittelt. Über das Ruby AWS SDK greifen die tasks direkt auf die Amazon Web Services zu, starten so Server zur Installation, und erstellen AMIs von den installierten Servern.

In der Praxis

In der Praxis kommen dann doch noch ein paar nötige Details dazu, die das Ganze auf den ersten Blick etwas komplizierter aussehen lassen. Neben zwei Zeilen für die Konfiguration des AWS SDK und Capistrano enthält folgendes rakefile die Definition von 6 Rake tasks:

# rakefile
require 'kiel'
require 'kiel/setup/capistrano'
require 'kiel/cloud/aws'

namespace :images do

    # configure the access to Amazon Web Services
    aws = Kiel::Cloud::AWS.new(
        region: 'eu-west-1',
        credentials: YAML.load_file( File.open( File.expand_path('../../../keys/aws.yml', __FILE__ ) ) ),
        start_options: { key_name: 'sioux', security_groups: [ 'ssh' ] } )

    # configure the SSH user, private key, shell  etc.
    capistrano = Kiel::Setup::Capistrano.new(
        ssh_options: { user: 'ec2-user', keys: [ File.expand_path('../../../keys/sioux.pem', __FILE__) ] },
        default_run_options: { pty: true, shell: :bash } )

    # create 6 rake task that create Amazon Machine Images
    Kiel::image [
            { name: :application, description: 'build the application image' },
            { name: :base_image, description: 'build the base image' },
            :sioux, :boost, :ruby,
            { name: :basics, scm_name: [ 'basics.rb', 'rakefile' ], setup_name: 'basics.rb' }
        ],
        { 
            base_image: 'ami-6d555119', 
            root_dir: File.expand_path( '..', __FILE__ ),
            setup: capistrano,
            cloud: aws
        }
end

Die Namen der Rake tasks sind application, base_image, sioux, boost, ruby und basics. Alle diese tasks befinden sich im Rake namespace image, sodass sich der task zum Erstellen des gesammten Applikations-AMIs mit:

rake images:application

starten lässt. Die tasks application und base_image haben zusätzlich eine Beschreibung bekommen, die dazu dient, dass sie bei der Auflistung aller tasks eines rakefiles (rake -T) mit der angegebenen Beschreibung aufgeführt werden. Für den task basics wurde eine zusätzliche Abhängigkeit zum dargestellten rakefile beschrieben, die dazu führt, dass alle images neu gebaut werden, wenn sich das rakefile ändert.

Der Inhalt jedes von Capistrano ausgeführten Skripts beschränkt sich dann nur noch auf das Nötigste. Hier z.B. die komplette Definition des ersten Installations-Schritts:

# basics.rb
namespace :deploy do

    task :step do
        run "#{sudo} yum install --assumeyes git gcc-c++ autoconf automake make patch zlib-devel libtool bzip2-devel"
        run "#{sudo} yum update --assumeyes"
    end
end

Zusammenfassung

Das Schöne an der Lösung ist, dass sich neue Schritte ganz einfach einfügen lassen. Einzelne Schritte werden wieder klein, handlich und gut testbar. Das spart Zeit.

Selbst wenn die Zielplattform nicht die Cloud ist: Mit Kiel lassen sich images erstellen, die auf Knopfdruck auf Rechnern in einer Cloud installiert werden können (und die kann ja auch im eigenen Haus stehen). Die gleiche Applikation auf 10 unterschiedlichen Betriebssystemen installiert und bei Bedarf abrufbar und ausführbar? Da schlägt das Tester-Herz doch höher ;-)

Kiel ist Open Source und auf GitHub zu finden, verfügt über umfangreiche Tests und Dokumentation. Das Gem liegt öffentlich auf RubyGems und lässt sich einfach mit

gem install kiel

installieren.