自宅OpenStackのAnsible解説

こんにちはこんにちは。
サイバーエージェント24卒の近藤です。普段はプライベートクラウドのIaaS周りの開発・運用をしています。

Ansible Advent Calendar 2024 14日目の記事ということで、私が自宅クラウドのOpenStackを構築するのに使っているAnsibleを題材にして、Ansibleの基本的な使い方について紹介したいと思います。
業務で使っているものとは何の関係もないので、これをこのまま本番環境で使える、みたいなものではないのす。ご注意ください。

---

## 目次

1. **Ansibleのディレクトリ構成**
2. **Inventoryファイルの解説**
3. **各コンポーネントの構築手順**
    - データベースノード
    - コントローラノード
    - コンピュートノード
4. **まとめ**
5. **参考資料**

---

## 1. Ansibleのディレクトリ構成

以下が本記事で使用するAnsibleのディレクトリ構成です。
~~全てのソースコードを見たい方は[こちら](https://github.com/TOMOFUMI-KONDO/openstack-ansible)からどうぞ。~~
諸事情によりprivate repoにしました。:pray:

なおOpenStackにはちゃんと公式のAnsible Playbookもありますが、今回は自分で作ってみたかったため作りました。

```bash
.
├── compute.yml
├── controller.yml
├── database.yml
├── inventory.yml
├── requirements.txt
├── roles
│   ├── common
│   │   └── tasks
│   │       └── main.yml
│   ├── compute
│   │   ├── files
│   │   │   └── chrony.conf
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── main.yml
│   │   │   ├── neutron.yml
│   │   │   ├── nova.yml
│   │   │   └── ntp.yml
│   │   └── templates
│   │       ├── neutron.conf.j2
│   │       ├── nova-compute.conf.j2
│   │       ├── nova.conf.j2
│   │       └── openvswitch_agent.ini.j2
│   ├── controller
│   │   ├── files
│   │   │   ├── apache2.conf
│   │   │   ├── apache2_openstack_dashboard.conf
│   │   │   ├── dhcp_agent.ini
│   │   │   ├── horizon_local_settings.py
│   │   │   └── ml2_conf.ini
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── cinder.yml
│   │   │   ├── etcd.yml
│   │   │   ├── glance.yml
│   │   │   ├── horizon.yml
│   │   │   ├── keystone.yml
│   │   │   ├── main.yml
│   │   │   ├── memcached.yml
│   │   │   ├── neutron.yml
│   │   │   ├── nova.yml
│   │   │   ├── ntp.yml
│   │   │   ├── placement.yml
│   │   │   └── rabbitmq.yml
│   │   └── templates
│   │       ├── chrony.conf.j2
│   │       ├── cinder.conf.j2
│   │       ├── etcd.j2
│   │       ├── glance-api.conf.j2
│   │       ├── keystone.conf.j2
│   │       ├── memcached.conf.j2
│   │       ├── metadata_agent.ini.j2
│   │       ├── neutron.conf.j2
│   │       ├── nova.conf.j2
│   │       ├── openvswitch_agent.ini.j2
│   │       └── placement.conf.j2
│   ├── database
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── create_user.yml
│   │   │   ├── main.yml
│   │   │   └── secure_installation.yml
│   │   └── templates
│   │       └── 99-openstack.cnf.j2
│   └── openstack
│       └── tasks
│           └── main.yml
└── vars
    ├── credentials.yml
    └── vars.yml
```

エントリポイントとなるplaybook

- database.yml: データベースノードの構築
- controller.yml: コントローラノードの構築
- compute.yml: コンピュートノードの構築

インベントリファイル

- inventory.yml: 対象ノードのIPアドレスやパラメータを定義

各コンポーネントのplaybook

- roles/common: 全ノード共通設定のplaybook
- roles/openstack: OpenStackノード共通設定のplaybook
- roles/database: データベースノードのplaybook
- roles/controller: コントローラノードのplaybook
- roles/compute: コンピュートノードのplaybook

変数ファイル

- vars/vars.yml: 変数ファイル
- vars/credentials.yml: センシティブな変数ファイル

## 2. インベントリファイルの解説

Ansibleでは、インベントリファイルに構築対象ノードのIPアドレスやパラメータを定義します。
今回用いたinventory.ymlファイルの中身は以下の様になっています。

```yaml
all:
  children:
    databases:
    controllers:
    computes:
  vars:
    ansible_user: admin

openstack:
  children:
    controllers:
    computes:

databases:
  hosts:
    master01:
      ansible_host: 192.168.1.101

controllers:
  hosts:
    master01:
      ansible_host: 192.168.1.101
      provider_interface: eth0

computes:
  hosts:
    worker01:
      ansible_host: 192.168.1.102
      provider_interface: enx68e1dcc5f7fa
    worker02:
      ansible_host: 192.168.1.103
      provider_interface: enp5s0f0
```

- all
    - 全てのノードを含むグループ
    - vars.ansible_user
        - Ansibleでssh接続するユーザー名を一括で設定
- openstack
    - OpenStack関連のノードを含むグループ
- databases
    - データベースノードを含むグループ
    - 今回はcontrollerと同居する構成
- controllers
    - OpenStackの主要コンポーネントを動作させるコントローラノードを含むグループ
    - master01の単一コントローラ構成
    - `provider_interface` 変数を定義してPlaybook内で使用
- computes
    - VMを起動するコンピュートノードを含むグループ
    - worker01, worker02の2台のハイパーバイザを使用
    - `provider_interface` 変数を定義してPlaybook内で使用

## 3. 各コンポーネントの構築手順

### 3.1 データベースノード

データベースノードを構築するPlaybookのエントリポイントのdatebase.ymlは以下の様になっています。

 ```yaml
 - hosts: databases
  become: yes
  roles:
    - database
  vars_files:
    - ./vars/vars.yml
    - ./vars/credentials.yml
```

ここで `database` roleを指定することで、`roles/database/tasks/main.yml` が実行されます。

`roles/database/tasks/main.yml` では、以下のようなタスクを実行します。

```yaml
---
- name: install_database
  ansible.builtin.apt:
    name: mariadb-server

- name: start_database
  ansible.builtin.service:
    name: mariadb
    state: started
    enabled: true 

- name: install_python_mysql_package
  ansible.builtin.apt:
    name: python3-pymysql

- name: put_config
  ansible.builtin.template:
    src: ./99-openstack.cnf.j2
    dest:  /etc/mysql/mariadb.conf.d/99-openstack.cnf 
    owner: root
    group: root
    mode: '644'
  notify: restart_database

- name: secure_installation
  ansible.builtin.include_tasks:
    file: secure_installation.yml

- name: create_viewer_user
  community.mysql.mysql_user:
    login_user: root
    login_unix_socket: /run/mysqld/mysqld.sock
    name: viewer
    host: "{{ item }}"
    password: "{{ database_viewer_pass }}"
    priv: '*.*:SELECT'
  loop:
    - localhost
    - '%'
  run_once: true

- name: create_openstack_dbs
  community.mysql.mysql_db:
    name: "{{ item }}"
    login_unix_socket: /run/mysqld/mysqld.sock
  loop:
    - keystone
    - glance
    - placement
    - nova_api
    - nova
    - nova_cell0
    - neutron
    - cinder
  run_once: true

- name: create_openstack_db_users
  ansible.builtin.include_tasks:
    file: create_user.yml
  with_items:
    - { user: 'keystone', dbs: ['keystone'], pass: "{{ database_keystone_pass }}" }
    - { user: 'glance', dbs: ['glance'], pass: "{{ database_glance_pass }}" }
    - { user: 'placement', dbs: ['placement'], pass: "{{ database_placement_pass }}" }
    - { user: 'nova', dbs: ['nova_api', 'nova', 'nova_cell0'], pass: "{{ database_nova_pass }}" }
    - { user: 'neutron', dbs: ['neutron'], pass: "{{ database_neutron_pass }}" }
    - { user: 'cinder', dbs: ['cinder'], pass: "{{ database_cinder_pass }}" }
  loop_control:
    loop_var: outer_item
  run_once: true
```

1つずつ説明していきましょう。

以下は `ansible.builtin.apt` モジュールを使用して、mariadb-serverのaptパッケージをインストールしています。
`sudo apt intsall mariadb-server` みたいなもんですね。
以降、同じ様なパッケージのインストールについては説明を省略します。

```yaml
- name: install_database
  ansible.builtin.apt:
    name: mariadb-server
```

以下は `ansible.builtint.service` モジュールを使用して、systemdのmariadbi.serviceを起動&登録しています。
`sudo systemctl enabled --now mariadb` に相当します。
こちらも同じ様なタスクが今後も出てくるため、以降は説明を省略します。

```yaml
- name: start_database
  ansible.builtin.service:
    name: mariadb
    state: started
    enabled: true 
```

以下は `ansible.builtin.template` モジュールを使用して、mariadbの設定ファイルを投入しています。

```yaml
- name: put_config
  ansible.builtin.template:
    src: ./99-openstack.cnf.j2
    dest:  /etc/mysql/mariadb.conf.d/99-openstack.cnf 
    owner: root
    group: root
    mode: '644'
  notify: restart_database
```

テンプレートの `99-openstack.cnf.j2` は `roles/database/templates/99-openstack.cnf.j2` に格納されています。
`{{ ansible_host }}` の様に `{{ }}` で囲まれた変数は、 `vars/vars.yml` に定義された変数を参照し、templateモジュールによって埋め込むことができます。

```ini
[mysqld]
bind-address = {{ ansible_host }}

default-storage-engine = innodb
innodb_file_per_table = on
max_connections = 4096
collation-server = utf8_general_ci
character-set-server = utf8
```

また、上記タスクでは `notify: restart_database` という記述により、もしこのタスクが実行されたならば全タスクの完了後に `restart_database` というハンドラを呼び出します。
ハンドラの記述は `roles/database/handlers/main.yml` にあります。

```yaml
---
- name: restart_database
  ansible.builtin.service:
    name: mariadb
    state: restarted
```

上記の仕組みにより、設定ファイルが変更された時のみその設定を反映するためにmariadbを再起動するというフローを実現することができます。

続いて以下は `ansible.builtin.include_tasks` モジュールを使用して、別ファイルのタスクをインクルードし、mariadbのsecure_installation相当の処理を実行しています。

```yaml
- name: secure_installation
  ansible.builtin.include_tasks:
    file: secure_installation.yml
```

インクルードされるタスクファイルは `roles/database/tasks/secure_installation.yml` に格納されています。
secure_installation タスクでは、`community.mysql` モジュールを使用して、rootユーザーのパスワード設定や不要なユーザーの削除、不要なデータベースの削除を行います。
`database_root_pass` は `vars/credentials.yml` に定義されたセンシティブな変数です。以降も同じ様な変数が度々登場します(パスワードとか)。

```yaml
---
- name: set_root_pass
  community.mysql.mysql_user:
      login_user: root
      login_unix_socket: /run/mysqld/mysqld.sock
      name: root
      host_all: true
      password: "{{ database_root_pass }}"
- name: disable_remote_root_login
  community.mysql.mysql_user:
      login_user: root
      login_unix_socket: /run/mysqld/mysqld.sock
      name: root
      host: "{{ ansible_fqdn | lower }}"
      state: absent
- name: remove_anonymous_users
  community.mysql.mysql_user:
      login_user: root
      login_unix_socket: /run/mysqld/mysqld.sock
      name: ''
      host_all: true
      state: absent
- name: remove_test_database
  mysql_db: 
      login_user: root
      login_unix_socket: /run/mysqld/mysqld.sock
      db: test
      state: absent
```

以下は `community.mysql.mysql_user` モジュールを使用して、運用時に使うread onlyのviewerユーザーを作成し、SELECT権限を付与しています。
ここでは `loop` ディレクティブを使用して、localhostと全てのホスト(`%`(に対して同じ設定を適用しています。
この様にすることで同じ様なタスクを重複した記述なく実行することができます。

```yaml
- name: create_viewer_user
  community.mysql.mysql_user:
    login_user: root
    login_unix_socket: /run/mysqld/mysqld.sock
    name: viewer
    host: "{{ item }}"
    password: "{{ database_viewer_pass }}"
    priv: '*.*:SELECT'
  loop:
    - localhost
    - '%'
  run_once: true
```

以下は `community.mysql.mysql_db` モジュールを使用して、OpenStackの各コンポーネント用のデータベースを作成しています。

```yaml
- name: create_openstack_dbs
  community.mysql.mysql_db:
    name: "{{ item }}"
    login_unix_socket: /run/mysqld/mysqld.sock
  loop:
    - keystone
    - glance
    - placement
    - nova_api
    - nova
    - nova_cell0
    - neutron
    - cinder
  run_once: true
```

最後に以下は、viewerユーザと同じように、OpenStackの各コンポーネント用のユーザを作成しています。

```yaml
- name: create_openstack_db_users
  ansible.builtin.include_tasks:
    file: create_user.yml
  with_items:
    - { user: 'keystone', dbs: ['keystone'], pass: "{{ database_keystone_pass }}" }
    - { user: 'glance', dbs: ['glance'], pass: "{{ database_glance_pass }}" }
    - { user: 'placement', dbs: ['placement'], pass: "{{ database_placement_pass }}" }
    - { user: 'nova', dbs: ['nova_api', 'nova', 'nova_cell0'], pass: "{{ database_nova_pass }}" }
    - { user: 'neutron', dbs: ['neutron'], pass: "{{ database_neutron_pass }}" }
    - { user: 'cinder', dbs: ['cinder'], pass: "{{ database_cinder_pass }}" }
  loop_control:
    loop_var: outer_item
  run_once: true
```

ここでインクルードしている `create_user.yml` は以下の様になっています。

```yaml
---
- name: create_user
  community.mysql.mysql_user:
    login_user: root
    login_unix_socket: /run/mysqld/mysqld.sock
    name: "{{ outer_item['user'] }}"
    host: "{{ item }}"
    password: "{{ outer_item['pass'] }}"
    priv: "{{ outer_item['dbs'] | map('regex_replace', '(.+)', '\\1.*:ALL') | join ('/') }}"
  loop:
    - localhost
    - "%"
```

タスクをインクルードするときに `loop_control.loop_var: outer_item` とすることで、`with_items` で渡した変数をインクルードされるタスク内で `outer_item` として参照することができます。

### 3.2 コントローラノード

OpenStackのコントローラノードを構築するPlaybookのエントリポイントのcontroller.ymlは以下の様になっています。
コントローラノードには、common, openstack, controllerの3つのroleを指定し、全ノード共通の設定、OpenStack用の設定、そしてOpenStackのコントローラノード構築の設定を行います。

```yaml
- hosts: controllers
  become: yes
  roles:
    - common
    - openstack
    - controller
  vars_files:
    - ./vars/vars.yml
    - ./vars/credentials.yml
  environment:
    OS_PROJECT_DOMAIN_NAME: Default
    OS_USER_DOMAIN_NAME: Default
    OS_PROJECT_NAME: admin
    OS_USERNAME: admin
    OS_PASSWORD: "{{ openstack_admin_pass }}"
    OS_AUTH_URL: http://controller:5000/v3
    OS_IDENTITY_API_VERSION: 3
    OS_IMAGE_API_VERSION: 2
```

OpenStack用の設定をするタスク `roles/openstack/tasks/main.yml` は以下の様になっています。

```yaml
---
- name: install_ubuntu_cloud_keyring
  ansible.builtin.apt:
    name: ubuntu-cloud-keyring

- name: add_cloud_archive_repo
  ansible.builtin.apt_repository:
    repo: deb http://ubuntu-cloud.archive.canonical.com/ubuntu {{ ansible_distribution_release }}-updates/dalmatian main

- name: install_packages
  ansible.builtin.apt:
    name: "{{ item }}"
  loop:
    - software-properties-common
    - cpu-checker
    - bridge-utils
    - vlan
    - libvirt-clients
    - zip
    - python3-openstackclient

- name: load_kernel_modules
  community.general.modprobe:
    name: "{{ item.name }}"
    params: "{{ item.params | default('') }}"
    persistent: 'present'
  loop:
    - { 'name': 'bridge'  }
    - { 'name': 'br_netfilter'  }
    - { 'name': '8021q'  }
    - { 'name': 'vhost_net'  }

- name: disable_iptables_for_bridge
  ansible.posix.sysctl:
    name: "{{ item }}"
    value: '0'
  loop:
    - net.bridge.bridge-nf-call-arptables
    - net.bridge.bridge-nf-call-ip6tables
    - net.bridge.bridge-nf-call-iptables
```

以下は `ansible.builtin.apt`, `ansible.builtin.apt_repository` モジュールを使用して、ubuntu-cloud-keyringパッケージをインストールし、OpenStackのリポジトリを追加しています。
OpenStack公式のInstallation GuideにはUbuntu Nobleでのリポジトリの追加方法が書いていなかったため、[Server Worldの記事](https://www.server-world.info/query?os=Ubuntu_24.04&p=openstack_dalmatian&f=1) を参考にさせていただきました。ありがとうございます。

```yaml
- name: install_ubuntu_cloud_keyring
  ansible.builtin.apt:
    name: ubuntu-cloud-keyring

- name: add_cloud_archive_repo
  ansible.builtin.apt_repository:
    repo: deb http://ubuntu-cloud.archive.canonical.com/ubuntu {{ ansible_distribution_release }}-updates/dalmatian main
```

以下は `community.general.modprobe` モジュールを使用して、OpenStackを動かすのに必要なカーネルモジュールをロードしています。

```yaml
- name: load_kernel_modules
  community.general.modprobe:
    name: "{{ item.name }}"
    params: "{{ item.params | default('') }}"
    persistent: 'present'
  loop:
    - { 'name': 'bridge'  }
    - { 'name': 'br_netfilter'  }
    - { 'name': '8021q'  }
    - { 'name': 'vhost_net'  }
```

以下は `ansible.posix.sysctl` モジュールを使用して、Installation Guideに書いてある通りにiptables周りの設定を無効化しています。

```yaml
- name: disable_iptables_for_bridge
  ansible.posix.sysctl:
    name: "{{ item }}"
    value: '0'
  loop:
    - net.bridge.bridge-nf-call-arptables
    - net.bridge.bridge-nf-call-ip6tables
    - net.bridge.bridge-nf-call-iptables
```

以下は `roles/controller/tasks/main.yml` に記述された、コントローラノード構築用のタスクの一覧です。

```yaml
---
- name: setup_ntp
  ansible.builtin.include_tasks:
    file: ntp.yml

- name: setup_rabbitmq
  ansible.builtin.include_tasks:
    file: rabbitmq.yml

- name: setup_memcached
  ansible.builtin.include_tasks:
    file: memcached.yml

- name: setup_etcd
  ansible.builtin.include_tasks:
    file: etcd.yml

- name: setup_keystone
  ansible.builtin.include_tasks:
    file: keystone.yml

- name: setup_glance
  ansible.builtin.include_tasks:
    file: glance.yml

- name: setup_placement
  ansible.builtin.include_tasks:
    file: placement.yml

- name: setup_nova
  ansible.builtin.include_tasks:
    file: nova.yml

- name: setup_neutron
  ansible.builtin.include_tasks:
    file: neutron.yml

- name: setup_horizon
  ansible.builtin.include_tasks:
    file: horizon.yml
```

#### ntp

setup_ntpタスクの内容は以下の様になっており、chronyのインストールと設定ファイルの配置を行なっています。

```yaml
---
- name: install_chrony
  ansible.builtin.apt:
    name: chrony

- name: start_chrony
  ansible.builtin.service:
    name: chrony
    state: started
    enabled: true

- name: put_chrony_config
  ansible.builtin.template:
    src: ./chrony.conf.j2
    dest:  /etc/chrony/chrony.conf
    owner: root
    group: root
    mode: '644'
  notify: restart_chrony
```

#### rabbitmq

setup_rabbitmqタスクの内容は以下の様になっており、rabbitmq-serverのインストールと設定を行なっています。
ここでは `community.rabbitmq.rabbitmq_user` モジュールを使用して、OpenStack用のユーザを作成しています。
この様に既存のモジュールを使用して楽に設定をすることができるのがAnsibleの強みです。

```yaml
---
- name: install_rabbitmq
  ansible.builtin.apt:
    name: rabbitmq-server

- name: start_rabbitmq
  ansible.builtin.service:
    name: rabbitmq-server
    state: started
    enabled: true

- name: create_openstack_user
  community.rabbitmq.rabbitmq_user:
    user: openstack
    password: "{{ rabbitmq_pass }}"
    configure_priv: .*
    read_priv: .*
    write_priv: .*
  run_once: true
```

#### memcached

setup_memcachedタスクの内容は以下の様になっており、memcachedのインストールと設定を行なっています。
なんか同じ様なのばっかですね、、、

```yaml
---
- name: install_memcached
  ansible.builtin.apt:
    name: memcached

- name: install_python_memcached_package
  ansible.builtin.apt:
    name: python3-memcache

- name: start_memcached
  ansible.builtin.service:
    name: memcached
    state: started
    enabled: true

- name: put_memcached_config
  ansible.builtin.template:
    src: ./memcached.conf.j2
    dest:  /etc/memcached.conf
    owner: root
    group: root
    mode: '644'
  notify: restart_memcached
```

#### etcd

setup_etcdタスクの内容は以下の様になっており、etcdのインストールと設定を行なっています。

```yaml
---
- name: install_etcd
  ansible.builtin.apt:
    name: etcd-server

- name: start_etcd
  ansible.builtin.service:
    name: etcd
    state: started
    enabled: true

- name: put_etcd_config
  ansible.builtin.template:
    src: ./etcd.j2
    dest:  /etc/default/etcd
    owner: root
    group: root
    mode: '644'
  notify: restart_etcd
```

#### Keystone

さてここからはOpenStackコンポーネントの設定を行っていきます。
setup_keystoneタスクの内容は以下の様になっており、Keystoneのインストールと設定を行なっています。

```yaml
---
- name: install_keystone
  ansible.builtin.apt:
    name: keystone

- name: put_keystone_config
  ansible.builtin.template:
    src: ./keystone.conf.j2
    dest:  /etc/keystone/keystone.conf
    owner: root
    group: root
    mode: '644'

- name: check_db_sync
  ansible.builtin.command:
    cmd: keystone-manage db_sync --check
  register: check_db_sync
  failed_when: check_db_sync.rc == 1
  changed_when: check_db_sync.rc != 0
  run_once: true

- name: run_db_sync
  ansible.builtin.command:
    cmd: su -s /bin/sh -c "keystone-manage db_sync" keystone
  when: check_db_sync.changed
  run_once: true

- name: run_fernet_setup
  ansible.builtin.command:
    cmd: keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
  when: check_db_sync.changed
  run_once: true

- name: run_credential_setup
  ansible.builtin.command:
    cmd: keystone-manage credential_setup --keystone-user keystone --keystone-group keystone
  when: check_db_sync.changed
  run_once: true

- name: run_bootstrap
  ansible.builtin.command:
    cmd: >
      keystone-manage bootstrap --bootstrap-password "{{ openstack_admin_pass }}"
        --bootstrap-admin-url http://controller:5000/v3/
        --bootstrap-internal-url http://controller:5000/v3/
        --bootstrap-public-url http://controller:5000/v3/
        --bootstrap-region-id RegionOne
  changed_when: false
  run_once: true

- name: put_apache2_config
  ansible.builtin.copy:
    src: ./apache2.conf
    dest:  /etc/apache2/apache2.conf
    owner: root
    group: root
    mode: '644'
  notify: restart_apache2
```

以下は `ansible.builtin.command` モジュールを使用して、Keystoneのデータベースの構築を行います。
shellでコマンドを実行する様なイメージです。

`register` によって、このタスクの結果を後から参照することができます。
また、`failed_when`、`changed_when` によってこのタスクが失敗した、実行されたとみなされる条件を定義しています。
`ansible.builtin.command` モジュールはデフォルトではコマンドのステータスコードが0かどうかしか確認しないため、実際に実行するコマンドによってカスタマイズすることが必要です。
また、`run_once` を定義することによって、もしコントローラノードが複数あった際にもこのタスクは一回しか実行されません。

```
- name: check_db_sync
  ansible.builtin.command:
    cmd: keystone-manage db_sync --check
  register: check_db_sync
  failed_when: check_db_sync.rc == 1
  changed_when: check_db_sync.rc != 0
  run_once: true
```

続く以下のタスクでは、先ほどのタスクの結果を参照し、先ほどのタスクが実行されていた場合のみ実行する様に制御しています。
これは `when` ディレクティブで `check_db_sync.changed` を参照することで実現しています。

```yaml
- name: run_fernet_setup
  ansible.builtin.command:
    cmd: keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
  when: check_db_sync.changed
  run_once: true

- name: run_credential_setup
  ansible.builtin.command:
    cmd: keystone-manage credential_setup --keystone-user keystone --keystone-group keystone
  when: check_db_sync.changed
  run_once: true
```

#### Glance

以下は OpenStackのVMディスクイメージ管理コンポーネントのGlanceを構築します。

```yaml
---
- name: create_glacne_user
  openstack.cloud.identity_user:
    name: glance
    password: "{{ openstack_glance_pass }}"
    domain: default

- name: add_admin_role_to_glance
  openstack.cloud.role_assignment:
    user: glance
    role: admin
    project: service

- name: add_glance_service
  openstack.cloud.catalog_service:
   name: glance
   type: image
   description: OpenStack Image

- name: create_endpoints
  openstack.cloud.endpoint:
     service: glance
     endpoint_interface: "{{ item }}"
     url: http://controller:9292
     region: RegionOne
  loop:
    - public
    - internal
    - admin
  register: endpoints

- name: install_glance
  ansible.builtin.apt:
    name: glance

- name: put_glance_config
  ansible.builtin.template:
    src: ./glance-api.conf.j2
    dest:  /etc/glance/glance-api.conf
    owner: root
    group: root
    mode: '644'
  vars:
    glance_public_endpoint: "{{ endpoints['results'][0]['endpoint']['id'] }}"
  notify: restart_glance_api

- name: add_reader_role_to_glance
  openstack.cloud.role_assignment:
    user: glance
    domain: Default
    system: all
    role: reader

- name: run_db_sync
  ansible.builtin.command:
    cmd: su -s /bin/sh -c "glance-manage db_sync" glance
  changed_when: false
  run_once: true
```

ここでは `openstack.cloud` モジュールを使用し、OpenStackのサービスユーザ作成や権限の設定とその他諸々をします。
ここでもエコシステムの恩恵に預かり楽をすることができます。素晴らしいですね。

```yaml
- name: create_glacne_user
  openstack.cloud.identity_user:
    name: glance
    password: "{{ openstack_glance_pass }}"
    domain: default

- name: add_admin_role_to_glance
  openstack.cloud.role_assignment:
    user: glance
    role: admin
    project: service

- name: add_glance_service
  openstack.cloud.catalog_service:
   name: glance
   type: image
   description: OpenStack Image

- name: create_endpoints
  openstack.cloud.endpoint:
     service: glance
     endpoint_interface: "{{ item }}"
     url: http://controller:9292
     region: RegionOne
  loop:
    - public
    - internal
    - admin
  register: endpoints
```

#### Placement

以下は OpenStackのリソース管理コンポーネントのPlacementを構築します。
Glanceとほとんど同じ様なタスクで特に説明することもないので詳細は割愛します。

```yaml
---
- name: create_placement_user
  openstack.cloud.identity_user:
    name: placement
    password: "{{ openstack_placement_pass }}"
    domain: default

- name: add_admin_role_to_placement
  openstack.cloud.role_assignment:
    user: placement
    role: admin
    project: service

- name: add_placement_service
  openstack.cloud.catalog_service:
   name: placement
   type: placement
   description: Placement API

- name: create_endpoints
  openstack.cloud.endpoint:
     service: placement
     endpoint_interface: "{{ item }}"
     url: http://controller:8778
     region: RegionOne
  loop:
    - public
    - internal
    - admin

- name: install_placement
  ansible.builtin.apt:
    name: placement-api

- name: put_placement_config
  ansible.builtin.template:
    src: ./placement.conf.j2
    dest:  /etc/placement/placement.conf
    owner: root
    group: root
    mode: '644'
  notify: restart_apache2

- name: run_db_sync
  ansible.builtin.command:
    cmd: su -s /bin/sh -c "placement-manage db sync" placement
  changed_when: false
  run_once: true
```

#### Nova

以下は OpenStackでの仮想マシン管理コンポーネントのNovaを構築します。

```yaml
---
- name: create_nova_user
  openstack.cloud.identity_user:
    name: nova
    password: "{{ openstack_nova_pass }}"
    domain: default

- name: add_admin_role_to_nova
  openstack.cloud.role_assignment:
    user: nova
    role: admin
    project: service

- name: add_nova_service
  openstack.cloud.catalog_service:
   name: nova
   type: compute
   description: OpenStack Compute

- name: create_endpoints
  openstack.cloud.endpoint:
     service: nova
     endpoint_interface: "{{ item }}"
     url: http://controller:8774/v2.1
     region: RegionOne
  loop:
    - public
    - internal
    - admin

- name: install_nova_packages
  ansible.builtin.apt:
    name: "{{ item }}"
  loop:
    - nova-api
    - nova-conductor
    - nova-novncproxy
    - nova-scheduler

- name: put_nova_config
  ansible.builtin.template:
    src: ./nova.conf.j2
    dest:  /etc/nova/nova.conf
    owner: root
    group: root
    mode: '644'
  notify: restart_nova

- name: run_api_db_sync
  ansible.builtin.command:
    cmd: su -s /bin/sh -c "nova-manage api_db sync" nova
  changed_when: false
  run_once: true

- name: list_nova_cells
  ansible.builtin.command:
    cmd: su -s /bin/sh -c "nova-manage cell_v2 list_cells" nova
  changed_when: false
  register: cell_list

- name: setl_cell_facts
  set_fact:
    cell0_record: '{{ cell_list.stdout_lines | select("regex", "[0-]{36}") }}'
    cell1_record: '{{ cell_list.stdout_lines | select("regex", " cell1 ") }}'

- name: run_map_cell0
  ansible.builtin.command: 
    cmd: su -s /bin/sh -c "nova-manage cell_v2 map_cell0" nova
  when: not cell0_record

- name: run_create_cell1
  ansible.builtin.command: 
    cmd: su -s /bin/sh -c "nova-manage cell_v2 create_cell --name=cell1 --verbose" nova
  when: not cell1_record

- name: run_db_sync
  ansible.builtin.command:
    cmd: su -s /bin/sh -c "nova-manage db sync" nova
  changed_when: false
```

特筆すべきことでいうとこちらのset_factを利用したタスク実行の制御ですかね。
`list_nova_cells` タスクの出力に対して正規表現で一致したものがあれば、すでに作成したいリソースが作成されていると判断して後続のタスクをスキップする様にしています。
ここら辺の処理は悔しいですが本家のopenstack-ansibleをめっちゃ参考にしました。

```yaml
- name: list_nova_cells
  ansible.builtin.command:
    cmd: su -s /bin/sh -c "nova-manage cell_v2 list_cells" nova
  changed_when: false
  register: cell_list

- name: setl_cell_facts
  set_fact:
    cell0_record: '{{ cell_list.stdout_lines | select("regex", "[0-]{36}") }}'
    cell1_record: '{{ cell_list.stdout_lines | select("regex", " cell1 ") }}'

- name: run_map_cell0
  ansible.builtin.command: 
    cmd: su -s /bin/sh -c "nova-manage cell_v2 map_cell0" nova
  when: not cell0_record

- name: run_create_cell1
  ansible.builtin.command: 
    cmd: su -s /bin/sh -c "nova-manage cell_v2 create_cell --name=cell1 --verbose" nova
  when: not cell1_record
```

#### Neutron

以下は OpenStackのネットワーク管理コンポーネントのNeutronを構築します。

```yaml
---
- name: create_neutron_user
  openstack.cloud.identity_user:
    name: neutron
    password: "{{ openstack_neutron_pass }}"
    domain: default

- name: add_admin_role_to_neutron
  openstack.cloud.role_assignment:
    user: neutron
    role: admin
    project: service

- name: add_neutron_service
  openstack.cloud.catalog_service:
   name: neutron
   type: network
   description: OpenStack Networking

- name: create_endpoints
  openstack.cloud.endpoint:
     service: neutron
     endpoint_interface: "{{ item }}"
     url: http://controller:9696
     region: RegionOne
  loop:
    - public
    - internal
    - admin

- name: install_neutron_packages
  ansible.builtin.apt:
    name: "{{ item }}"
  loop:
    - neutron-server
    - neutron-plugin-ml2
    - neutron-openvswitch-agent
    - neutron-dhcp-agent
    - neutron-metadata-agent

- name: create_br_provider
  openvswitch.openvswitch.openvswitch_bridge:
    bridge: br-provider
    fail_mode: secure

- name: create_br_provider_port
  openvswitch.openvswitch.openvswitch_port:
    bridge: br-provider
    port: "{{ provider_interface }}"

- name: put_neutron_config_templates
  ansible.builtin.template:
    src: "{{ item.src }}"
    dest: "{{ item.dest }}"
    owner: root
    group: root
    mode: '644'
  notify: restart_neutron
  loop:
    - { src: './neutron.conf.j2', dest: '/etc/neutron/neutron.conf' }
    - { src: './openvswitch_agent.ini.j2', dest: '/etc/neutron/plugins/ml2/openvswitch_agent.ini' }
    - { src: './metadata_agent.ini.j2', dest: '/etc/neutron/metadata_agent.ini' }

- name: put_neutron_config_files
  ansible.builtin.copy:
    src: "{{ item.src }}"
    dest: "{{ item.dest }}"
    owner: root
    group: root
    mode: '644'
  notify: restart_neutron
  loop:
    - { src: './ml2_conf.ini', dest: '/etc/neutron/plugins/ml2/ml2_conf.ini' }
    - { src: './dhcp_agent.ini', dest: '/etc/neutron/dhcp_agent.ini' }

- name: populate_neutron_db
  ansible.builtin.command:
    cmd: >
      su -s /bin/sh -c "neutron-db-manage \
        --config-file /etc/neutron/neutron.conf \
        --config-file /etc/neutron/plugins/ml2/ml2_conf.ini \
        upgrade head" neutron
  changed_when: false
  run_once: true
```

Neutronの設定で特筆する所は、`openvswitch.openvswitch` モジュールを使ってノードのovsの設定をいじっている所です。

以下は `openvswitch.openvswitch.openvswitch_bridge` モジュールを使用して、br-providerという外部ネットワーク用のインターフェースにつながるbridgeを作成しています。

```yaml
- name: create_br_provider
  openvswitch.openvswitch.openvswitch_bridge:
    bridge: br-provider
    fail_mode: secure
```

以下は `openvswitch.openvswitch.openvswitch_port` モジュールを使用して、br-providerに外部ネットワーク用のインターフェースを接続しています。
ここで接続するインターフェースはノードごとに異なるので、インベントリファイルで指定したインターフェース名を使用して指定します。
ノードのネットワーク設定でMacアドレス指定でインターフェース名を書き換えて、全ノードでインターフェース名を揃えるのでもいいかもしれないですね。

```yaml
- name: create_br_provider_port
  openvswitch.openvswitch.openvswitch_port:
    bridge: br-provider
    port: "{{ provider_interface }}"
```

#### Horizon

以下は OpenStackのダッシュボードコンポーネントのHorizonを構築します。
こちらは特に目新しいものはなく、パッケージをインストールして設定ファイルを置き、サービスを再起動する、といった感じです。

```yaml
---
- name: install_horizon
  ansible.builtin.apt:
    name: openstack-dashboard

- name: put_horizon_config
  ansible.builtin.copy:
    src: ./horizon_local_settings.py
    dest: /etc/openstack-dashboard/local_settings.py 
    owner: root
    group: root
    mode: '644'
  notify: reload_apache2

- name: put_apache2_config
  ansible.builtin.copy:
    src: ./apache2_openstack_dashboard.conf
    dest: /etc/apache2/conf-available/openstack-dashboard.conf 
    owner: root
    group: root
    mode: '644'
  notify: reload_apache2
```

### 3.3 コンピュートノード

OpenStackのコンピュートノードを構築するPlaybookのエントリポイントのcompute.ymlは以下の様になっています。
コンピュートノードには、common, openstack, computeの3つのroleを指定し、全ノード共通の設定、OpenStack用の設定、そしてOpenStackのコンピュートノード構築の設定を行います。

```yaml
- hosts: computes
  become: yes
  roles:
    - common
    - openstack
    - compute
  vars_files:
    - ./vars/vars.yml
    - ./vars/credentials.yml
  environment:
    OS_PROJECT_DOMAIN_NAME: Default
    OS_USER_DOMAIN_NAME: Default
    OS_PROJECT_NAME: admin
    OS_USERNAME: admin
    OS_PASSWORD: "{{ openstack_admin_pass }}"
    OS_AUTH_URL: http://controller:5000/v3
    OS_IDENTITY_API_VERSION: 3
    OS_IMAGE_API_VERSION: 
```

以下は `roles/compute/tasks/main.yml` に記述されたタスクの一覧です。
ntpについてはコントローラノードと大差ないため、ここでは説明を省略します。

```yaml
---
- name: setup_ntp
  ansible.builtin.include_tasks:
    file: ntp.yml

- name: setup_nova
  ansible.builtin.include_tasks:
    file: nova.yml

- name: setup_neutron
  ansible.builtin.include_tasks:
    file: neutron.yml
```

#### Nova

以下は コンピュートノード上で動作し実際に仮想マシンの作成等を行うnova-computeを構築します。
特筆すべきところとして、ここでも既存の `community.libvirt` モジュールを使用して、libvirtの設定を行っています。
libvirtはインストールするとデフォルトで `default` という名前のネットワークを作成しますが、OpenStackではこのネットワークを使わないため、変な競合が起こらないようにこのネットワークを削除しています。

```yaml
---
- name: install_packages
  ansible.builtin.apt:
    name: "{{ item }}"
  loop:
    - nova-compute
    - seabios

- name: disable_libvirt_default_network
  community.libvirt.virt_net:
    name: default
    state: absent
    autostart: false

- name: put_nova_config
  ansible.builtin.template:
    src: "{{ item.src }}"
    dest: "{{ item.dest }}"
    owner: root
    group: root
    mode: '644'
  notify: restart_nova
  loop:
    - { src: './nova.conf.j2', dest: '/etc/nova/nova.conf' }
    - { src: './nova-compute.conf.j2', dest: '/etc/nova/nova-compute.conf' }
```

#### Neutron

以下は コンピュートノード上で動作し、実際にovsをいじって仮想マシンのネットワーク設定を行うneutron-openvswitch-agentを構築します。
コンピュートノードでもコントローラノードと同じように、`openvswitch.openvswitch` モジュールを使用してovsのbridgeの作成と、bridgeへのインターフェースの接続を行なっています。

```yaml
---
- name: install_neutron_ovs_agent
  ansible.builtin.apt:
    name: neutron-openvswitch-agent

- name: create_br_provider
  openvswitch.openvswitch.openvswitch_bridge:
    bridge: br-provider
    fail_mode: secure

- name: create_br_provider_port
  openvswitch.openvswitch.openvswitch_port:
    bridge: br-provider
    port: "{{ provider_interface }}"

- name: put_neutron_config_templates
  ansible.builtin.template:
    src: "{{ item.src }}"
    dest: "{{ item.dest }}"
    owner: root
    group: root
    mode: '644'
  notify: restart_neutron
  loop:
    - { src: './neutron.conf.j2', dest: '/etc/neutron/neutron.conf' }
    - { src: './openvswitch_agent.ini.j2', dest: '/etc/neutron/plugins/ml2/openvswitch_agent.ini' }
```

## 4. まとめ

この記事では、Ansibleを使用してOpenStack環境を構築する方法を解説しました。
OpenStackはコンポーネントや必要な設定が多く、毎回手動で構築するのはかなり大変です。
Ansibleの様なツールを利用することで繰り返し行う作業を自動化し、効率的にOpenStack環境を構築することができます。

最後まで読んだいただき、ありがとうございました。

## 5. 参考資料

- [1] [公式のopenstack-ansible](https://opendev.org/openstack/openstack-ansible)
- [2] [OpenStack Installation Guide](https://docs.openstack.org/install-guide/)
- [3] [Ubuntu NobleでのOpenStack Dalmatianのインストール](https://www.server-world.info/query?os=Ubuntu_24.04&p=openstack_dalmatian&f=1)
- ~~[4] https://github.com/TOMOFUMI-KONDO/openstack-ansible~~
    - 諸事情によりprivate repoにしました。:pray: