[AWS][goofys][Docker] バックアップを考える


AWS EC2 上でのバックアップの仕組みを作ったので覚え書き。

バックアップとしては、特定のディレクトリを tar ball にアーカイブできればよいという単純な要件だけど、ちゃんと定時の仕組みにしようとすると面倒くさいものだったりします。

- 固めた tar ball はどこに置くのか？
- バックアップのサイクルは？(日次など)
- 世代管理も必要ですよね？

他に、個人的な要望として、以下のようなものもありました。

- OS へのセットアップはしたくない
- できるだけ docker コンテナで～

ということで、以下の形で構成。

- 格納先は S3 バケットへ
- S3 へのアクセスは goofys マウントで
- goofys マウントを docker コンテナ上で
- バックアップのシェルスクリプトの実行を busybox コンテナ上で
- 世代管理は自前のスクリプトで

すべてコンテナ上で実現したので、いくらかの設定ファイルとスクリプトだけ用意するだけで済みました。
世代管理のスクリプトは自前で面倒ですが、一度作っておくと再利用できて便利です。

通常、OS の設定が必要になる goofys マウントや世代管理の logrotate など使わなくてよい構成です。
こうしておけば、Kubernetes な環境になったときにも流用しやすいかなというのもあります。

![fig-goofys-backup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558772/18e41752-d406-2e27-16cd-c0a461134150.png)


## 全体のシーケンス

全体の流れは以下のステップとなりました。

1. S3 バケットをマウント
1. バックアップ実行
1. S3 バケットのマウント解除

全体を一発で流せるようにとも考えたのですが、バックアップ完了後にマウント解除を連動するのがうまくいかなかったのと、リストア処理の連動などマウントだけを利用したいケースもあるのでこの形で。


## S3 バケットをマウント

S3 バケットをファイルシステムとしてマウントする goofys を利用します。
以下のコンテナが用意されています。

https://github.com/cloudposse/goofys

これを使って docker-compose.yml を用意するだけで簡単に S3 バケットのマウントができます。

```docker-compose.yml
version: '3.8'
services:
  goofys:
    image: cloudposse/goofys
    privileged: true
    environment:
      - REGION=ap-northeast-1
      - BUCKET=my-backup-backet
      - UID=1000
      - GID=1000
    volumes:
      - /backup_store:/mnt/s3:shared
```

```
$ docker-compose up -d        # S3 バケットをマウント
$ ls /backup_store/           # バケットを参照できるようになる
    :
$ docker-compose down         # S3 バケットのマウント解除
$ sudo umount /backup_store   # ホスト上でのマウントが解除されないのでアンマウント
```

goofys コンテナ内で /mnt/s3 へ S3 バケットがマウントされます。
このポイントをホストへの外部ボリュームにしておくことで、ホスト側で S3 バケットへのファイルシステム参照ができるようになります。


## バックアップスクリプト実行

バックアップ処理自体はシェルスクリプトを作成し、スクリプトの実行を busybox コンテナで動かします。
これも docker-compose.yml を用意しておくと簡単。

> 環境変数設定、コマンド名はスクリプトの内容による

```docker-compose.command.yml
version: '3.8'
services:
  backup-base: &base
    image: busybox
    environment:
      - ENV_BACKUP_TARGET_DIR=/backup_target        # バックアップ対象
      - ENV_BACKUP_STORE_DIR=/backup_store/         # バックアップ先 (S3 マウント先)
      - ENV_BACKUP_DAYS=5                           # 世代指定
    volumes:
      - /backup_store:/backup_store:slave
      - /data:/backup_target                        # バックアップ対象(ホスト上のディレクトリ)
      - ./backup.sh:/backup.sh                      # バックアップ用のスクリプト
      - ./restore.sh:/restore.sh                    # リストア用のスクリプト

  backup-store:
    <<: *base
    command:
      - "sh"
      - "/backup.sh"

  backup-restore:
    <<: *base
    command:
      - "sh"
      - "/restore.sh"
```

```
$ docker-compose -f docker-compose.command.yml run backup-store       # バックアップを実行
$ docker-compose -f docker-compose.command.yml run backup-restore     # リストアを実行
```

これらの docker-compose のコマンドイメージを Makefile などにまとめておくと便利です。

なお、busybox には bash ではなく ash という組み込み用のコンパクトなシェルが入っているので、シェルスクリプトを書く際には注意が必要です。


## おまけ: 世代管理処理のサンプル

対象ファイルと保存する世代数を指定して実行する形です。(日次実行)

```sh
function rotate() {
    _tgd=$1
    _tgf=$2
    _days=$3

    _now=$( date +%Y/%m/%d )
    _nod=$( date +%Y%m%d )
    _thd=$( date +%Y%m%d -d "1970.01.01-00:00:$(( $( date +%s ) - $(( ${_days} * 24*60*60 )) ))" )

    if test -f ${_tgd}/${_tgf} ; then
        _mod=$( stat -c%y ${_tgd}/${_tgf} | cut -d " " -f1 | sed -e 's/[-:]//g' )
        mv ${_tgd}/${_tgf} ${_tgd}/${_tgf}-${_mod}
    fi

    _a=""
    ( cd ${_tgd} ; ls -1 ${_tgf}-* 2>&1 > /dev/null )
    if test $? -eq 0 ; then
        _a=$( cd ${_tgd} ; ls -1 ${_tgf}-* | sort -r )
    fi
    _t=${_tgf}-${_thd}
    for _i in ${_a}
    do
        if test ${_i} \< ${_t} || test ${_i} = ${_t} ; then
            rm -f ${_tgd}/${_i}
        fi
    done
}
```

以下のように実行すると、ファイルの作成日別のファイルにリネームし、指定した世代分だけ残して他を削除します。

```
rotate /backup_store aaa.tar.gz 3
----
/backup_store/aaa.tar.gz-20201128
/backup_store/aaa.tar.gz-20201127
/backup_store/aaa.tar.gz-20201126
```

なお、ash 用に考慮が必要だったのは以下の点。

```
# -d に "- 5 day" のような形式が使えない
date -d "2020/11/28 - 5 day"
```

```
# 配列が使えない
array=()
```

```
# if [[ ]] が使えない
if [[ $i < $t >]] ; then
    :
fi
```