新卒アプリ運用エンジニアが学んだ，オペミスをしないためのサーバオペレーションに対する心構え


# この記事はなに？

今年の4月に，Web系企業に新卒で就職し，新卒研修を経て6月からそこそこの規模の内製サービスを扱うエンジニアとして働いています．

巷ではJenkinsなどのCIツールが全盛期，と言った感じですが，自動化するほどでもない作業や，メンテナンス等の繊細さが求められる作業では，まだまだ手動でサーバにログインしてのオペレーション（手動オペレーション，手オペ）が必要です．

ここでは，1年弱の間いろいろな機会で学んだオペレーションに対するあれこれについて，今自分が考えていることをまとめたいと思います．

## 注釈

* 本記事の対象者は，「サーバは触ったことあるしコマンドもある程度知ってるけど，実際にサービスが動いているサーバを触る経験は少ない人」です（自分がそうだったので）．経験のある方には釈迦に説法かと．
* 本記事での「アプリ運用エンジニア」とは，バックエンドのAPIやフロントエンドのWebアプリ，バッチプログラムなどを保守・運用し，必要に応じて障害対応やメンテナンスなどを行うエンジニアのことを指します．
* 基本的にLinuxサーバに対するオペレーションが対象ですが，他のOSに対しても転用できる知識はあると思います．

# 自動化すべき作業は自動化

そもそもですが，2010年台も半分を過ぎたこのご時世，**「手オペを行うことそのものが邪道」**というのが私の考えです．
各種CIツールやデプロイツールなどを積極的に利用し，定形作業については，できるだけサーバに入らず済ませられるようにすべきです．

これにより，作業の手間を減らすのはもちろん，作業をコードとして記すことで，手順書で表現するのは難しい確認のロジックを明確に示し，オペミスを減らすことができます（もちろん，システムに理解がある人のレビューがあって成り立つものです）．

目安として，ある程度の頻度で繰り返し行うことが決まっており，かつ，手動で3回以上作業を行い，手順がある程度明確になってきた作業については，自動化を検討すべきでしょう．

# 手順書をしっかり書く

ここからは，前述の「自動化すべき作業」に該当しないもの，つまり，1回きりしか行わないことがわかっている作業や，まだ手順が確定しきっていない作業についての話になります．

今となっては「当たり前だ」と思うのですが，私が配属されて初めて行ったリリースで，先輩に手順書のレビューを受け，真っ赤になって返ってきた手順書を見て，「ここまでする必要があるのかなぁ…」と思ったのは事実です．

これまで，サービスが動いている本番サーバでの作業などしたことのなかった自分に，手順書の重要性を理解することなど到底できなかったのです．

手順書を書くメリットとして，

1. 自分の記憶力に頼らず，上から順にコマンドを投げていくだけで，オペレーションが完了する
2. しっかり書いた手順書を渡せば，他人に作業させることも可能
3. 手順書を書く過程で，確認すべき項目や手順のミスに気づける
4. ドキュメントとして残しておけば，将来類似の作業をする人のために役立つ

などがあります．

1について，手オペが長くなればなるほど，ミスをする確率が増加します（一度やってみるとわかりますが，手順の一つ一つは単調な作業であることがほとんどなので，思いの外すぐに集中が切れます）．
「ああ，あのファイルがあそこにあるはずで，このファイルがここにあるから，とりあえずdiffを取って…」のように，行き当たりばったりで作業をすると，必ずどこかでミスると思った方がいいかと思います．

しっかりとした手順書を書くことで，基本的に上から順にコマンドをコピペして投げていくだけでよくなり，集中が切れても問題なく作業を進められるようになります．

2について，特に企業などでは，ある程度定型化したオペレーションについては作業者に任せ，自分の稼働を他の仕事に回すということが往々にしてあります．
一度手順書を書いてしまえば，（もちろんある程度の事前説明は必要かと思いますが）そういったことも容易になるかと思います．

3について，これはわりと重要だと思うのですが，手順を書く内に，「ああ，ここの確認も必要だな」とか，「ここの設定変更忘れてた」など，かなりのミスが見つかります（文章の推敲と同じですね）．
単純なミスについては，手順書を書く過程でそのほとんどを潰せるのではないかと思います．

4について，これはドキュメント全般に言えることですが，一度形にして残しておけば，将来同様の作業をする人（1ヶ月後の自分も含む）に対しても，大きな助けとなるでしょう．

これを読んでいる方の中に，「手順書を書くのは初めて」という方がいれば，一度自分でそれっぽいものを書いてみた上で，それを検証環境で試し，先輩にレビューしてもらうと，勉強になるのではないかと思います．

