リモート拠点のグローバルIPアドレスの変更を検知し通知する

# はじめに

今回は、リモート拠点にあるLinuxベースの機器で、リモート拠点側のグローバルIPアドレスの変更を検知し、Slackに通知してもらう方法について書きたいと思います。

# システム概要図

簡単ですが、今回のシステム構成図です。

![システム概要図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1236186/af5333cf-5bfb-7053-b599-f2dd4c48e363.png)

※この構成図はLucidchartを利用させてもらい作成しました。(無料枠)
　Webサービスなのでもっさりした感はありましたが、UIが直観的でスムーズに作成することができました。

大まかなフローは以下の通りです。

1. グローバルIP問い合わせ
1. 通知(新しいグローバルIP)
1. 確認
1. SSH接続(ポートフォワード)

## グローバルIP問い合わせ

グローバルIPアドレスの取得と変更を検出するスクリプトをcronで定期的に動かします。
今回はRaspberryPiを利用した場合の説明を記載します。
cron＋シェルスクリプトが動くLinuxベースの機器であれば問題ありません。(Armadilloなど)

なお今回利用するNginxは、自社サービス向けに立てているWebサーバーを間借りして利用しています。
おそらく今回程度の利用(不特定多数の公開はしない or アクセス数が少ない)であればt2.microの無料枠でも実現可能かもしれません。

## 通知(新しいグローバルIP)

シェルスクリプトでグローバルIPアドレスの変更を検知した場合に、その情報をSlackに投稿します。
今回、これまた諸事情もあり旧式ではありますがWebHookの仕組みを利用します。(ここで紹介するのはWebHookのURLおよびcurlコマンド例だけで、詳細については省きます)

## 確認

Slackに投稿されると通知が来ますので、変わったことを知ることができます。

## SSH接続(ポートフォワード)

ポートフォワードによって、グローバルIPアドレスベースでリモート拠点内のRaspberry Pi・各種デバイスにSSH接続します。(ここではそういう方式でSSH接続しますよという説明だけで、詳細については省きます)

# 詳細説明

このシステム構成の中で主に紹介するのは、

* グローバルIPアドレスを取得するNginxの設定
* グローバルIPアドレスをチェックし、変更を検知したら通知するシェルスクリプト

の2点です。
仰々しくシステム構成図まで作成しましたが、なんてことはない、手順①だけの説明でしたね。

## グローバルIPアドレスを取得するNginxの設定

昨今、オフィスや拠点のグローバルIPアドレスを知るサービスはあまたあります。
今回はシェルスクリプト内から利用するので、curlなどのCLIで確認できるサービスになりますが、

httpbin.org/ip
ifconfig.io
inet-ip.info

などなど。
詳しく知りたい方は「curl グローバルIP 確認」などで検索してみてください。

私も実はちょっと前まで上記のサービスの一つを利用していました。

ですが応答が空になったことがあったり、またこの手のサービスは公開終了などが考えられましたので、自前で立てる方法はないかなと調べたら、なんとNginxの設定でサクッとできることが分かったので、今回はその方法を記載しようと思います。難しいスクリプトは作らずnginx.confに数行を追加するだけのシンプルなものです。

具体的な設定は以下の通りです。(後半の4行のみ。その他は追加場所が分かりやすいように記載しているだけです)

```Text:nginx.conf
http {
    server {
        location / {
            root   D:/nginx/html;
            index  index.html index.htm;
        }

        location /remote_addr {
            default_type text/plain;
            return 200 "$remote_addr\n";
        }
```

これだけ。
この設定を間借りしているAmazon Linux 2上のNginxに追加します。

この設定は、

```
http://xxx.secual-inc.com/remote_addr
```

へのリクエストがあったらレスポンスBodyに$remote_addrの内容を返すだけの設定です。

## グローバルIPアドレスをチェックし、変更を検知したら通知するシェルスクリプト

```Bash:
#!/bin/sh

WORKDIR="/public/toolz/check_globalip"
RESULT_NOW=$WORKDIR"/check_globalip_now.txt"
RESULT_PREV=$WORKDIR"/check_globalip_prev.txt"
RESULT_DIFF=$WORKDIR"/check_globalip_diff.txt"
RESULT_JSON=$WORKDIR"/check_globalip_json.txt"

LOCKFILE=$WORKDIR"/check_globalip.lock"
if [ -f $LOCKFILE ]; then
  exit 0
fi
trap "{ rm $LOCKFILE; exit 255; }" EXIT
touch $LOCKFILE

curl http://xxx.secual-inc.com/remote_addr > $RESULT_NOW

if [ -s $RESULT_PREV ]; then
  diff $RESULT_NOW $RESULT_PREV > $RESULT_DIFF
  if [ ! -s $RESULT_DIFF ]; then
    exit
  fi
fi

cat $RESULT_NOW > $RESULT_PREV

echo 'payload={"channel": "#service_checker", "username": "service checker", "text": "@here グローバルIP変更通知 \n```' > $RESULT_JSON
cat $RESULT_NOW | sed -z 's/\n/\\n/g' >> $RESULT_JSON
echo '```", "icon_emoji": ":guardsman:", "link_names" : true}' >> $RESULT_JSON

