GitHub EnterpriseをAWSで使おう – backup-utilsを利用したバックアップ/リストア

2016.12.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、中山です。

GitHub Enterprise(以下GHE) on AWSシリーズの第5弾です。今回はGHEのバックアップとリストア方法についてご紹介します。

GHEにおけるバックアップ/リストア

みなさんご存知のようにAWSにはEBSのスナップショットという機能があります。EBSのある瞬間のデータをS3上に簡単に保存でき、EC2のデータを保存したい場合に大変便利な仕組みです。これはこれで大変便利なのですが、GHEにはgithub/backup-utilsというGitHub謹製のバックアップ/リストアツールが用意されています。スナップショットを利用したバックアップ/リストアと比較して、GHEに特化している分扱いやすいという利点があります。個人的にはGHEのバックアップ戦略としてはbackup-utilsを主に使い、念のためにEBSスナップショットを取得しておくという方法がよいかと思っています。

backup-utilsには以下3つのコマンドが同梱されています。

  • ghe-backup
    • バックアップ用スクリプト
  • ghe-restore
    • リストア用スクリプト
  • ghe-host-check
    • バックアップ/リストアを実施する前、GHEにSSHログイン(GHEの場合122ポート)できるかチェックするためのスクリプト

それぞれの使い方は以下のように非常に簡単です。

# バックアップ
$ ghe-backup
Starting backup of 172.31.25.216 in snapshot 20161214T030402
Connect 172.31.25.216:122 OK (v2.8.3)
Backing up GitHub settings ...
Backing up SSH authorized keys ...
Backing up SSH host keys ...
Backing up MySQL database ...
Backing up Redis database ...
Backing up audit log ...
Backing up hookshot logs ...
Backing up Git repositories ...
Backing up GitHub Pages ...
Backing up asset attachments ...
Backing up storage data ...
Backing up hook deliveries ...
Backing up custom Git hooks ...
Backing up Elasticsearch indices ...
Completed backup of 172.31.25.216:122 in snapshot 20161214T030402 at 03:04:10
Checking for leaked ssh keys ...
* No leaked keys found
# リストア
$ ghe-restore 172.31.25.216
Checking for leaked keys in the backup snapshot that is being restored ...
* No leaked keys found
Connect 172.31.25.216:122 OK (v2.8.3)

WARNING: All data on GitHub Enterprise appliance 172.31.25.216 (v2.8.3)
         will be overwritten with data from snapshot 20161214T031235.
Please verify that this is the correct restore host before continuing.
Type 'yes' to continue: yes

Starting restore of 172.31.25.216:122 from snapshot 20161214T031235
Stopping cron and github-timerd ...
Restoring MySQL database ...
 --> Importing MySQL data...
Restoring Redis database ...
Restoring Git repositories ...
Restoring GitHub Pages ...
Restoring SSH authorized keys ...
Restoring asset attachments ...
Restoring storage data ...
Restoring hook deliveries ...
Restoring custom Git hooks ...
Restoring Elasticsearch indices ...
Restoring hookshot logs ...
Configuring storage ...
Starting cron ...
Restoring SSH host keys ...
Completed restore of 172.31.25.216:122 from snapshot 20161214T031235
Visit https://172.31.25.216/setup/settings to review appliance configuration.
# SSHログインのチェック
$ ghe-host-check
Connect 172.31.25.216:122 OK (v2.8.3)

すべてのコマンドで設定ファイルを読み込んでくれるため基本的にオプションは不要です( ghe-restore 以外 )。設定ファイルの設置場所はこちらのドキュメントに詳しいです。設定ファイルのサンプルはこちらにあります。適宜参照してください。

今回の構成

単純にバックアップ用インスタンスを立ててこのツールを使っただけではあまりひねりがないので、今回は以下のような構成にしてみました。

  • バックアップ構成

ghe5-1

