OpenStackとAnsibleと私

OpenStack Advent Calendar 2015とAnsible Advent Calendar 2015の12月25日の記事です。

沖縄でハンズオンをやってきました

12/14-18で開催されたOkinawa Open Days 2015で、OpenStackとAnsibleとDockerを組み合わせたハンズオンセミナーをやってきました。

OpenStackとAnsibleのAdvent Calendarの最終日としてふさわしいかどうかわかりませんが、ハンズオンに向けての準備で得られた知見を、ここにメモしておきたいと思います。

"OpenStack"と"Ansible v2"

Ansibleは標準でOpenStackを操作するためのモジュールを持っています。 このモジュールを利用すれば、OpenStackが提供するAPIを利用して、仮想マシンインスタンス、ネットワーク、ボリュームなどの仮想リソースの管理をAnsibleのPlaybookを利用して実施することができます。 また、OpenStack管理下にある仮想マシンインスタンス群の管理を容易にするために、ダイナミックインベントリも提供しています。 このOpenStack向けに提供される標準モジュールとダイナミックインベントリは、v2で大きく進化しており、v1向けのモジュールよりも簡単便利に使えるようになりました。

Ansible v2向けモジュールの進化

v2向けのOpenStack用モジュールは、github上にある公式リポジトリから入手可能です。

従来のv1向けのモジュールやダイナミックインベントリは、OpenStackの各コンポーネントが提供するクライアントライブラリ(python-novaclientやpython-neutronclientなど)を直接利用していましたが、v2向けのモジュールは、shadeライブラリを利用するよう全面的に刷新されました。

v1向けOpenStack用モジュールの特徴

例えば、nova-computeモジュールでは、python-novaclientを直接利用していました。 OpenStackの各コンポーネントが提供するクライアントモジュール(pyhon-*client)を直接利用しており、OpenStack環境へのログイン情報をパラメータとして指定する必要がありました。 さらに、OpenStackのクライアントモジュールはコンポーネント毎に認証方式が微妙に違っており、この差異はAnsibleのモジュール側の実装で吸収していました。

例) nova_computeモジュール

+--------------+    +-------------------+         +-----------+
| nova_compute +----+ python-novaclient +==(API)=>+ openstack |
+--------------+    +-------------------+         +-----------+

例) quantum_networkモジュール

+-----------------+    +----------------------+         +-----------+
| neutron_network +----+ python-neutronclient +==(API)=>+ openstack |
+-----------------+    +----------------------+         +-----------+

v2向けOpenStack用モジュールで改善された点

v2向けのモジュールでは、shadeライブラリを利用しています。shadeはOpenStackクライアントライブラリ(python-*client)の上に一枚被さる形で各クライアントライブラリの認証部分など共通に利用する部分の差異を吸収して抽象化したレイヤーを、上位のプログラムに対して提供してくれます。 さらに、v1時代のモジュールのように、OpenStackへの接続情報をPlaybookに書く必要はなく、外部の設定ファイルに複数のOpenStack環境の認証情報を記述して実行時に選択することで、接続先を切り替えるも可能です。

+--------------+    +-------+    +----------------+         +-----------+
| os_* modules +----+ shade +----+ python-*client +==(API)=>+ openstack |
+--------------+    +---+---+    +----------------+         +-----------+
                        |
               +--------+---------+    +-------------------------------------+
               | os-client-config +----+ $HOME/.config/openstack/clouds.yaml |
               +------------------+    +-------------------------------------+

shadeが利用しているos-client-configについては、OSS とクラウドの狭間で: 環境変数1つで OpenStack 環境を切り替える os-client-config で、もときさんが詳しく紹介してくれています。必読です。

v1向けモジュールのClientクラスインスタンスの初期化

nova_computeモジュールと、quantum_networkモジュールの例を以下に紹介します。ログイン情報をモジュールパラメータとして指定する必要があります。 さらにpython-novaclientとpython-neutronclientのClientクラスインスタンスは、生成する際の引数が微妙に違います。

nova_compute.py

...
 41: options:
 42:    login_username:
 43:     description:
 44:        - login username to authenticate to keystone
 45:     required: true
 45:     default: admin
 46:   login_password:
 47:     description:
 48:        - Password of login user
 49:     required: true
 50:     default: 'yes'
 51:   login_tenant_name:
 52:     description:
 53:        - The tenant name of the login user
 54:     required: true
 55:     default: 'yes'
...
534: def main():
...
572:     nova = nova_client.Client(module.params['login_username'],
573:                               module.params['login_password'],
574:                               module.params['login_tenant_name'],
575:                               module.params['auth_url'],
576:                               region_name=module.params['region_name'],
577:                               service_type='compute')
...

