AnsibleでCloudStackを操作する（応用編：テンプレートのカスタマイズ）

AnsibleによるCloudStackの[基礎編](http://qiita.com/atsaki/items/6db23d3f4aa3a3c5da41)として、前回はホストの作成とプロビジョニング（Nginxのインストール）を行いました。

[AnsibleでCloudStackを操作する（基礎編：仮想マシン作成とプロビジョニング） - Qiita](http://qiita.com/atsaki/items/6db23d3f4aa3a3c5da41)

今回は、応用編として既存テンプレートのカスタマイズを行います。
CloudStackモジュールの特長である対応リソース・操作の多さや
複雑な処理が書けることが活きる例ではないかと思います。

使用しているファイルは以下レポジトリにあります。

https://github.com/atsaki/idcf-custom-template

## 環境

今回は例としてIDCFクラウドを使用します。

テンプレートは環境に依存している部分が大きいので、
他の環境では変更が必要かもしれません。

## テンプレート作成の流れ

以下の流れでテンプレートを作成していきます。

1. 仮想マシン作成
2. プロビジョニング
3. 仮想マシンからボリューム作成
4. ボリューム・ファイルシステムのサイズ変更
5. ボリューム内の不要ファイル削除
6. ボリュームからテンプレート作成

## 環境の準備

Ansible・CloudStackモジュールの準備については基本的に[基礎編](http://qiita.com/atsaki/items/6db23d3f4aa3a3c5da41)と同じです。

1つ変更点として[jq](https://stedolan.github.io/jq/)をインストールしておきます。
CloudStackモジュールはすでに多くのモジュールをサポートしていますが、未対応のリソース・操作も存在します。これらの未対応のリソース・操作を```cs``` と```jq``` を使って実行します。

Dockerを使って環境を用意する場合は以下の```Dockerfile``` で```jq``` がインストールされたイメージをビルドすることができます。

```Dockerfile:Dockerfile
FROM python:2.7
RUN pip install cs ansible sshpubkeys
RUN curl -s -o /usr/bin/jq http://stedolan.github.io/jq/download/linux64/jq && \
    chmod +x /usr/bin/jq
RUN mkdir -p /usr/share/ansible && \
    cd /usr/share/ansible && \
    git clone https://github.com/resmo/ansible-cloudstack.git
RUN mkdir -p /etc/ansible ; \
    echo "[defaults]"                                       > /etc/ansible/ansible.cfg; \
    echo "host_key_checking = False"                       >> /etc/ansible/ansible.cfg; \
    echo "library = /usr/share/ansible/ansible-cloudstack" >> /etc/ansible/ansible.cfg
```

## 各種設定ファイル

今回使用する設定ファイルについて説明します。

### Inventoryファイル

今回の例では、CloudStackのパスワード・SSH鍵設定機能を使えるテンプレートでディスクを拡張したもの(`large_disk`グループ）とVagrantから使用するために`vagrant` ユーザーの設定等を行うテンプレート（`vagrant_box`グループ）を作成します。

**※今回作成するテンプレートはIDCフロンティアが提供しているVagrant用テンプレートとは別のものです。**

それぞれを、`CentOS 6.6`、`CentOS 7.1`、`Ubuntu 14.01`について作成するので、以下の6つを作成することになります。

|名前|OS|ディスク(GB)|Vagrant用|
|:--:|:--:|:--:|:--:|
|CentOS66-30G|CentOS 6.6|30|No|
|CentOS71-30G|CentOS 7.1|30|No|
|Ubuntu1401-30G|Ubuntu 14.01|30|No|
|CentOS66-Vagrant-30G|CentOS 6.6|30|Yes|
|CentOS71-Vagrant-30G|CentOS 7.1|30|Yes|
|Ubuntu1401-Vagrant-30G|Ubuntu 14.01|30|Yes|

Inventoryファイルには、これらのテンプレートに対応するホストを書きます。

```ini:hosts
[large_disk]
CentOS66-30G
CentOS71-30G
Ubuntu1401-30G

[vagrant_box]
CentOS66-Vagrant-30G
CentOS71-Vagrant-30G
Ubuntu1401-Vagrant-30G

[cloudstack_template:children]
large_disk
vagrant_box

[cloudstack:children]
cloudstack_template

[all:vars]
ansible_ssh_user=root
ansible_ssh_private_key_file=./ansible_ssh
ansible_python_interpreter=python
cache_dir=./cache
```

### グループ変数

グループ変数はあまり変更ありません。

`cloudstack`グループの変数には、ゾーンやSSH用のファイアウォール・ポートフォワーディングルールなどの設定を書いています。

```yaml:group_vars/cloudstack
---

ssh_public_key_file: "./ansible_ssh.pub"
zone_name: "augusta"
network_name: "augusta-network1"
service_offering_name: "light.S1"
template_name: "CentOS 7.1 64-bit"
firewall_rules:
  - { protocol: "tcp", start_port: 22, end_port: 22 }
port_forwarding_rules:
  - { protocol: "tcp", public_port: 22, private_port: 22 }
```

`cloudstack_template`グループの変数には、テンプレート作成時の処理・設定を書いています。

```yaml:group_vars/cloudstack_template
---

cloudstack_template_remove_root_ssh_files: true
cloudstack_template_remove_root_history_files: true
cloudstack_template_remove_log_files: true
cloudstack_template_is_dynamically_scalable: true
cloudstack_template_password_enabled: true
```

`vagrant_box`グループの変数には、Vagrant用テンプレート作成時の処理・設定を書いています。Vagrantテンプレートではパスワードを固定で設定しているためパスワードリセット機能を無効化しています。

```yaml:group_vars/vagrant_box
---

cloudstack_template_password_enabled: false
```

### ホスト変数

各テンプレートのベースとなるテンプレートとディスクサイズを指定しています。

例えば、CentOS 6.6 Vagrant用テンプレートの設定は次のようになります。

```yaml:host_vars/CentOS66-Vagrant-30G
---

template_name: "CentOS 6.6 64-bit"
template_volume_size: 30
```

## Playbook

Playbookは以下のコマンドで実行可能です。

```bash
# ローカルにAnsibleがインストールされている場合
$ ansible-playbook -i hosts PLAYBOOK.yml
# Dockerを使用している場合
$ docker run --env-file=.env -v $(pwd):/data -w /data --rm ansible ¥
    ansible-playbook -i hosts PLAYBOOK.yml
```
### 仮想マシン作成

仮想マシン作成には`deploy.yml` を使用します。

`deploy.yml` は[基礎編](http://qiita.com/atsaki/items/6db23d3f4aa3a3c5da41)で使用したものと同じです。中身についてはレポジトリと基礎編の記事を参照してください。

### プロビジョニング

```vagrant_box.yml``` を使用し[Vagrant Box用の設定](https://docs.vagrantup.com/v2/boxes/base.html)を行います。

Vagrant用テンプレートではCloudStackのパスワード・SSH鍵設定機能は
使用しないため無効化しておきます。

```yaml:vagrant_box.yml
---

- hosts: vagrant_box
  gather_facts: no
  pre_tasks:
    - include: set_ansible_ssh_host.yml
  tasks:
    - name: "vagrantユーザーを追加"
      user:
        name: "vagrant"
        password: "{{ 'vagrant'|password_hash('sha512') }}"

    - name: "rootユーザーのパスワードをvagrantに変更"
      user:
        name: "root"
        password: "{{ 'vagrant'|password_hash('sha512') }}"

    - name: "vagrantユーザーの公開鍵をauthorized_keysに登録"
      authorized_key:
        user: "vagrant"
        key: "https://raw.githubusercontent.com/mitchellh/vagrant/master/keys/vagrant.pub"

    - name: "sudoの設定変更"
      copy:
        dest: "/etc/sudoers.d/{{ item }}"
        content: |
          {{ item }} ALL=(ALL) NOPASSWD: ALL
          Defaults:{{ item }} !requiretty
          \n 
      with_items:
        - "root"
        - "vagrant"

    - name: "CloudStackのパスワード、SSH鍵設定機能の無効化"
      service:
        name: "{{ item }}"
        enabled: no
      with_items:
        - cloud-set-guest-sshkey
        - cloud-set-guest-sshkey.service
        - cloud-set-guest-password
        - cloud-set-guest-password.service
      ignore_errors: yes
```

### 仮想マシンのテンプレート化

```clouudstack_template.yml``` を使用して仮想マシンをテンプレート化します。
```clouudstack_template.yml``` は先述のテンプレート作成工程のうち以下を行います。

* 仮想マシンからボリューム作成
* ボリューム・ファイルシステムのサイズ変更
* ボリューム内の不要ファイル削除
* ボリュームからテンプレート作成

#### 仮想マシンからボリューム作成

プロビジョニングした仮想マシンをいったん停止し、ROOTボリュームのスナップショットを取得してスナップショットからボリュームを作成します。

ボリュームスナップショットを扱えるモジュールがまだないため、スナップショットは```cs``` でAPIを直接実行して作成しています。

```yaml:cloudstack_template.yml
    - name: "ROOTボリュームのスナップショットを作成"
      shell: >
        cs createSnapshot \
          name={{ inventory_hostname }} \
          volumeid={{ (volume.stdout|from_json).id }}
      connection: local
    
    - name: "スナップショットからテンプレート作成用ボリュームを作成"
      cs_volume:
        zone: "{{ zone_name }}"
        name: "{{ inventory_hostname }}"
        snapshot: "{{ inventory_hostname }}"
      connection: local
```

#### ボリューム・ファイルシステムのサイズ変更

ボリュームのサイズ変更は`cs_volume`モジュールで`size` を変更するだけでできます。

```yaml:clodustack_template.yml
    - name: "テンプレート作成用ボリュームのサイズ変更"
      cs_volume:
        zone: "{{ zone_name }}"
        name: "{{ inventory_hostname }}"
        size: "{{ template_volume_size }}"
      connection: local
```

このボリュームを元の仮想マシンにアタッチし、拡張したボリュームに合わせてファイルシステムを拡張します。拡張方法についてはIDCFのエンジニアブログを参考にしました。

[IDCFクラウド rootディスクの拡張 | IDCF Engineers' Blog](http://www.idcf.jp/blog/cloud/rootdisk/)

```yaml:clodustack_template.yml
    - name: "テンプレート作成用ボリュームをアタッチ"
      cs_volume:
        zone: "{{ zone_name }}"
        name: "{{ inventory_hostname }}"
        vm: "{{ inventory_hostname }}"
        state: attached
      connection: local

    - name: "テンプレート作成用ボリュームのファイルシステムサイズ変更"
      shell: | 
        for f in $(ls /sys/class/scsi_host/)
        do
          echo "- - -" > /sys/class/scsi_host/$f/scan
        done

        echo "2048,,83,*" | sfdisk -uS --force /dev/sdb

        blkid | grep /dev/sdb1 | grep -q xfs
        if [ $? -eq 0 ]
        then
          mkdir -p /mnt/data
          mount -o nouuid /dev/sdb1 /mnt/data
          xfs_growfs /mnt/data
          umount /mnt/data
          rmdir /mnt/data
        else
          e2fsck -fp /dev/sdb1
          resize2fs /dev/sdb1
        fi
```

#### ボリューム内の不要ファイル削除

テンプレートからベースにした仮想マシンの情報を消すため、
rootのSSH関連ファイル、ヒストリ、ログを削除します。

```yaml:clodustack_template.yml
    - name: "テンプレートからrootのssh関連ファイルを削除"
      shell: |
        rm -fv /mnt/data/root/.ssh/*
      when: cloudstack_template_remove_root_ssh_files

    - name: "テンプレートからrootのヒストリファイルを削除"
      shell: |
        rm -fv /mnt/data/root/.*history*
      when: cloudstack_template_remove_root_history_files

    - name: "テンプレートからログファイルを削除"
      shell: |
        find /mnt/data/var/log/ -type f | xargs -I {} sh -c "echo '' > {}"
      when: cloudstack_template_remove_log_files
```

NICの情報が残らないようにネットワーク関連のファイルも削除しておきます。

```yaml:clodustack_template.yml
    - name: "テンプレートから不要なネットワーク関連ファイルを削除"
      shell: |
        rm -frv /mnt/data/{{ item }}
      with_items:
        - /etc/udev/rules.d/70-persistent-net.rules
        - /var/lib/NetworkManager/*
        - /var/lib/dhcp/dhclient.*
        - /var/lib/dhclient/*
```

#### ボリュームからテンプレート作成

サイズ変更・不要ファイルを削除したボリュームからテンプレートを作成します。

テンプレートの作成もCloudStackモジュールではできなかったので、`cs`で実行しています。

```yaml:clodustack_template.yml
    - name: "テンプレート作成用ボリュームのスナップショットからテンプレート作成"
      shell: >
        cs createTemplate \
          name="{{ inventory_hostname }}" \
          displaytext="{{ inventory_hostname }}" \
          snapshotid="{{ (snapshot.stdout|from_json).id }}" \
          ostypeid="{{ (original_template.stdout|from_json).ostypeid }}" \
          isdynamicallyscalable="{{ cloudstack_template_is_dynamically_scalable }}" \
          passwordenabled="{{ cloudstack_template_password_enabled }}"
      connection: local
```

#### リソースの削除

```clean.yml``` でテンプレート作成に利用したリソースを削除することができます。

## Vagrantで作成したテンプレートを使う

作成したテンプレートをVagrantから使用してみます。
使用するにはあらかじめVagrantと```vagrant-cloudstack``` をインストールしておく必要があります。

```vagrant-cloudstack``` は、下記コマンドでインストールできます。

```bash
$ vagrant plugin install vagrant-cloudstack
```

```Vagrantfile``` を用意します。
環境変数`VAGRANT_BOX`を使ってVagrant用テンプレート、通常のIDCFテンプレート両方に対応できるようになっています。（ログインユーザー・SSH鍵の変更や[requirettyの対策](http://qiita.com/atsaki/items/467ce1569fbc2cb11cb8)）

```rb:Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.provider :cloudstack do |cloudstack, override|
    cloudstack.host    = "#{ENV['CLOUDSTACK_HOST']}"
    cloudstack.path    = "#{ENV['CLOUDSTACK_PATH']}"
    cloudstack.port    = "#{ENV['CLOUDSTACK_PORT']}"
    cloudstack.scheme  = "#{ENV['CLOUDSTACK_SCHEME']}"

    cloudstack.api_key    = "#{ENV['CLOUDSTACK_KEY']}"
    cloudstack.secret_key = "#{ENV['CLOUDSTACK_SECRET']}"

    cloudstack.zone_name             = "#{ENV['CLOUDSTACK_ZONE_NAME']}"
    cloudstack.template_name         = "#{ENV['CLOUDSTACK_TEMPLATE_NAME']}"
    cloudstack.service_offering_name = "#{ENV['CLOUDSTACK_SERVICE_OFFERING_NAME']}"

    cloudstack.pf_ip_address   = "#{ENV['CLOUDSTACK_PF_IP_ADDRESS']}"
    override.ssh.host          = "#{ENV['CLOUDSTACK_PF_IP_ADDRESS']}"

    if ENV['VAGRANT_BOX'] == "0"
      override.ssh.username = "root"

      cloudstack.keypair            = "#{ENV['CLOUDSTACK_KEYPAIR']}"
      override.ssh.private_key_path = "#{ENV.fetch('CLOUDSTACK_PRIVATE_KEY_PATH',
                                                   '~/.ssh/id_rsa')}"
      override.ssh.pty = true

      config.vm.synced_folder ".", "/vagrant", type: "rsync",
          rsync__rsync_path: "rsync"
    end

    config.vm.provision "shell", inline: "echo Hello" 
  end
end
```

CloudStack用の環境変数をセットします。
Docker用に`.env` ファイルを使っている場合は、`.env`ファイルに環境変数を追記し[direnv](https://github.com/direnv/direnv)で読み込ませると管理・設定が楽です。

```bash
$ export CLOUDSTACK_API=<API_KEY>
$ export CLOUDSTACK_SECRET=<SECRET_KEY>
$ export CLOUDSTACK_HOST=compute.jp-east.idcfcloud.com
$ export CLOUDSTACK_PATH=/client/api
$ export CLOUDSTACK_PORT=443
$ export CLOUDSTACK_SCHEME=https
$ export CLOUDSTACK_ZONE_NAME=augusta
$ export CLOUDSTACK_TEMPLATE_NAME=CentOS71-Vagrant-30G
$ export CLOUDSTACK_SERVICE_OFFERING_NAME=light.S1
$ export CLOUDSTACK_PF_IP_ADDRESS=<ポートフォワードを設定するIP>
$ export CLOUDSTACK_KEYPAIR=asasaki
$ export CLOUDSTACK_PRIVATE_KEY_PATH=~/.ssh/id_rsa
```

`vagrant up`で仮想マシンが起動します。
`CentOS71-30G`などVagrant用の設定をしていないテンプレートを使用する場合は、`VAGRANT_BOX=0`をつけて実行します。（`vagrant up`以外でもつけてください。）

```bash
# Vagrant用テンプレートでない場合はVAGRANT_BOX=0 vagrant up
$ vagrant up
```

※ `vagrant up` が失敗してしまう場合は、ポートフォワードの設定あたりで`Ctrl-Z`でいったん処理を中断し、起動が完了するまで少し待ってから再開させるとうまくいくようです。

正常に起動したら、```vagrant ssh``` でログインできます。

```bash
# Vagrant用テンプレートでない場合はVAGRANT_BOX=0 vagrant ssh
$ vagrant ssh
```

容量を確認すると30Gに拡張されていることがわかります。

```bash
$ vagrant ssh
$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
/dev/sda1         30G  1.2G   29G    4% /
devtmpfs         485M     0  485M    0% /dev
tmpfs            494M     0  494M    0% /dev/shm
tmpfs            494M   13M  481M    3% /run
tmpfs            494M     0  494M    0% /sys/fs/cgroup
```

仮想マシンが不要になったら```vagrant destroy``` コマンドで削除します。

```bash
# Vagrant用テンプレートでない場合はVAGRANT_BOX=0 vagrant destroy
$ vagrant destroy
```

## 最後に

CloudStackモジュールは多くのリソース、操作をサポートしているため
ほとんどの処理をモジュールの機能で実現することができます。
足りない部分については今後実装して貢献していきたいと思っています。