バックアップのためだけにインスタンスを24時間立て続けるのはお財布に優しくありません。クラウドのよいところは必要な時に必要なリソースを使えるというオンデマンドな部分だと思います。という訳で、バックアップを取得する時だけインスタンスが立ち上がり、それが終わったら消えるようにスケジュールに基づくスケーリングの設定をしたオートスケーリンググループを利用します。ターミネートさせる処理の実装方法はいろいろと考えられますが、今回は簡単にmin/max/desiredが全て0になるようなシェルスクリプトをユーザデータに仕込みます。

また、スケールアウトで立ち上がってくるインスタンスは常に同じEBSにバックアップを書き込む必要があります。ルートボリュームにバックアップデータを直接書き込んでしまうと後々扱いづらいものになる恐れがあるため、事前にバックアップデータ用のEBSを用意しておきユーザデータでインスタンス起動時にマウントさせ、そこにバックアップデータを書き込むという処理にします。

  • リストア構成

ghe5-2

先程のオートスケーリンググループはバックアップ処理に特化しているため、リストアには別途インスタンスを立てる構成にします。インスタンス起動後、バックアップ用EBSをマウントして ghe-restore コマンドを実行するという流れです。

私は試せていないのですが、GHEにEBSをマウントさせ、自分自身でリストアするという方法も恐らくできると思います。ただ、GHEで用意されているSSHアクセスは基本的に既存のコマンドを実行することが想定されています。該当する文面を引用します。

Administrative shell access is permitted for troubleshooting and performing documented operations procedures only. Modifying system and application files, running programs, or installing unsupported software packages may void your support contract. Please contact GitHub Enterprise technical support if you have a question about the activities allowed by your support contract.

backup-utilsはGHEにデフォルトでは入っていません。そのため、個人的には別のインスタンスを立ててそこでリストアする方がよいのではと考えています。

今回作成するGHEやネットワーク周りの設定などのベースとなる環境は、GitHub社が提供してくれているCloudFromationテンプレートを利用します。テンプレートの利用方法については以前私が書いた以下のエントリを参照してください。

注意点として、起動したGHEは最低限マネジメントコンソールで「Save settings」まで完了しているか、CLIで ghe-config-apply が実行済みになっている必要があります。この処理が完了していないとGHE内部で利用されている各種ミドルウェア(MySQL/Redisなど)がまだ起動していないためバックアップの取得に失敗します。例えば以下のようなエラーが出力されます。

$ ghe-backup
Starting backup of 172.31.25.216 in snapshot 20161213T232318
Connect 172.31.25.216:122 OK (v2.8.3)
Backing up GitHub settings ...
cat: /data/user/common/github.conf: No such file or directory
Enterprise is not configured yet
Backing up SSH authorized keys ...
Backing up SSH host keys ...
Backing up MySQL database ...
mysqldump: Got error: 2002: Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2) when trying to connect
Backing up Redis database ...
Could not connect to Redis at 127.0.0.1:6379: Connection refused
Backing up audit log ...
/usr/share/rbenv/versions/2.1.7-github/lib/ruby/2.1.0/uri/common.rb:176:in `split': bad URI(is not URI?): http://localhost:9201/<!DOCTYPE (URI::InvalidURIError)
        from /usr/share/rbenv/versions/2.1.7-github/lib/ruby/2.1.0/uri/common.rb:211:in `parse'
        from /usr/share/rbenv/versions/2.1.7-github/lib/ruby/2.1.0/uri/common.rb:747:in `parse'
        from /usr/local/share/enterprise/ghe-es-dump-json:14:in `<main>'
/usr/share/rbenv/versions/2.1.7-github/lib/ruby/2.1.0/uri/common.rb:176:in `split': bad URI(is not URI?): http://localhost:9201/html> (URI::InvalidURIError)
        from /usr/share/rbenv/versions/2.1.7-github/lib/ruby/2.1.0/uri/common.rb:211:in `parse'
        from /usr/share/rbenv/versions/2.1.7-github/lib/ruby/2.1.0/uri/common.rb:747:in `parse'
        from /usr/local/share/enterprise/ghe-es-dump-json:14:in `<main>'
