Name Service SwitchでConoHaインスタンスのIPアドレスを引く

この記事は [ConoHa Advent Calendar 2019](https://qiita.com/advent-calendar/2019/conoha) 20日目の記事です。

[GMOホスコン　ユーザーミートアップ！](https://gmohoscon.connpass.com/event/128889/)で話した内容を再編集したものになります。

# はじめに

同じConoHaアカウント内に存在するインスタンスのIPアドレスをシュッと引きたくないですか？引きたいです。
たとえば`[nametag].conoha`のようなホスト名でインスタンスのIPアドレスを引けるようなったら、いろいろ活用方法がありそうです。

ということで、これをどう実現するかを考えてみます。

# 選択肢

## /etc/hostsに書いておく

- Pros
	- 最もシンプル
	- トラブルシュートが楽そう
- Cons
	- インスタンスを増減させるたびに更新する手間がある
	- すべてのインスタンスにhostsを配布する手間がある
		- 別途仕組み化する必要がありそうです

## 内部DNSを立てる

- Pros
	- hostsと比較すると、インスタンスが増減した際にDNSサーバ側だけ変更すれば良くなる
- Cons
	- インスタンスを増減させるたびに更新する手間がある
	- そもそもDNSを管理したくない

## Consulやそれに準ずるツールを使う

一番現実的な選択肢に思えます。商用環境でもよく利用されています。

- Pros
	- インスタンスを増減させても手放しでいける
- Cons
	- Consulクラスタを運用管理する手間

## mDNS (avahi) を使う

ローカルネットワーク内でのIPアドレスに限れば、シンプルで良さそうな選択肢に見えます。

- Pros
	- インスタンスを増減させても手放しでいける
- Cons
	- 採用事例が少なそう
	- トラブルシュートが大変そう

## Name Service SwitchとAPI(ConoHa API)を組み合わせる

これはIaaS側のAPIや、IPMIなんかでマシンのIPアドレスを拾ってこれる場合に利用できそうです。

- Pros
	- インスタンスを増減させても手放しでいける
- Cons
	- 採用事例がない
	- トラブルシュートが大変

Consが最悪ですが、今回はこの方法でどの様に冒頭に述べたアレを実現するかを考えていきます。

# Name Service SwitchとConoHa API

## Name Service Switch (NSS)

> Name Service Switch とは、UNIX系OSにおいて、システムが使用する各種情報の検索順を指定するために使用される。NSSと略されることが多い。たとえば、ユーザ情報は、/etc/passwd、NIS、LDAPなどに格納されているが、システムに問い合わせられたユーザ情報をどの情報源から、どの順番で検索するかを指定する。
（Wikipediaより）

といったような、システムが使う情報のプロバイダを切り替えたり、新たに定義したりできる仕組みのことです。
UNIXやそれに準ずるシステムではだいたい使われています。

ユーザー情報以外にも、ホスト名に対応するアドレスのルックアップでもNSSが利用されており、先程例に上げた`/etc/hosts`やmDNSも、実はこのNSSを介して情報が取得されています！

大抵は、たとえばmdnsならばlibnss-mdnsのようなモジュールをどこからか拾ってきて導入して使う、という場合が多いです。
このような情報を提供する側のプログラムを自分で作成することはあまりないのですが、本記事ではこのモジュールを自作してしまおう！という目論見です。

## ConoHa API

ConoHa VPSを利用されている方にはおなじみかと思いますが、ConoHaはOpenStackベースであり、これに準じたAPIが提供されています。
もちろん、ここからインスタンスに紐ついたIPアドレスも取得することができます。

したがって、ホスト名からアドレスを引く要求を受け取ったときにConoHa APIを叩いてアドレスを取得し、これを返答するようなNSSモジュールを作成すれば、当初の目標が達成できそうです。

## NSSモジュールを書いてみる

👇のドキュメントが詳しいです。
https://www.gnu.org/software/libc/manual/html_node/Extending-NSS.html

もろもろを端折ってざっくり解説してしまうと、

```c
enum nss_status _nss_hoge_gethostbyname_r (const char *name, struct hostent *ret, char *buf, size_t buflen, int *errnop, int *h_errnop);
enum nss_status _nss_hoge_gethostbyname2_r (const char *name, int af, struct hostent *ret, char *buf, size_t buflen, int *errnop, int *h_errnop);
```

といったようなインタフェースを持つ関数を含んだ共有ライブラリを特定の場所にインストールすると、[gethostbyname(3)](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/gethostbyname.3.html)の呼び出しをフックできる、と考えるとわかりやすいかと思います。

たとえば、`****.hoge`という問い合わせに対して、仮にアドレス`1.14.5.14`を回答するNSSモジュールは以下のように書けます。
`name`が問い合わせを受けているホスト名です。

```c
enum nss_status _nss_hoge_gethostbyname2_r (const char *name, int af, struct hostent *ret, char *buf, size_t buflen, int *errnop, int *h_errnop) {
	*errnop = *h_errnop = 0;

	if (af != AF_INET) {
		return NSS_STATUS_NOTFOUND;
	}
	if (strstr(name, ".hoge") == NULL) {
		return NSS_STATUS_NOTFOUND;
	}

	ret->h_name = (char*)name;
	ret->h_aliases = calloc(sizeof(char*), 1);
	ret->h_addrtype = af;
	ret->h_length = 4;
	ret->h_addr_list = calloc(sizeof(char*), 2);
	unsigned char *addr = ret->h_addr_list[0] = calloc(sizeof(char), 4);
	addr[0] = 1; addr[1] = 14; addr[2] = 5; addr[3] = 14;

	return NSS_STATUS_SUCCESS;
}
```

### C言語がつらい件

わかる。Golangで書きたいですよね。
先述したインタフェースを持った共有ライブラリとして出力できればいいので、cgoをいい感じに使えばできます。
`go build -buildmode=c-shared`としてビルドすればOKです。

👇ここにサンプルがありますので、ご覧ください。
https://github.com/kaz/cgo-nss

## ConoHa APIと統合する

[Compute APIのこの辺](https://www.conoha.jp/docs/compute-get_vms_detail.php)を叩くと、`.servers[].addresses[].addr`にIPアドレスが入っています。
ローカルネットワークでのIPアドレスを返すか、グローバルのアドレスを返すか、はたまた問い合わせホスト名を工夫してどちらかを返すか……などはがんばりどころですが、今回は横着してざっくり全部のアドレスを返す実装にします。

実装したものがこちらになります。
https://github.com/kaz/libnss_conoha_api

Golangで実装しています。外部のAPIと通信して、JSONをデコードして……というのはCだとけっこうつらいので、このあたりはGoだとシュッと書けて捗ります。

## 使ってみる

ビルドして`/lib`に放り込んで……

```
[root@25ad398d1f2 libnss_conoha_api]$ export NSS_CONOHA_REGION=tyo1
[root@25ad398d1f2 libnss_conoha_api]$ export NSS_CONOHA_TENANT_ID=*****
[root@25ad398d1f2 libnss_conoha_api]$ export NSS_CONOHA_USERNAME=*****
[root@25ad398d1f2 libnss_conoha_api]$ export NSS_CONOHA_PASSWORD=*****
[root@25ad398d1f2 libnss_conoha_api]$ make
go build -buildmode=c-shared -o libnss_conoha.so.2 *.go
strip libnss_conoha.so.2
install -m 0755 libnss_conoha.so.2 /lib
```

`/etc/nsswitch.conf`を編集して、作ったモジュールを読み込むようにします。
（`hosts:`の行に追加しました。）

```
# Name Service Switch configuration file.
# See nsswitch.conf(5) for details.

passwd: files mymachines systemd
group: files mymachines systemd
shadow: files

publickey: files

hosts: conoha files mymachines myhostname resolve [!UNAVAIL=return] dns
networks: files

protocols: files
services: files
ethers: files
rpc: files

netgroup: files
```

すると……

<img width="857" alt="image11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150934/76533a41-2d28-fee9-7bcd-99b214d2a457.png">
<img width="809" alt="image10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150934/ba13571e-449d-1b09-ce14-b4faf4fe6733.png">

ちゃんとConoHaのネームタグからIPアドレスが引けています！

# おわりに

NSSを使うとサクッとOSの機能を拡張できて便利ですね！
今回の`hostname`以外にもユーザやグループもNSSで拡張できたりするので、夢が広がります。