mackerel-agentでのAWSインテグレーション周りのソースコード(主にCustomIdentifier)

自分用のメモ

![logo.png](https://qiita-image-store.s3.amazonaws.com/0/20668/acbf5c13-e6bc-ee0f-f00f-4416660c0854.png)

さくらのクラウドとMackerelの連携ツール「[sackerel](https://github.com/sacloud/sackerel/)」を実装した時のメモ。

---

[mackerel-agent](https://github.com/mackerelio/mackerel-agent/blob/002d9512fc794df20a8a78a664e0e8b672684b07/command/command.go#L472)の0.34.0にてAWSインテグレーションができるようになったそうです。
詳細は[Mackerelブログ8月19日の記事](https://mackerel.io/ja/blog/entry/weekly/20160819)にありました。

エージェント側でどういう風にインテグレーションを行っているか気になったので調べました。

**以下は執筆時点でのmasterブランチ最新コミット[(cd6a67a)](https://github.com/mackerelio/mackerel-agent/tree/cd6a67acfa4970d21891885b01a165e6e399a498)を元に調査しています。
今後の開発により処理方法が変更になる可能性があります。**

### ホスト情報の収集(command/command.go)

それらしいものが以下にありました。

引用元 : [command/command.go#L472](https://github.com/mackerelio/mackerel-agent/blob/002d9512fc794df20a8a78a664e0e8b672684b07/command/command.go#L472)

```go:command/command.go
// collectHostSpecs collects host specs (correspond to "name", "meta", "interfaces" and "customIdentifier" fields in API v0)
func collectHostSpecs() (string, map[string]interface{}, []spec.NetInterface, string, error) {
	// (省略)

	// 479行目〜
	cGen := spec.SuggestCloudGenerator()

	// (省略)

	// 485行目〜
	var customIdentifier string
	if cGen != nil {
		customIdentifier, err = cGen.SuggestCustomIdentifier()
		if err != nil {
			logger.Warningf("Error while suggesting custom identifier. err: %s", err.Error())
		}
	}

	// (省略)
```

`collectHostSpecs()`はMackerelエージェントがホストの情報を収集するためのもののようです。
その中で`CustomIdentifire`という値の収集のために`spec.SuggestCloudGenerator()`を呼んでいます。
こいつの`SuggestCustomIdentifier()`が`CustomIdentifier`を返してくれるようですね。

`CustomIdentifire`はマニュアルなどに記載はないですが、MackerelのホストIDとは別に
任意のIDを持たせるためのフィールドのようです。
`GET /api/v0/hosts`などの、ホスト情報参照APIでもレスポンス内に値がきちんと含まれていました。

それでは呼ばれる先の`spec.SuggestCloudGenerator()`を見てみましょう。

### クラウド事業者ごとにCustomIdentifireの生成をする(spec/cloug.go)

引用元 : [spec/cloud.go#L47](https://github.com/mackerelio/mackerel-agent/blob/master/spec/cloud.go#L47)

```go:spec/cloud.go
// SuggestCloudGenerator returns suitable CloudGenerator
func SuggestCloudGenerator() *CloudGenerator {
	if isEC2() {
		return &CloudGenerator{&EC2Generator{ec2BaseURL}}
	}
	if isGCE() {
		return &CloudGenerator{&GCEGenerator{gceMetaURL}}
	}

	return nil
}
```

`CustomIdentifire`を生成するためのジェネレータを作成して返すファクトリメソッドです。
ここで`isEC2()`または`isGCE()`でプラットフォームを判定し、適切なジェネレータを作成しています。

もし対応クラウドプラットフォームを増やしたい場合はここに追加対応ですね！
([過去のソース](https://github.com/mackerelio/mackerel-agent/commit/f527e708550fef3119709505af0d72576e56b2da)にDigitalOcean対応の残骸があったりします)

引用元 : [spec/cloud.go#L59](https://github.com/mackerelio/mackerel-agent/blob/master/spec/cloud.go#L59)

```go:spec/cloug.do
func isEC2() bool {
	cl := http.Client{
		Timeout: timeout,
	}
	// '/ami-id` is may be aws specific URL
	// (筆者注 : ec2BaseURL := "http://169.254.169.254/latest/meta-data")
	resp, err := cl.Get(ec2BaseURL.String() + "/ami-id")
	if err != nil {
		return false
	}
	defer resp.Body.Close()

	return resp.StatusCode == 200
}
```

[メタデータ](http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ec2-instance-metadata.html)を取得して判定していますね。
EC2であるかの判定で、`/ami-id`をリクエストすることでOpenStackなどと区別するようですね。

実際の`CustomIdentifire`生成処理は以下にあります。

引用元 : [spec/cloud.go#L156](https://github.com/mackerelio/mackerel-agent/blob/master/spec/cloud.go#L156)

```go:spec/cloud.go
// SuggestCustomIdentifier suggests the identifier of the EC2 instance
func (g *EC2Generator) SuggestCustomIdentifier() (string, error) {
	client := http.Client{Timeout: timeout}
	key := "instance-id"
	resp, err := client.Get(g.baseURL.String() + "/" + key)

	// (省略)

	instanceID := string(body)

	// (省略)

	return instanceID + ".ec2.amazonaws.com", nil
}
```

メタデータからインスタンスIDを取得し、`インスタンスID.ec2.amazonaws.com`を返すだけです。


AWSやGCE以外のクラウドプラットフォーム対応する場合はここも実装が必要ですね。

---

収集した`CustomIdentifier`は以下でMackerelへ送信されます。

#### ホスト情報の登録

引用元 : [command/command.go#L29](https://github.com/mackerelio/mackerel-agent/blob/002d9512fc794df20a8a78a664e0e8b672684b07/command/command.go#L501)

```go:command/command.go

// prepareHost collects specs of the host and sends them to Mackerel server.
// A unique host-id is returned by the server if one is not specified.
func prepareHost(conf *config.Config, api *mackerel.API) (*mackerel.Host, error) {
	// (省略)

			// (66行目〜)
			doRetry(func() error {
				hostID, lastErr = api.CreateHost(mackerel.HostSpec{
					Name:             hostname,
					Meta:             meta,
					Interfaces:       interfaces,
					RoleFullnames:    conf.Roles,
					DisplayName:      conf.DisplayName,
					CustomIdentifier: customIdentifier,
				})
				return filterErrorForRetry(lastErr)
			})

	// (省略)

}
```

#### ホスト情報の更新

引用元 : [command/command.go#L501](https://github.com/mackerelio/mackerel-agent/blob/002d9512fc794df20a8a78a664e0e8b672684b07/command/command.go#L501)

```go:command/command.go
// UpdateHostSpecs updates the host information that is already registered on Mackerel.
func (c *Context) UpdateHostSpecs() {
	// (省略)

	hostname, meta, interfaces, customIdentifier, err := collectHostSpecs()

	// (省略)

	err = c.API.UpdateHost(c.Host.ID, mackerel.HostSpec{
		Name:             hostname,
		Meta:             meta,
		Interfaces:       interfaces,
		RoleFullnames:    c.Config.Roles,
		Checks:           c.Config.CheckNames(),
		DisplayName:      c.Config.DisplayName,
		CustomIdentifier: customIdentifier,
	})

	// (省略)
}
```

---

というわけで、実装を見てみました。
mackerel-agent側でカスタムIDを生成すると何が嬉しいかというと、AWSインテグレーションみたいなエージェントレス監視に対し、後からエージェントをインストールした時に同一ホストかの判定ができるんですね。

さくらのクラウドとMackerelの連携ツール「[sackerel](https://github.com/sacloud/sackerel/)」がまさにこれが必要でした。

以上です。