/usr/share/rbenv/versions/2.1.7-github/lib/ruby/2.1.0/uri/common.rb:176:in `split': bad URI(is not URI?): http://localhost:9201/<!-- (URI::InvalidURIError)
        from /usr/share/rbenv/versions/2.1.7-github/lib/ruby/2.1.0/uri/common.rb:211:in `parse'
        from /usr/share/rbenv/versions/2.1.7-github/lib/ruby/2.1.0/uri/common.rb:747:in `parse'
        from /usr/local/share/enterprise/ghe-es-dump-json:14:in `<main>'
<snip>

説明が長くなりました。それでは、バックアップ/リストアに分類してご紹介していきます。backup-utilsを利用するインスタンスの構築はAWS CLIを使って構築します。マネジメントコンソールでの構築となるとスクリーンショットを利用した説明となり、ちょっとシンドいので。。。

バックアップ

おおまかな流れは以下のとおりです。

  1. バックアップデータ用EBSの作成
  2. セキュリティグループの作成
  3. ローンチコンフィグレーションの作成
  4. オートスケーリンググループの作成

まずはバックアップデータを保存するためのEBSを作成します。データのサイズはGitHub社が公開しているこちらのドキュメントが参考になります。引用すると以下のとおりです。

人数 バックアップ用データサイズ
0 - 500 100 GB
500 - 3000 250 GB
3000 - 5000 500 GB

今回は100GBでEBSを作成します。

$ aws ec2 create-volume \
  --size 100 \
  --volume-type gp2 \
  --availability-zone <_YOUR_AZ_>
{
    "AvailabilityZone": "ap-northeast-1a",
    "Encrypted": false,
    "VolumeType": "gp2",
    "VolumeId": "vol-0937b1d9f6c9b444e",
    "State": "creating",
    "Iops": 300,
    "SnapshotId": "",
    "CreateTime": "2016-12-14T00:06:50.796Z",
    "Size": 100
}

次にバックアップ用インスタンスにひも付けるセキュリティグループを作成します。

$ aws ec2 create-security-group \
  --group-name backup-sg \
  --description backup-sg \
  --vpc-id <_YOUR_VPC_ID_>
{
      "GroupId": "sg-f34afe94"
}
$ aws ec2 authorize-security-group-ingress \
  --group-name backup-sg \
  --protocol tcp \
  --port 22 \
  --cidr "$(curl -s inet-ip.info)/32"

今回利用したGitHub社が用意してくれているテンプレートでは利用するポートがanyで開放されているので気付きにくいですが、backup-utilsでは内部的にSSHを利用しています。もし、バックアップ用インスタンスからSSHできない場合は適宜ポートの開放を行ってください。

続いてローンチコンフィグレーションを作成します。ユーザデータには以下のようなスクリプトを仕込んでおきます。各シェルコマンドの意味は適宜コメントを入れてあるので参照してください。適当な名前( user_data.sh など)で保存しておいてください。

#!/usr/bin/env bash

set -xe

export AWS_DEFAULT_REGION="ap-northeast-1"

# Cloning backup-utils
yum install -y git
git clone -b stable https://github.com/github/backup-utils.git /opt/backup-utils

# Fetching SSH private key from S3 bucket
aws s3 cp s3://<_YOUR_S3_BUCKET_>/id_rsa /root/.ssh/id_rsa
chmod 400 /root/.ssh/id_rsa

# Creating backup-utils config file
cat <<'EOT' > /opt/backup-utils/backup.config
GHE_HOSTNAME="<_YOUR_GHE_PRIVATE_IP_>"
GHE_DATA_DIR="/ghe-backup"
GHE_NUM_SNAPSHOTS=7
#GHE_RESTORE_HOST="github-standby.example.com"
GHE_EXTRA_SSH_OPTS="-l admin -i /root/.ssh/id_rsa -o StrictHostKeyChecking=no"
#GHE_EXTRA_RSYNC_OPTS=""
#GHE_CREATE_DATA_DIR=yes
#GHE_BACKUP_FSCK=no
EOT

