Astrails "safe" kann jetzt auch PostgreSQL 0

Posted by fwoeck
on Saturday, June 06

Das safe-gem der Firma Astrails führt regelmäßige revisionierte Backups von Ordnern und Datenbanken durch. Es bietet einem dabei eine kompakte DSL und kann die Daten wahlweise verschlüsseln und auf S3-Buckets duplizieren.

Kürzlich ist der Support für PostgreSQL-Datenbanken dazugekommen und damit eignet sich die Sache vielleicht für unser Firmenwiki. Ich habe als ersten Test meinen EC2-Host mit diesem Blog aufgetakelt, den ich bisher nur mit manuellen tars und mysqldumps gesichert hatte.

die Installation

Man nimmt direkt die github-Version und erzeugt eine default-Konfiguration:

# gem install astrails-safe --source http://gems.github.com/
Successfully installed astrails-safe-0.1.9
1 gem installed
Installing ri documentation for astrails-safe-0.1.9...
Installing RDoc documentation for astrails-safe-0.1.9...

# astrails-safe /root/my-backup.conf
ERROR: Created default /root/my-backup.conf. Please edit and run again.

Konfiguration

Es folgt eine Beispielkonfiguration in Ausschnitten – das default-File ist gut kommentiert:

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
safe do

  local do
    path "/mnt/backup/:kind"
  end

  s3 do
    key "my-Amazon-Key-here"
    secret "my-secret-Amazon-secret-here"
    bucket "the-bucket-name"
    path "backups/:kind/" # this is default
  end

  gpg do
    key "fw@bm.net"
  end

  keep do
    local 4 # keep 4 local backups
    s3 20 # keep 20 S3 backups
  end

  mysqldump do
    options "-ceKq --single-transaction --create-options"

    user "root"
    password "p4ssw0rd"
    socket "/var/run/mysqld/mysqld.sock"

    database :mephisto

  end

  tar do

    archive "etc-files" do
      files "/etc"
    end

    archive "root-files" do
      files "/root"
    end

    archive "mephisto" do
      files "/var/www/mephisto/"
      exclude ["/var/www/mephisto/log", "/var/www/mephisto/tmp"]
    end

  end
end

Wie man sieht, ist das Ganze recht übersichtlich. Damit die gpg-Verschlüsselung funktioniert, benötigen wir ein Schlüsselpaar:

gpg-Schlüsselpaar erzeugen

Anfangs hatte ich die symmetrische Verschlüsselung mit einem Passwort versucht, aber das hat mit der Version 1.9 des gem nicht so recht geklappt – wenn man ein Schlüsselpaar spendiert, funktioniert es aber prima. Das Paar erzeugt man am Besten auf einem anderen Rechner und kopiert nur den öffentlichen Schlüssel auf den Backup-Host.

> gpg --key-gen
...
> gpg -a --export fw@bm.net > fw@bm.net.pub
> scp fw@bm.net.pub root@sat.bm.net:/root/

Auf dem Backup-Host (hier sat.bm.net) wird der öffentliche Schlüssel in den Schlüsselring des Backup-Users importiert (hier root) und als vertrauenswürdig eingestuft:

# gpg --import fw@bm.net.pub
gpg: key 45CA9403: public key "F.W. fw@bm.net" imported
gpg: Total number processed: 1
gpg:               imported: 1

# gpg --edit-key fw@bm.net
...
Command> trust
...
1 = I don't know or won't say
2 = I do NOT trust
3 = I trust marginally
4 = I trust fully
5 = I trust ultimately
m = back to the main menu
Your decision? 5
...
Command> quit

Wer möchte, kann auf dem lokalen Rechner noch eine Kopie des privaten Schlüssels anfertigen – nur für den Fall:

$ gpg -a --export-secret-key fw@bm.net > fw@bm.net.key

die Automation

Das wär’s schon. Es fehlt nur noch die Automation via crond – z.B. morgens, etwa um drei Uhr:

10 3 * * * /opt/ruby-enterprise/bin/astrails-safe /root/my-backup.conf >/dev/null 2>&1

