AnsibleでCloudStackを操作する（基礎編：仮想マシン作成とプロビジョニング）

最近よく耳にする構成管理ツールの[Ansible](http://www.ansible.com/)ですがサーバーの設定だけでなくCloudStackの各種リソースを扱うこともできます。

今回はAnsibleによるCloudStackの操作の基礎として仮想マシン作成と簡単なプロビジョニングを行ってみます。

[IDCF Cloud Advent Calendar 2015](http://qiita.com/advent-calendar/2015/idcfcloud)の@snicker_jp さんの記事と内容がかなり重複しているので、ぜひこちらの記事も読んでみてください。

[AnsibleだけでIDCFクラウドの仮想マシン立ち上げから、HAProxyを使ったSSLオフローダーを構築する #idcfrontier : 元うなぎ屋](http://snickerjp.blogspot.jp/2015/12/idcfcloud-ansible-launch.html)

今回の記事で使用しているファイルは下記レポジトリにあります。

https://github.com/atsaki/ansible-cloudstack-example-nginx

## AnsibleのCloudStackモジュールについて

AnsibleはファイルやユーザーなどOSの操作だけでなく、各種クラウドの仮想マシンやボリュームなどのリソースを扱うこともできます。

CloudStackのモジュールが開発が進められており、Ansible 2.0から標準でインストールされる予定です。

少しややこしいのですがCloudStackモジュールは以下の2つのレポジトリで
開発が進められています。
同じ開発者が中心となって開発を進めており互換性はありますが、完全に同じと
いうわけではないので注意が必要です。

* [ansible/ansible-modules-extras](https://github.com/ansible/ansible-modules-extras)
    * Ansibleの一部として開発がすすめられているレポジトリ
    * Ansible 2.0のリリース時にはこちらが組み込まれる
* [resmo/ansible-cloudstack](https://github.com/resmo/ansible-cloudstack)
    * CloudStackモジュール単体で開発が進められているレポジトリ
    * こちらで修正を行ってから```ansible/ansible-modules-extras``` に反映されるケースが多い

今回は```resmo/ansible-cloudstack``` を使用します。

## CloudStackモジュールのインストール

まずはCloudStackモジュールを使用できる環境を用意します。
ここでは、以下の2つの方法を紹介します。

* [Ansibleがインストールされている場合](#Ansibleがインストールされている場合)
* [Dockerを使用する場合](#Dockerを使用する場合)

### Ansibleがインストールされている場合

すでにAnsibleがインストールされている場合は、playbookと同じディレクトリの
```library``` というディレクトリにCloudStackモジュールをダウンロードします。

```bash
$ git clone https://github.com/resmo/ansible-cloudstack.git library
```

CloudStackモジュールが必要とするPythonパッケージもインストールしておきます。

```bash
$ pip install cs sshpubkeys
```

### Dockerを使用する場合

Dockerを使える環境の構築方法は多くの資料があるので割愛します。
[Docker Machine](https://github.com/docker/machine)を使用するのが簡単かと思います。

AnsibleとCloudStackモジュールのイメージを作成するためのDockerfileを用意します。
Pythonパッケージのインストール、CloudStackモジュールの取得、ライブラリのパスへの追加などを行っています。

```Dockerfile:Dockerfile
FROM python:2.7
RUN pip install cs ansible sshpubkeys
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
```

Dockerfileからイメージをビルドします。

```bash
$ docker build -t ansible .
```

```docker run``` で作成したイメージにAnsibleがインストールされていることが確認できます。

```bash
$ docker run --rm ansible ansible --version
ansible 1.9.4
  configured module search path = None
```

### csの設定

CloudStackモジュールが使用している[cs](https://github.com/exoscale/cs)がCloudStack APIを実行できるようにエンドポイント、APIキー、シークレットキーの設定を行います。

設定ファイルを使用して設定することもできますが、今回は環境変数を使って設定してみます。

環境変数`CLOUDSTACK_ENDPOINT`、`CLOUDSTACK_KEY`、`CLOUDSTACK_SECRET`にエンドポイント、APIキー、シークレットキーをセットします。

また、タイムアウトの設定が厳しめ（デフォルト10秒）で失敗してしまうことがあるので、
余裕を見て1分に伸ばしておきます。（`CLOUDSTACK_TIMEOUT=60`）

ローカルのAnsibleを使用する場合は、以下のようにエクスポートします。

```bash
$ export CLOUDSTACK_ENDPOINT=<APIのエンドポイント>
$ export CLOUDSTACK_KEY=<APIキー>
$ export CLOUDSTACK_SECRET=<シークレットキー>
$ export CLOUDSTACK_TIMEOUT=60
```

Dockerを使用する場合は、まず`.env`というファイルを以下の内容で作成します。

```bash:.env
CLOUDSTACK_ENDPOINT=<APIのエンドポイント>
CLOUDSTACK_KEY=<APIキー>
CLOUDSTACK_SECRET=<シークレットキー>
CLOUDSTACK_TIMEOUT=60
```

`docker run`のオプション`--env-file`にこのファイルを指定すると実行時に環境変数がセットされます。

設定が正しくできていれば、```cs``` コマンドでAPIが実行できるようになっています。

```bash
# ローカルのAnsibleを使用する場合は
$ cs listZones
# Dockerを使用する場合
$ docker run --env-file=.env --rm ansible cs listZones
```

### SSH鍵の作成

ホストへ接続するために使用するSSH鍵を作成しておきます。

```bash
$ ssh-keygen -f ansible_ssh -N ""
```

## AnsibleでNginxサーバーを構築する

準備が整ったので、仮想マシン作成とプロビジョニングのNginxサーバーを構築してみます。

### Inventoryファイル

Inventoryファイルに作成するホストとグループの設定を書きます。

今回は```nginx01``` というホストをCloudStack上に作成し、Nginxをインストールします。

Nginxをインストールするホストのグループを```nginx``` 、CloudStack上に作成されるホストを```cloudstack``` グループとし、```nginx01``` を両方のグループに所属させています。（```nginx``` グループ全体を```cloudstack``` グループに含めています。）

```[all:vars]``` セクションにはログインユーザーや秘密鍵など共通で使用する設定を書いておきます。

```ini:hosts
[nginx]
nginx01

[cloudstack:children]
nginx

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

### 変数の設定

```group_vars/cloudstack``` には、デフォルトで使用するゾーンやテンプレートなどのCloudStackの設定を書いておきます。

```yaml:group_vars/cloudstack
---

ssh_public_key_file: "./ansible_ssh.pub"
zone_name: "joule"
network_name: "joule-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 }
```

nginxグループについてはSSHのポートだけでなくHTTPのポートも開けます。

```yaml:group_vars/nginx
---

firewall_rules:
  - { protocol: "tcp", start_port: 22, end_port: 22 }
  - { protocol: "tcp", start_port: 80, end_port: 80 }
port_forwarding_rules:
  - { protocol: "tcp", public_port: 22, private_port: 22 }
  - { protocol: "tcp", public_port: 80, private_port: 80 }
```

### Playbookの作成

今回は以下の3つのPlaybookを作成します。

* deploy.yml
    * 仮想マシンのデプロイ、IPアドレスの取得、ポートフォワーディングの設定を行う
* nginx.yml
    * 仮想マシンにNginxをインストールする
* clean.yml
    * 作成・取得したリソースを削除・解放する

Playbookは```ansible-playbook``` コマンドで実行できます。

```bash
$ ansible-playbook -i hosts PLAYBOOK.yml
```

Dockerを使用している場合には、カレントディレクトリをマウントしコンテナからPlaybookが見えるようにします。

```bash
$ docker run --env-file=.env -v $(pwd):/data -w /data --rm ansible ¥
    ansible-playbook -i hosts PLAYBOOK.yml
```

以下それぞれのPlaybookについて簡単に内容を説明します。
詳細については[レポジトリ](https://github.com/atsaki/ansible-cloudstack-example-nginx)のファイルを参照してください。

#### deploy.yml


```deploy.yml``` はホストをCloudStack上に作成し、ファイアウォール・ポートフォワーディング等の設定を行いホストにSSH接続可能な状態にします。

##### 仮想マシンのデプロイ

仮想マシンの管理は```cs_instance``` モジュールで行うことができます。
各パラメータを設定するだけで、仮想マシンの状態を指定されている状態にしてくれます。

SSH接続するためには仮想マシンが起動している必要があります。
```state: started``` としておくと、```deploy.yml``` が実行されたとき仮想マシンが停止していたら起動することができます。

``` yaml:deploy.yml
    - name: "VMの作成"
      cs_instance:
        name: "{{ inventory_hostname }}"
        zone: "{{ zone_name }}"
        networks: ["{{ network_name }}"]
        service_offering: "{{ service_offering_name }}"
        template: "{{ template_name }}"
        ssh_key: "{{ inventory_hostname }}"
        state: started
      register: vm
```

##### IPアドレスの取得

仮想マシンへのSSH接続とWebページへの接続のためにIPアドレスを取得します。

IPアドレスの管理は```cs_ip_address``` モジュールで行うことができます。

IPアドレスの取得については注意が必要です。
Ansibleは基本的に前回実行した処理を覚えていないので、単に```state: present``` で取得すると、実行のたびにIPアドレスを取得してしまいます。

対策は色々ありますが、今回は取得したIPアドレスを書いたファイルを保存しておくことにしました。ファイルが存在する場合は取得済みなので処理をスキップします。

```yaml:deploy.yml
    - name: "キャッシュ用ディレクトリの作成"
      file:
        path: "{{ vm_cache_dir }}"
        state: "directory"
        mode: "0755"

    - name: "取得済みIPアドレスの確認"
      stat:
        path: "{{ vm_cache_dir + '/ip_address' }}"
      register: cache_ip_address

    - name: "新規IPアドレスの取得"
      cs_ip_address:
        zone: "{{ zone_name }}"
        network: "{{ network_name }}"
      register: ip_address
      when: not cache_ip_address.stat.exists

    - name: "取得したIPアドレスをキャッシュへ書き込み"
      copy:
        content: "{{ ip_address.ip_address }}"
        dest: "{{ vm_cache_dir + '/ip_address' }}"
      when: not cache_ip_address.stat.exists

    - name: "IPアドレスのキャッシュ読み込み"
      set_fact:
        ip_address: "{{ lookup('file', vm_cache_dir + '/ip_address') }}"
```

##### ファイアウォール・ポートフォワーディングルールの設定

IPアドレスが取得できたので、SSHとHTTPのファイアウォールと
ポートフォワーディングルールを設定します。

ファイアウォールとポートフォワーディングルールの管理にはそれぞれ```cs_firewall```、```cs_portforward``` を使用します。

これらのモジュールを使用する際の注意点は、必ずゾーンを指定することです。
ゾーンなしでは意図していないゾーンでIPアドレスを探してエラーになる場合があります。

```yaml:deploy.yml
    - name: "ファイアウォールルールの設定"
      cs_firewall:
        zone: "{{ zone_name }}"
        ip_address: "{{ ip_address }}"
        protocol: "{{ item.protocol }}"
        start_port: "{{ item.start_port }}"
        end_port: "{{ item.end_port }}"
        cidr: "{{ item.cidr|default('0.0.0.0/0') }}"
      with_items: firewall_rules
```

##### SSH接続可能になるまで待機

最後に仮想マシンがSSH接続可能になるまで待機します。

```yaml:deploy.yml
    - name: "SSHで接続可能になるまで待機"
      wait_for:
        host: "{{ ip_address }}"
        port: "{{ ansible_ssh_port|default(22) }}"
        search_regex: "OpenSSH"

    - name: "authorized_keysの設定に時間がかかる場合があるので余分に待機"
      pause: minutes=2
      when: vm|changed
```

##### 作成した仮想マシンにSSHログイン

作成した仮想マシンには以下のコマンドでログインできます。

```bash
$ ssh -i ansible_ssh root@$(cat cache/nginx01/ip_address)
```

#### nginx.yml

```nginx.yml```はホストにNginxをインストールします。

```yaml:nginx.yml
---

- hosts: nginx
  gather_facts: no
  pre_tasks:
    - include: set_ansible_ssh_host.yml
  tasks:
    - name: "Nginxをインストール(Ubuntu)"
      apt:
        name: "nginx"
        update_cache: yes
      when: ansible_distribution == 'Ubuntu'

    - name: "EPELをインストール(CentOS)"
      yum:
        name: "epel-release"
      when: ansible_distribution == 'CentOS'

    - name: "Nginxをインストール(CentOS)"
      yum:
        name: "nginx"
      when: ansible_distribution == 'CentOS'

    - name: "Nginxを起動"
      service:
        name: "nginx"
        enabled: yes
        state: started
```

Nginxをインストールするタスクの実行前に、```pre_tasks``` で```set_ansible_ssh_host.yml``` を実行しています。
```set_ansible_ssh_host.yml``` では取得済みのIPアドレスを調べ```ansible_ssh_host``` にをセットし、対象ホストにSSHできるようにしています。

```set_ansible_ssh_host.yml``` は、ホスト情報の取得も行います。
これによりホストの情報によって処理を変更することができます。
```nginx.yml```では、ディストリビューションによって使用するモジュールを
変更しCentOS、Ubuntuの両方に対応しています。

```yaml:set_ansible_ssh_host.yml
---

- set_fact:
    vm_cache_dir: "{{ cache_dir + '/' + inventory_hostname }}"

- name: "取得済みIPアドレスの確認"
  stat:
    path: "{{ vm_cache_dir + '/ip_address' }}"
  register: cache_ip_address
  connection: local
  failed_when: not cache_ip_address.stat.exists

- name: "ansible_ssh_hostに取得済みのIPをセット"
  set_fact:
    ansible_ssh_host: "{{ lookup('file', vm_cache_dir + '/ip_address') }}"

- name: "仮想マシンのステータスを確認"
  cs_instance:
    name: "{{ inventory_hostname }}"
    zone: "{{ zone_name }}"
    state: present
  connection: local
  register: instance

- name: "ホスト情報の取得"
  setup:
  when: instance.state == "Running"
```

プロビジョニングが正常に完了していれば、ブラウザで```cache/nginx01/ip_address``` のIPアドレスを
開くとNginxの画面が表示されます。

#### clean.yml

clean.ymlは作成・取得したリソースを解放・解除するためのPlaybookです。

削除の際もゾーンの指定を忘れないよう注意する必要があります。
ゾーンを指定していない場合、複数ゾーンに同じ名前のリソースが存在すると
意図していないほうが消される場合があります。

```yaml:clean.yml
---

- hosts: cloudstack
  connection: local
  tasks:
    - set_fact:
        vm_cache_dir: "{{ cache_dir + '/' + inventory_hostname }}"
    
    - name: "SSH公開鍵の削除"
      cs_sshkeypair:
        name: "{{ inventory_hostname }}"
        state: absent
    
    - name: "取得済みIPアドレスの確認"
      stat:
        path: "{{ vm_cache_dir + '/ip_address' }}"
      register: cache_ip_address
    
    - name: "IPアドレスのキャッシュ読み込み"
      set_fact:
        ip_address: "{{ lookup('file', vm_cache_dir + '/ip_address') }}"
      when: cache_ip_address.stat.exists
    
    - name: "IPアドレスの解放"
      cs_ip_address:
        zone: "{{ zone_name }}"
        network: "{{ network_name }}"
        ip_address: "{{ ip_address }}"
        state: absent
      when: cache_ip_address.stat.exists
    
    - name: "IPアドレスキャッシュの削除"
      file:
        path: "{{ vm_cache_dir + '/ip_address' }}"
        state: absent
      when: cache_ip_address.stat.exists
    
    - name: "VMの削除"
      cs_instance:
        name: "{{ inventory_hostname }}"
        zone: "{{ zone_name }}"
        state: expunged

    - name: "キャッシュ用ディレクトリの削除"
      file:
        path: "{{ vm_cache_dir }}"
        state: absent
```

## 最後に

ここまでAnsibleで仮想マシンを作成しプロビジョニングする方法をみてきましたが、
今回扱った内容であればVagrantやTerraformを使用した方が良いと思います。
これらのツールはIDでリソースを管理しているため、意図しないリソースを操作
してしまう危険が少なく安全です。また、用途が限られているので設定も簡単になっています。

AnsibleでCloudStackを操作するメリットは、細かい処理の手順を記述しやすいこと、
既存のリソースを管理対象にしやすいこと、対応しているリソースが多いことにあると思います。

次回は、応用編としてこれらのメリットが活用できる例として既存テンプレートのカスタマイズを行います。

[AnsibleでCloudStackを操作する（応用編：IDCFクラウドテンプレートのカスタマイズ） - Qiita](http://qiita.com/atsaki/items/bb2145bf50e6009d6e6b)