Packerで作ったイメージを世代管理

この記事は [Livesense - 自 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/livesense-ji) 4日目の記事です。
本日はインフラストラクチャーグループの@nashioxがお送りします。

インフラストラクチャーグループの「自」といえばもちろん「自動化」ですね。

## Packerでイメージ作成を自動化する
リブセンスではPackerを使って「AMI」「OpenStackイメージ」などを作っています。
イメージの作成そのものはCIツールを利用してGitリポジトリのPush/Margeをフックして`packer build`を実行すれば良いわけですが、気をつけなければいけないのがイメージの削除です。

> Packer only builds images. It does not attempt to manage them in any way.

[Packerの公式ドキュメント](https://www.packer.io/intro/getting-started/build-image.html#managing-the-image)にも書いてありますが、Packerはイメージのビルドのみを行います。
そのため、何度も何度も自動でビルドされていくうちにたくさんのイメージが出来上がっていくわけです。
イメージが積み重なっていくとどれが最新のイメージかわからなくなったり、AMIの場合だとそれだけでお金がどんどんかかっていくことになってしまいます。
せっかくイメージの作成を自動化しても、削除は手動では旨味が少なくなってしまいます。

Packerを使ってイメージ作成の自動化を考えていく上ではイメージの削除・世代管理を考えることが重要になってきます。

## イメージの削除・世代管理を考える
イメージの削除・世代管理を考えるのが重要というのはわかりましたが、Packerそのものに機能がない以上どうすれば良いのか。
今回は「AMI」と「OpenStackイメージ」について考えてみました。

### AMIの場合
AMIの場合はすでに同じことを考えていた人がいらっしゃいました。
[PackerのAMI手動管理を卒業するプラグインを作ってみた](http://tech.sideci.com/entry/2016/08/18/130609)

Packerのプラグイン機構を利用して、`packer build`後の`post-processor`としてイメージの削除・世代管理を行ってくれるというものです。
https://github.com/wata727/packer-post-processor-amazon-ami-management

#### インストール
インストールは簡単。
`~/.packer.d/plugins/`以下に[ダウンロード](https://github.com/wata727/packer-post-processor-amazon-ami-management/releases)してきて配置するだけです。

```bash
### 無ければ作る
$ mkdir -p ~/.packer.d/plugins

### ダウンロード(今回はLinux amd64)
$ curl -L -O https://github.com/wata727/packer-post-processor-amazon-ami-management/releases/download/v0.2.0/packer-post-processor-amazon-ami-management_linux_amd64.zip

### 解凍
$ unzip packer-post-processor-amazon-ami-management_linux_amd64.zip

### 配置
$ mv packer-post-processor-amazon-ami-management ~/.packer.d/plugins/
```

#### 設定
Packerの設定ファイルに以下のように`post-processors`に`"type": "amazon-ami-management"`を設定することで世代管理されます。

```json
{
  "builders": [{
    "type": "amazon-ebs",
    "region": "ap-northeast-1",
    "source_ami": "ami-da9e2cbc",
    "instance_type": "t2.micro",
    "ssh_username": "ec2-user",
    "ssh_pty": "true",
    "ami_name": "packer-example {{timestamp}}",
    "tags": {
        "Amazon_AMI_Management_Identifier": "packer-example"
    }
  }],
  "provisioners":[{
    "type": "shell",
    "inline": [
      "echo 'running...'"
    ]
  }],
  "post-processors":[{
    "type": "amazon-ami-management",
    "region": "ap-northeast-1",
    "identifier": "packer-example",
    "keep_releases": "3"
  }]
}
```

`builders`の段階で`tags`に任意の`Amazon_AMI_Management_Identifier`を設定しておけば`identifier`で世代管理するイメージをフィルターすることが可能です。
作成日時の新しい順に`keep_releases`で設定した数で世代管理されます。

これで手動でイメージをポチポチ消していく必要がなくなります。

### OpenStackの場合
OpenStackの場合は色々調べてみましたが、良い方法を見つけることが出来ませんでした。

なので*packer-post-processor-amazon-ami-management*にインスパイアされて、OpenStack版も作ってみました。
https://github.com/nashiox/packer-post-processor-openstack-image-management

#### インストール
`~/.packer.d/plugins/`以下に[ダウンロード](https://github.com/nashiox/packer-post-processor-openstack-image-management/releases)してきて配置するだけです。

```bash
### 無ければ作る
$ mkdir -p ~/.packer.d/plugins

### ダウンロード(今回はLinux amd64)
$ curl -L -O https://github.com/nashiox/packer-post-processor-openstack-image-management/releases/download/v0.0.1/packer-post-processor-openstack-image-management_linux_amd64.zip

### 解凍
$ unzip packer-post-processor-openstack-image-management_linux_amd64.zip

### 配置
$ mv packer-post-processor-openstack-image-management_linux_amd64/packer-post-processor-openstack-image-management ~/.packer.d/plugins/
```

#### 設定
Packerの設定ファイルに以下のように`post-processors`に`"type": "openstack-image-management"`を設定することで世代管理されます。

```json
{
  "builders": [{
    "type": "openstack",
    "identity_endpoint": "http://<destack-ip>:5000/v3",
    "tenant_name": "admin",
    "domain_name": "Default",
    "username": "admin",
    "password": "<your admin password>",
    "region": "RegionOne",
    "ssh_username": "root",
    "image_name": "sample-image",
    "source_image": "<image id>",
    "flavor": "m1.tiny",
    "insecure": "true"
  }],
  "provisioners":[{
    "type": "shell",
    "inline": [
      "echo 'running...'"
    ]
  }],
  "post-processors":[{
    "type": "openstack-image-management",
    "identity_endpoint": "http://<destack-ip>:5000/v3",
    "tenant_name": "admin",
    "domain_name": "Default",
    "username": "admin",
    "password": "<your admin password>",
    "region": "RegionOne",
    "insecure": "true",
    "identifier": "sample-image",
    "keep_releases": "2"
  }]
}
```

`identifier`にはイメージ名を利用しています。
作成日時の新しい順に`keep_releases`で設定した数で世代管理されます。

## プラグインの実装
Packerプラグインの実装に関しては詳しくは[公式ドキュメント](https://www.packer.io/docs/extending/plugins.html)を御覧ください。
今回は作成した*packer-post-processor-openstack-image-management*の仕組みについて簡単に紹介します。

### プラグインの起動
プラグインの起動は`main`関数で以下のようにします。

```go
package main

import (
	"github.com/hashicorp/packer/packer/plugin"
	openstackimagemanagement "github.com/nashiox/packer-post-processor-openstack-image-management/openstack-image-management"
)

func main() {
	server, err := plugin.Server()
	if err != nil {
		panic(err)
	}

	server.RegisterPostProcessor(new(openstackimagemanagement.OpenStackPostProcessor))
	server.Serve()
}
```

`github.com/hashicorp/packer/packer/plugin`を利用してイメージを作成しています。
今回はイメージ作成後、古いイメージを削除していきたいので`post-processor`の仕組みを利用します。
`server.RegisterPostProcessor`でプラグインを登録します。

### イメージリストの取得
イメージのリストは以下のように取得しています。

```go
var imageList []images.Image

log.Println("Describing images for generation management")
pager := images.List(p.conn, images.ListOpts{Name: p.config.Identifier})
pager.EachPage(func(page pagination.Page) (bool, error) {
	imgs, err := images.ExtractImages(page)
	if err != nil {
		return false, err
	}

	imageList = append(imageList, imgs...)
	return true, nil
})

sort.Slice(imageList, func(i, j int) bool {
	return imageList[i].CreatedAt.After(imageList[j].CreatedAt)
})
```

`images.ListOpts`でフィルターを設定することが出来ます。
今回はイメージ名でフィルターをするので`Name: p.config.Identifier`を設定しています。

`images.List`でイメージリストのページャーが取得出来るので、`pager.EachPage`でページごとにイメージを取得、`imageList`Sliceに追加をしていきます。

そして`sort.Slice`を使って、`CreatedAt`が新しい順にソートします。

Go言語でSliceのソートをするのは結構面倒な印象だったのですが、`sort.Slice`がGo 1.8から追加されて非常に書きやすくなっていました。
https://github.com/golang/go/commit/22a2bdfedb95612984cec3141924953b88a607b7

### イメージの削除
イメージは以下のように削除・世代管理しています。

```go
for i, img := range imageList {
	if i < p.config.KeepReleases {
		continue
	}

	ui.Message(fmt.Sprintf("Deleting image: %s", img.ID))
	log.Printf("Deteting image (%s)", img.ID)
	if result := images.Delete(p.conn, img.ID); result.Err != nil {
		return nil, true, result.Err
	}
}
```

`KeepReleases`で世代数を確認し、`images.Delete`で対象となるイメージを削除しています。

## まとめ
Packerを利用するとイメージの作成が自動化されてすごく便利でしたが、削除まわりで今一歩感がありました。
しかし今回Packerのプラグイン機構を利用すればそれも簡単にできるようになりました。

ただ、Packerのプラグイン機構はまだあまり一般的ではないのか、探すのが大変でした。
Goの1バイナリを置けば動いてしまうのは簡単で良いですが、GitHubの広い海を探して回るのはなかなか大変なので何かいい方法をご存知の方がいれば教えてください。