通信状況が悪いネットワーク環境で名前解決の通信をもなくして負荷軽減を図る

# はじめに

少々特殊ではありますが、以下の図のようなネットワーク構成を持つ環境があったとします。

![WiFiのブリッジ構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1236186/1d0dbac8-5ea6-c981-1612-bab7820cefae.png)

1. 「管理施設」にプロバイダからの終端装置やメインとなるルーター(GW/DHCP/DNS/WiFi AP)がある。
2. 少し離れた位置に「リモートエリアA、B」があり、「管理施設」～「リモートエリアA」～「リモートエリアB」はWiFi APでブリッジ接続している。
3.  各リモートエリアにはIPカメラが存在している。(リモートエリアAには「IPカムA」、リモートエリアBには「IPカムB」)
4.  IPカメラからは定期的にクラウド側(上図からは省いています)のAPIを呼び出している。

この環境においてWiFiの状況が少し不安定な状況下ということもあり、上記No4のAPI呼び出しが失敗することがあります。
API呼び出しの失敗にはHTTP通信エラー(タイムアウト)などがありますが、URLに含まれるホスト名の名前解決(正引き)が失敗することもありました。

API呼び出しそのものが失敗すること自体も解決したいところですが、まずはAPI呼び出し前に発生している名前解決が失敗することがあることをどうにかしたいなと思い、その対策について記載していきたいと思います。

# 解決案

解決案を模索する中で思いつたのが、

* DNSキャッシュのTTLを延ばす。
* Proxyを立てる。
* hostsファイル名にホスト名とIPアドレスを記載し、名前解決にDNSを使わないようにする。

の3つでした。

# DNSキャッシュのTTLを延ばす

DNSサーバー側で設定しているTTLが5分程度でしたので、これを1日にするなどすれば日中は名前解決を行わなくなります。ただLet's Encrypt＋ワイルドカードを利用している関係もあって更新時にその時だけTTLを5分に戻す、などが少々面倒なので却下。(ここでその詳細については省きます)

# Proxyを立てる

「管理施設」にフォワードProxyを立てれば、名前解決はProxy側で行ってくれますのでIPカメラ自身が名前解決する必要がなくなります(ここも詳細については省きます)ので、IPカメラ自身が名前解決しなくて済むという目的は達成できます。

ただし、

* 管理施設内に専用の機器が必要。(実はArmadilloやRaspberryPiがあるのでできなくはなかった)
* フォワードProxyの環境構築が必要。(これも本案を断念した理由の1つ)
* IPカメラ側にProxy設定が必要。(これも諸事情あり本案を断念した理由の1つ)

ということで却下。

# hostsファイル名にホスト名とIPアドレスを記載し、名前解決にDNSを使わないようにする

この案を実現するためには、

* 名前解決
* hosts作成
* hosts転送(配置)

が必要になりますが、

* 名前解決および↑の実現はスクリプトで実施すればよく、管理施設内にはArmadilloやRaspberryPiがありますのでそこで実行すればOK。(既に別のスクリプトも動かしているし)
* hosts転送だけなら転送量も微々たるもの(数十バイト)で、間隔を数時間～1日などにすれば転送のための通信コストも許容できるのでOK。
* IPカメラへ手を加える必要がない。(/etc/hostsを置き換えるだけで即座に解決)

ということで諸事情を踏まえこれがベストな案と判断しました。

ただし本案の懸念事項は名前解決順です。
Windows、Mac、Linuxでも名前解決をする優先順序(hostsを使うかDNSを使うか)が決まっています。
名前解決にDNSを優先する設定になっていた場合、その優先順を変更する対応も必要になってきます。(前述した「IPカメラ側にProxy設定が必要。(これも諸事情あり本案を断念した理由の1つ)」相当に近い対応が必要になりそうなので避けたかった)

さてIPカメラはどうでしょう？
幸いにも今回のIPカメラはLinuxライクなOSを持つデバイスで、UNIX系OSで各種情報の解決を行うName Service Switch(nss)が利用されていました。そこで /etc/nsswitch.conf を確認してみます。

```Text:名前解決順(/etc/nsswitch.confから抜粋)
hosts:          files mdns4_minimal [NOTFOUND=return] dns mdns
```

他のLinuxディストリビューションと同じでhostsファイルが優先的に使わているようです。(ここでは"files"が"dns"よりも先に記載されている==hostsファイルが優先的に使われる、とご理解ください)

```Text:1口メモ
これまでにも /etc/nsswitch.conf を見る機会はありましたが、ふとmdisってなんじゃらほい？と思い立ち調べてみました。
マルチキャストDNSの略のようで、それ自体の仕組みを理解できたのはもとより、Apple謹製のソフトを入れると見かけるソフト/プロセスの名前を見かけ、そういうことかーと納得する場面も。
是非みなさんも調べてみてください。(ということで上記 /etc/nsswitch.conf の内容についても省きます)
```

ということで本案を採用＆進めるにあたり懸念点も払拭できましたので実装を進めます。

できたスクリプトが以下です。

# スクリプト

