GCEのメタデータサーバーを愛でる ~GCEメタデータのすべて~

GCPで1番好きなサービスはGAEです。でも2番目はメタデータサーバーな大橋です。
書く書く詐欺をし続けてとうとう書くことになりました。

皆さんGCEの[メタデータサーバー](https://cloud.google.com/compute/docs/metadata)使ってますか？
使ってない? 使ってないなら今日から使いましょう。もったいない事この上ないです。

今回は私の大好きなメタデータサーバーの概要や使い方について書いていきたいとも思います。
## まとめ

3行でMetadata Serverをまとめると

* GCEの様々な情報が乗っているHTTP(curlなども可)アクセス可能なデータサーバ 
* Key-Value形式で好きなデータを投入可能
* 更新検知が可能

です。
これだけシンプルですが非常に強力で、利用シーンがとても多いです。

## 概要

メタデータサーバーはGCEのインスタンスや、プロジェクトのメタデータを保存しているサーバです。
Web APIが用意されており、GCEのインスタンスからであれば**認証無し**でアクセスが可能です。

もしGCEをよく触る方であれば、[Startup Script](https://cloud.google.com/compute/docs/startupscript)や[Shutdown Script](https://cloud.google.com/compute/docs/shutdownscript)、[SSH-key](https://cloud.google.com/compute/docs/instances/managing-ssh-keys)の設定などをしたことがあるかもしれませんが、
これらの情報はすべてメタデータサーバに保存されており、
それぞれのタイミングでGCEのインスタンスがメタデータサーバにアクセスし、情報を取得・利用しています。

メタデータサーバへアクセスするURLは決まっており

* Full URL: http://metadata.google.internal/computeMetadata/v1/
* Shorthand URL: http://metadata/computeMetadata/v1/
* IP address: http://169.254.169.254/computeMetadata/v1/

のいずれかとなっています。 これらのホスト名はインスタンスが起動する際に/etc/hostsに書き込まれます。

よくGCEのネットワーク周りを設定する方だと
このIPアドレス(`169.254.169.254`)がL4LBのヘルスチェックの際に利用されるIPだと気がつくかもしれません
メタデータサーバはそういった方法でも利用されています。

## GCEの様々な情報が乗っているHTTP(curlなども可)アクセス可能なデータサーバ 

### Project metadataとInstance metadata

メタデータには**Project metadata**と**Instance metadata**が存在し、
Project metadataはプロジェクト全体のインスタンスから、
Instance metadataはそのインスタンスからのみ取得可能となっています。

### Default metadata keys

GCEでは予めKeyが決まっている[Default metadata](https://cloud.google.com/compute/docs/metadata#default)が存在します。
Default metadataはReadonlyな情報ですが、GCEインスタンスが立ち上がった時点で既に利用可能になっています。

すべてのkeyを紹介すると多いので詳しくは上のリンクを参照していただきたいですが、
この`key`をベースにURIがきまり、メタデータサーバ上からデータを取得できます。

例えばProject metadataのプロジェクトIDであれば
` http://metadata/computeMetadata/v1/project/project-id`
で、Instance metadataのPublic IPアドレスであれば
` http://metadata/computeMetadata/v1/instance/network-interfaces/0/ip`

また
`http://metadata/computeMetadata/v1/instance/network-interfaces/`
などslashで終わるURIでアクセスすると、
ディレクトリに対するアクセスとなり、中に含まれるkeyの一覧が取得できます。


### データの取得

KeyのURIを利用してHTTP経由でメタデータを取得するには、HTTPヘッダーに以下を追加します。

```
Metadata-Flavor: Google
```

例えば`curl`利用してメタデータを取得する場合は以下のようになります。

```
curl "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip" -H "Metadata-Flavor: Google"
```

### クエリ

メタデータサーバへのアクセス時にクエリパラメータを追加すると様々なフォーマットでデータを取得できます。

#### alt

`alt`パラメータを利用する返却値のフォーマットを**テキスト**か**json*にできます。

```bash

$ curl "http://metadata.google.internal/computeMetadata/v1/instance/tags" -H "Metadata-Flavor: Google"
["https-server","http-server","front","env","blue"]

$ curl "http://metadata.google.internal/computeMetadata/v1/instance/tags?alt=text" -H "Metadata-Flavor: Google"
https-server
blue
http-server
front
env
```

#### recursive

`recursive`パラメータを利用するとディレクトリへのアクセスを再帰的に処理して、子供のKeyの値をすべて取得できます。

```bash
$ curl "http://metadata.google.internal/computeMetadata/v1/instance/disks/?alt=text&recursive=true" -H "Metadata-Flavor: Go
ogle"

0/device-name instance-1-boot
0/index 0
0/mode READ_WRITE
0/type PERSISTENT

$ curl "http://metadata.google.internal/computeMetadata/v1/instance/disks/?alt=json&recursive=true" -H "Metadata-Flavor: Go
ogle"
[{"deviceName":"instance-1-boot","index":0,"mode":"READ_WRITE","type":"PERSISTENT"}]
```

## カスタムメタデータ

先にも書いたとおり、メタデータには`Key-Value`形式で任意の値が設定可能です。
[Startup Script]()や[Shutdown Script]()、[SSH-key]()の設定などはこのカスタムメタデータは`Key`が決められたカスタムメタデータといえます。

### カスタムメタデータの設定

カスタムメタデータを設定する方法は大きく分けて3種類あります。

* Developer Consoleで設定する
* gcloudコマンドを利用する
* APIを呼び出す

Developer Consoleでの設定は左メニューのメタデータからプロジェクトメタデータが、
GCEインスタンスの編集からインスタンスメタデータが設定できます。

頻繁に更新する必要が無い場合はDeveloper Consoleからの設定で十分だと思います
以降は`gcloud`を使った設定方法を説明します。

#### インスタンスメタデータの設定

インスタンスを立ち上げる際に設定する場合は `gcloud compute instances create`コマンドにて`--metadata KEY=VALUE`フラグを付与します。

```bash
$ gcloud compute create new-hoge-instance --metadata hogekey:fugavalue
```

またStartup Scriptのようにデータサイズが大きい場合は`--metadata-from-file KEY=LOCAL_FILE_PATH`を利用します。

```bash
$ gcloud compute create new-hoge-instance --metadata-from-file env_setting:env_setting.json
```

既に立ち上がっているインスタンスのメタデータを更新する場合は `gcloud compute instances add-metadata`コマンドを利用します。
削除する場合は`remove-metada`になります。

```bash
$ gcloud compute instances add-metadata exist-instance --metadata hogekey:fugavalue
$ gcloud compute instances remove-metadata exist-instance --metadata hogekey:fugavalue
```

#### プロジェクトメタデータの設定

ほとんどコマンドはインスタンスのメタデータと一緒です。 `gcloud compute instances`を`gcloud compute project-info`に変更します。

```bash
$ gcloud compute project-info add-metadata --metadata hogehogekey:fugafugavalue
$ gcloud compute project-info remove-metadata --metadata hogehogekey:fugafugavalue
```

#### カスタムメタデータの取得

カスタムメタデータを取得する場合はデフォルトメタデータと同じようにHTTP経由で取得できます。URIは`attributes/KEY`の形式になります。

```bash
$ curl "http://metadata.google.internal/computeMetadata/v1/instance/attributes/hogekey" -H "Metadata-Flavor: Google"
fugavalue
$ curl "http://metadata.google.internal/computeMetadata/v1/project/attributes/hogehogekey" -H "Metadata-Flavor: Google"
fugafugavalue
```


## 更新検知

メタデータサーバではメタデータが更新されたことを検知するために「レスポンスをデータが更新されるまで待たせる」(`wait-for-change`)事ができます。
これを利用するには`wait_for_change=true`パラメータを付与します。

```bash
$ curl "http://metadata.google.internal/computeMetadata/v1/instance/tags?wait_for_change=true" -H "Metadata-Flavor: Google"
```

上記では `tags` が更新されるまで`curl`は戻ってきません。実際にこのインスタンスのタグを更新すると上記リクエストが帰ってくることが確認できると思います。
なおディレクトリに対して`wait_for_change=true`を利用する場合`recursive=true`パラメータが必須となります。

```bash
$ curl "http://metadata.google.internal/computeMetadata/v1/instance/attributes/?recursive=true&wait_for_change=true" -H "Metadata-Flavor: Google"
```

### ETagを利用して確実に更新された事を取得する

`wait-for-change`を利用する際、
1. `wait-for-change`リクエストを投げる
2. 更新を待つ
3. レスポンスが戻る
4. 処理をする
5. 1.に戻る

というような更新検知の仕組みを作ることが多いと思いますが、
この際 3-5の間に値が更新されてしまうと、 1.に戻ってきた際この更新を検知することができなくなってしまいます。

この問題を解決するためには`last_etag`パラメータと`ETag`ヘッダーを利用します。
メタデータサーバがレスポンスを返却する際に、`ETag` HTTPヘッダーを返却します。
この値を次のクエリの際に`last_etag`パラメータとして渡すとこの`ETag`時点よりデータが最新であればレスポンスが返却されるようになります。
また`last_etag`パラメータに`0`を設定すると即時に値が返却されます。

これを利用して以下の様なループを作成すると常に更新検知を行う仕組みを作成できます。

```py
import httplib2

METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/"

def main():
  http = httplib2.Http()
  tagsUrl = METADATA_URL + "instance/tags?wait_for_change=true"

  # 初回は0を設定
  last_etag = 0
  while True:
    resp,content = http.request(uri=tagsUrl + "&last_etag=" + str(last_etag), method="GET", body="", headers={"Metadata-Flavor": "Google"})

    if resp != 500:
      # エラーがない限りはlast_etagを更新
      last_etag = resp["etag"]
      print content

if __name__ == "__main__":
  main()
```


## こんな使い方

これらの仕組みを利用すると、オートスケール環境での複数サーバへのアプリケーションデプロイに利用できます。

例えば

1. プロジェクトメタデータに`version`というメタデータを用意し、アプリケーションバージョン番号を設定しておく。
2. 各インスタンスがStartup Script時に`version`メタデータに対して、`wait-for-change`リクエストを投げて更新検知
3. アプリケーションが更新されたら、CIなどでインスタンスから見れる場所に配置 (例えばDockerであればCloud Registry等)、`version`メタデータを更新する
4. 各インスタンスが`version`メタデータが更新されたことを検知し取得したアプリケーションバージョン番号から新しいアプリケーションを取得・デプロイ
5. 再度次の更新を待つ

の様な流れが作れます。
簡単なコードを書くと以下の感じです。

```py
import httplib2

METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/"

def main():
  http = httplib2.Http()
  tagsUrl = METADATA_URL + "project/attributes/version?wait_for_change=true"

  # 初回は0を設定
  last_etag = 0
  while True:
    resp,content = http.request(uri=tagsUrl + "&last_etag=" + str(last_etag), method="GET", body="", headers={"Metadata-Flavor": "Google"})

    if resp != 500:
      # エラーがない限りはlast_etagを更新
      last_etag = resp["etag"]
      deploy(content)

def deploy(version):
    ...

if __name__ == "__main__":
  main()
```

かなり簡単な仕組みで複数サーバへのアプリケーションデプロイが行えるようになります。
またStartup Scriptと混ぜることで、オートスケール環境でも常に足並みの揃ったアプリケーションバージョンのインスタンスを用意することが可能になります。


## 注意点とイケていない点

経験上の注意点としてカスタムメタデータはデータの保存や更新時に単一の`KEY:VALUE`を設定しているわけではなく、
保存されているすべてのカスタムメタデータを毎回保存しにいっているように見えます。
※特にAPIコールの場合は設定したい一部ではなくすべてのカスタムメタデータを送信する。

このため、複数のインスタンスから頻繁にメタデータサーバを更新すると同期例外や、古いデータで更新される可能性があります。
例えば
「オートスケールなどを設定しており、インスタンスが立ち上がったタイミングでプロジェクトメタデータにIPを保存する」
のようなメタデータ運用をした場合複数のインスタンスが同時にメタデータサーバに書き込みをしようとし、
失敗したり古いデータが登録されるケースが有ります。
このため可能であればプロジェクトメタデータの設定を行うのは決められたサーバだけなどにしておくと幸せなことが多いです。

また、データの取得は`curl`経由で可能ですが、データの登録は`curl`経由でやるにはかなりめんどくさい点も行けてないですね


## 最終的なまとめ

メタデータを利用するとかなりな部分を自動化することが簡単にできるようになります。
また単純なHTTPでアクセスが可能なので扱いやすい言語で自動化ができるようになります。

GCEで利用する簡易なKey-Value Storeとしてはベストな選択肢なのではないでしょうか？