instance_id="$(curl -s http://169.254.169.254/latest/meta-data/instance-id)"

# Waiting EC2 instance to be running
aws ec2 wait instance-running --instance-id "$instance_id"

# Attaching EBS volume
aws ec2 attach-volume --volume-id "<_YOUR_EBS_VOLUME_ID_>" --device /dev/xvdf --instance-id "$instance_id"

# Waiting EBS volume to be in-use and creating backup directory
aws ec2 wait volume-in-use --volume-ids "<_YOUR_EBS_VOLUME_ID_>" &&  mkdir /ghe-backup

# Waiting EBS volume to be recognized
while true; do
  sleep 5
  [[ -b "/dev/xvdf" ]] && break
done

# Formatting EBS volume only if EBS volume has not been formatted
file -s /dev/xvdf | grep -qE 'data$' && mkfs -t ext4 /dev/xvdf

# Mounting EBS volume
mount /dev/xvdf /ghe-backup

# Using ghe-backup to backup
/opt/backup-utils/bin/ghe-backup

# Terminating AutoScalingGroup
aws autoscaling update-auto-scaling-group \
  --desired-capacity 0 \
  --min-size 0 \
  --max-size 0 \
  --auto-scaling-group-name backup-asg

スクリプトの中でS3からSSH用秘密鍵をダウンロードさせています。このSSH鍵はバックアップインスタンスからGHEにログインする際に利用します。以下のコマンドで事前に用意しておいてください。

$ ssh-keygen -f id_rsa -N ''
<snip>
$ aws s3 cp id_rsa s3://<_YOUR_BUCKET_NAME_>
upload: ./id_rsa to s3://<_YOUR_BUCKET_NAME_>/id_rsa

また、以下のコマンドで公開鍵をGHEに登録しておく必要があります。

$ cat id_rsa.pub | ssh -i path/to/key -p 122 admin@<_YOUR_GHE_PUBLIC_IP_> 'ghe-import-authorized-keys'
 --> Importing SSH authorized keys...

IAMインスタンスプロファイルとローンチコンフィグレーションを以下のコマンドで作成します。

# IAMポリシーが記述されたファイルを用意
$ cat <<'EOT' > policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      }
    }
  ]
}
EOT
# IAMインスタンスプロファイの作成
$ aws iam add-role-to-instance-profile \
  --instance-profile-name "$(aws iam create-instance-profile \
    --instance-profile-name backup-profile \
    --query 'InstanceProfile.InstanceProfileName' \
    --output text)" \
  --role-name "$(aws iam create-role \
    --role-name backup-role \
    --assume-role-policy-document file://policy.json \
    --query 'Role.RoleName' \
    --output text)"
# PowerUser権限を付与(さまざまなAWSリソースにアクセスするため)
$ aws iam attach-role-policy \
  --role-name backup-role \
  --policy-arn 'arn:aws:iam::aws:policy/PowerUserAccess'
# ローンチコンフィグレーションの作成
$ aws autoscaling create-launch-configuration \
  --launch-configuration-name backup-lc \
  --key-name <_YOUR_KEY_PAIR_> \
  --security-groups sg-f34afe94 \
  --associate-public-ip-address \
  --instance-type t2.nano \
  --user-data file://user_data.sh \
  --iam-instance-profile backup-profile \
  --image-id "$(aws ec2 describe-images \
    --owners amazon \
    --filters \
      Name=virtualization-type,Values=hvm \
      Name=root-device-type,Values=ebs \
      Name=architecture,Values=x86_64 \
      Name=block-device-mapping.volume-type,Values=gp2 \
      Name=name,Values='amzn-ami-hvm-*' \
    --query 'Images[0].ImageId' \
    --output text)"

最後にオートスケーリンググループを作成します。

