Ansible Moleculeにてmolecule-vagrantを使用せずにVirtualBox上のVMを利用する例

備忘

# 課題
Ansible MoleculeにてVirtualBox上のVMを利用する場合、molecule-vagrantが提供されている。
しかしサポートされる環境が少なく今後のサポートも不安が残る。
例えばfedora上のVirtualBoxはサポートされていない。



# ソリューション

delegatedドライバーを使用してvagrantコマンドを実行することで制御を行う

# 環境

- Fedora Linux 35 (Workstation Edition)
- Molecule 3.5.2
- Ansible 2.11.6
- Vagrant 2.2.19
- VirtualBox

# 構成例

## molecule定義テンプレートを含むロールテンプレートを作成

```
$ molecule init role delegated_vagrant
...
$ cd delegated_vagrant
$ tree .
.
├── README.md
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── molecule
│   └── default　　　　　　　　# ここにvagrant定義ファイルであるVagrantfileを追加
│       ├── INSTALL.rst
│       ├── converge.yml
│       ├── create.yml      # <= vm作成ロジックを追記
│       ├── destroy.yml　　　# <= vm削除ロジックを追記
│       ├── molecule.yml    # platforms の項を Vagrantfileの定義名と合わせる
│       └── verify.yml
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml
```

## meta/main.ymlを修正

```yaml:meta/main.yml
galaxy_info:
  namespace: my_galaxy_namespace  # 適当な英小文字、数字、アンダーバーからなる文字列で追記。指定しないとauthorが使用され、スペースが許されないとエラーとなる。
  author: your name
  description: your role description
  company: your company (optional)
...
```

## molecule.yml を編集

```yaml:molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: delegated
platforms:
- name: instance1  # VagrantFile の config.vm.define の値に合わせる
- name: instance2
provisioner:
  name: ansible
verifier:
  name: ansible
```

## Vagrantfile を配置

ここでは2つのvmを定義

```ruby:molecule/default/Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

  config.vm.box = "generic/rhel8"
  config.vm.box_version = "3.0.20"  # RHEL8.2

  config.vm.define "instance1" do |ins|  # molecule.yml における platforms 要素の name と一致させる
    ins.vm.hostname = "instance1"
    ins.vm.network "private_network", ip: "192.168.56.10"
    ins.vm.provider "virtualbox" do |vb|
      vb.memory = 2048
    end
  end

  config.vm.define "instance2" do |ins|  # molecule.yml における platforms 要素の name と一致させる
    ins.vm.hostname = "instance2"
    ins.vm.network "private_network", ip: "192.168.56.11"
    ins.vm.provider "virtualbox" do |vb|
      vb.memory = 1024
    end
  end

end
```

molecule.yml に全ての情報を記載してVagrantfileをテンプレートから作る方法もあり得る。

## このVagrantfileが存在するディレクトリーで以下のようなことが可能。

```
$ cd molecule/default

$ ls Vagrantfile 
Vagrantfile
```

### vmを構成して起動

```
$ vagrant up
Bringing machine 'instance1' up with 'virtualbox' provider...
Bringing machine 'instance2' up with 'virtualbox' provider...
==> instance1: Importing base box 'generic/rhel8'...
==> instance1: Matching MAC address for NAT networking...
==> instance1: Checking if box 'generic/rhel8' version '3.0.20' is up to date...
...
```

### vmの状態表示

```
$ vagrant status
Current machine states:

instance1                 running (virtualbox)
instance2                 running (virtualbox)

This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run `vagrant status NAME`.
```

### vmへのログイン

```
$ vagrant ssh instance1
[vagrant@instance1 ~]$ head /etc/os-release 
NAME="Red Hat Enterprise Linux"
VERSION="8.2 (Ootpa)"
ID="rhel"
ID_LIKE="fedora"
VERSION_ID="8.2"
PLATFORM_ID="platform:el8"
PRETTY_NAME="Red Hat Enterprise Linux 8.2 (Ootpa)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:redhat:enterprise_linux:8.2:GA"
HOME_URL="https://www.redhat.com/"
[vagrant@instance1 ~]$ logout
Connection to 127.0.0.1 closed.
```

### vmに対する接続情報を表示