quantum_network.py

...
 37: options:
 38:   login_username:
 39:     description:
 40:        - login username to authenticate to keystone
 41:     required: true
 42:     default: admin
 43:   login_password:
 44:     description:
 45:        - Password of login user
 46:     required: true
 47:     default: 'yes'
 48:   login_tenant_name:
 49:     description:
 50:        - The tenant name of the login user
 51:     required: true
 52:     default: 'yes'
...
130: def _get_ksclient(module, kwargs):
131:    try:
132:         kclient = ksclient.Client(username=kwargs.get('login_username'),
133:                                  password=kwargs.get('login_password'),
134:                                  tenant_name=kwargs.   get('login_tenant_name'),
135:                                  auth_url=kwargs.get('auth_url'))
...

v2向けモジュールのClientクラスインスタンスの初期化

v2向けモジュールでは、認証情報をパラメータとして直接モジュールに渡す必要はありません。shadeがos-client-configを利用して設定ファイルを取得して利用してくれるからです。 ちなみに、何もしなくてもopenrcなどによって反映された環境変数(OS_USERNAMEなど)を接続情報として利用してくれます。なにこれ便利すぎる!

v2向けのOpenStack操作用モジュール(os_*モジュール)は、認証情報をパラメータとしてセットする必要がありません。さらに、内部的にはnovaを操作する場合(例:os_nova_flavorモジュール)も、neutronを操作する場合(例:os_portモジュール)も同一の方法で利用することができます。

os_nova_flavor.py

...
201:         cloud = shade.operator_cloud(**module.params)
...

os_port.py

...
336:         cloud = shade.openstack_cloud(**module.params)
...

設定ファイル($HOME/.config/openstack/clouds.yaml)

以下の例では、libertyとdevstackの2つの環境を定義しており、os_*モジュールのcloudパラメータに環境名(ここではlibertyまたはdevstack)を指定することで、接続先となるOpenStack環境を切り替えることが可能です。 また、cloudパラメータに何もしていしなかった場合は、openrcなどで反映されている環境変数(OS_USERNAMEなど)が利用されます。

clouds:
  liberty:
    auth:
      auth_url: http://192.168.0.1:5000
      username: saitou
      password: changeme
      project_domain_id: default
      user_domain_id: default
      project_name: SYSTEM00
    identity_api_version: '3'
  devstack:
    auth:
      auth_url: http://172.16.31.1:35357
      username: admin
      password: changeme
      project_domain_id: default
      user_domain_id: default
      project_name: admin
    identity_api_version: '3'
    region_name: RegionOne

利用方法

この例では、os_*モジュールにはcloudパラメータを指定していません。 この状態だと、ansible-playbookコマンドの実行環境で反映されている環境変数(OS_USERNAMEなど)が接続情報として利用されます。

  1. 公開鍵を登録する(os_keypairモジュール)
  2. セキュリティグループを作成する(os_security_groupモジュール)
  3. セキュリティグループにルールを登録する(os_security_group_ruleモジュール)
  4. 仮想マシンインスタンスを起動する(os_serverモジュール)
  5. 仮想マシンインスタンスにフローティングIPアドレスを割り当てる(quantum_floating_ipモジュール)

比較のために利用しているquantum_floating_ipモジュールはv1向けのモジュールです。v1向けのモジュールは、実行時にOpenStack環境への接続情報がパラメータとして必要となるため、わざわざ環境変数から取得するようvarsセクションでlookup()しています。

