初心者向けのWebサーバのバックアップシェル解説

#はじめに

初めまして、フロントエンドエンジニア志望のタケダです。
企画・制作を行っている<a href="https://www.lockup.jp">株式会社LockUP</a>にインターンしてほぼ未経験からエンジニアの作業を行った時の記録になります。

今回はシェルが分からないところからバックアップを実行するシェルの改修を行った記録です。
私はコマンドはある程度知っており、シェルという言葉は知っていましたが実際使ったことはありませんでした。
ですがこのお仕事を通じてシェルの基本的な読み書きができるようになり、全く分からないことでも調べて解決するということが出来るようになりました。
現在では保守のお手伝いとして今回のスクリプトを使ってバックアップ取得作業などを行っています。
この記事を初心者エンジニアや未経験の方に読んでいただき、少しでも力になれればと思います。

今回は実際のサービスで運用している形に近いシェルを見てもらい、それを解説するという流れで進めます。
シェルを書いたことない人は是非一緒に進めてみましょう！
また下記のようなサイトでも合わせて勉強してみましょう！

- <a href="https://prog-8.com/lessons/commandline/study/1">Progate Command Line 基礎編　※無料(2020年2月現在)</a>

- <a href="https://dotinstall.com/lessons/basic_shellscript_v2">ドットインストール シェルスクリプト入門　※有料(2020年2月現在)</a>

##記事の対象者

この記事は以下のような人を対象にしています。

- Linux,Unix系を使い始めた人。
- シェルを覚え始めたけどまだ一人では書けない人、コマンドがある程度使えるがシェルで書いたことがないエンジニア。
- 自社Webサービスや受託Webサービスの保守などの担当になったフロントエンドエンジニア。

###1.作業の時の流れ

- シェルを読み込む。
- 分からないところが出てくるので調べる。(※ここに非常に時間がかかります。)
- なんとなくやってることを理解したら実行したいコマンドを書き出す。
- コマンドが成功したら次に進む。失敗したら原因を調べて正しいコマンドに直す。
- 完成したコマンドを書き出したらそのコマンドを実行するための条件をつける。
- シェルを書いて実行する。
- 間違っていたらエラーを調べて原因を探る。そして改修する。
- どうしても解決できなかった場合は先輩やメンターの方に聞く。いなければ質問できるサイトを頼る。

###2.まずシェルを読み込んで解析

まずはシェルの改修のために参考にするスクリプトを上から読んでいきます。
実際に使用しているものに近いスクリプトを一緒に見ていきましょう。
下記は参考にするだけで今回使わないコマンドなども多くありますので流し見で良いです。

```bash

#! /bin/sh
if [ $1 = "production" ]
then
  echo "本番環境のバックアップを実行します。"
  path_to_exec='/var/www/html/XXX/exec/'

elif [ $1 = "staging" ] 
then
  echo "ステージング環境のバックアップを実行します。"
  path_to_exec='/var/www/html/XXX/exec/' 
fi

if [ ! -e $path_to_exec/html_list.txt ]; then 
  ip=`/sbin/ifconfig eth0 | grep 'inet addr' | awk '{print $2;}' | cut -d: -f2`
  echo not exist html_list.txt in $ip 
  echo /home/username
  exit 1
fi

exit;

emp_area=`df -h`
today=`date +"%Y%m%d"`
purgeday=`date -d '1 week ago' +"%Y%m%d"`

for dt in `grep -v ^# ${path_to_exec}/html_list.txt`
do
  arr=(`echo $dt | tr -s ',' ' '`)　
  dir=`basename ${arr[0]}`
  cd ${arr[0]}　
  cd .. 
  if [ -z "${arr[2]}" ]; then
    tar czf /home/username/backup/${arr[1]}.tar.gz $dir　
  else
    tar czf /home/lusername/backup/${arr[1]}_$today.tar.gz $dir -X ${arr[2]}
  fi