# 手順書を書く際のコツ

以上，手順書の重要性について述べたのですが，自分で何度かやってみて，**「手順書を書く上で，どういった所が重要なのか，その勘所」**がよくわかりませんでした．
ここからは，先輩からレビューを受ける中で，自分なりに考えた「オペミスをしないためのコツ」みたいなものについて記します．

## 確認事項を（ちゃんと）明記する

まず最初に，「そのコマンドで何をしたいのか」はできるだけ手順書中に明記するようにしましょう．

例えば，

```bash
ls -l test.txt
  # test.txtが存在することを確認
```

のように，コマンドのすぐそばに（ソースコードへのコメントのように）確認事項を書いておくと，確認を怠らずにオペレーションを進めることができ，かつ，他人に見せる際にもわかりやすい手順書になるでしょう．
最初は面倒かと思いますが，これを一度できるようになっておくと，**「自分でも何をやっているかわからなくなる」**といったことはなくなるはずです．

また，もう少し手順が長くなってきた場合は，手順書をいくつかのセクションに分けるようにすると，よりわかりやすくなるでしょう．

一例を置いておきます．

以下は，あるバッチプログラムbatch.shを実行し，結果を確認するまでの手順を記した手順書です．
batch.shは，自分と同一のディレクトリに存在する入力ファイルinput.txtを受け取って処理を行い，結果として出力ファイルoutput.txtを出力します．

```bash
### （以下，作業ディレクトリに，バッチへの入力ファイル/path/to/work/dir/input.txtが存在するものと仮定）


### 入力ファイルのチェック
cd /path/to/work/dir/
pwd; ls -l
  # コマンドのtypoなどによりディレクトリの移動に失敗していないことを確認
  
ls -l input.txt
  # input.txtが存在しており，所有ユーザ，パーミッションが正しいことを確認
wc -l input.txt
  # 入力ファイルの行数が想定通りであることを確認
head input.txt
  # 入力ファイルの内容（先頭から10行）が想定通りであることを確認
  # （ファイルの行数が少ない場合は，catやlessなどでの確認でもよい）
tail input.txt
  # 入力ファイルの内容（末尾から10行）が想定通りであることを確認
###


### バッチを実行する
## 入力ファイルを作業ディレクトリからバッチが存在するディレクトリにコピーする
cd /path/to/the/dir/batch/exists
pwd; ls -l
  # コマンドのtypoなどによりディレクトリの移動に失敗していないことを確認

ls -l /path/to/work/dir/input.txt input.txt
  # /path/to/work/dir/input.txt（作業ディレクトリにあるinput.txt）のみが存在し，カレントディレクトリにinput.txtが存在しないことを確認
  
cp -ip /path/to/work/dir/input.txt input.txt

ls -l /path/to/work/dir/input.txt input.txt
  # /path/to/work/dir/input.txt，input.txtの両方が存在することを確認
diff /path/to/work/dir/input.txt input.txt
  # 差分がないことを確認
##

## バッチの実行
ls -l batch.sh
  # batch.shが存在しており，所有ユーザ，パーミッションが正しいことを確認
  
./batch.sh
  # バッチを実行
##
  
## **バッチ実行中，別窓にて**
tail -f /path/to/log/dir/log.txt
  # ログを確認し，エラー等がないことを確認
##

## バッチの実行結果を確認
ls -l output.txt
  # output.txtが存在することを確認
wc -l output.txt
  # 出力ファイルの行数が想定通りであることを確認
less output.txt
  # 出力ファイルの内容に問題がないことを確認
##
###
```

処理をブロックに分割することにより，ただコマンドを羅列するより見やすくなることがわかるかと思います．
また，不幸にもオペレーションの途中で別の作業が入り，作業を中断することになったような場合でも，ある程度の区切りまで進めておくことで，どこから再開すればいいかが明確になります．

## 参照系，更新系コマンドを（ちゃんと）意識する

Linuxには，cat，less，head，tail，grepなど，ファイルの内容や状態を参照するだけの「参照系」のコマンドと，cp，mv，rm，mkdir，touch，viなど，ファイル/ディレクトリを作成，変更し，サーバ内部の内容を書き換える「更新系」のコマンドが存在します．

Linuxを少し触ったことのある方はご存知かと思いますが，特にcp，mv，rm，と言ったコマンドは，（ゴミ箱やダイアログボックスの表示などの救済措置のある他のOSとは違い，）エンターキー1つで問答無用で他のファイルを上書き/削除する，**非常に危険な**コマンドです．
かく言う自分も，cpとrmに5回ずつぐらい泣かされたことがあります（泣）
参照系と更新系のコマンドの違いをちゃんと意識するだけで，オペミスをする確率はぐっと減ります．