Weblinks

  1. postgress-and-svndump-support-for-astrails-safe-s3-backup
  2. github.com/astrails/safe

EC2-MySql-Backups auf S3-Buckets 0

Posted by fwoeck
on Saturday, February 21

Basierend auf der Arbeit von Paul Dowman (s.u.) habe ich diese Lösung zum Backup auf S3-Speicher erstellt. Sie erweitert das Konzept auf alle vorhandenen MySql-Datenbanken eines Hosts und führt eine dynamische Benennung der Buckets durch, um für jeden gestarteten Host entsprechend seiner internen IP einen Speicherort zu schaffen.

Jedes Vollbackup erzeugt einen neuen Bucket der Weise “179-25-120-89.nnnn” und inkrementiert den Index nnnn um 1. Jedes Teilbackup nutzt den höchsten vorhandenen Bucket der eigenen IP-Adresse (Buckets können leider nicht umbenannt werden).

Für den Zugriff auf die Sql-Daten wird ein User angelegt, der auf alle Banken schauen darf:

GRANT RELOAD ON *.* TO 'backupuser'@'localhost' IDENTIFIED BY 'password';
GRANT SUPER ON *.* TO 'backupuser'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON *.* TO 'backupuser'@'localhost' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;

Für die Teilbackups aktiviert man das Bin-log-Feature von MySql in der my.cnf:

log_bin = /var/log/mysql/mysql-bin.log

Achtung: Da die root-Partition der EC2-Hosts recht begrenzt ist, könnte es sich als günstig erweisen, den Logort auf eine gemountete Partition zu verlegen.

Das Backupschema wird dann am besten via crond umgesetzt:

*/5 * * * * /bin/ruby /usr/local/bin/incremental_backup.rb
31 5 * * * /bin/ruby /usr/local/bin/full_backup.rb

Hier wird fünfminütlich ein Job angestoßen, was man für weniger stark frequentierte Server sicher reduzieren kann. Dann könnte man allerdings in Erwägung ziehen, ausschließlich Vollbackups anzufertigen. Die kombinierte Strategie entfaltet ihre ganze Effizienz also eher auf großen Datenbanken mit viel Durchsatz. Abgesehen davon bedenke man: wenn der Host terminiert, sind die Daten futsch!

Es folgen die beteiligten Dateien.

config.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@aws_access_key_id = "???"
@aws_secret_access_key = "??????"

@mysql_user = "backupuser"
@mysql_password = "???"
@mysql_databases = `mysql -u#{@mysql_user} -p#{@mysql_password} \
    -e 'show databases;'`.split[1..-1]
@mysql_bin_log_dir = "/var/log/mysql"
@temp_dir = "/tmp/mysql-backup"

raise "Another process is still active, terminating!" if File.exist?(@temp_dir)

@s3_buckbase = `export LANGUAGE=en_US; /sbin/ifconfig eth0 | grep 'inet ' \
    | cut -d: -f2 | cut -d" " -f1 | sed 's/\\\./-/g' `.strip
AWS::S3::Base.establish_connection!(:access_key_id => @aws_access_key_id, \
    :secret_access_key => @aws_secret_access_key, :use_ssl => true)