done
/home/username/backup/*.tar.gz bck@$bcksrv:~/backup

if [ -e /home/username/bin/db_list.txt ]; then
  for dt in `grep -v ^# /home/XXX/bin/db_list.txt` 
  do
    arr=(`echo $dt | tr -s ',' ' '`) 
    MYSQL_PWD="${arr[1]}"  mysqldump -u ${arr[0]} ${arr[2]} > /home/username/backup/${arr[3]}_$today.dmp
    unset MYSQL_PWD
  done
fi

BACKUP_DIR="/home/username/backup"
RES="${BACKUP_DIR}/result_log"
find ${BACKUP_DIR}/ -maxdepth 1 -regex '.*\.\(gz\|txt\|dmp\)' -mtime +28 -type f | xargs rm -rf >> ${RES}

php ./backup_slackbot.php $today "$emp_area";
```

分かりましたでしょうか？
私のレベルは

- 条件の中身は分からないけどifで条件分岐をしている。
- echoで文字を返している。
- cdでディレクトリを移動している。

そんなレベルでした。全部は理解できなくでも大丈夫です。
次は調べていきましょう。

###3.分からなかったらまず調べる

調べるサイトをいくつか紹介します。

- <a href="https://www.google.com/?hl=ja" target="_blank">google</a><br>
言わずと知れたGoogle先生。まずはググりましょう。
- <a href="https://qiita.com/" target="_blank">Qiita</a><br>
皆さんが今見ているであろうQiitaには技術系の記事がかなりあります。<br>
Qiita内で検索することでより求めている情報が手に入りやすいです。<br>
ググった後にここに行き着くことも多々ありました。
- <a href="https://hacknote.jp/" target="_blank">hacknote</a><br>
直接hacknote内で検索することはありませんでしたが、ググった後に行き着いて記事を参考にすることもありました。

では実際に調べて行きましょう。
\$1=は何を表しているか。「\$1 シェル」などでググります。<br>すると以下のようなページに行き着きました。

<a href="https://shellscript.sunone.me/parameter.html">Unix・Linuxシェルスクリプトコマンドリファレンス</a>

> \$1 - \$n	シェルスクリプト実行時に指定した引数の値がそれぞれ設定される変数。 1番目に指定した引数は \$1 に、2番目に指定した引数は \$2 に、n 番目に指定した引数は \$n に設定される。

要するにシェルを実行する際に「./backup.sh 引数」とすることで\$1に値が入るということです。
そして今回はproductionという引数かstagingという引数の時それぞれのディレクトリのパスを指定しています。
こうやって情報を探し出し1個ずつ潰して行きます。
全部書いていくと長くなるので今回はとりあえずシェルを書きます。
調べることは非常に時間がかかりますが、必ず身につくのでここは時間をかけてしっかり行いましょう。

###4.とりあえずシェルを書いてみる

まず完成したシェルを見て行きましょう。

```bash:backup.sh

#! /bin/sh
if [ $1 = "production" ]
then
  echo "本番環境のバックアップを実行します。"
  path_to_exec='/home/username/backup_resources/backup_list/production'

elif [ $1 = "staging" ]
then
  echo "ステージング環境のバックアップを実行します。"
  path_to_exec='/home/username/backup_resources/backup_list/staging'
fi

emp_area=`df -h`
today=`date "+%Y%m%d"`

for dt in `grep -v ^# ${path_to_exec}/html_list.txt`
do
  arr=(`echo $dt | tr -s ',' ' '`)
  dir=`basename ${arr[0]}`
  cd ${arr[0]}
  cd ..
  if [ -z "${arr[2]}" ]
    #tarcommand="tar czf /var/www/backup/${arr[1]}.tar.gz $dir"
    tarcommand="tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir"
    echo $tarcommand
    tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir
  else
    tarcommand="tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir --exclude ${arr[2]}"
    echo $tarcommand
    tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir --exclude ${arr[2]}
  fi
done

if [ -e ${path_to_exec}/db_list.txt ]; then
  for dt_wp in `grep -v ^# ${path_to_exec}/db_list.txt`
  do
    arr_wp=(`echo $dt_wp | tr -s ',' ' '`)
    mysqlcommand="mysqldump --defaults-file=${arr_wp[1]} -u ${arr_wp[0]} -h localhost ${arr_wp[2]} > /var/www/backup/${arr_wp[3]}_${today}.dmp"
    echo $mysqlcommand
    mysqldump --defaults-file=${arr_wp[1]} -u ${arr_wp[0]} -h localhost ${arr_wp[2]} > /var/www/backup/${arr_wp[3]}_${today}.dmp
fi

backup_dir="/var/www/backup"
find ${backup_dir}/ -maxdepth 1 -regex '.*\.\(gz\|dmp\|txt\|sql\)' -mtime +28 -type f | xargs rm -rf

cd /home/username/backup_resources

php ./backup_slackbot.php $today "$emp_area";
```
####コマンドを書き出す
いきなり完成したシェルを書きたいのですが、初めに行うのは実行するコマンドを書き出すところから行います。
まずはtarのコマンドを用意します。

```terminal
tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir
```
シェルの中では変数などを扱っていますが実際に実行できるコマンドを用意します。

```terminal
tar czf /var/www/backup/projectname_2020205.tar.gz html
```
このコマンドが実行できることを確認したら、徐々に条件文などをつけてカスタマイズしていきます。
tarのオプションが分からない方は本来ならググるところですが、今回は「tar czf オプション」でググった結果の<a href="https://www.atmarkit.co.jp/ait/articles/1608/15/news015.html"  target="_blank">ここ</a>を参考にします。

\${today}はdateコマンドからyyyyMMddの形式で取得したスクリプト実行時の日付が入ります。<br>
${arr[1]}はhtml_list.txtの中にある情報をarrという配列に入れ、表示したものです。
html_list.txtにはバックアップを取得するパス、生成されるファイル名、バックアップを除外するディレクトリなどが記載されています。

```text:html_list.txt

#<バックアップパス>,<tar.gzファイル名>,<除外リストファイル名>
#例）/var/www/html,example-html,/var/www/html/exec
/var/www/html,sample_stg,
```
まずgrepコマンドでdtという変数に上記の先頭に#がついていな列の文字を入れます。
その後trコマンドで,を空白に置き換えます。そうすることでarrに配列としてarr[0]にディレクトリのパスである/var/www/html、arr[1]にバックアップを作成するときの名前、sample_stg、arr[2]に除外したいファイル、今回の場合は何も入りません。

実際にtarで毎週データを取るとなると、膨大な容量になる場合もありますので、週次のバックアップからは除外したいディレクトリも出てくると思います。
除外したいファイルがあった場合はコマンドを付け足さなければなりません。
なのでtarコマンドをもう一つ用意します。

```terminal
tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir --exclude ${arr[2]}
```

除外したいファイルがある場合のtarコマンドとない場合のtarコマンドの2種類が用意できましたので、if文を使って処理を分岐させてあげます。

```bash
if [ -z "${arr[2]}" ]
    tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir
```
まずは${arr[2]}に文字列がないとき、つまり除外したいファイルがない場合のコマンドを書きます。

```bash
else
    tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir --exclude ${arr[2]}

```
次に除外するファイルがある場合は--excludeで除外してあげます。

さらに肉付けしていきます。
html_list.txtファイルからディレクトリ情報を取得します。
バックアップを取得するにはそのディレクトリまで移動する必要があるのでcdします。
コマンドが実行されたか確認するためにechoコマンドも追加します。
ファイル名の日付を設定するコマンドも追記します。
これで大枠はできました。

```bash

　today=`date "+%Y%m%d"`

　for dt in `grep -v ^# ${path_to_exec}/html_list.txt`
  dir=`basename ${arr[0]}`
  cd ${arr[0]}
  cd ..
 if [ -z "${arr[2]}" ]
    tarcommand="tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir"
    echo $tarcommand
    tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir
  else
    tarcommand="tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir --exclude ${arr[2]}"
    echo $tarcommand
    tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir --exclude ${arr[2]}
  fi
```

最後にシェルを実行する際に渡す引数によって、本番環境かステージング環境のパスを設定するか条件分岐をさせてあげます。

```bash
if [ $1 = "production" ]
then
  echo "本番環境のバックアップを実行します。"
  path_to_exec='/home/username/backup_resources/backup_list/production'

elif [ $1 = "staging" ]
then
  echo "ステージング環境のバックアップを実行します。"
  path_to_exec='/home/username/backup_resources/backup_list/staging'
fi

today=`date "+%Y%m%d"`

　for dt in `grep -v ^# ${path_to_exec}/html_list.txt`
  dir=`basename ${arr[0]}`
  cd ${arr[0]}
  cd ..
 if [ -z "${arr[2]}" ]
    tarcommand="tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir"
    echo $tarcommand
    tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir
  else
    tarcommand="tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir --exclude ${arr[2]}"
    echo $tarcommand
    tar czf /var/www/backup/${arr[1]}_${today}.tar.gz $dir --exclude ${arr[2]}
  fi

```
backup_list配下にproductionとstagingのディレクトリを作成し、html_list.txtをそれぞれ置いてあげます。
ひとまずはtarコマンドでバックアップを取得するスクリプト完成です。

次はmysqldumpファイルを取得するスクリプトを作成していきます。
基本的にはコマンドが変わるだけでやり方は同じですので手短にいきます。

まずはじめにmysqldumpを取得するコマンドを用意します。

```terminal
mysqldump --defaults-file=${arr_wp[1]} -u ${arr_wp[0]} -h localhost ${arr_wp[2]} > /var/www/backup/${arr_wp[3]}_${today}.dmp
```
arr_wpにはhtml_list.txtと同様にdb_list.txtにmysqlに必要な情報が書いてあります。

```text:db_list.txt
#<ユーザ名>,<パスワード>,<DB名>,<ダンプファイル名>
#例）rootd,Take0203,wordpress,testsite-wordpress
user_stage,/home/user/.stage.cnf,stage_db,project_stg

```

これを先ほどと同じ要領でarr_wpに入れてmysqldumpコマンドで使います。
続けて条件ですが、mydqldumpを必要としないサーバーもあると思いますので、db_list.txtがある場合だけ処理を行いたいです。そのための条件をつけます。

```bash
if [ -e ${path_to_exec}/db_list.txt ]; then
  for dt_wp in `grep -v ^# ${path_to_exec}/db_list.txt`
  do
    arr_wp=(`echo $dt_wp | tr -s ',' ' '`)
    mysqlcommand="mysqldump --defaults-file=${arr_wp[1]} -u ${arr_wp[0]} -h localhost ${arr_wp[2]} > /var/www/backup/${arr_wp[3]}_${today}.dmp"
    echo $mysqlcommand
    mysqldump --defaults-file=${arr_wp[1]} -u ${arr_wp[0]} -h localhost ${arr_wp[2]} > /var/www/backup/${arr_wp[3]}_${today}.dmp
fi
```
これでmysqldumpファイルも取得できるコマンドができました。

バックアップファイルは取り続けているとサーバーを圧迫してしまうと思うので、今回は28日をすぎたファイルは削除してあげるように記述をします。

```terminal
find ${backup_dir}/ -maxdepth 1 -regex '.*\.\(gz\|dmp\|txt\|sql\)' -mtime +28 -type f | xargs rm -rf
```

最後に今回の記事では使わないのですが、ディスク容量をチェックするコマンドとそれらをslackに通知するアプリケーションのコマンドを記載し、今までのコマンドをマージすれば完成です。

実行するときは、./backup.sh [production]or[staging]という風に引数を指定してあげます。


###5.仕事の進め方・質問の仕方・シェルの作り方

####仕事の進め方
基本はトライアンドエラーでどんどん試していきます。
一個一個コマンドを実行する際には報告をします。
もしrmコマンドなどが入っていて必要なフォルダを消してしまっては大問題です。
少なくともなんのコマンドを実行したかの履歴は残しておきましょう。

####質問の仕方
まずは質問する前に分からないことは調べます。
そして質問する際は自分がどこまで分かっていて、どこが分かっていないのかを把握してから質問します。
これをすれば相手も何を教えればいいか把握できます。
調べ方が分からない、調べる用語が分からない場合は思い切って質問しましょう。

####シェルの作り方
最初は参考になるシェルなどを読み込みます。
読み込むことでそのあと自分がやりたいことがかけるようになります。
そしてシェルを書くときは実行したいコマンドから書き、そのあとに条件など考えていきます。
そして動かなかったら参考になるシェルがどのような処理をしているかを読んで、なぜ自分のコードが動かないか確かめます。<br>

###6.メモを取ってやったことをまとめる

調べたコマンドなど用語などをまとめましょう。またどんなことで躓いたかなども書いておきます。
メモを取ることで次回同じ作業を行った時に詰まった場所や分からなかったコマンドの解決が早くなります。
調べたサイトなども貼っておくと便利だと思います。
メモに使えるアプリケーションなども教えてもらいました。
今回はDropbox Paperを使いました。Markdownも使えて便利です。

- <a href="https://www.dropbox.com/ja/paper" target="_blank">Dropbox Paper</a>
- <a href="https://evernote.com/intl/jp" target="_blank">Evernote</a>

###7.作ったスクリプトを2台目のサーバにも対応させる作業、スクリプトの改善

1台目の作業がうまく行ったら2台目のサーバーに適応させるために改修を行います。
修正すべき点はディレクトリ名、ユーザー名、リストファイルです。
そして一つのスクリプトが完成した時には気づかなかったミスや改善点が見つかります。
今回の例でいうと/var/www/backup/と何回も書いている場所は
backup_dir="/var/www/backup"を用意してあげて、${backup_dir}と書くことで
次回別サーバのバックアップ対象が/var/backupになったら1行の修正だけで済みます。
findコマンドの直前に書いてあるのにもっと早く使えることに気づきませんでした。
また、シェルを実行する際に引数を私のではなくreadコマンドなどで入力を受け付ける方式などにした方が、使う人にとっては分かりやすいのではないかと思いました。

#まとめ
今回のインターンでは私はシェルについては知っている程度で一人で書くことは到底できませんでした。調べ方や進め方も正直分からなかったです。
ですが、全然分からないことでも1つ1つ潰して手を動かしていけば理解できるようになることが分かったので、未経験の方やシェルを書いたことない人、コマンドすら知らない人でもシェルは怖くないということを伝えたいです！
また、一つ分からないことを理解できるようになると他のことにも応用がきいてくるので他の言語や分野に挑戦できます。
これをきっかけとして、シェルだけでなく触ったことのない言語やプログラムに挑戦する人が増えればいいなと思います。