```Bash:deploy_hosts.sh
#!/bin/bash

WORKDIR="/home/pi/tools/deploy_hosts"
MAXRETRY=10

# ------------------------------------------------------------------------------
# ホスト(FQDN)リスト
# ------------------------------------------------------------------------------

HOSTLIST=(
    "api-a.secual-inc.com"
    "api-b.secual-inc.com"
)

# ------------------------------------------------------------------------------
# IPカメラリスト
# ------------------------------------------------------------------------------

TARGETLIST=(
    "IpCamA 192.168.1.11"
    "IPCamB 192.168.1.12"
)

# ------------------------------------------------------------------------------
# 二重起動防止
# ------------------------------------------------------------------------------

LOCKFILE=$WORKDIR"/deploy_hosts.lock"
if [ -f $LOCKFILE ]; then
    echo "Already exist lock file. ["$LOCKFILE"]"
    exit 9
fi
trap '{
    rm $LOCKFILE;
}' EXIT
touch $LOCKFILE

# ------------------------------------------------------------------------------
# 名前解決とhosts生成と配置
# ------------------------------------------------------------------------------

HOSTS_ORG=$WORKDIR/"hosts"           # base hosts
HOSTS_NEW=$WORKDIR/"hosts.new"       # for work.

RESOLVETIME=0

for TARGET in "${TARGETLIST[@]}"; do
    # ------------------------------------------------------------------------------
    # 名前解決とhosts生成
    # ------------------------------------------------------------------------------

    NOWTIME=`date +%s`

    echo "RESOLVE="$((RESOLVETIME + 3600))", NOW="$NOWTIME

    if [ $((RESOLVETIME + 3600)) -le $NOWTIME ]; then
        echo "UPDATE HOSTS"

        cp $HOSTS_ORG $HOSTS_NEW

        for HOST in "${HOSTLIST[@]}"; do
            RESULT_HOST=`host -4 -R 3 -s $HOST | grep "has address" | sed -e "s/.* has address //g"`

            NEW_HOST_ENTRY=""
            for IP in $RESULT_HOST; do
                echo $IP" "$HOST >> $HOSTS_NEW
                # use only first entry. ex) 5x.1xx.6x.2x2.
                break
            done
        done

        RESOLVETIME=`date +%s`
    else
        echo "DO NOT UPDATE HOSTS"
    fi

    # ------------------------------------------------------------------------------
    # 配置
    # ------------------------------------------------------------------------------

    TARGETMAP=(${TARGET[@]})
    TARGETNAME=${TARGETMAP[0]}
    TARGETIP=${TARGETMAP[1]}

    echo "##### TARGETNAME="$TARGETNAME", TARGETIP="$TARGETIP

    ssh-keygen -f "/home/pi/.ssh/known_hosts" -R $TARGETIP > /dev/null 2>&1

    ERRCOUNT=0

    # deploy

    while [ $ERRCOUNT -lt $MAXRETRY ]
    do
        sshpass -f /home/pi/.ssh/pass_ipcam scp -o "StrictHostKeyChecking no" $HOSTS_NEW root@$TARGETIP:/etc/hosts
        RESULT_SCP=$?
        if [ $RESULT_SCP -eq 0 ]; then
            ERRCOUNT=0
            break
        else
            ERRCOUNT=$(($ERRCOUNT+1))
        fi

        sleep 30s
    done
done
```

それでは各ロジックについて解説していきます。

# 各ロジックの解説

## ホスト(FQDN)リスト

```bash
HOSTLIST=(
    "api-a.secual-inc.com"
    "api-b.secual-inc.com"
)
```

IPカメラが利用するホスト(FQDN)を列挙しておきます。


## IPカメラリスト

```bash
TARGETLIST=(
    "IpCamA 192.168.1.11"
    "IPCamB 192.168.1.12"
)
```

対象となるIPカメラのIPアドレスを列挙しておきます。

## 二重起動防止

詳細は以下をご覧ください。

https://qiita.com/SECUAL_masa/items/5b1b7e8c4ed13c420435


## 名前解決とhosts生成

```bash
    # ------------------------------------------------------------------------------
    # 名前解決とhosts生成
    # ------------------------------------------------------------------------------
```

ここは分けて解説します。

### 再解決

```bash
    NOWTIME=`date +%s`

    echo "RESOLVE="$((RESOLVETIME + 3600))", NOW="$NOWTIME

    if [ $((RESOLVETIME + 3600)) -le $NOWTIME ]; then
　　　　・
　　　　・
　　　　・

        RESOLVETIME=`date +%s`
    else
        echo "DO NOT UPDATE HOSTS"
    fi
```

冒頭で「WiFiの状況が少し不安定」と書きました。
ひょっとするとhostsの配置がスムーズに上手くいかず、リトライしている間にIPアドレスが変わってしまう可能性があるかもしれませんので、それを視野に入れてある程度時間が経ってしまった場合は再度名前解決するようにしました。

ということで、上記ロジックはその判定です。

### 名前解決とhosts生成