curl -s -X POST -d @$RESULT_JSON https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx
```

ロックファイルやtrapの話は、是非以下の記事を参照ください。

シェルスクリプト＋ロックファイルで二重実行防止 - Qiita
https://qiita.com/SECUAL_masa/items/5b1b7e8c4ed13c420435

それ以外については、個別に説明していきます。

```Text:変数定義
WORKDIR="/public/toolz/check_globalip"
RESULT_NOW=$WORKDIR"/check_globalip_now.txt"
RESULT_PREV=$WORKDIR"/check_globalip_prev.txt"
RESULT_DIFF=$WORKDIR"/check_globalip_diff.txt"
RESULT_JSON=$WORKDIR"/check_globalip_json.txt"
```

今回のグローバルIPアドレス変更検知は、"今取得したIPアドレスと前回取得したIPアドレスとの比較"で実施しています。各情報はファイルに保存しているので、そのファイルの変数を定義しています。

```Text:グローバルIPアドレスの取得
curl http://xxx.secual-inc.com/remote_addr > $RESULT_NOW
```

"/remote_addr"は、上記nginx.confの"location /remote_addr {"のことですね。
こうすることでグローバルIPアドレスがcheck_globalip_now.txtに書き出されます。

```Text:新しく取得したIPと前回のIPとを比較
if [ -s $RESULT_PREV ]; then
  diff $RESULT_NOW $RESULT_PREV > $RESULT_DIFF
  if [ ! -s $RESULT_DIFF ]; then
    exit
  fi
fi
```

各ファイルに保存された情報をdiffで比較して、その結果をcheck_globalip_diff.txtに保存しています。

* 比較結果が一致(前回と一緒)　　　　　　 → check_globalip_diff.txtのファイルサイズは0　　　-> 中断
* 比較結果が一致(前回と異なる==変更有)　→ check_globalip_diff.txtのファイルサイズは0以外　-> 続行

```Text:次回用に保存
cat $RESULT_NOW > $RESULT_PREV
```

現在の結果を次回の比較用として保存しています。

```Text:Slack投稿用JSONデータの構築
echo 'payload={"channel": "#service_checker", "username": "service checker", "text": "@here グローバルIP変更通知 \n```' > $RESULT_JSON
cat $RESULT_NOW | sed -z 's/\n/\\n/g' >> $RESULT_JSON
echo '```", "icon_emoji": ":guardsman:", "link_names" : true}' >> $RESULT_JSON
```

Slackに投稿するJSONデータを構築しています。
JSONフォーマットについては、Slackの仕様を確認してください。

```Text:Slackへ投稿
curl -s -X POST -d @$RESULT_JSON https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx
```

前述で構築したJSONをWebHook URLを呼び出して投稿(POST)しています。

## cron

あとはスクリプトをcronで指定して定期的に実行するようにします。

```Text:cron
0 */1 * * * /home/pi/tools/check_globalip/check_globalip.sh > /dev/null 2>&1
```

## Slackへの通知

今回の仕組みで最終的に以下のようにSlackに投稿されます。

![投稿結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1236186/de527683-a7ee-ae9e-7731-10e1f4026f05.png)


# なぜこの仕組みが必要だったか？

一般的にリモート拠点のメンテナンスを行う場合はリモート拠点側のグローバルIPアドレスを固定にし、VPNを利用します。しかし今回は

* 固定のIPアドレスが取得できない(グローバルIPアドレスはDHCP)
* ルーターはコンシューマー向けのWiFiルーター

ということもあり、ポートフォワード(*1)によりリモート拠点内のデバイスにアクセスするようにしました。
そのためにリモート拠点側のグローバルIPアドレスが変わった場合はそのグローバルIPアドレスを知る必要がありました。

*1：YAMAHAのルーターなど、IPSecを利用したVPNで片方の拠点のグローバルIPアドレスがDHCPの場合でも利用可能な方法(設定)はありますが、今回はリモート拠点側のルーターがコンシューマー向けのWiFiルーターということもあり、ポートフォワードを採用しました。