🐁

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

commits28 min read

概要

以前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のリポジトリに対して、プッシュしたりPRしたりすると、ソースコードのビルド、テスト、デプロイなどを自動で行ってくれます。

ワークフロー

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

イベント

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

ジョブ

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

ステップ

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

アクション

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

ランナー

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

  • Ubuntu
  • Windows
  • Mac

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

やること

以前の記事で作成したリポジトリに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の中身を以下のように書き換えます。

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にテスト対象となるタスクを記載します。

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に記載します。

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ファイルを編集します。
ここでは、公式サイトに記載されている情報を基にワークフローファイルを作成しました。
molecule-demo.yml
---
name: Molecule Test
on:
  pull_request:
    branches: [master]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 4
      matrix:
        python-version: [3.7, 3.8]

    steps:
      - uses: actions/checkout@v2

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
        # キャッシュを活用
      - uses: actions/cache@v2
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
          restore-keys: ${{ runner.os }}-pip-

      - name: Install dependencies
        run: |
          python3 -m pip install --upgrade pip
          python3 -m pip install ansible
          python3 -m pip install "molecule[docker,lint]"

      - name: Test with molecule
        run: |
          cd testmol ; molecule test

公式のサンプルでは、pushしたらGitHub Actionsが動きますが個人的にはあまり好きではありませんので、masterブランチにPRしたらGitHub Actionsが動くように変更しました。
GitHub Actionsが動くとランナーが起動し、Ansible Molecule関連のモジュールインストールとテストが実行されます。


https://github.com/Yuhta28/ansible-Molecule/runs/2752323977?check_suite_focus=true

所感

Ansible ModuleとGitHub Actionsを組み合わせることでCI/CDの一端にふれることができました。GitHub Actionsはまだ新しいサービスで資料も少なかったですが、GitHubへコミット、プッシュすればすぐに開始されたり、ローカルマシン上に何かしらの用意する必要もないので触ってみて便利な代物だと思いました。会社ではCircle CIを使っていますのですぐに導入することはないかもしれませんが、今後の知見のために触ってみようと思います。

追記 2021/6/5

見返すと少し情報が古くなっていたり、理解が足りていないところがありましたので修正しました。
当初はdockerをランナー内にインストールして、docker内でテスト対象のインスタンスを起動させましたがいつの間にか公式のサンプルソースコードからdockerのインストールが削除されていてそのままでも実行できるようになっていました。
たまに古い記事を見返すもの新しい発見になっていいものだと思いました。

LT資料

LTのスライド資料です。

GitHubで編集を提案

Discussion

ログインするとコメントできます