Playbook: create_instance.yml
---
- hosts: localhost

  vars:
    ansible_python_interpreter: /home/centos/handson/bin/python
    os_auth_url: "{{ lookup('env','OS_AUTH_URL') }}"
    os_username: "{{ lookup('env','OS_USERNAME') }}"
    os_password: "{{ lookup('env','OS_PASSWORD') }}"
    os_project_name: "{{ lookup('env','OS_TENANT_NAME') }}"
    os_region_name: "{{ lookup('env','OS_REGION_NAME') }}"
    keypairs:
      - name: "step-server"
        public_key_file: "/home/centos/.ssh/id_rsa.pub"
    secgroups:
      - name: "eplite"
        desc: "secgroup for eplite"
        rules:
          - protocol: "icmp"
            port_range_min: -1
            port_range_max: -1
            remote_ip_prefix: "0.0.0.0/0"
          - protocol: "tcp"
            port_range_min: 22
            port_range_max: 22
            remote_ip_prefix: "0.0.0.0/0"
          - protocol: "tcp"
            port_range_min: 80
            port_range_max: 80
            remote_ip_prefix: "0.0.0.0/0"
          - protocol: "tcp"
            port_range_min: 3306
            port_range_max: 3306
            remote_ip_prefix: "0.0.0.0/0"
    servers:
      - name: "{{ hostname }}"
        key_name: "step-server"
        flavor: "m1.small"
        image: "Docker01"
        secgroups:
          - "eplite"
        nics:
          - net-name: "work-net"
        auto_ip: no
        ext_net: "ext-net_1214"
        int_net: "work-net"

  tasks:
    - name: import keypairs
      os_keypair:
        state=present
        name="{{ item.name }}"
        public_key_file="{{ item.public_key_file }}"
      with_items: keypairs
    - name: create security group
      os_security_group:
        state=present
        name="{{ item.name }}"
        description="{{ item.desc}}"
      with_items: secgroups
    - name: add rules to secgroup
      os_security_group_rule:
        state=present
        security_group="{{ item[0].name }}"
        protocol="{{ item[1].protocol }}"
        port_range_min="{{ item[1].port_range_min }}"
        port_range_max="{{ item[1].port_range_max }}"
        remote_ip_prefix="{{ item[1].remote_ip_prefix }}"
      with_subelements:
        - secgroups
        - rules
    - name: create servers
      os_server:
        state: present
        timeout: 200
        name: "{{ item.name }}"
        key_name: "{{ item.key_name }}"
        flavor: "{{ item.flavor }}"
        image: "{{ item.image }}"
        security_groups: "{{ item.secgroups }}"
        nics: "{{ item.nics }}"
        auto_ip: "{{ item.auto_ip }}"
      with_items: servers
    - name: create and assign floating_ip to server
      quantum_floating_ip:
        state=present
        login_username="{{ os_username }}"
        login_password="{{ os_password }}"
        login_tenant_name="{{ os_project_name }}"
        network_name="{{ item.ext_net }}"
        instance_name="{{ item.name }}"
        internal_network_name="{{ item.int_net }}"
      with_items: servers
Playbookを実行してみる

OpenStackモジュールを利用する前に、まずはshadeのインストールをしなければなりません。ここではpipを利用してvirtualenv環境にインストールしていますが、みなさんの環境にあわせてインストールしておいてください。

$ cd ~ && virtualenv develop
(develop)$ pip install shade functools32

-eオプションで指定したホスト名で、仮想マシンインスタンスを作成します。

$ source openrc
$ ansible-playbook -i ansible_hosts -e "hostname=web00" create_instance.yml

OpenStack管理機能を提供するv2向けモジュール

仮想マシンインスタンスを起動するという目的を達成するために、必要最低限の機能を提供していたv1向けモジュールに対して、v2向けのモジュールは大幅に機能追加されています。 これらv2向けモジュールのほとんど(すべて検証したわけではないけれど)は、Ansible v1.9.4でも正常動作するため、みなさんのv1環境でも恩恵を受けることができます。

是非試してみてください。

v1向けOpenStack用モジュールリスト

  • glance_image.py
  • keystone_user.py
  • nova_compute.py
  • nova_keypair.py
  • quantum_floating_ip_associate.py
  • quantum_floating_ip.py
  • quantum_network.py
  • quantum_router_gateway.py
  • quantum_router_interface.py
  • quantum_router.py
  • quantum_subnet.py

v2向けOpenStack用モジュールリスト

  • os_auth.py
  • os_client_config.py
  • os_floating_ip.py
  • os_image_facts.py
  • os_image.py
  • os_ironic_node.py
  • os_ironic.py
  • os_keypair.py
  • os_network.py
  • os_networks_facts.py
  • os_nova_flavor.py
  • os_object.py
  • os_port.py
  • os_router.py
  • os_security_group.py
  • os_security_group_rule.py
  • os_server_actions.py
  • os_server_facts.py
  • os_server.py
  • os_server_volume.py
  • os_subnet.py
  • os_subnets_facts.py
  • os_user_group.py
  • os_user.py
  • os_volume.py

刷新されたダイナミックインベントリプログラム

Ansibleのソースコードには、ダイナミックインベントリのサンプルプログラムが含まれています。v2のソースコードにも、もちろんOpenStack環境用のダイナミックインベントリプログラムが含まれているのですが、今回はこのプログラムも全面刷新され、shadeを利用するようになりました。

v1時代のOpenStack環境用ダイナミックインベントリプログラムは、あくまでもサンプル的な内容でしたが、v2は違います。一度取得した情報を一定時間キャッシュして再利用することで、問い合わせ回数を削減するなどの工夫がされており、そのまま特に手を加えることなく利用できるレベルの仕上がりで、足りない機能を自身で追加するにしても、コードがシンプルで改修もしやすくなっています。

実際に利用してみる

まずは、上記サイトからダイナミックインベントリプログラムと設定ファイルを入手します。