@s3_buckets = AWS::S3::Bucket.list.map {|b| b.name}.select {|n| n =~ \
    /^#{@s3_buckbase}\.\d{4}$/}.sort
@s3_bucket = @s3_buckets.last || "#{@s3_buckbase}.0000"

common.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
require "rubygems"
require "aws/s3"
require "config"

def run(command)
  result = system(command)
  raise("error, process exited with status #{$?.exitstatus}") unless result
end

def execute_sql(sql)
  cmd = %{mysql -u#{@mysql_user} -e "#{sql}"}
  cmd += " -p'#{@mysql_password}' " unless @mysql_password.nil?
  run cmd
end

[ Der Filename des aws/s3-gems lautet aws-s3. FIXME: Bisher ist unklar, ob EU-Buckets unterstützt werden. ]

full_backup.rb

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
#!/usr/bin/env ruby

require "fileutils"
FileUtils.cd(File.dirname(__FILE__))
require "common"

@s3_bucket = @s3_bucket.sub(/.{5}$/,"") + ".%.4d" % (@s3_bucket[-4..-1].to_i + 1)
AWS::S3::Bucket.create(@s3_bucket)

@mysql_databases.each do |mysql_database|
  begin
    FileUtils.mkdir_p @temp_dir
    dump_file = "#{@temp_dir}/#{mysql_database}_dump.sql.gz"
    
    cmd = "mysqldump --quick --single-transaction --create-options \
        -u#{@mysql_user}  --flush-logs --master-data=2 --delete-master-logs"
    cmd += " -p'#{@mysql_password}'" unless @mysql_password.nil?
    cmd += " #{mysql_database} | gzip > #{dump_file}"
    run(cmd)
    
    AWS::S3::S3Object.store(File.basename(dump_file), open(dump_file), @s3_bucket)
  ensure
    FileUtils.rm_rf(@temp_dir)
  end
end

incremental_backup.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env ruby

require "fileutils"
FileUtils.cd(File.dirname(__FILE__))
require "common"

AWS::S3::Bucket.create(@s3_bucket)

begin
  FileUtils.mkdir_p @temp_dir
  execute_sql "flush logs"
  logs = Dir.glob("#{@mysql_bin_log_dir}/mysql-bin.[0-9]*").sort
  logs_to_archive = logs[0..-2] # all logs except the last
  logs_to_archive.each do |log|
    AWS::S3::S3Object.store(File.basename(log), open(log), @s3_bucket)
  end
  execute_sql "purge master logs to '#{File.basename(logs[-1])}'"
ensure
  FileUtils.rm_rf(@temp_dir)
end

Falls nicht anders konfiguriert, werden die Änderungen aller Datenbanken in den binären Logs protokolliert. Um sich die Updates einer einzelnen daraus anzusehen, kann man das mysqlbinlog-Hilfstool benutzen:

mysqlbinlog mysql-bin.000001 --database=pplpool

Dies dient später dazu, aus der Menge der Logs die rechten auszufiltern und im Restore wieder einzuspielen.

restore.rb

FIXME: es fehlt noch die Logik, die eine separate DB auswählt, am besten via Zeilenparamenter

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
require "common"
 
def retrieve_file(file)
  key = File.basename(file)
  AWS::S3::S3Object.find(key, @s3_bucket)
  
  open(file, 'w') do |f|
    AWS::S3::S3Object.stream(key, @s3_bucket) do |chunk|
      f.write chunk
    end
  end
end
 
def list_keys(filename_prefix)
  AWS::S3::Bucket.objects(@s3_bucket, :prefix => 
      filename_prefix).collect{|obj| obj.key}
end
 
def retrieve_files(filename_prefix, local_dir)
  list_keys(filename_prefix).each do |k|
    file = "#{local_dir}/#{File.basename(k)}"
    retrieve_file(file)
  end
end
 
begin
  FileUtils.mkdir_p @temp_dir
  
  file = "#{@temp_dir}/dump.sql.gz"
  retrieve_file(file)
  
  cmd = "gunzip -c #{file} | mysql -u#{@mysql_user} "
  cmd += " -p'#{@mysql_password}' " unless @mysql_password.nil?
  cmd += " #{@mysql_database}"
  run cmd
  
  retrieve_files("mysql-bin.", @temp_dir)
  logs = Dir.glob("#{@temp_dir}/mysql-bin.[0-9]*").sort
  
  logs.each do |log|
    cmd = "mysqlbinlog --database=#{@mysql_database} #{log} | \
              mysql -u#{@mysql_user} "
    cmd += " -p'#{@mysql_password}' " unless @mysql_password.nil?
    run cmd
  end
  
  execute_sql("reset master")
ensure
  FileUtils.rm_rf(@temp_dir)
end

Webreferenzen

  1. pauldowman.com
  2. mysql_s3_backup

Amazon EC2, EBS und S3 - Skalierbare Infrastruktur auf Knopfdruck 0

Posted by fwoeck
on Friday, November 28

Amazon hat seine AWS-Produktpalette kürzlich um das EBS (Elastic Block Storage) erweitert und bietet jetzt persistenten hochverfügbaren Speicher im Netz an.

Also Anmelden und ausprobieren: man erhält u.a. ein Schlüsselpaar, ein paar Passphrases und andere Nummern – in etwa 10 Minuten ist man durch.

Es folgt die Konfiguration der ec2-Tools. Das Schlüssel/Cert-Paar, mit dem u.a. später selbst erzeugte Maschinenimages verschlüsselt werden, sollte in ~/.ec2/ landen:

export EC2_PRIVATE_KEY=~/.ec2/pk-O3QHC...Z33KGTK.pem
export EC2_CERT=~/.ec2/cert-O3QHC...Z33KGTK.pem
export EC2_HOME=~/
export EC2_URL="https://eu-west-1.ec2.amazonaws.com" 
export PATH=$PATH:$EC2_HOME/bin
export JAVA_HOME=/usr

Ach ja und ein aktuelles Sun-Java ist nötig. Hier ein paar Beispiele, wie man eine Instanz zum Laufen bringt, sich seine Instanzen ansieht, die Firewall konfiguriert, etc:

ec2-describe-instances

ec2-run-instances ami-89d733e0 -k gsg-keypair

ec2-get-console-output i-0d388b64

ec2-authorize default -p 22

ssh -i id_rsa-gsg-keypair root@ec2-75-101-203-146.compute-1.amazonaws.com

Die Templates für die Instanzen können aus dem öffentlichen Repo oder dem eigenen S3-Bucket kommen.

Beispiel für eine aktive Instanz:

RESERVATION    r-0664c56f    219423224891    default
INSTANCE    i-e105b688    ami-316a8e58    ec2-75-101-240-80.compute-1.amazonaws.com    domU-12-31-39-00-4D-A6.compute-1.internal    running    gsg-keypair    0        m1.small    2008-11-27T07:32:52+0000    us-east-1a    aki-a71cf9ce    ari-a51cf9cc

Da das laufende Image nach dem Shutdown sämtlich gelöscht wird, kann man seine eigene Konfiguration in ein individuelles Image überführen (bundlen). Die Kommandos können alle in der laufenden Maschine erfolgen, falls man die Tools mit Keypair auch dort hinterlegt hat:

ec2-bundle-vol -d /mnt -k ~/.ec2/pk-O3QHC...RZ33KGTK.pem -c ~/.ec2/cert-O3QHC...Z33KGTK.pem -u 21...891 -r i386 -p sat_081127

ec2-upload-bundle -b mine02 -m /mnt/sat_081127.manifest.xml -a 0Z8E...DKR2 -s <aws-secret-access-key>

Das neue Image kann registriert werden und erhält damit eine ID, über die ich es an eine Instanz binden kann:

me@dev71:~$ ec2-register mine02/sat_081127.manifest.xml
IMAGE    ami-3e749057

ec2-deregister ami-3e749057

ec2-delete-bundle -b mine02 -p sat_081127 -a 0Z8E...DKR2 -s <aws-secret-access-key>

Nach der Benutzung, Deregistrieren nicht vergessen!

Neue Instanzen haben immer eine dynamische IP, ich kann aber eine feste IP-Adresse anfordern und einer beliebigen Instanz zuweisen:

ec2-allocate-address
ADDRESS    174.129.251.99

ec2-associate-address -i i-e105b688 174.129.251.99
ADDRESS    174.129.251.99    i-e105b688

Fehlt noch das EBS:

> ec2-create-volume -s 20 -z us-east-1a

> ec2-attach-volume vol-05be5a6c -i i-e105b688 -d /dev/sdh

> yes | mkfs -t ext3 /dev/sdh

> mount /dev/sdh /var -o acl

WICHTIG! Ein EBS-Platz kann (noch?) nicht über die /etc/fstab beim Boot aktiviert werden und auch nicht als geniuner Platz für ein ganzes Image dienen. Zuwiderhandlungen führen zum Absturz beim Boot…

Hier gibt’s die ganze Story:

GettingStartedGuide