今までの話:
- Ansible で AWS にワンタッチで Redmine を作る: https://qiita.com/propella/items/7209dfda27f665766867
- Let's Encrypt と Nginx を使った Web アプリの HTTPS 化: https://qiita.com/propella/items/6a4b964fd010585530d2
Ansible や CloudFormation を使ってインフラ構成をコードで書くと、気楽にサーバを作ったり消したり出来て気持ちいよいです。寒いデータセンターに籠もってケーブルやシリアル端末と格闘した日々を比べると神の気分です。しかしうっかり大切なデータを消してしまわないよう定期的に S3 に保存する事にしました。¯¯¯
まず、S3 の作成と EC2 から書き込む権限を与える CloudFormation スタックの抜粋です。作った S3 の名前を EC2 の /home/ec2-user/backup-bucket.txt に書き込んで後でバックアップスクリプトに使います。AWS::S3::Bucket の BucketName で S3 の名前を指定するとこのような小技を使わなくて良いですが、なんとなく AWS に名前をつけてもらった方が応用が効くかなと思いこうしました。
一応バックアップを上書きしても履歴が残るよう S3 の VersioningConfiguration オプションを有効にしています。
Backup: # バックアップ用 S3 の作成
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: Enabled
BackupRole: # EC2 に S3 に書き込める権限を与える設定(ロール)
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
BackupPolicy: # バックアップ用の S3 に書き込める権限を作成
Type: AWS::IAM::Policy
Properties:
PolicyName: redmine-bucket-policy
Roles:
- !Ref BackupRole # AWS::IAM::Policy はいわゆるインラインポリシーを作るので、Groups, Roles, Users のどれかが必須です。ここでちょっと悩みました。
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:PutObjectAcl
- s3:ListBucket
- s3:DeleteObject
- s3:GetBucketLocation
Resource:
- !GetAtt Backup.Arn
- !Sub "${Backup.Arn}/*"
WebServer:
Type: AWS::EC2::Instance
Properties:
InstanceType: t3.nano
ImageId: ami-02892a4ea9bfa2192
KeyName: hogekey
SecurityGroups:
- Ref: WebServerSecurityGroup
IamInstanceProfile: !Ref WebServerProfile # ここで作成したロールを設定する
UserData: # インスタンス作成時に一度だけ実行される。S3 の名前を backup-bucket.txt に書き出す。
Fn::Base64: !Sub |
#!/bin/bash -xe
echo -n "${Backup}" > /home/ec2-user/backup-bucket.txt
https://redmine.jp/faq/system_management/backup/ によると、redmine のバックアップは以下の二点だそうです。
- 添付ファイルは /usr/src/redmine/files を保存
- その他のデータは mysqldump を保存
そこで、バックアップを考慮した docker-compose.yml を作ります。
- ホストの /home/ec2-user/redmine-files を redmine にマウントして添付ファイルを置きます。
- mysql のパスワードはコンテナの環境変数 MYSQL_ROOT_PASSWORD で渡しています。
app:
image: redmine
container_name: redmine_app
restart: always
ports:
- 3000:3000
volumes:
- /home/ec2-user/redmine-files:/usr/src/redmine/files
environment:
REDMINE_DB_MYSQL: db
REDMINE_DB_PASSWORD: ${DBPASS}
REDMINE_SECRET_KEY_BASE: supersecretkey
REDMINE_DB_ENCODING: utf8mb4
db:
image: mysql:5.7
container_name: redmine_db
restart: always
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: ${DBPASS}
MYSQL_DATABASE: redmine
S3 の前を /home/ec2-user/backup-bucket.txt に書き込んであるので、あとは mysqldump と redmine-fils を書き込む backup.sh を書くだけです。mysql のパスワードをコマンドラインで渡してはいけないと警告が出ますね。後で考えます。バックアップは次のようにしました。
- mysqldump はそのまま gzip で圧縮して保存。
- /usr/src/redmine/files は aws s3 sync を使って同期。なので、新規ファイル以外は転送されないはず。
# !/bin/sh
set -o errexit
set -o pipefail
set -o nounset
set -o xtrace
DIR=/home/ec2-user
BACKUP_BUCKET=$(cat $DIR/backup-bucket.txt)
docker exec redmine_db /bin/bash -c 'mysqldump -uroot -p$MYSQL_ROOT_PASSWORD --default-character-set=utf8mb4 redmine' | gzip > $DIR/redmine-mysql.sql.gz
aws s3 cp $DIR/redmine-mysql.sql.gz s3://${BACKUP_BUCKET}/redmine-mysql.sql.gz
aws s3 sync $DIR/redmine-files s3://${BACKUP_BUCKET}/redmine-files --delete
リストア用のスクリプト restore.sh です。
ここでトリッキーなのは、添付ファイルを redmine が読み書きできるよう chown -R 999:999 する部分です。ダサいことに uid 決め打ちです。chown が必要なので sudo restore.sh のように実行する必要があります。Docker ドキュメントでは exec を使ってコンテナの中で解凍するのをお勧めしています。mysqldump の方はお勧めに従ったのですが、files の方は redmine の中に aws コマンドが無く面倒なので root ユーザで書き込んで chwon する力技にしてしまいました。
# !/bin/sh
set -o errexit
set -o pipefail
set -o nounset
set -o xtrace
BACKUP_BUCKET=$(cat /home/ec2-user/backup-bucket.txt)
# Restore files
aws s3 sync s3://${BACKUP_BUCKET}/redmine-files /home/ec2-user/redmine-files --delete
chown -R 999:999 /home/ec2-user/redmine-files
# Restore SQL
aws s3 cp s3://${BACKUP_BUCKET}/redmine-mysql.sql.gz redmine-restore.sql.gz
zcat redmine-restore.sql.gz | docker exec -i redmine_db /bin/bash -c 'mysql -uroot -p$MYSQL_ROOT_PASSWORD redmine'
あとは backup.sh を cron に仕込めば完成です。Ansible ではこんな感じです。
- name: Schedule backup
cron:
name: Backup
hour: "18"
minute: "37"
job: "/home/ec2-user/backup.sh"
失敗談
と簡単そうに書きましたが大量の失敗談があります。
バックアップ方式の選定
迷った末です。
- 案1: 添付ファイルのバックアップ用 S3 を redmine の /usr/src/redmine/files としてマウント。
- 無限の容量が使えて嬉しいが遅いので辞めた。
- 案2: redmine コンテナ内で files と mysqldump を S3 に送る。
- マウントの必要無くスッキリしているがコンテナ内に awscli も mysql コマンドも無いので辞めた。
- 案3: ホストに files をマウント、docker exec で mysqldump を実行して S3 に送る。
- これを採用しました。
定期実行方式の選定
今どきの linux は systemd を使うのが正義だと思って cron の代わりに systemd timer を試みましたが止めました。一応やり方を書いておきます。
/etc/systemd/system/backup.service を作成
[Unit]
Description=Backup Redmine
[Service]
Type=simple
ExecStart=/home/ec2-user/backup.sh
User=ec2-user
Group=ec2-user
/etc/systemd/system/backup.timer を作成
[Unit]
Description=Backup Timer
[Timer]
OnUnitActiveSec=24hours
[Install]
WantedBy=timers.target
backup.service と backup.timer を両方有効化して開始
sudo systemctl enable backup
sudo systemctl start backup
sudo systemctl enable backup.timer
sudo systemctl start backup.timer
設定の確認とログを見る
systemctl list-timers
journalctl
system timer はこのように一つのジョブを設定するのに .service と .timer という二つの設定ファイルが必要です。
systemd timer の最大の問題は、タイマーを設定する時にコマンド (この場合 backup.sh) が走ってしまう事です。例えばバックアップスクリプトの場合、インスタンス起動時に backup.sh が動くと、せっかく残しておいたバックアップを上書きしてしまうので困るのです。こんな仕様のわけが無いと思って色々試しましたが、どう頑張っても systemctl start backup.timer
の前に systemctl start backup
が必要でした(お恥ずかしい話ですが、誰かが気づいて教えてくれる事を期待して書いている)。
結局労力の割に合わないので伝統的な cron を使う事にしました。