※以降、すでにshadeはインストール済みであるものと仮定します。

テスト環境

ここでは以下のような環境を例に、ダイナミックインベントリプログラムの利用例を紹介します。

  • OpenStackコントローラ: 10.0.0.2
  • ユーザ: saitou
  • パスワード: changeme
  • ドメイン: default
  • プロジェクト名: OOD2015
  • 環境名: liberty

設定ファイルの取得と編集

(develop)$ sudo mkdir /etc/ansible/ (develop)$ cd /etc/ansible (develop)$ sudo wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/openstack.yml

/etc/ansible/openstack.yml
clouds:
  liberty:
    auth:
      auth_url: http://10.0.0.2:5000
      username: saitou
      password: changeme
      project_domain_id: default
      user_domain_id: default
      project_name: OOD2015
    identity_api_version: '3'

ダイナミックインベントリプログラムの取得とテスト実行

公式サイトからダイナミックインベントリプログラムを取得して、実行権を付与しておきます。

(develop)$ cd ~
(develop)$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib /inventory/openstack.py
(develop)$ chmod +x openstack.py

早速テストしてみます。

(develop)$ ./openstack.py --list

実行結果は以下の通り。ちゃんと _meta も使ってくれています。素敵!

{
  "": [
    "9da2a418-61bd-4118-8eac-1e3d4d639c73", 
    "9da2a418-61bd-4118-8eac-1e3d4d639c73", 
    "ea5bd227-3b9c-4a43-80ea-8feb34eb6eb5", 
    "ea5bd227-3b9c-4a43-80ea-8feb34eb6eb5", 
    "f40e9f60-2f9c-4405-99dc-50f37b988ef6", 
    "f40e9f60-2f9c-4405-99dc-50f37b988ef6"
  ], 
  "_meta": {
    "hostvars": {
      "9da2a418-61bd-4118-8eac-1e3d4d639c73": {
        "ansible_ssh_host": "10.0.1.143", 
        "openstack": {
          "HUMAN_ID": true, 
          "NAME_ATTR": "name", 
          "OS-DCF:diskConfig": "AUTO", 
          "OS-EXT-AZ:availability_zone": "nova", 
          "OS-EXT-STS:power_state": 4, 
          "OS-EXT-STS:task_state": null, 
          "OS-EXT-STS:vm_state": "stopped", 
          "OS-SRV-USG:launched_at": "2015-12-06T07:17:22.000000", 
          "OS-SRV-USG:terminated_at": null, 
          "accessIPv4": "10.0.1.143", 
          "accessIPv6": ""
...以下略...

これをansibleから利用してみます。まずはallで全インスタンスpingモジュールを適用してみる。

(develop)$ ansible all -i ./openstack.py -m ping -u centos
ea5bd227-3b9c-4a43-80ea-8feb34eb6eb5 | success >> {
    "changed": false,
    "ping": "pong"
}

9da2a418-61bd-4118-8eac-1e3d4d639c73 | success >> {
    "changed": false,
    "ping": "pong"
}

f40e9f60-2f9c-4405-99dc-50f37b988ef6 | success >> {
    "changed": false,
    "ping": "pong"
}

Ansibleのダイナミックインベントリは、ホスト名やフレーバなどの情報を元に自動でグループを作ってくれるようなので、これもテストしてみます。

(develop)$ ansible step -i ./openstack.py -m ping -u centos
9da2a418-61bd-4118-8eac-1e3d4d639c73 | success >> {
    "changed": false,
    "ping": "pong"
}

(develop)$ ansible docker7 -i ./openstack.py -m ping -u centos
ea5bd227-3b9c-4a43-80ea-8feb34eb6eb5 | success >> {
    "changed": false,
    "ping": "pong"
}

ホスト名でのグループ指定もできてる!便利! 実はshadeはAnsibleのダイナミックインベントリを処理するため(としか思えない)に利用できるOpenStackInventoryクラスを持っています。 Ansible v2に含まれるダイナミックインベントリは、このクラスを利用しているため、v1用と比較して、コード自体が非常に簡潔になっておりカスタマイズもしやすくなっていますので、カスタマイズベースとしても使う価値ありです!

まとめ

Ansible v2では、OpenStack環境との親和性が飛躍的に高まりました。 OpenStackモジュールは刷新されており、shadeのおかげでシンプルでわかりやすい構造となっています。 OpenStack環境でAnsibleを利用しているひとは、切り替える価値が十分ありますので、是非試してみてください。

とは言っても

肝心のv2ってまだリリースされてないですよねぇ サンタはリリース情報をもって来てくれないかもしれないですが、みなさん素敵なクリスマスを:-)