```bash
        cp $HOSTS_ORG $HOSTS_NEW

        for HOST in "${HOSTLIST[@]}"; do
            RESULT_HOST=`host -4 -R 3 -s $HOST | grep "has address" | sed -e "s/.* has address //g"`

            NEW_HOST_ENTRY=""
            for IP in $RESULT_HOST; do
                echo $IP" "$HOST >> $HOSTS_NEW
                # use only first entry. ex) 5x.1xx.6x.2x2.
                break
            done
        done
```

名前解決の方法としてhostコマンドを利用しました。
hostコマンドの結果は以下のようになりますので、その結果からIPアドレスだけを取り出し\$HOSTS_NEWに出力しています。

```bash:hostの結果
# ex) result of host command.
root@raspberrypi:/srv/deploy_hosts# host api-xxx.secual-inc.com
api-xxx.secual-inc.com has address 5x.1xx.6x.2x2
api-xxx.secual-inc.com has address 5x.6x.9x.2x4
```

複数のIPアドレスが返却される可能性もあるので、1行目を優先的に利用するようにbreakしています。

## 配置

```bash
    # ------------------------------------------------------------------------------
    # 配置
    # ------------------------------------------------------------------------------
```

ここも分けて解説します。

### 署名削除

```bash
    ssh-keygen -f "/home/pi/.ssh/known_hosts" -R $TARGETIP > /dev/null 2>&1
```

このIPカメラはFW更新でホストのフィンガープリントが変わり、次のsshやscpで警告が出る場合があります。
それを回避するために事前にknown_hostsから該当エントリ(フィンガープリント、公開鍵)を削除しています。

### 配置

```bash
    # deploy

    while [ $ERRCOUNT -lt $MAXRETRY ]
    do
        sshpass -f /home/pi/.ssh/pass_ipcam scp -o "StrictHostKeyChecking no" $HOSTS_NEW root@$TARGETIP:/etc/hosts
        RESULT_SCP=$?
        if [ $RESULT_SCP -eq 0 ]; then
            ERRCOUNT=0
            break
        else
            ERRCOUNT=$(($ERRCOUNT+1))
        fi

        sleep 30s
    done
```

生成したhostsを無条件にIPカメラに配置しています。
またリモートエリアを繋ぐブリッジ接続のWiFi APの故障やIPカメラの故障を視野に入れ、ここはリトライ回数に制限を設けることで回避しました。

またここで、

* \$HOSTS_LASTと\$HOSTS_NEWを比較し、異なる場合のみ配置したのでいいのでは？
* IPカメラのhostsと比較して、違いがある場合だけ配置したのでいいのでは？

という疑問もでるかもしれません。

前者については、リトライとリトライの中断にも関係しますが、成功するまでリトライをし続けている訳ではありませんので、IPカメラのhostsファイルが\$HOSTS_NEW に更新されていないことを視野に入れる必要があります。また\$HOSTS_LASTと\$HOSTS_NEW に違いが出るまで配置処理が走らないことにも視野に入れる必要があります。

また後者については、IPカメラ側のhostsとローカルの\$HOSTS_NEWとを比較する通信コストを考えた場合、そもそも\$HOSTS_NEW のサイズは数十KB でもあるので一律無条件に配置してしまった方が通信コストは低い、と判断したためです。ですのでここは \$HOSTLIST のエントリ数に応じて変更する必要が出てくるかもしれません。

# 補足

必要に応じてIPカメラのhostsをバックアップした方がよいケースもあるかと思います。

```bash:IPカメラのhostsバックアップ
    # backup

    while [ $ERRCOUNT -lt $MAXRETRY ]
    do
       RESULT=`sshpass -f /home/pi/.ssh/pass_ipcam ssh -o "StrictHostKeyChecking no" root@$TARGETIP "[ ! -e /etc/hosts.org ] && cp -rp /etc/hosts /etc/hosts.org || ls -la /etc/hosts*"`
       RESULT_SSH=$?
       # echo "SSH="$RESULT_SSH
       if [ $RESULT_SSH -eq 0 ]; then
           ERRCOUNT=0
           break
       else
           ERRCOUNT=$(($ERRCOUNT+1))
       fi
    
       sleep 30s
    done
```

その場合は、上記に記載のスクリプトの

```bash
    ERRCOUNT=0

    # deploy
```

の "ERRCOUNT=0" と "# deploy" の間に上記のバックアップロジックを入れ、

```bash
    ERRCOUNT=0

    # backup

    while [ $ERRCOUNT -lt $MAXRETRY ]
    do
　　　・
　　　・
　　　・
    done


    # deploy
```

とするとよいです。


# 定期実行(cron)

最後にcronに以下の設定を行い定期的に実施するようにします。

## 例1(1時間毎)

```cron
0 */1 * * * /home/pi/tools/deploy_hosts/deploy_hosts.sh
```

## 例2(4:00と14:00の1日2回)

```cron
0 4 * * * /home/pi/tools/deploy_hosts/deploy_hosts.sh
0 14 * * * /home/pi/tools/deploy_hosts/deploy_hosts.sh
```