$ aws autoscaling create-auto-scaling-group \
  --auto-scaling-group-name backup-asg \
  --launch-configuration-name backup-lc \
  --vpc-zone-identifier <_YOUR_SUBNET_ID_> \
  --min-size 1 \
  --max-size 1 \
  --desired-capacity 1

オートスケーリング作成後、インスタンスが自動で起動しバックアップの取得が行われます。バックアップが完了した後にオートスケーリンググループのmin/max/desiredを全て0にする処理が入っているので、すぐインスタンスは停止してしまいます。バックアップの様子を見たい場合は、その処理をコメントアウトしローンチコンフィグレーションとオートスケーリンググループを再作成すればOKです。 /var/log/cloud-init-output.log にログが出力されています。

スケジュールに基づくスケーリングの設定は以下のコマンドで指定可能です。 --recurrence オプションはUTCで指定する点にご注意ください。

$ aws autoscaling put-scheduled-update-group-action \
  --auto-scaling-group-name backup-asg \
  --scheduled-action-name backup-schedule \
  --recurrence '* 17 * * *' \
  --max-size 1 \
  --min-size 1 \
  --desired-capacity 1

リストア

続いてリストアを実施してみます。おおまかな流れは以下のとおりです。

  1. EC2インスタンスの起動
  2. バックアップ用EBSのマウント
  3. EC2インスタンス上で各種ツールの準備
  4. GHEをメンテナンスモードに変更
  5. リストア

まずはリストア用EC2インスタンスを立ち上げます。今回はセキュリティグループとIAMインスタンスプロファイルはバックアップ用のものを転用します。

$ aws ec2 run-instances \
  --instance-type t2.nano \
  --key-name <_YOUR_KEY_PAIR_> \
  --security-group-ids sg-f34afe94 \
  --subnet-id <_YOUR_SUBNET_ID_> \
  --associate-public-ip-address \
  --iam-instance-profile Name=backup-profile \
  --image-id "$(aws ec2 describe-images \
    --owners amazon \
    --filters \
      Name=virtualization-type,Values=hvm \
      Name=root-device-type,Values=ebs \
      Name=architecture,Values=x86_64 \
      Name=block-device-mapping.volume-type,Values=gp2 \
      Name=name,Values='amzn-ami-hvm-*' \
    --query 'Images[0].ImageId' \
    --output text)"
<snip>

続いてEC2インスタンスがrunningステータスに変わったらEBSをアタッチさせます。

$ aws ec2 attach-volume \
  --volume-id <_YOUR_EBS_VOLUME_ID_> \
  --device /dev/xvdf \
  --instance-id <_YOUR_INSTANCE_ID_>
{
    "AttachTime": "2016-12-14T03:57:38.618Z",
    "InstanceId": "i-01d85323d198971c2",
    "VolumeId": "vol-0937b1d9f6c9b444e",
    "State": "attaching",
    "Device": "/dev/xvdf"
}

EC2インスタンスにSSHログインしてEBSをマウントさせます。EBSのマウント方法についてはこちらのドキュメントが詳しいですが、基本的には以下のコマンドを実行すればOKです。

$ ssh -i path/to/key ec2-user@<_YOUR_INSTANCE_PUBLIC_IP_>
<snip>
# EBSボリュームがOSから認識されていることを確認
$ lsblk
NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
xvda    202:0    0    8G  0 disk
└─xvda1 202:1    0    8G  0 part /
xvdf    202:80   0  100G  0 disk
# マウント
$ sudo mount /dev/xvdf /mnt
# バックアップデータの確認
$ ls -l /mnt
total 32
drwxr-xr-x 11 root root  4096 Dec 14 02:44 20161214T024409
drwxr-xr-x 11 root root  4096 Dec 14 02:59 20161214T025949
drwxr-xr-x 11 root root  4096 Dec 14 03:04 20161214T030402
drwxr-xr-x 11 root root  4096 Dec 14 03:12 20161214T031235
lrwxrwxrwx  1 root root    15 Dec 14 03:12 current -> 20161214T031235
drwx------  2 root root 16384 Dec 14 02:24 lost+found