ファイルを書き換える更新系のコマンドについては，

実行前に，

* 予期しないファイルを上書き/削除しないか
* ファイルを上書きする場合は，上書きする内容が正しいかどうか

実行後に，

* ファイルのコピー/削除をした場合は，しっかりとコピー/削除ができているか（パーミッションの問題や，ディスクの容量の問題など，様々な予期せぬ理由でコマンドがうまく実行されないことがあります）
* ファイルの所有ユーザやパーミッションの情報が正しいかどうか

の観点から，チェックをする必要があります．
ここで重要になるのが，各種参照系のコマンドです．

個人的におすすめなのは，

### ファイルを作成する場合

```bash
### test.txtを作成する

ls -l test.txt
  # test.txtが存在しないことを確認
  
touch test.txt
  # （作成．
  # tipsだが，更新系コマンドの前後で1行空ける癖をつけておくと，実行する前に気をつけるようになってよい）
  
ls -l test.txt
  # test.txtが存在することを確認
```

### ファイルをコピー（バックアップ）する場合

```bash
### test.txtの内容をtest.txt.backupにバックアップする

ls -l test.txt test.txt.backup
  # test.txtが存在し，test.txt.backupは存在しないことを確認

cp -ip test.txt test.txt.backup
  # （上書き．
  # cp -iは，上書きする際に本当に上書きしてもよいかユーザに問い合わせる．
  # cp -pは所有ユーザ，グループ，パーミッション，タイムスタンプを保持したままコピーする）
  
ls -l test.txt test.txt.backup
  # test.txt，test.txt.backupの両方が存在し，所有ユーザ，パーミッションが正しいことを確認
diff test.txt test.txt.backup
  # test.txt，test.txt.backupの差分がないことを確認
```

### ファイルを上書きする場合

```bash
### test.txt.newの内容で，test.txtの内容を上書きする

ls -l test.txt.new test.txt
  # test.txt.new，test.txtの両方が存在することを確認
diff test.txt.new test.txt
  # test.txt.new test.txtの差分を確認
  # （想定される差分の内容をあらかじめ書いておくとよりよい）
  
cp -ip test.txt.new test.txt
  # （上書き）
  
ls -l test.txt.new test.txt
  # test.txt.new test.txtの両方が存在し，所有ユーザ，パーミッションが正しいことを確認
diff test.txt.new test.txt
  # test.txt.new test.txtの差分がないことを確認
```

### ファイルを削除する場合

```bash
### test.txtを削除する

ls -l test.txt
  # test.txtが存在することを確認
  
rm -i test.txt
  # （削除．rm -iは，削除する際に本当に削除してもよいかユーザに問い合わせる）

ls -l test.txt
  # test.txtが存在しないことを確認
```

みたいな感じで手順書を書くことです．

こうすれば，現在のサーバの状態について余計な心配をしなくても，上から順にコマンドを実行して逐一出力を確かめていくだけで，安全なファイルの更新が可能です（それでも，更新系コマンドを叩く際には一呼吸置くようにすると良いと思います）．

また，このフレームワークを覚えておけば，手順書を作成する際にも，わりと思考停止的に書けるので，私のようなずぼらな人にはおすすめです．

## バックアップを（ちゃんと）取る

前述の通り，Linuxにおいて更新系のコマンドは危ないので，事前に現在のファイルのバックアップを取っておくことは非常に重要です．

自分がよく使うのは，事前にバックアップ用のディレクトリを作成し，そこにファイルのバックアップを取って，必要に応じて戻す，といった流れです．

### バックアップ