```
$ vagrant ssh-config  
Host instance1
  HostName 127.0.0.1
  User vagrant
  Port 2205
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /home/foo/repo/delegated_vagrant/molecule/default/.vagrant/machines/instance1/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL

Host instance2
  HostName 127.0.0.1
  User vagrant
  Port 2206
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /home/foo/repo/delegated_vagrant/molecule/default/.vagrant/machines/instance2/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL
```

### 簡易YAML化ロジック例
パス名に空白などが入っていない前提

```
$ vagrant ssh-config | sed '/^$/d' | awk '$1 == "Host" {print "- "$1": "$2} $1 != "Host" {print "  "$1": "$2}'
- Host: instance1
  HostName: 127.0.0.1
  User: vagrant
  Port: 2205
  UserKnownHostsFile: /dev/null
  StrictHostKeyChecking: no
  PasswordAuthentication: no
  IdentityFile: /home/foo/repo/delegated_vagrant/molecule/default/.vagrant/machines/instance1/virtualbox/private_key
  IdentitiesOnly: yes
  LogLevel: FATAL
- Host: instance2
  HostName: 127.0.0.1
  User: vagrant
  Port: 2206
  UserKnownHostsFile: /dev/null
  StrictHostKeyChecking: no
  PasswordAuthentication: no
  IdentityFile: /home/foo/repo/delegated_vagrant/molecule/default/.vagrant/machines/instance2/virtualbox/private_key
  IdentitiesOnly: yes
  LogLevel: FATAL
```

### vmを削除

```
$ vagrant destroy -f  
==> instance2: Forcing shutdown of VM...
==> instance2: Destroying VM and associated drives...
==> instance1: Forcing shutdown of VM...
==> instance1: Destroying VM and associated drives...
```

### 元のディレクトリーに戻る

```
$ cd -                
```

## vmを作成するplaybookのテンプレートにvagrant upロジックを追記

```yaml:molecule/default/create.yml
---
- name: Create
  hosts: localhost
  connection: local
  gather_facts: false
  no_log: "{{ molecule_no_log }}"
  tasks:

    - name: vagrant up                     # vmを作成、起動
      shell: vagrant up

    - name: vagrant ssh-config             # 作成vmへの接続情報をYAML化
      shell: >-
        vagrant ssh-config | sed '/^$/d' | awk '$1 == "Host" {print "- "$1": "$2} $1 != "Host" {print "  "$1": "$2}'
      register: r

    - name: Populate instance config dict  # 以降で作成vmへの接続情報をmoleculeに連携
      loop: "{{ r.stdout|from_yaml }}"
      set_fact:
        instance_conf_dict:
          instance: "{{ item.Host }}"
          address: "{{ item.HostName }}"
          user: "{{ item.User }}"
          port: "{{ item.Port }}"
          identity_file: "{{ item.IdentityFile }}"
      register: instance_config_dict

    - name: Convert instance config dict to a list
      set_fact:
        instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"

    - name: Dump instance config
      copy:
        content: |
          # Molecule managed

          {{ instance_conf | to_json | from_json | to_yaml }}
        dest: "{{ molecule_instance_config }}"
        mode: 0600
```

## vmを削除するplaybookのテンプレートにvagrant destroy -fロジックを追記


```yaml:molecule/default/destroy.yml
---
- name: Destroy
  hosts: localhost
  connection: local
  gather_facts: false
  no_log: "{{ molecule_no_log }}"
  tasks:
    # Developer must implement.

    - name: vagrant destroy -f
      shell: vagrant destroy -f

    # Mandatory configuration for Molecule to function.

    - name: Populate instance config
      set_fact:
        instance_conf: {}

    - name: Dump instance config
      copy:
        content: |
          # Molecule managed

          {{ instance_conf | to_json | from_json | to_yaml }}
        dest: "{{ molecule_instance_config }}"
        mode: 0600
      when: server.changed | default(false) | bool
```

## tasks/main.ymlの例

適当に編集

```yaml:tasks/main.yml
---
- debug:
    var: ansible_distribution
- debug:
    var: ansible_distribution_version
```

## 動作確認

### 初期ステータス

