GitLab CIでAnsibleの自作モジュールのCIをやってみる

こちらは [Ansible Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ansible) の8日目の記事です。

ここでは、GitLab CIを使ったAnsible自作モジュールのCIについてやってみたいと思います。
以前

* [Ansibleモジュールを自作したらsanityテストをしてみよう](https://qiita.com/sky_jokerxx/items/b997117ea70bb8c8c1ec)
* [Ansibleモジュールを自作したらunitテストをしてみよう](https://qiita.com/sky_jokerxx/items/67e5617643d05344fdc0)

という記事を書いたのですが、これは手動でテストをしているのでsanity/unit test共に自動化して効率化を図ってみたいと思います。
ここではいくつか説明を省略している部分があるので上記の記事を一読していただいてからの方が理解しやすかと思われます。

# 1. 環境

|  項目  |        バージョン        |
|--------|--------------------------|
| OS     | CentOS7.5                |
| GitLab | Community Edition 11.5.0 |

# 2. 準備

## 2-1. GitLabインストール

GitLabのインストールについては以下を参考にしてください。

[https://about.gitlab.com/install/#centos-7](https://about.gitlab.com/install/#centos-7)

## 2-2. gitlab-runnnerの準備

以下を参考にして準備してください。
ちなみに、dockerを使います。

[https://qiita.com/sky_jokerxx/items/2a264a0194a5cbc7bd12](https://qiita.com/sky_jokerxx/items/2a264a0194a5cbc7bd12)

# 3. CIをやってみる

## 3-1. Dockerイメージの作成

Dockerイメージは以下の選択肢があります。

* 公式のDockerイメージを使う
* 自分でイメージを作る

前者にいたっては、既に使用可能になっているものなので、簡単に使えます。
ただ、Pythonのバージョンが全部(Python2.6〜3.7がインストールされている)入っており容量的には大きくないります。
後者だと、一から準備する手間がありますが必要なものだけインストールすれば公式よりは容量が少なくなります。
例えば、テストするPythonのバージョンが決まりきっているのであれば一から作るという選択肢もあります。
ここでは、前者のあらかじめ用意されているイメージを使ってみようと思います。

### 3-1-1. Dockerfile作成

自作したモジュールは `ansible/modules` 配下の適切なグループに保存してもらえればいいのですが、ここでは自作ということで `ansible/modules/salf_made` と `test/units/modules/salf_made` というディレクトリを作成し、その中に自作したモジュールとテスト用ソースを保存する想定で作成します。

```yaml:Dockerfile
FROM quay.io/ansible/default-test-container:latest
RUN ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN cd /opt && \
    git clone https://github.com/ansible/ansible.git && \
    cd ansible && \
    mkdir lib/ansible/modules/salf_made/ && \
    mkdir test/units/modules/salf_made
RUN apt-get -y install man-db
```

### 3-1-2. Dockerイメージの作成

```shell-session
[root@GitLab ~]# mkdir ansible-ci-docker
[root@GitLab ~]# cd ansible-ci-docker/
[root@GitLab ansible-ci-docker]# vi Dockerfile
(上記のDockerfileをコピー)
[root@GitLab ansible-ci-docker]# docker build . -t ansible-ci:latest
```

## 3-2. リポジトリの作成

ここでは、GitLabに以下のリポジトリを作成してみました。

![スクリーンショット 2018-11-25 0.16.28.png](https://qiita-image-store.s3.amazonaws.com/0/180118/ab137c76-4728-bdfd-8548-240dbbf645d4.png)

`__init__.py` は空です。
`output_name.py` のソースは以下のようになっています。[ここ](https://qiita.com/sky_jokerxx/items/b997117ea70bb8c8c1ec#3-%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB)のを元にunit testが出来るよう一部修正(sum関数を追加してるだけ)して再利用しています
`test_output_name.py` のソースについては `Unit test CI` で説明します。(sanityテストでは不要です)

```python
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2018, sky_joker
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'community'
}


DOCUMENTATION = '''
module: output_name
short_description: module that outputs a name.
author:
    - sky_joker (@sky-jocker)
version_added: 2.x
description:
    - Output the specified name.
requirements:
    - python >= 2.7
options:
    name:
        description:
            - Specify name.
        required: True
'''

EXAMPLES = '''
---
- output_name:
    name: taro
  register: r

- debug: msg="{{ r }}"
'''

from ansible.module_utils.basic import AnsibleModule


def sum(a, b):
    return a + b


def main():
    argument_spec = dict(
        name=dict(type="str", required=True)
    )

    module = AnsibleModule(argument_spec, supports_check_mode=True)

    result = dict(changed=False)

    name = module.params['name']
    if(name):
        result['name'] = name
        module.exit_json(**result)
    else:
        module.fail_json(msg="Please specify name.")


if __name__ == "__main__":
    main()
```

GitLab CI用のファイルを作成してみます。
ここでは、Python2.7、3.6、3.7でsanity testのCIをやってみます。

```yaml:.gitlab-ci
image: ansible-ci:latest
stages:
    - sanity_test
python2.7:
    stage: sanity_test
    script: 
        - cp __init__.py /opt/ansible/lib/ansible/modules/salf_made
        - cp output_name.py /opt/ansible/lib/ansible/modules/salf_made
        - cd /opt/ansible
        - . hacking/env-setup
        - ansible-test sanity --python 2.7 output_name
    only:
        - master

python3.6:
    stage: sanity_test
    script: 
        - cp __init__.py /opt/ansible/lib/ansible/modules/salf_made
        - cp output_name.py /opt/ansible/lib/ansible/modules/salf_made
        - cd /opt/ansible
        - . hacking/env-setup
        - ansible-test sanity --python 3.6 output_name
    only:
        - master

python3.7:
    stage: sanity_test
    script: 
        - cp __init__.py /opt/ansible/lib/ansible/modules/salf_made
        - cp output_name.py /opt/ansible/lib/ansible/modules/salf_made
        - cd /opt/ansible
        - . hacking/env-setup
        - ansible-test sanity --python 3.7 output_name
    only:
        - master
```

## 3-3. Sanity Test CI

`.gitlab-ci.yml` を作成してコミットしてリポジトリにマージするとCIが実行されます。

```shell-session
[root@GitLab example]# vi .gitlab-ci.yml
(上記の.gitlab-ci.ymlをコピペする)
[root@GitLab example]# git add .
[root@GitLab example]# git commit -m 'update'
[master 64a2491] update
 1 file changed, 12 insertions(+), 3 deletions(-)
 [root@GitLab example]# git push origin master
Username for 'http://192.168.0.115': example
Password for 'http://example@192.168.0.115':
Counting objects: 5, done.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 383 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To http://192.168.0.115/example/example.git
   1763223..64a2491  master -> master
```

結果は以下のようになります。

https://sky-joker.tech/wp-content/uploads/2018/11/sanity_test_result.gif

## 3-4. Unit test CI

CIのステージに `unit_test` を追加してCIを実行してみます。
`.gitlab-ci.yml` は以下のようになります。

```yaml:.gitlab-ci
image: ansible-ci:latest
stages:
    - sanity_test
    - unit_test
sanity_python2.7:
    stage: sanity_test
    script: 
        - cp __init__.py /opt/ansible/lib/ansible/modules/salf_made
        - cp output_name.py /opt/ansible/lib/ansible/modules/salf_made
        - cd /opt/ansible
        - . hacking/env-setup
        - ansible-test sanity --python 2.7 output_name
    only:
        - master

sanity_python3.6:
    stage: sanity_test
    script: 
        - cp __init__.py /opt/ansible/lib/ansible/modules/salf_made
        - cp output_name.py /opt/ansible/lib/ansible/modules/salf_made
        - cd /opt/ansible
        - . hacking/env-setup
        - ansible-test sanity --python 3.6 output_name
    only:
        - master

sanity_python3.7:
    stage: sanity_test
    script: 
        - cp __init__.py /opt/ansible/lib/ansible/modules/salf_made
        - cp output_name.py /opt/ansible/lib/ansible/modules/salf_made
        - cd /opt/ansible
        - . hacking/env-setup
        - ansible-test sanity --python 3.7 output_name
    only:
        - master

unit_python2.7:
    stage: unit_test
    script:
        - cp __init__.py /opt/ansible/lib/ansible/modules/salf_made
        - cp output_name.py /opt/ansible/lib/ansible/modules/salf_made
        - cp test_output_name.py /opt/ansible/test/units/modules/salf_made
        - cd /opt/ansible
        - . hacking/env-setup
        - ansible-test units --python 2.7 output_name
    only:
        - master

unit_python3.6:
    stage: unit_test
    script:
        - cp __init__.py /opt/ansible/lib/ansible/modules/salf_made
        - cp output_name.py /opt/ansible/lib/ansible/modules/salf_made
        - cp test_output_name.py /opt/ansible/test/units/modules/salf_made
        - cd /opt/ansible
        - . hacking/env-setup
        - ansible-test units --python 3.6 output_name
    only:
        - master

unit_python3.7:
    stage: unit_test
    script:
        - cp __init__.py /opt/ansible/lib/ansible/modules/salf_made
        - cp output_name.py /opt/ansible/lib/ansible/modules/salf_made
        - cp test_output_name.py /opt/ansible/test/units/modules/salf_made
        - cd /opt/ansible
        - . hacking/env-setup
        - ansible-test units --python 3.7 output_name
    only:
        - master
```

テストで使う `test_output_name` は以下のようにしました。

```python:test_output_name.py
from ansible.modules.salf_made.output_name import sum

def test_sum():
    assert sum(1, 2) == 3
```

結果は以下のようになります。

[![unit_test_result.gif](https://qiita-image-store.s3.amazonaws.com/0/180118/5b1ff93c-d36d-5aef-2360-03ca97f88c8d.gif)](https://qiita-image-store.s3.amazonaws.com/0/180118/5b1ff93c-d36d-5aef-2360-03ca97f88c8d.gif)

# 4. 最後に

Ansibleの自作モジュールの静的解析のsanityテストとunitテストのCIをGitLabでやってみました。
複数のPythonバージョンでテストをしようと思うと結構手順があるので自動化することで効率化ができたり記録が残るのでエビデンスが取れて良いと思います :)
後、GitLabを使うことでコードの管理とCIが同時に出来て便利ですね :)