ghe-backup で取得したバックアップデータは上記のように current というシンボリックリンクが最新のデータを指しています。分かりやすいですね。

続いてEC2インスタンス上で各種ツールの用意をします。これはローンチコンフィグレーションで指定したユーザデータと大体同じ処理ですが、ユーザデータがrootで実行されるのに対して、今回はec2-userで実行可能なようにしておきます。

$ sudo yum install -y git
$ git clone -b stable https://github.com/github/backup-utils.git ~/backup-utils
$ cat <<'EOT' > ~/backup-utils/backup.config
GHE_HOSTNAME="<_YOUR_GHE_PRIVATE_IP_>"
GHE_DATA_DIR="/mnt"
GHE_NUM_SNAPSHOTS=7
#GHE_RESTORE_HOST="github-standby.example.com"
GHE_EXTRA_SSH_OPTS="-l admin -i /home/ec2-user/.ssh/id_rsa -o StrictHostKeyChecking=no"
#GHE_EXTRA_RSYNC_OPTS=""
#GHE_CREATE_DATA_DIR=yes
#GHE_BACKUP_FSCK=no
EOT
$ aws s3 cp s3://<_YOUR_S3_BUCKET_>/id_rsa ~/.ssh/id_rsa
$ chmod 400 ~/.ssh/id_rsa

EC2側の用意が整ったらGHEをメンテナンスモードに遷移させます。 ghe-restore コマンドを利用したリストアではGHEがメンテナンスモードになっていないとエラーが出て処理が進みません。メンテナンスモードへの変更方法は以前書いた以下のエントリを参照してください。

準備が整ったのでいよいよリストアしてみます。結果を分かりやすくするために、まだバックアップとして保存されてない適当なリポジトリを作成してリストアの前後でそのリポジトリの有無を確認したいと思います。以下のように適当なリポジトリを作成しておきます。

ghe5-3

実際にリストアしてみます。

# SSHログイン可能か確認
$ cd ~/backup-utils
$ ./bin/ghe-host-check
Connect 172.31.25.216:122 OK (v2.8.3)
# リストア(データがroot権限になっているのでsudoをつける)
$ sudo ./bin/ghe-restore <_YOUR_INSTANCE_PRIVATE_IP_>
Checking for leaked keys in the backup snapshot that is being restored ...
* No leaked keys found
Connect 172.31.25.216:122 OK (v2.8.3)

WARNING: All data on GitHub Enterprise appliance 172.31.25.216 (v2.8.3)
         will be overwritten with data from snapshot 20161214T031235.
Please verify that this is the correct restore host before continuing.
Type 'yes' to continue: yes

Starting restore of 172.31.25.216:122 from snapshot 20161214T031235
Stopping cron and github-timerd ...
Restoring MySQL database ...
 --> Importing MySQL data...
Restoring Redis database ...
Restoring Git repositories ...
Restoring GitHub Pages ...
Restoring SSH authorized keys ...
Restoring asset attachments ...
Restoring storage data ...
Restoring hook deliveries ...
Restoring custom Git hooks ...
Restoring Elasticsearch indices ...
Restoring hookshot logs ...
Configuring storage ...
Starting cron ...
Restoring SSH host keys ...
Completed restore of 172.31.25.216:122 from snapshot 20161214T031235
Visit https://172.31.25.216/setup/settings to review appliance configuration.

GHEをメンテナンスモードから解除後、アクセスしてみます。以下のようにバックアップ後に作成したリポジトリが消えていることを確認できました。

ghe5-4

まとめ

いかがだったでしょうか。

backup-utilsを利用したバックアップ/リストア方法をご紹介しました。コマンド自体が非常にシンプルに作成されているため、簡単に使えるというのがよい点だと思います。GHE上で管理するコードは重要なものが多くなるかと思います。もしもの時を想定して日頃からバックアップを用意しておきましょう。

本エントリがみなさんの参考になったら幸いに思います。