```
$ molecule list
INFO     Running default > list
                ╷             ╷                  ╷               ╷         ╷            
  Instance Name │ Driver Name │ Provisioner Name │ Scenario Name │ Created │ Converged  
╶───────────────┼─────────────┼──────────────────┼───────────────┼─────────┼───────────╴
  instance1     │ delegated   │ ansible          │ default       │ false   │ false      
  instance2     │ delegated   │ ansible          │ default       │ false   │ false      
                ╵             ╵                  ╵               ╵         ╵            
```

### インスタンス作成

```
$ molecule create 
...

PLAY [Create] ******************************************************************

TASK [vagrant up] **************************************************************
changed: [localhost]

TASK [vagrant ssh-config] ******************************************************
changed: [localhost]

TASK [Populate instance config dict] *******************************************
ok: [localhost] => (item={'Host': 'instance1', 'HostName': '127.0.0.1', 'User': 'vagrant', 'Port': 2205, 'UserKnownHostsFile': '/dev/null', 'StrictHostKeyChecking': False, 'PasswordAuthentication': False, 'IdentityFile': '/home/foo/repo/delegated_vagrant/molecule/default/.vagrant/machines/instance1/virtualbox/private_key', 'IdentitiesOnly': True, 'LogLevel': 'FATAL'})
ok: [localhost] => (item={'Host': 'instance2', 'HostName': '127.0.0.1', 'User': 'vagrant', 'Port': 2206, 'UserKnownHostsFile': '/dev/null', 'StrictHostKeyChecking': False, 'PasswordAuthentication': False, 'IdentityFile': '/home/foo/repo/delegated_vagrant/molecule/default/.vagrant/machines/instance2/virtualbox/private_key', 'IdentitiesOnly': True, 'LogLevel': 'FATAL'})

TASK [Convert instance config dict to a list] **********************************
ok: [localhost]

TASK [Dump instance config] ****************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=5    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

INFO     Running default > prepare
WARNING  Skipping, prepare playbook not configured.
```

### 立ち上げたインスタンスに構成変更playbook(converge.yml)を適用
この例ではdebugモジュールでOSの種類と番号を表示しているのみ。

```
$ molecule converge 
...

PLAY [Converge] ****************************************************************

TASK [Gathering Facts] *********************************************************
ok: [instance2]
ok: [instance1]

TASK [Include delegated_vagrant] ***********************************************

TASK [delegated_vagrant : debug] ***********************************************
ok: [instance1] => {
    "ansible_distribution": "RedHat"
}
ok: [instance2] => {
    "ansible_distribution": "RedHat"
}

TASK [delegated_vagrant : debug] ***********************************************
ok: [instance1] => {
    "ansible_distribution_version": "8.2"
}
ok: [instance2] => {
    "ansible_distribution_version": "8.2"
}

PLAY RECAP *********************************************************************
instance1                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
instance2                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

### moleculeが認識している状態を表示

```
$ molecule list
INFO     Running default > list
                ╷             ╷                  ╷               ╷         ╷            
  Instance Name │ Driver Name │ Provisioner Name │ Scenario Name │ Created │ Converged  
╶───────────────┼─────────────┼──────────────────┼───────────────┼─────────┼───────────╴
  instance1     │ delegated   │ ansible          │ default       │ true    │ true       
  instance2     │ delegated   │ ansible          │ default       │ true    │ true       
                ╵             ╵                  ╵               ╵         ╵         
```

### vmにログイン

```
$ molecule login -h instance1
INFO     Running default > login
Last login: Sat Nov 20 17:56:00 2021 from 10.0.2.2
[vagrant@instance1 ~]$ hostname
instance1
[vagrant@instance1 ~]$ head -2 /etc/os-release 
NAME="Red Hat Enterprise Linux"
VERSION="8.2 (Ootpa)"
[vagrant@instance1 ~]$ logout
Connection to 127.0.0.1 closed.
```

### vmの削除

```
$ molecule destroy 
...

PLAY [Destroy] *****************************************************************

TASK [vagrant destroy -f] ******************************************************
changed: [localhost]

TASK [Populate instance config] ************************************************
ok: [localhost]

TASK [Dump instance config] ****************************************************
skipping: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

INFO     Pruning extra files from scenario ephemeral directory
```