MoleculeのDockerドライバーについて解説してみる

Molecueの`Dockerドライバー`である[Molecule Docker Plugin](https://github.com/ansible-community/molecule-docker)について解説します。

# Dockerドライバーとは

Moleculeの`Dockerドライバー`はMoleculeがdockerコンテナに接続する処理を行うヘルパーです。テスト環境用コンテナを作成、テスト終了後にそのコンテナを破棄するまでを`molecule.yml`への簡潔な記述で処理します。

内部的には主に「**[community.docker.docker_container](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_container_module.html#ansible-collections-community-docker-docker-container-module)**」と「**[community.docker.docker_network](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_network_module.html#ansible-collections-community-docker-docker-network-module)**」を利用しています。

他にも外部のコンテナレジストリからDockerイメージを取得するため「**[community.docker.docker_login](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_login_module.html)**」が、`Dockerfile`からイメージをビルドするために「**[community.docker.docker_image](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_image_module.html)**」も採用されています。


またコンテナは常に`detachedモード`で起動されます。これは現在のところ変更できないようです。

# Dockerドライバーが行なっている処理

Dockerドライバーの処理の定義は[GitHubリポジトリ](https://github.com/ansible-community/molecule-docker)の`src/molecule_docker/playbooks/*`にあります。ここに配置されているPlaybookファイルにより処理が行われます。またこれらのPlaybookに対して変数を渡す場合は主には`molecule.yml`のplatformセクションへ記述していきます。

注意点としてはDockerドライバーでは「**[community.docker.docker_container](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_container_module.html#ansible-collections-community-docker-docker-container-module)**」や「**[community.docker.docker_network](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_network_module.html#ansible-collections-community-docker-docker-network-module)**」等のモジュールで利用できるすべてのパラメーターを設定できるわけではないと言うことです。`src/molecule_docker/playbooks/*`の定義ファイルを良く見ながら設定できるパラメーターを把握する必要があります。


# Dockerドライバーで利用する主なパラメーター

基本的には「**[community.docker.docker_container](https://docs.ansible.com/ansible/latest/collections/community/docker/docker_container_module.html#ansible-collections-community-docker-docker-container-module)**」のパラメーターに準ずるのですが今回は`Dockerドライバー`でよく利用されるであろう基本的なパラメーターのみ解説します。

## env

環境変数を設定します。クラウドの認証情報、SSH鍵等の秘匿情報を渡すことができます。まだデータベースコンテナを起動する際はユーザー名やパスワードなどを渡すことができます。

## image

DockerイメージのコンテナリポジトリのURIを設定します。またPrivateリポジトリからもDockerイメージをpullできます。

## name

Moleculeが起動するDockerコンテナの名前です。Dockerの特性として複数のコンテナを起動する場合、同名のコンテナは起動できません。複数のテスト用コンテナを起動する場合は一意の名前をつけると良いでしょう。

## network_mode

DockerのNetworkモードを指定します。何も指定しないと`bridge`が設定されます。

## pre_build_image

通常は`false`になっています。コンテナを起動した後にAnsibleが接続できる環境を構築したい場合は`true`にします。

例えばPythonがインストールされていないイメージを起動してもAnsibleはコンテナに接続できません。`pre_build_image`を`true`にすると起動されたコンテナに対してPythonがインストールされその他必要な処理が行なわれAnsibleがコンテナへ接続できる状態になります。

`pre_build_image`に対応したコンテナは[GitHubリポジトリ](https://github.com/ansible-community/molecule-docker)の`src/molecule_docker/playbooks/Dockerfile.j2`に定義されています。処理内容を見る限り現在のところLinuxコンテナのみ対応のようです。

```Dockerfile:src/molecule_docker/playbooks/Dockerfile.j2
# Molecule managed

{% if item.registry is defined %}
FROM {{ item.registry.url }}/{{ item.image }}
{% else %}
FROM {{ item.image }}
{% endif %}

{% if item.env is defined %}
{% for var, value in item.env.items() %}
{% if value %}
ENV {{ var }} {{ value }}
{% endif %}
{% endfor %}
{% endif %}

RUN if [ $(command -v apt-get) ]; then export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y python3 sudo bash ca-certificates iproute2 python3-apt aptitude && apt-get clean && rm -rf /var/lib/apt/lists/*; \
    elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install /usr/bin/python3 /usr/bin/python3-config /usr/bin/dnf-3 sudo bash iproute && dnf clean all; \
    elif [ $(command -v yum) ]; then yum makecache fast && yum install -y /usr/bin/python /usr/bin/python2-config sudo yum-plugin-ovl bash iproute && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
    elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python3 sudo bash iproute2 && zypper clean -a; \
    elif [ $(command -v apk) ]; then apk update && apk add --no-cache python3 sudo bash ca-certificates; \
    elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python3 sudo bash ca-certificates iproute2 && xbps-remove -O; fi
```


## privileged

Dockerコンテナを権限モードで起動します。

用途に応じて使い分けます。systemdなどを利用する場合は **`privileged: true`** にしておきます。

systemdを利用しない処理だと有効にする必要はありません。

## その他

Gitlab、Jenkins等のホスティング型のCIツールを利用している場合はテストシナリオ毎にコンテナのCPU・メモリ・ブロックIO等のリソースに制限をかけたい時があるかもしれません。

# 用途別サンプル

:::note info
DockerイメージのリポジトリのURIを設定する項目はCIツール側から渡すため環境変数にしてあります。
:::

## systemdとネットワークを利用しない

もっとも単純な設定になります。

```yaml:molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: ${MOLECULE_IMAGE}
provisioner:
  name: ansible
verifier:
  name: ansible
scenario:
  test_sequence:
    - dependency
    - syntax
    - create
    - converge
    - verify
```

### サンプルリポジトリ

* [ansible-roles-mamono210/cowsay | GitHub](https://github.com/ansible-roles-mamono210/cowsay)

## systemdは利用するがネットワークは利用しない

**`command: /sbin/init`**、 **`privileged: True`** にします。`/sbin/init`の部分はDockerイメージによって異なる事があります。


```yaml:molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: ${MOLECULE_IMAGE}
    command: /sbin/init
    privileged: True
provisioner:
  name: ansible
verifier:
  name: ansible
scenario:
  test_sequence:
    - dependency
    - syntax
    - create
    - converge
    - verify
```

### サンプルリポジトリ

* [ansible-roles-mamono210/vnc_server | GitHub](https://github.com/ansible-roles-mamono210/vnc_server)

## ポートを利用する

`command`に必要なパラメーターを設定しテスト処理に開放が必要なポートを`exposed_ports`、`published_ports`で設定します。

Ansibleからライブラリ等をインストールするだけの処理ならポートの開放は必要ないと思われます。しかし動的テストまでMoleculeで実行したい場合はポートを解放します。

```yaml:molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: ${MOLECULE_IMAGE}
    docker_host: "${DOCKER_HOST:-unix://var/run/docker.sock}"
    command:
      - '/sbin/init'
      - '--web.enable-lifecycle'
      - '--config.file=/etc/prometheus/prometheus.yml'
    privileged: True
    exposed_ports:
      - 9100/tcp
      - 9090/tcp
    published_ports:
      - 0.0.0.0:9100:9100/tcp
      - 0.0.0.0:9090:9090/tcp
provisioner:
  name: ansible
verifier:
  name: ansible
scenario:
  test_sequence:
    - syntax
    - create
    - converge
    - verify
```

### サンプルリポジトリ

* [ansible-roles-mamono210/prometheus | GitHub](https://github.com/ansible-roles-mamono210/prometheus)

## ネットワークを作成、コンテナをネットワークへ参加させる

現時点では`Dockerドライバー`では処理できません。

[GitHubリポジトリ](https://github.com/ansible-community/molecule-docker)の`src/molecule_docker/playbooks/*`にあるDockerドライバーの処理の定義ファイルを見る限りは単一コンテナの起動・破棄しか想定されていないため、コンテナを複数利用する場合は必ず`delegatedドライバー`を利用する事になります。

`delegatedドライバー`でDockerのネットワーク機能を利用する際はDockerリソースの作成順序に注意します。通常は

1. Dockerネットワーク
1. データベースコンテナ
1. アプリケーションコンテナ

の順になります。

削除順序は反対に

1. アプリケーションコンテナ
1. データベースコンテナ
1. Dockerネットワーク

が良いでしょう。

また`molecule.yml`にDockerコンテナの設定をまとめて書いておくとあとで見返した時に分かりやすいです。

この辺りは好みの問題ですが。

```yaml:molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: delegated
platforms:
  - name: wordpress
    image: ${MOLECULE_IMAGE}
    command: /sbin/init
    detach: yes
    networks:
      - name: molecule_wordpress
    privileged: yes
    ports:
      - "80:80"
    state: started
    tty: yes
  - name: db
    image: mysql:5.7
    networks:
      - name: molecule_wordpress
    state: started
    env:
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
  - name: selenium
    image: selenium/standalone-firefox:latest
    networks:
      - name: molecule_wordpress
    ports:
      - "4444:4444"
    state: started
provisioner:
  name: ansible
  config_options:
    defaults:
      callbacks_enabled: ansible.posix.profile_tasks
  connection_options:
    wp_host: 'wordpress'
    php_packages:
      - php
      - php-gd
      - php-gmp
      - php-intl
      - php-ldap
      - php-mbstring
      - php-mcrypt
      - php-mysqlnd
      - php-opcache
      - php-pdo
      - php-pear
      - php-pear-Net-Curl
      - php-pecl-apcu-bc
      - php-pecl-redis
      - php-pecl-zip
      - php-soap
      - php-xml
verifier:
  name: ansible
scenario:
  converge_sequence:
    - converge
  destroy_sequence:
    - destroy
  test_sequence:
    - dependency
    - syntax
    - create
    - converge
    - verify
    - destroy
```

```yaml:molecule/default/create.yml
---
- name: Create
  hosts: localhost
  connection: local

  tasks:
    - name: set_fact molecule_platforms
      ansible.builtin.set_fact:
        molecule_platforms: "{{ molecule_yml.platforms }}"

    - name: Create a network
      community.docker.docker_network:
        name: "{{ molecule_platforms.0.networks[0].name }}"

    - name: Setup MySQL container
      community.docker.docker_container:
        name: "{{ molecule_platforms.1.name }}"
        image: "{{ molecule_platforms.1.image }}"
        networks:
          - name: "{{ molecule_platforms.1.networks[0].name }}"
        state: "{{ molecule_platforms.1.state }}"
        env:
          MYSQL_ROOT_PASSWORD: "{{ molecule_platforms.1.env.MYSQL_ROOT_PASSWORD }}"
          MYSQL_DATABASE: "{{ molecule_platforms.1.env.MYSQL_DATABASE }}"
          MYSQL_USER: "{{ molecule_platforms.1.env.MYSQL_USER }}"
          MYSQL_PASSWORD: "{{ molecule_platforms.1.env.MYSQL_PASSWORD }}"

    - name: Setup WordPress container
      community.docker.docker_container:
        name: "{{ molecule_platforms.0.name }}"
        command: "{{ molecule_platforms.0.command }}"
        detach: "{{ molecule_platforms.0.detach }}"
        image: "{{ molecule_platforms.0.image }}"
        networks:
          - name: "{{ molecule_platforms.0.networks[0].name }}"
        privileged: "{{ molecule_platforms.0.privileged }}"
        ports:
          - "{{ molecule_platforms.0.ports[0] }}"
        state: "{{ molecule_platforms.0.state }}"
        tty: "{{ molecule_platforms.0.tty }}"

    - name: Setup Selenium container
      community.docker.docker_container:
        name: "{{ molecule_platforms.2.name }}"
        image: "{{ molecule_platforms.2.image }}"
        networks:
          - name: "{{ molecule_platforms.2.networks[0].name }}"
        ports:
          - "{{ molecule_platforms.2.ports[0] }}"
        state: "{{ molecule_platforms.2.state }}"
```

```yaml:molecule/default/destroy.yml
---
- name: Destroy
  hosts: localhost
  connection: local

  tasks:
    - name: set_fact molecule_platforms
      ansible.builtin.set_fact:
        molecule_platforms: "{{ molecule_yml.platforms }}"

    - name: Remove Selenium container
      community.docker.docker_container:
        name: "{{ molecule_platforms.2.name }}"
        state: absent

    - name: Remove WordPress container
      community.docker.docker_container:
        name: "{{ molecule_platforms.0.name }}"
        state: absent

    - name: Remove MySQL container
      community.docker.docker_container:
        name: "{{ molecule_platforms.1.name }}"
        state: absent

    - name: Delete a network
      community.docker.docker_network:
        name: "{{ molecule_platforms.0.networks }}"
        state: absent
        force: yes
```

### サンプルリポジトリ

* [ansible-roles-mamono210/wordpress_insall | GitHub](https://github.com/ansible-roles-mamono210/wordpress_install)

# まとめ

コンテナのネットワークが単純な構成なら`Dockerドライバー`を積極的に採用すると良いでしょう。


ネットワークが複雑なら`delegatedドライバー`を採用し`create.yml`・`destroy.yml`へネットワークとコンテナを起動・削除する処理を書きます。

# 関連する記事

* [Moleculeテスト環境用のDockerゴールデンイメージを作成する - Qiita](https://qiita.com/mamono210/items/eed86f8cf5182b7598a1)