```bash
### test.txtの内容をバックアップ用のディレクトリにバックアップする

## バックアップ用のディレクトリを作成
BACKUP_DIR=~/backup/`date +"%Y%m%d"`; echo ${BACKUP_DIR}
  # "/home/user/backup/YYYYMMDD"が表示されることを確認
  # （tipsとして，オペレーション中よく使うパスなどは，事前にシェル変数に代入しておくと，再利用できてよい）
  
ls -l ${BACKUP_DIR}
  # /home/user/backup/YYYYMMDDが存在しないことを確認
  
mkdir -p ${BACKUP_DIR}
  # （ディレクトリを作成）
  
ls -l ${BACKUP_DIR}
  # /home/user/backup/YYYYMMDDが存在することを確認
##

## test.txtをバックアップ
ls -l test.txt ${BACKUP_DIR}/test.txt.`date +"%Y%m%d%H%M%S"`.backup
  # test.txtのみが存在し，${BACKUP_DIR}/test.txt.YYYYMMDDhhmmss.backupは存在しないことを確認
  
cp -ip test.txt ${BACKUP_DIR}/test.txt.`date +"%Y%m%d%H%M%S"`.backup
  # (バックアップ)
  
ls -l test.txt ${BACKUP_DIR}/test.txt.`date +"%Y%m%d%H%M%S"`.backup
  # test.txt，${BACKUP_DIR}/test.txt.YYYYMMDDhhmmss.backupの両方が存在することを確認
ls -l test.txt ${BACKUP_DIR}/test.txt.`date +"%Y%m%d%H%M%S"`.backup  
  # test.txt，${BACKUP_DIR}/test.txt.YYYYMMDDhhmmss.backupに差分がないことを確認
##
```

### リストア

```bash
### ${BACKUP_DIR}/test.txt.YYYYMMDDhhmmss.backupの内容をtest.txtに上書き（リストア）する

ls -l ${BACKUP_DIR}/test.txt.`date +"%Y%m%d%H%M%S"`.backup test.txt 
  # ${BACKUP_DIR}/test.txt.YYYYMMDDhhmmss.backup，test.txtの両方が存在することを確認
  
cp -ip ${BACKUP_DIR}/test.txt.`date +"%Y%m%d%H%M%S"`.backup test.txt
  # (リストア)
  
ls -l ${BACKUP_DIR}/test.txt.`date +"%Y%m%d%H%M%S"`.backup test.txt
  # ${BACKUP_DIR}/test.txt.YYYYMMDDhhmmss.backup，test.txtの両方が存在することを確認
ls -l ${BACKUP_DIR}/test.txt.`date +"%Y%m%d%H%M%S"`.backup test.txt  
  # ${BACKUP_DIR}/test.txt.YYYYMMDDhhmmss.backup，test.txtに差分がないことを確認
```

## ログを（ちゃんと）確認する

学生時代からプログラミングの経験はあったのですが，エラー時に原因を確かめるためにメッセージを確認したりということはあっても，しっかりとログを読むという機会はあまりなかったのではないかと思います．

多くの既製のソフトウェア（ライブラリ，フレームワークなども含む）では，ログは非常に重要な情報源です．
**「ソフトウェアを使っていて生じる問題の90％は，ログをちゃんと読んで（必要に応じてググれば）解決する」**ということを知れたのは，自分のこの1年で最も大きな学びの1つではないかと思います．

また，プログラムそのものは正常稼働していても，例えば，「実行時間が遅い」，「サーバに大きな負荷がかかる」などの問題が発見された場合，ログを通して原因を探っていくことがほとんどです．

加えて，これは忘れられがちなのですが，「ログが表示されない」ということも，エラーを解決するために重要な情報です．
その場合，そのソフトウェア自体には問題がなく，ソフトウェアの実行環境に問題があることがほとんどでしょう．

ログを閲覧するためには，tail -fや，grepと言ったコマンドが有用になってくるでしょう．
個人的には，オペレーション中にバッチなどのプログラムを動かす場合に，実行後にエラーログを確認したり，別窓でログを`tail -f`したりすることが多いです．

```bash
## バッチプログラムbatch.shを実行

cd /path/to/batch.sh

ls -l batch.sh
  # batch.shが存在することを確認
  
./batch.sh
  # （batch.shを実行）
  
ls -l /path/to/error.log
  # Errorログに何も表示されていないことを確認

## 別窓で
tail -f /path/to/info.log
  # Infoログに想定されないログが表示されないことを確認
```

実際に運用でログを利用することで，開発において「どのようなログの仕様が扱いやすいのか」について考えるようになりました．
この辺りはまだ自分の中でまとまっていないのですが，いつか記事にできればいいなと思っています．

# まとめ

* ちゃんと手順書書こう
* 事前・事後の確認を怠らないようにしよう
* 戻すための手順について考えておこう

…まあ，当たり前といえば当たり前なのですが，これを（ちゃんと）実行するのはなかなか難しいです（私もまだまだです）．
ですが，こういった細々としたことができるようになると，自分が起因でのオペミスはほぼなくなると思います．

今回の記事では，作業者の視点で，どうすればオペミスを減らせるかについての考え方を記しました．
機会があれば，手動オペレーションにおいて自分がよく使っているtipsなんかも晒せたらいいなと思っています．