Ansible MoleculeとGitHub ActionsでCI/CDを体験してみた

公開:2020/11/03
更新:2020/11/21
31 min読了の目安(約28200字TECH技術記事

概要

以前Moleculeを使ってRoleのテストをしてみた記事を書きました。
(https://note.com/tar28/n/n0878cd660c4a)
今回はその時に使ったRoleを基にGitHub ActionsによるCI/CD、テストの自動化を行っていこうと思います。

Moleculeについて

改めての説明になりますが、MoleculeはAnsible Roleのテストを支援してくれるためのツールです。
(https://molecule.readthedocs.io/en/latest/)
MoluculeはAnsibleの最新安定バージョンと一個前のバージョンのみサポートされています。
(2020/10現在のAnsibleの最新安定バージョンは2.9系なため、2.8系までサポート対象です)
molecule testを実行することで、構文規約のチェックからテスト用環境の構築、テスト用環境内でのRoleの実行、テストが終了した開発環境の破棄までを行ってくれる便利なツールです。

GitHub Actionsについて

GitHub ActionsはGitHub社が提供するCI/CDツールです。
(https://github.co.jp/features/actions)
GitHubのリポジトリに対して、プッシュしたりプルリクしたりすると、ソースコードのビルド、テスト、デプロイなどを自動で行ってくれます。

ワークフロー

GitHub Actionsはワークフローという単位で一連の処理を自動化します。
ソフトウェアのビルド、パッケージ作成、デプロイなどを処理をワークフローとして自動化できます。またGitHub ActionsはIssueやプルリクが発行された時にSlackへ通知する作業もワークフローとして自動化できます。

イベント

ワークフローを起動させるトリガーになるものをイベントと呼びます。例として、リポジトリへのコミットのプッシュ、プルリク作成、タグ・リリースの作成などがあります。

ジョブ

ワークフローは1つ以上のジョブで構成されており、ジョブの中身が一連のステップとなっております。複数のジョブをワークフローに設定することができ、同時並行の実行や、Aジョブが完了したらBジョブを起動するといった依存関係の設定も行えます。

ステップ

ジョブは1つ以上のステップから構成されており、ステップはアクションを実行できる個々のタスクとなっております。

アクション

アクションが実際の処理をまとめたGitHub Actionsの最小のコンポーネントとなっております。
独自にアクションを作成することも可能ですが、GitHub公式に用意されているアクションもいくつかあります。

ランナー

ランナーはジョブを実行するためのGitHubが用意している仮想環境です。現在用意されている仮想環境は

  • Ubuntu
  • Windows
  • Mac

以上3つが用意されています。(それぞれ最新バージョン以外もちろん使用可能。RedHat系は残念ながらないようです。)

やること

以前の記事で作成したリポジトリにGitHub Actionsを導入して、テストの自動化を行います。
使うリポジトリはこちらです。
https://github.com/Yuhta28/ansible-Molecule
またテストの内容自体はSoftware Design2020年6月号で紹介されている単純なものを使用しております。(https://gihyo.jp/magazine/SD/archive/2020/202006)

テスト準備

まずはMoleculeをインストールしてみます
基本的には公式リファレンス通り進めば問題ありません。前提条件として、AnsibleとPython3系がインストールされている必要があります。
pip install --user "molecule[docker,lint]"
pipパッケージからインストールすることができ、インストールが完了されましたら、テンプレート構成を作成します。

$ molecule --version
molecule 3.1.5
    ansible:2.9.13 python:3.8
    delegated:3.1.5 from molecule
    docker:0.2.4 from molecule_docker
    
$ molecule init role testmol
--> Initializing new role testmol...
Initialized role in /home/yuta/Desktop/work/ansible/molecule/testmol successfully.
$ tree
.
├── README.md
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── molecule
│   └── default
│       ├── INSTALL.rst
│       ├── converge.yml
│       ├── create.yml
│       ├── destroy.yml
│       ├── molecule.yml
│       └── verify.yml
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml

10 directories, 14 files

molecule init role ロール名でカレントディレクトリにロール名のディレクトリが作成され、その直下にmoleculeのテンプレートディレクトリが作成されます。
この時molecule/default配下にcreate.ymldestory.ymlがありますが、今回の検証では使わない上、放置したままmolecule testを実行しますとテスト動作が上手くいかないことがありますので、削除しておきます。(ローカルマシンがCentOS 8のときはこの2つのファイルは作成されていなかったのですが、OSによるものなのかMoleculeのバージョン更新が原因なのかはいまいち理由がよくわかっていません)

2020/11/21追記

上記の件についてTeratailで質問した所、Moleculeのバージョン更新によるものとわかりました。

moleculeは3.1.0(α版含む)からデフォルトドライバがdelegatedになったので、最新版だと自動的に create.ymldestroy.yml が作成されるようになりました。

(https://teratail.com/questions/301543)

moleculeディレクトリに、テスト実行のための環境実装ファイルやテスト項目ファイルなどを配置します。molecule/default/molecule.ymlの中身を以下のように書き換えます。

---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance1
    image: docker.io/centos:7
    pre_build_image: true
    privileged: True
    command: /sbin/init

  - name: instance2
    image: docker.io/centos:8
    pre_build_image: true
    privileged: True
    command: /sbin/init

provisioner:
  name: ansible
verifier:
  name: ansible

テスト実行用のCentoS7,8のDockerコンテナをビルドしています。
次に、tasks/main.ymlにテスト対象となるタスクを記載します。

---
# tasks file for testmol
- name: install httpd package
  yum:
    name: httpd
    state: latest

- name: start httpd
  systemd:
    name: httpd
    state: started
    enabled: yes

ご覧の通り、内容としてはApacheをインストールして、起動しているだけのシンプルな実装となっております。このタスクに対するテストをmolecule/default/verify.ymlに記載します。

---
# This is httpd install playbook to execute Ansible tests.

- name: Verify
  hosts: all
  tasks:
  - ignore_errors: yes
    block:
    - name: httpdパッケージの存在を確認する
      yum:
        list: httpd
      register: result_rpm

    - name: httpdプロセスが起動していることを確認する
      shell: ps -ef | grep http[d]
      register: result_proc

    - name: httpdサービスが自動起動になっているかを確認する
      shell: systemctl is-enabled httpd
      register: result_enabled

  - name: 結果をまとめて確認する
    assert:
      that: "{{ result.failed == false }}"
    loop:
      - "{{ result_rpm }}"
      - "{{ result_proc }}"
      - "{{ result_enabled }}"
    loop_control:
      loop_var: result

これでテストの準備が完了しましたので、molecule testで実行します。

$ molecule test
--> Test matrix
    
└── default
    ├── dependency
    ├── lint
    ├── cleanup
    ├── destroy
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    ├── cleanup
    └── destroy
    
--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'lint'
--> Lint is disabled.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
--> Sanity checks: 'docker'
    
    PLAY [Destroy] *****************************************************************
    
    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=instance1)
    changed: [localhost] => (item=instance2)
    
    TASK [Wait for instance(s) deletion to complete] *******************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    
    TASK [Delete docker network(s)] ************************************************
    
    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
    
--> Scenario: 'default'
--> Action: 'syntax'
    
    playbook: /home/yuta/Desktop/work/docker/ansible-Molecule/testmol/molecule/default/converge.yml
--> Scenario: 'default'
--> Action: 'create'
    
    PLAY [Create] ******************************************************************
    
    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None) 
    skipping: [localhost] => (item=None) 
    
    TASK [Check presence of custom Dockerfiles] ************************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    
    TASK [Create Dockerfiles from image names] *************************************
    skipping: [localhost] => (item=None) 
    skipping: [localhost] => (item=None) 
    
    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    
    TASK [Build an Ansible compatible image (new)] *********************************
    skipping: [localhost] => (item=molecule_local/docker.io/centos:7) 
    skipping: [localhost] => (item=molecule_local/docker.io/centos:8) 
    
    TASK [Create docker network(s)] ************************************************
    
    TASK [Determine the CMD directives] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost] => (item=None)
    ok: [localhost]
    
    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=instance1)
    changed: [localhost] => (item=instance2)
    
    TASK [Wait for instance(s) creation to complete] *******************************
    changed: [localhost] => (item=None)
    FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost]
    
    PLAY RECAP *********************************************************************
    localhost                  : ok=5    changed=2    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0
    
--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'
    
    PLAY [Converge] ****************************************************************
    
    TASK [Gathering Facts] *********************************************************
    ok: [instance2]
    ok: [instance1]
    
    TASK [Include testmol] *********************************************************
    
    TASK [testmol : install httpd package] *****************************************
    changed: [instance2]
    changed: [instance1]
    
    TASK [testmol : start httpd] ***************************************************
    changed: [instance2]
    changed: [instance1]
    
    PLAY RECAP *********************************************************************
    instance1                  : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    instance2                  : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    
--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Running Ansible Verifier
    
    PLAY [Verify] ******************************************************************
    
    TASK [Gathering Facts] *********************************************************
    ok: [instance2]
    ok: [instance1]
    
    TASK [httpdパッケージの存在を確認する] ******************************************************
    ok: [instance2]
    ok: [instance1]
    
    TASK [httpdプロセスが起動していることを確認する] *************************************************
    changed: [instance2]
    changed: [instance1]
    
    TASK [httpdサービスが自動起動になっているかを確認する] **********************************************
    changed: [instance2]
    changed: [instance1]
    
    TASK [結果をまとめて確認する] *************************************************************
    ok: [instance1] => (item={'results': [{'envra': '0:httpd-2.4.6-93.el7.centos.x86_64', 'name': 'httpd', 'repo': 'base', 'epoch': '0', 'version': '2.4.6', 'release': '93.el7.centos', 'yumstate': 'available', 'arch': 'x86_64'}, {'envra': '0:httpd-2.4.6-93.el7.centos.x86_64', 'name': 'httpd', 'repo': 'installed', 'epoch': '0', 'version': '2.4.6', 'release': '93.el7.centos', 'yumstate': 'installed', 'arch': 'x86_64'}], 'failed': False, 'changed': False}) => {
        "ansible_loop_var": "result",
        "changed": false,
        "msg": "All assertions passed",
        "result": {
            "changed": false,
            "failed": false,
            "results": [
                {
                    "arch": "x86_64",
                    "envra": "0:httpd-2.4.6-93.el7.centos.x86_64",
                    "epoch": "0",
                    "name": "httpd",
                    "release": "93.el7.centos",
                    "repo": "base",
                    "version": "2.4.6",
                    "yumstate": "available"
                },
                {
                    "arch": "x86_64",
                    "envra": "0:httpd-2.4.6-93.el7.centos.x86_64",
                    "epoch": "0",
                    "name": "httpd",
                    "release": "93.el7.centos",
                    "repo": "installed",
                    "version": "2.4.6",
                    "yumstate": "installed"
                }
            ]
        }
    }
    ok: [instance2] => (item={'msg': '', 'results': [{'name': 'httpd', 'arch': 'x86_64', 'epoch': '0', 'release': '21.module_el8.2.0+494+1df74eae', 'version': '2.4.37', 'repo': '@System', 'nevra': '0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64', 'yumstate': 'installed'}, {'name': 'httpd', 'arch': 'x86_64', 'epoch': '0', 'release': '21.module_el8.2.0+494+1df74eae', 'version': '2.4.37', 'repo': 'AppStream', 'nevra': '0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64', 'yumstate': 'available'}], 'failed': False, 'changed': False}) => {
        "ansible_loop_var": "result",
        "changed": false,
        "msg": "All assertions passed",
        "result": {
            "changed": false,
            "failed": false,
            "msg": "",
            "results": [
                {
                    "arch": "x86_64",
                    "epoch": "0",
                    "name": "httpd",
                    "nevra": "0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64",
                    "release": "21.module_el8.2.0+494+1df74eae",
                    "repo": "@System",
                    "version": "2.4.37",
                    "yumstate": "installed"
                },
                {
                    "arch": "x86_64",
                    "epoch": "0",
                    "name": "httpd",
                    "nevra": "0:httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64",
                    "release": "21.module_el8.2.0+494+1df74eae",
                    "repo": "AppStream",
                    "version": "2.4.37",
                    "yumstate": "available"
                }
            ]
        }
    }
    ok: [instance1] => (item={'changed': True, 'end': '2020-11-03 07:59:36.635845', 'stdout': 'root         363       1  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       364     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       365     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       366     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       367     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       368     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'cmd': 'ps -ef | grep http[d]', 'rc': 0, 'start': '2020-11-03 07:59:35.547485', 'stderr': '', 'delta': '0:00:01.088360', 'stdout_lines': ['root         363       1  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache       364     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache       365     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache       366     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache       367     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache       368     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND'], 'stderr_lines': [], 'failed': False}) => {
        "ansible_loop_var": "result",
        "changed": false,
        "msg": "All assertions passed",
        "result": {
            "changed": true,
            "cmd": "ps -ef | grep http[d]",
            "delta": "0:00:01.088360",
            "end": "2020-11-03 07:59:36.635845",
            "failed": false,
            "rc": 0,
            "start": "2020-11-03 07:59:35.547485",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "root         363       1  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       364     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       365     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       366     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       367     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       368     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
            "stdout_lines": [
                "root         363       1  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
                "apache       364     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
                "apache       365     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
                "apache       366     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
                "apache       367     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
                "apache       368     363  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND"
            ]
        }
    }
    ok: [instance2] => (item={'cmd': 'ps -ef | grep http[d]', 'stdout': 'root         404       1  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       429     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       431     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       432     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       433     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'stderr': '', 'rc': 0, 'start': '2020-11-03 07:59:35.568724', 'end': '2020-11-03 07:59:35.587274', 'delta': '0:00:00.018550', 'changed': True, 'stdout_lines': ['root         404       1  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache       429     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache       431     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache       432     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND', 'apache       433     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND'], 'stderr_lines': [], 'failed': False}) => {
        "ansible_loop_var": "result",
        "changed": false,
        "msg": "All assertions passed",
        "result": {
            "changed": true,
            "cmd": "ps -ef | grep http[d]",
            "delta": "0:00:00.018550",
            "end": "2020-11-03 07:59:35.587274",
            "failed": false,
            "rc": 0,
            "start": "2020-11-03 07:59:35.568724",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "root         404       1  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       429     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       431     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       432     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND\napache       433     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
            "stdout_lines": [
                "root         404       1  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
                "apache       429     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
                "apache       431     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
                "apache       432     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND",
                "apache       433     404  0 07:58 ?        00:00:00 /usr/sbin/httpd -DFOREGROUND"
            ]
        }
    }
    ok: [instance1] => (item={'changed': True, 'end': '2020-11-03 07:59:39.966874', 'stdout': 'enabled', 'cmd': 'systemctl is-enabled httpd', 'rc': 0, 'start': '2020-11-03 07:59:38.881374', 'stderr': '', 'delta': '0:00:01.085500', 'stdout_lines': ['enabled'], 'stderr_lines': [], 'failed': False}) => {
        "ansible_loop_var": "result",
        "changed": false,
        "msg": "All assertions passed",
        "result": {
            "changed": true,
            "cmd": "systemctl is-enabled httpd",
            "delta": "0:00:01.085500",
            "end": "2020-11-03 07:59:39.966874",
            "failed": false,
            "rc": 0,
            "start": "2020-11-03 07:59:38.881374",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "enabled",
            "stdout_lines": [
                "enabled"
            ]
        }
    }
    ok: [instance2] => (item={'cmd': 'systemctl is-enabled httpd', 'stdout': 'enabled', 'stderr': '', 'rc': 0, 'start': '2020-11-03 07:59:39.213457', 'end': '2020-11-03 07:59:39.235024', 'delta': '0:00:00.021567', 'changed': True, 'stdout_lines': ['enabled'], 'stderr_lines': [], 'failed': False}) => {
        "ansible_loop_var": "result",
        "changed": false,
        "msg": "All assertions passed",
        "result": {
            "changed": true,
            "cmd": "systemctl is-enabled httpd",
            "delta": "0:00:00.021567",
            "end": "2020-11-03 07:59:39.235024",
            "failed": false,
            "rc": 0,
            "start": "2020-11-03 07:59:39.213457",
            "stderr": "",
            "stderr_lines": [],
            "stdout": "enabled",
            "stdout_lines": [
                "enabled"
            ]
        }
    }
    
    PLAY RECAP *********************************************************************
    instance1                  : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    instance2                  : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    
Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'
    
    PLAY [Destroy] *****************************************************************
    
    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=instance1)
    changed: [localhost] => (item=instance2)
    
    TASK [Wait for instance(s) deletion to complete] *******************************
    changed: [localhost] => (item=None)
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost]
    
    TASK [Delete docker network(s)] ************************************************
    
    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
    
--> Pruning extra files from scenario ephemeral directory

Moleculeの動きについて

Moleculeはmolecule testコマンドでテストを実行することが可能となり、ステップという単位で処理が順次実行されます。
ステップの内容は以下の通りで順次実行され、Roleのテストを行います。

ステップ 実施内容
dependency 依存関係を処理
lint 規約チェック
cleanup 環境の掃除
destroy テスト環境の削除
syntax 構文チェック
create テスト環境の構築
prepare テストの前処理を実行
converge Roleの実行
idempotence Roleの再実行
side_effect テスト実行のための副作用を発生させる
verify テストの実行
clenup 環境の掃除
destroy テスト環境の削除

※引用 SoftwareDesign 2020/6月号
(https://gihyo.jp/magazine/SD/archive/2020/202006)
上記のコマンド結果はdefaultというシナリオの中で必要なステップが順次実行されております。
(ステップはすべて実行されることはなく、省略されているものもあります)
ここまででAnsible MoleculeでRoleのテストを行えることができました。
次からGitHub ActionsによるCI/CDの実装に入りたいと思います。

GitHub Action導入

GitHub Actionsの導入はGitHubのリポジトリ内にあるActionsタブから新規に作成することができます。
GitHub側が用意したいくつかのテンプレートワークフローから適したものを選ぶこともできますし、自分で一から作成することもできます。
Ansible Moleculeの公式サイトにサンプルがありましたので、ここではSimple workflowを選択して一からセットアップします。
(https://molecule.readthedocs.io/en/latest/ci.html#github-actions)
セットアップしますと、.github/workflows/ディレクトリが作成されその中にワークフローを設定するYAMLファイルを編集します。
ここでは、公式サイトに記載されている次のワークフロー設定ファイルを記載します。

---
name: Molecule Test
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 4
      matrix:
        python-version: [3.6, 3.7]

    steps:
      - uses: actions/checkout@v2
        with:
          path: molecule_demo
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          sudo apt install docker
          python3 -m pip install --upgrade pip
          python3 -m pip install -r requirements.txt
      - name: Test with molecule
        run: |
          molecule test

さてこのワークフロー設定ファイルの中にはいくつか私のリポジトリにそぐわない部分がありますのでいくつか合わせていきます。まず私のリポジトリをランナーの中に導入したいので、gitをインストールしてランナー環境でgit cloneできるようにします。次にランナーにpipパッケージをインストールするためのrequirements.txtがありますので、私のリポジトリにも用意しておきます。

$ pip freeze > requirements.txt
$ tree
.
├── README.md
├── requirements.txt
└── testmol
    ├── README.md
    ├── defaults
    │   └── main.yml
    ├── handlers
    │   └── main.yml
    ├── meta
    │   └── main.yml
    ├── molecule
    │   └── default
    │       ├── INSTALL.rst
    │       ├── converge.yml
    │       ├── molecule.yml
    │       └── verify.yml
    ├── tasks
    │   └── main.yml
    ├── tests
    │   ├── inventory
    │   └── test.yml
    └── vars
        └── main.yml

9 directories, 14 files

最後にmolecule testコマンドの実行場所ですが、これですとtestmolディレクトリの一個上のディレクトリで実行しており、失敗しますのでcd testmolでテストが実行できる場所に移動します。
この結果修正したワークフロー設定ファイルがこちらです。

---
name: Molecule Test
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 4
      matrix:
        python-version: [3.7, 3.8]

    steps:
      - uses: actions/checkout@v2
        with:
          path: molecule_demo
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          sudo apt install git docker
          git clone -b Yuhta28-patch-2 https://github.com/Yuhta28/ansible-Molecule.git
          python3 -m pip install --upgrade pip
          python3 -m pip install -r ansible-Molecule/requirements.txt
      - name: Test with molecule
        run: |
          cd ansible-Molecule/testmol
          molecule test

ここまで完了しましたら右上のStart commitボタンをクリックしてレポジトリにコミットします。
コミットしますとワークフローが実行されテストが走ります。

しばらくしたら、処理が完了しテストが無事に完了するかと思われました。

テスト失敗

ワークフローの処理結果を確認しますとテストが失敗していました。
GitHub Actionsではワークフローの実行結果ログもGitHubから確認できますので、中身を見てますとDockerの起動に失敗しているというメッセージが有りました。
それで一つ気になったのが、Moleculeの公式サイトで紹介されていたdockerのインストール方法です。sudo apt install dockerと記されていますが、Dockerの公式リファレンスにかかれているUbuntu上でのDockerのインストール手順と異なっています。(https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)
なので、Docker公式リファレンスに則った方法でDockerインストール手順に関して修正しました。
修正した設定ファイルがこちらです。

---
name: Molecule Test
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 4
      matrix:
        python-version: [3.7, 3.8]

    steps:
      - uses: actions/checkout@v2
        with:
          path: molecule_demo
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          sudo apt install git apt-transport-https ca-certificates curl gnupg-agent software-properties-common
          curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
          sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
          sudo apt-get update
          sudo apt-get install docker-ce docker-ce-cli containerd.io
          git clone -b Yuhta28-patch-3 https://github.com/Yuhta28/ansible-Molecule.git
          python3 -m pip install --upgrade pip
          python3 -m pip install -r ansible-Molecule/requirements.txt
      - name: Test with molecule
        run: |
          cd ansible-Molecule/testmol
          molecule test

このように変更してもう一度commitし、ワークフローを走らせると問題なくテストが完了したことが確認できました。

所感

Ansible ModuleとGitHub Actionsを組み合わせることでCI/CDの一端にふれることができました。GitHub Actionsはまだ新しいサービスで資料も少なかったですが、GitHubへコミット、プッシュすればすぐに開始されたり、ローカルマシン上に何かしらの用意する必要もないので触ってみてい便利な代物だと思いました。ただ途中のMoleculeの公式リファレンスに紹介されていたサンプルコードはやや引っかかりを覚えましたので、人生初のOSSへプルリクを行いました。
プルリク中ですが、もしマージされましたら改めて記事にして報告しようと思います。